sumona-banerjeee 96cc1bb684 Added working-day-only calculation, Fixed leave logic so balance is deducted only on approval and pending requests are considered to prevent over-application.
Ensured consistent day calculation using stored days field and removed duplicate daysBetween logic.
Implemented working-day-only calculation, excluding weekends (Saturday & Sunday), and blocked invalid date selections.
Corrected handling of Unpaid leave so it does not affect leave balance.
Added routing and integrated Sonner notifications for login, manager actions, and employee leave submission.
2026-04-29 19:14:58 +05:30

216 lines
7.0 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 { Loader2 } from "lucide-react";
import { applyLeave, daysBetween, isWeekend, LeaveRequest } from "@/lib/leavesStore";
import { toast } from "sonner";
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 [loading, setLoading] = useState(false);
const today = new Date().toISOString().split("T")[0];
const daysCount =
fromDate && toDate && toDate >= fromDate
? daysBetween(fromDate, toDate)
: 0;
const reset = () => {
setType("Casual");
setFromDate("");
setToDate("");
setReason("");
};
const handleClose = () => {
reset();
onOpenChange(false);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!fromDate || !toDate || !reason.trim()) {
toast.error("All fields are required");
return;
}
if (toDate < fromDate) {
toast.error("End date must be after start date");
return;
}
setLoading(true);
const result = await applyLeave(userId, { type, fromDate, toDate, reason });
setLoading(false);
if (!result.ok) {
toast.error(result.error ?? "Failed to apply");
return;
}
toast.success("Leave applied! Your request is pending approval.");
handleClose();
onSuccess();
};
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>
<form onSubmit={handleSubmit} className="space-y-5 pt-2">
<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 LeaveRequest["type"])}>
<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>
<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) => {
const val = e.target.value;
if (isWeekend(val)) {
toast.error("Start date cannot be a Saturday or Sunday");
return;
}
setFromDate(val);
if (toDate && val > 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) => {
const val = e.target.value;
if (isWeekend(val)) {
toast.error("End date cannot be a Saturday or Sunday");
return;
}
setToDate(val);
}}
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>
)}
<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>
<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>
);
}