sumona-banerjeee c5946ce95e first commit for the leave management system. built a Leave Management System with role-based access:
Employees can apply for leave and track balance, status, and history.
Managers can view all requests and approve/reject them.
Login with separate dashboards for employees and managers with the dummy json.
Tables with filters (All, Pending, Approved, Rejected) for easy management.
2026-04-29 11:22:14 +05:30

240 lines
8.2 KiB
TypeScript

"use client";
import { useState } from "react";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle, CheckCircle2, Loader2 } from "lucide-react";
import { applyLeave } from "@/lib/leavesStore";
interface LeaveSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
userId: number;
leaveBalance: number;
onSuccess: () => void;
}
const LEAVE_TYPES = ["Sick", "Casual", "Annual", "Unpaid"] as const;
export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess }: LeaveSheetProps) {
const [type, setType] = useState<typeof LEAVE_TYPES[number]>("Casual");
const [fromDate, setFromDate] = useState("");
const [toDate, setToDate] = useState("");
const [reason, setReason] = useState("");
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
const today = new Date().toISOString().split("T")[0];
const daysCount =
fromDate && toDate && toDate >= fromDate
? Math.ceil((new Date(toDate).getTime() - new Date(fromDate).getTime()) / 86400000) + 1
: 0;
const reset = () => {
setType("Casual");
setFromDate("");
setToDate("");
setReason("");
setError("");
setSuccess(false);
};
const handleClose = () => {
reset();
onOpenChange(false);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (!fromDate || !toDate || !reason.trim()) {
setError("All fields are required");
return;
}
if (toDate < fromDate) {
setError("End date must be after start date");
return;
}
setLoading(true);
const result = await applyLeave(userId, { type, fromDate, toDate, reason });
setLoading(false);
if (!result.ok) {
setError(result.error ?? "Failed to apply");
return;
}
setSuccess(true);
setTimeout(() => {
handleClose();
onSuccess();
}, 1500);
};
return (
<Sheet open={open} onOpenChange={handleClose}>
<SheetContent
className="w-full sm:max-w-xl overflow-y-auto p-6"
style={{ background: "var(--surface-card)", borderLeft: "1px solid var(--surface-border)" }}
>
<SheetHeader className="pb-4">
<SheetTitle style={{ color: "var(--text-primary)", fontSize: "1.125rem" }}>
Apply for Leave
</SheetTitle>
<SheetDescription style={{ color: "var(--text-secondary)" }}>
You have{" "}
<span className="font-semibold" style={{ color: "var(--brand-600)" }}>
{leaveBalance} days
</span>{" "}
remaining
</SheetDescription>
</SheetHeader>
{success ? (
<div className="flex flex-col items-center justify-center py-16 gap-3">
<div
className="w-16 h-16 rounded-full flex items-center justify-center"
style={{ background: "var(--color-success-bg)" }}
>
<CheckCircle2 className="w-8 h-8" style={{ color: "var(--color-success)" }} />
</div>
<p className="font-semibold text-base" style={{ color: "var(--text-primary)" }}>
Leave Applied!
</p>
<p className="text-sm" style={{ color: "var(--text-muted)" }}>
Your request is pending approval
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-5 pt-2">
{/* Leave Type */}
<div className="space-y-1.5">
<Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}>
Leave Type
</Label>
<Select value={type} onValueChange={(v) => setType(v as any)}>
<SelectTrigger style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{LEAVE_TYPES.map((t) => (
<SelectItem key={t} value={t}>
{t}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Date range */}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}>
From Date
</Label>
<Input
type="date"
min={today}
value={fromDate}
onChange={(e) => {
setFromDate(e.target.value);
if (toDate && e.target.value > toDate) setToDate("");
}}
style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }}
/>
</div>
<div className="space-y-1.5">
<Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}>
To Date
</Label>
<Input
type="date"
min={fromDate || today}
value={toDate}
onChange={(e) => setToDate(e.target.value)}
style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }}
/>
</div>
</div>
{daysCount > 0 && (
<div
className="text-sm px-3 py-2 rounded-md font-medium"
style={{
background: "var(--brand-50)",
color: "var(--brand-700)",
border: "1px solid var(--brand-200)",
borderRadius: "var(--radius-sm)",
}}
>
{daysCount} day{daysCount > 1 ? "s" : ""} selected
</div>
)}
{/* Reason */}
<div className="space-y-1.5">
<Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}>
Reason
</Label>
<Textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="Brief reason for leave..."
rows={3}
style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)", resize: "none" }}
/>
</div>
{error && (
<Alert style={{ borderColor: "var(--color-danger-border)", background: "var(--color-danger-bg)", borderRadius: "var(--radius-sm)" }}>
<AlertCircle className="h-4 w-4" style={{ color: "var(--color-danger)" }} />
<AlertDescription style={{ color: "var(--color-danger)" }}>{error}</AlertDescription>
</Alert>
)}
<div className="flex gap-2 pt-2">
<Button
type="button"
variant="outline"
className="flex-1"
onClick={handleClose}
style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }}
>
Cancel
</Button>
<Button
type="submit"
className="flex-1 font-semibold"
disabled={loading}
style={{
background: "linear-gradient(135deg, var(--brand-600), var(--brand-500))",
border: "none",
borderRadius: "var(--radius-sm)",
}}
>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : "Apply Leave"}
</Button>
</div>
</form>
)}
</SheetContent>
</Sheet>
);
}