Compare commits

..

No commits in common. "96cc1bb68474e2efdfcff22fc9834deeffa31a06" and "73b31778dbaa1a9257f179e221c029c4f3a274dc" have entirely different histories.

11 changed files with 190 additions and 282 deletions

View File

@ -20,7 +20,6 @@ import {
XCircle, XCircle,
Wallet, Wallet,
} from "lucide-react"; } from "lucide-react";
import { ROUTES } from "@/lib/routes";
export default function EmployeePage() { export default function EmployeePage() {
const { user, logout, isLoading } = useAuth(); const { user, logout, isLoading } = useAuth();
@ -42,10 +41,8 @@ export default function EmployeePage() {
}, [user]); }, [user]);
useEffect(() => { useEffect(() => {
// if (!isLoading && !user) router.replace("/"); if (!isLoading && !user) router.replace("/");
// else if (!isLoading && user?.role === "manager") router.replace("/manager"); else if (!isLoading && user?.role === "manager") router.replace("/manager");
if (!isLoading && !user) router.replace(ROUTES.home);
else if (!isLoading && user?.role === "manager") router.replace(ROUTES.manager);
else if (!isLoading && user) loadData(); else if (!isLoading && user) loadData();
}, [user, isLoading, router, loadData]); }, [user, isLoading, router, loadData]);

View File

@ -3,12 +3,11 @@ import "./globals.css";
import { AuthProvider } from "@/context/AuthContext"; import { AuthProvider } from "@/context/AuthContext";
import { Geist } from "next/font/google"; import { Geist } from "next/font/google";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Toaster } from "@/components/ui/sonner";
const geist = Geist({subsets:['latin'],variable:'--font-sans'}); const geist = Geist({subsets:['latin'],variable:'--font-sans'});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "LeaveFlow - Leave Management", title: "LeaveFlow Leave Management",
description: "Streamlined leave management for teams", description: "Streamlined leave management for teams",
}; };
@ -25,7 +24,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</head> </head>
<body> <body>
<AuthProvider>{children}</AuthProvider> <AuthProvider>{children}</AuthProvider>
<Toaster richColors position="top-right" />
</body> </body>
</html> </html>
); );

View File

@ -14,8 +14,6 @@ import {
XCircle, XCircle,
LayoutDashboard, LayoutDashboard,
} from "lucide-react"; } from "lucide-react";
import { ROUTES } from "@/lib/routes";
import { toast } from "sonner";
type FilterStatus = "All" | "Pending" | "Approved" | "Rejected"; type FilterStatus = "All" | "Pending" | "Approved" | "Rejected";
@ -34,27 +32,23 @@ export default function ManagerPage() {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!isLoading && !user) router.replace(ROUTES.home); if (!isLoading && !user) router.replace("/");
else if (!isLoading && user?.role === "employee") router.replace(ROUTES.employee); else if (!isLoading && user?.role === "employee") router.replace("/employee");
else if (!isLoading && user) loadData(); else if (!isLoading && user) loadData();
}, [user, isLoading, router, loadData]); }, [user, isLoading, router, loadData]);
const handleApprove = async (id: number) => { const handleApprove = async (id: number) => {
const leave = leaves.find((l) => l.id === id);
setLoadingId(id); setLoadingId(id);
await updateLeaveStatus(id, "Approved"); await updateLeaveStatus(id, "Approved");
await loadData(); await loadData();
setLoadingId(null); setLoadingId(null);
toast.success(`Leave approved for ${leave?.userName ?? "employee"}`);
}; };
const handleReject = async (id: number) => { const handleReject = async (id: number) => {
const leave = leaves.find((l) => l.id === id);
setLoadingId(id); setLoadingId(id);
await updateLeaveStatus(id, "Rejected"); await updateLeaveStatus(id, "Rejected");
await loadData(); await loadData();
setLoadingId(null); setLoadingId(null);
toast.error(`Leave rejected for ${leave?.userName ?? "employee"}`);
}; };
if (isLoading || !user || !dataLoaded) { if (isLoading || !user || !dataLoaded) {
@ -150,4 +144,4 @@ export default function ManagerPage() {
</main> </main>
</div> </div>
); );
} }

View File

@ -7,34 +7,37 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CalendarDays, Loader2 } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { ROUTES } from "@/lib/routes"; import { CalendarDays, AlertCircle, Loader2 } from "lucide-react";
import { toast } from "sonner";
export default function LoginPage() { export default function LoginPage() {
const { login, user, isLoading } = useAuth(); const { login, user, isLoading } = useAuth();
const router = useRouter(); const router = useRouter();
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (!isLoading && user) { if (!isLoading && user) {
router.replace(user.role === "manager" ? ROUTES.manager : ROUTES.employee); router.replace(user.role === "manager" ? "/manager" : "/employee");
} }
}, [user, isLoading, router]); }, [user, isLoading, router]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!username || !password) { if (!username || !password) {
toast.error("Please enter username and password"); setError("Please enter username and password");
return; return;
} }
setLoading(true); setLoading(true);
setError("");
const result = await login(username, password); const result = await login(username, password);
setLoading(false); setLoading(false);
if (!result.ok) { if (!result.ok) {
toast.error(result.error ?? "Login failed"); setError(result.error ?? "Login failed");
} else if (result.ok) {
// redirect handled by useEffect
} }
}; };
@ -72,6 +75,7 @@ export default function LoginPage() {
<h1 className="text-2xl font-bold tracking-tight" style={{ color: "var(--text-primary)" }}> <h1 className="text-2xl font-bold tracking-tight" style={{ color: "var(--text-primary)" }}>
LeaveFlow LeaveFlow
</h1> </h1>
</div> </div>
<Card <Card
@ -117,6 +121,13 @@ export default function LoginPage() {
/> />
</div> </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>
)}
<Button <Button
type="submit" type="submit"
className="w-full font-semibold" className="w-full font-semibold"

View File

@ -19,9 +19,9 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Loader2 } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { applyLeave, daysBetween, isWeekend, LeaveRequest } from "@/lib/leavesStore"; import { AlertCircle, CheckCircle2, Loader2 } from "lucide-react";
import { toast } from "sonner"; import { applyLeave } from "@/lib/leavesStore";
interface LeaveSheetProps { interface LeaveSheetProps {
open: boolean; open: boolean;
@ -38,13 +38,15 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
const [fromDate, setFromDate] = useState(""); const [fromDate, setFromDate] = useState("");
const [toDate, setToDate] = useState(""); const [toDate, setToDate] = useState("");
const [reason, setReason] = useState(""); const [reason, setReason] = useState("");
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const today = new Date().toISOString().split("T")[0]; const today = new Date().toISOString().split("T")[0];
const daysCount = const daysCount =
fromDate && toDate && toDate >= fromDate fromDate && toDate && toDate >= fromDate
? daysBetween(fromDate, toDate) ? Math.ceil((new Date(toDate).getTime() - new Date(fromDate).getTime()) / 86400000) + 1
: 0; : 0;
const reset = () => { const reset = () => {
@ -52,6 +54,8 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
setFromDate(""); setFromDate("");
setToDate(""); setToDate("");
setReason(""); setReason("");
setError("");
setSuccess(false);
}; };
const handleClose = () => { const handleClose = () => {
@ -61,24 +65,27 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError("");
if (!fromDate || !toDate || !reason.trim()) { if (!fromDate || !toDate || !reason.trim()) {
toast.error("All fields are required"); setError("All fields are required");
return; return;
} }
if (toDate < fromDate) { if (toDate < fromDate) {
toast.error("End date must be after start date"); setError("End date must be after start date");
return; return;
} }
setLoading(true); setLoading(true);
const result = await applyLeave(userId, { type, fromDate, toDate, reason }); const result = await applyLeave(userId, { type, fromDate, toDate, reason });
setLoading(false); setLoading(false);
if (!result.ok) { if (!result.ok) {
toast.error(result.error ?? "Failed to apply"); setError(result.error ?? "Failed to apply");
return; return;
} }
toast.success("Leave applied! Your request is pending approval."); setSuccess(true);
handleClose(); setTimeout(() => {
onSuccess(); handleClose();
onSuccess();
}, 1500);
}; };
return ( return (
@ -100,116 +107,133 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
</SheetDescription> </SheetDescription>
</SheetHeader> </SheetHeader>
<form onSubmit={handleSubmit} className="space-y-5 pt-2"> {success ? (
<div className="space-y-1.5"> <div className="flex flex-col items-center justify-center py-16 gap-3">
<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 <div
className="text-sm px-3 py-2 rounded-md font-medium" className="w-16 h-16 rounded-full flex items-center justify-center"
style={{ style={{ background: "var(--color-success-bg)" }}
background: "var(--brand-50)",
color: "var(--brand-700)",
border: "1px solid var(--brand-200)",
borderRadius: "var(--radius-sm)",
}}
> >
{daysCount} day{daysCount > 1 ? "s" : ""} selected <CheckCircle2 className="w-8 h-8" style={{ color: "var(--color-success)" }} />
</div> </div>
)} <p className="font-semibold text-base" style={{ color: "var(--text-primary)" }}>
Leave Applied!
<div className="space-y-1.5"> </p>
<Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}> <p className="text-sm" style={{ color: "var(--text-muted)" }}>
Reason Your request is pending approval
</Label> </p>
<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>
) : (
<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>
<div className="flex gap-2 pt-2"> {/* Date range */}
<Button <div className="grid grid-cols-2 gap-3">
type="button" <div className="space-y-1.5">
variant="outline" <Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}>
className="flex-1" From Date
onClick={handleClose} </Label>
style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }} <Input
> type="date"
Cancel min={today}
</Button> value={fromDate}
<Button onChange={(e) => {
type="submit" setFromDate(e.target.value);
className="flex-1 font-semibold" if (toDate && e.target.value > toDate) setToDate("");
disabled={loading} }}
style={{ style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }}
background: "linear-gradient(135deg, var(--brand-600), var(--brand-500))", />
border: "none", </div>
borderRadius: "var(--radius-sm)", <div className="space-y-1.5">
}} <Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}>
> To Date
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : "Apply Leave"} </Label>
</Button> <Input
</div> type="date"
</form> 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> </SheetContent>
</Sheet> </Sheet>
); );

View File

@ -1,49 +0,0 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: (
<CircleCheckIcon className="size-4" />
),
info: (
<InfoIcon className="size-4" />
),
warning: (
<TriangleAlertIcon className="size-4" />
),
error: (
<OctagonXIcon className="size-4" />
),
loading: (
<Loader2Icon className="size-4 animate-spin" />
),
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast: "cn-toast",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@ -1,5 +1,4 @@
"use client"; "use client";
import { ROUTES } from "@/lib/routes";
import React, { import React, {
createContext, createContext,
@ -62,8 +61,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const logout = () => { const logout = () => {
setUser(null); setUser(null);
localStorage.removeItem("lms_user"); localStorage.removeItem("lms_user");
// router.push("/"); router.push("/");
router.push(ROUTES.home);
}; };
return ( return (

View File

@ -5,7 +5,6 @@
"type": "Sick", "type": "Sick",
"fromDate": "2026-04-25", "fromDate": "2026-04-25",
"toDate": "2026-04-26", "toDate": "2026-04-26",
"days": 2,
"reason": "Fever", "reason": "Fever",
"status": "Approved", "status": "Approved",
"appliedAt": "2026-04-20" "appliedAt": "2026-04-20"
@ -16,7 +15,6 @@
"type": "Casual", "type": "Casual",
"fromDate": "2026-04-28", "fromDate": "2026-04-28",
"toDate": "2026-04-30", "toDate": "2026-04-30",
"days": 3,
"reason": "Family trip", "reason": "Family trip",
"status": "Pending", "status": "Pending",
"appliedAt": "2026-04-27" "appliedAt": "2026-04-27"

View File

@ -5,8 +5,6 @@ export interface LeaveRequest {
type: "Sick" | "Casual" | "Annual" | "Unpaid"; type: "Sick" | "Casual" | "Annual" | "Unpaid";
fromDate: string; fromDate: string;
toDate: string; toDate: string;
/** Immutable day count stored at apply-time; used for all balance calculations. */
days: number;
reason: string; reason: string;
status: "Pending" | "Approved" | "Rejected"; status: "Pending" | "Approved" | "Rejected";
appliedAt: string; appliedAt: string;
@ -68,46 +66,25 @@ export async function applyLeave(
): Promise<{ ok: boolean; error?: string }> { ): Promise<{ ok: boolean; error?: string }> {
await seedIfNeeded(); await seedIfNeeded();
// Block if fromDate or toDate is a weekend // Calculate days
if (isWeekend(data.fromDate) || isWeekend(data.toDate)) { const from = new Date(data.fromDate);
return { ok: false, error: "Leave cannot start or end on a weekend (Saturday/Sunday)." }; const to = new Date(data.toDate);
} const days = Math.ceil((to.getTime() - from.getTime()) / 86400000) + 1;
// Calculate days using the shared util (no inline duplication) // Check balance
const days = daysBetween(data.fromDate, data.toDate);
if (days === 0) {
return { ok: false, error: "Selected range has no working days." };
}
// Check balance — account for already-pending requests
const users = getUsers(); const users = getUsers();
const user = users.find((u) => u.id === userId); const user = users.find((u) => u.id === userId);
if (!user) return { ok: false, error: "User not found" }; if (!user) return { ok: false, error: "User not found" };
if ((user.leaveBalance ?? 0) < days)
return { ok: false, error: `Insufficient balance. You have ${user.leaveBalance} days left.` };
const leaves: LeaveRequest[] = JSON.parse(localStorage.getItem(LEAVES_KEY) ?? "[]"); const leaves: LeaveRequest[] = JSON.parse(localStorage.getItem(LEAVES_KEY) ?? "[]");
// Only check balance for non-Unpaid leave
if (data.type !== "Unpaid") {
const pendingDays = leaves
.filter((l) => l.userId === userId && l.status === "Pending" && l.type !== "Unpaid")
.reduce((sum, l) => sum + (l.days ?? daysBetween(l.fromDate, l.toDate)), 0);
const availableBalance = (user.leaveBalance ?? 0) - pendingDays;
if (availableBalance < days)
return {
ok: false,
error: `Insufficient balance. You have ${user.leaveBalance} total days, but ${pendingDays} are reserved by pending requests (${availableBalance} available).`,
};
}
const newLeave: LeaveRequest = { const newLeave: LeaveRequest = {
id: Date.now(), id: Date.now(),
userId, userId,
type: data.type, type: data.type,
fromDate: data.fromDate, fromDate: data.fromDate,
toDate: data.toDate, toDate: data.toDate,
days,
reason: data.reason, reason: data.reason,
status: "Pending", status: "Pending",
appliedAt: new Date().toISOString().split("T")[0], appliedAt: new Date().toISOString().split("T")[0],
@ -131,32 +108,30 @@ export async function updateLeaveStatus(
leaves[idx] = { ...leave, status }; leaves[idx] = { ...leave, status };
localStorage.setItem(LEAVES_KEY, JSON.stringify(leaves)); localStorage.setItem(LEAVES_KEY, JSON.stringify(leaves));
// Adjust balance using the immutable `days` stored at apply-time. // Adjust balance
// Fallback to daysBetween for legacy records that pre-date the `days` field.
const users = getUsers(); const users = getUsers();
const user = users.find((u) => u.id === leave.userId); const user = users.find((u) => u.id === leave.userId);
if (!user || user.role !== "employee") return; if (!user || user.role !== "employee") return;
const days = leave.days ?? daysBetween(leave.fromDate, leave.toDate); const from = new Date(leave.fromDate);
const to = new Date(leave.toDate);
const days = Math.ceil((to.getTime() - from.getTime()) / 86400000) + 1;
// Only adjust balance for non-Unpaid leave if (status === "Approved" && !wasApproved) {
if (leave.type !== "Unpaid") { user.leaveBalance = (user.leaveBalance ?? 0) - days;
if (status === "Approved" && !wasApproved) { } else if (status === "Rejected" && wasApproved) {
user.leaveBalance = (user.leaveBalance ?? 0) - days; user.leaveBalance = (user.leaveBalance ?? 0) + days;
} else if (status === "Rejected" && wasApproved) { }
user.leaveBalance = (user.leaveBalance ?? 0) + days;
}
localStorage.setItem(USERS_KEY, JSON.stringify(users)); localStorage.setItem(USERS_KEY, JSON.stringify(users));
// Also update session user if it's the same person // Also update session user if it's the same person
const sessionUser = localStorage.getItem("lms_user"); const sessionUser = localStorage.getItem("lms_user");
if (sessionUser) { if (sessionUser) {
const su = JSON.parse(sessionUser); const su = JSON.parse(sessionUser);
if (su.id === user.id) { if (su.id === user.id) {
su.leaveBalance = user.leaveBalance; su.leaveBalance = user.leaveBalance;
localStorage.setItem("lms_user", JSON.stringify(su)); localStorage.setItem("lms_user", JSON.stringify(su));
}
} }
} }
} }
@ -167,22 +142,8 @@ export function getUserBalance(userId: number): number {
return u?.leaveBalance ?? 0; return u?.leaveBalance ?? 0;
} }
// Returns true if date is Saturday or Sunday
export function isWeekend(dateStr: string): boolean {
const day = new Date(dateStr).getDay();
return day === 0 || day === 6;
}
// Count only working days (MonFri) between two dates
export function daysBetween(from: string, to: string): number { export function daysBetween(from: string, to: string): number {
const start = new Date(from); const f = new Date(from);
const end = new Date(to); const t = new Date(to);
let count = 0; return Math.ceil((t.getTime() - f.getTime()) / 86400000) + 1;
const current = new Date(start);
while (current <= end) {
const day = current.getDay();
if (day !== 0 && day !== 6) count++;
current.setDate(current.getDate() + 1);
}
return count;
} }

22
package-lock.json generated
View File

@ -12,12 +12,10 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^1.11.0", "lucide-react": "^1.11.0",
"next": "16.2.4", "next": "16.2.4",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"shadcn": "^4.5.0", "shadcn": "^4.5.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0" "tw-animate-css": "^1.4.0"
}, },
@ -8616,16 +8614,6 @@
} }
} }
}, },
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@ -10195,16 +10183,6 @@
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/sonner": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

View File

@ -13,12 +13,10 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^1.11.0", "lucide-react": "^1.11.0",
"next": "16.2.4", "next": "16.2.4",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"shadcn": "^4.5.0", "shadcn": "^4.5.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0" "tw-animate-css": "^1.4.0"
}, },