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

153 lines
8.0 KiB
TypeScript

"use client";
import { useState, useEffect, useCallback } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/context/AuthContext";
import { getLeaves, updateLeaveStatus, LeaveRequest } from "@/lib/leavesStore";
import { LeaveTable } from "@/components/LeaveTable";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
CalendarDays,
Clock,
CheckCircle2,
XCircle,
LayoutDashboard,
} from "lucide-react";
import { ROUTES } from "@/lib/routes";
import { toast } from "sonner";
type FilterStatus = "All" | "Pending" | "Approved" | "Rejected";
export default function ManagerPage() {
const { user, logout, isLoading } = useAuth();
const router = useRouter();
const [leaves, setLeaves] = useState<LeaveRequest[]>([]);
const [filter, setFilter] = useState<FilterStatus>("All");
const [loadingId, setLoadingId] = useState<number | null>(null);
const [dataLoaded, setDataLoaded] = useState(false);
const loadData = useCallback(async () => {
const data = await getLeaves();
setLeaves(data.sort((a, b) => b.appliedAt.localeCompare(a.appliedAt)));
setDataLoaded(true);
}, []);
useEffect(() => {
if (!isLoading && !user) router.replace(ROUTES.home);
else if (!isLoading && user?.role === "employee") router.replace(ROUTES.employee);
else if (!isLoading && user) loadData();
}, [user, isLoading, router, loadData]);
const handleApprove = async (id: number) => {
const leave = leaves.find((l) => l.id === id);
setLoadingId(id);
await updateLeaveStatus(id, "Approved");
await loadData();
setLoadingId(null);
toast.success(`Leave approved for ${leave?.userName ?? "employee"}`);
};
const handleReject = async (id: number) => {
const leave = leaves.find((l) => l.id === id);
setLoadingId(id);
await updateLeaveStatus(id, "Rejected");
await loadData();
setLoadingId(null);
toast.error(`Leave rejected for ${leave?.userName ?? "employee"}`);
};
if (isLoading || !user || !dataLoaded) {
return (
<div className="min-h-screen flex items-center justify-center" style={{ background: "var(--surface-base)" }}>
<div className="flex flex-col items-center gap-3">
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ background: "linear-gradient(135deg, var(--brand-600), var(--brand-400))" }}>
<CalendarDays className="w-5 h-5" style={{ color: "var(--text-on-brand)" }} />
</div>
<p className="text-sm font-medium" style={{ color: "var(--text-muted)" }}>Loading</p>
</div>
</div>
);
}
const pending = leaves.filter((l) => l.status === "Pending").length;
const approved = leaves.filter((l) => l.status === "Approved").length;
const rejected = leaves.filter((l) => l.status === "Rejected").length;
const filtered = filter === "All" ? leaves : leaves.filter((l) => l.status === filter);
const stats = [
{ label: "Total Requests", value: leaves.length, icon: LayoutDashboard, color: "var(--brand-600)", bg: "var(--brand-50)", border: "var(--brand-200)" },
{ label: "Pending", value: pending, icon: Clock, color: "var(--color-pending)", bg: "var(--color-pending-bg)", border: "var(--color-pending-border)" },
{ label: "Approved", value: approved, icon: CheckCircle2, color: "var(--color-success)", bg: "var(--color-success-bg)", border: "var(--color-success-border)" },
{ label: "Rejected", value: rejected, icon: XCircle, color: "var(--color-danger)", bg: "var(--color-danger-bg)", border: "var(--color-danger-border)" },
];
const filters: FilterStatus[] = ["All", "Pending", "Approved", "Rejected"];
const filterCounts: Record<FilterStatus, number> = { All: leaves.length, Pending: pending, Approved: approved, Rejected: rejected };
return (
<div className="min-h-screen" style={{ background: "var(--surface-base)" }}>
<header style={{ background: "var(--surface-card)", borderBottom: "1px solid var(--surface-border)", position: "sticky", top: 0, zIndex: 40 }}>
<div className="max-w-6xl mx-auto px-4 py-3.5 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl flex items-center justify-center" style={{ background: "linear-gradient(135deg, var(--brand-600), var(--brand-400))", boxShadow: "0 4px 10px -2px rgb(99 102 241 / 0.3)" }}>
<CalendarDays className="w-4.5 h-4.5" style={{ color: "var(--text-on-brand)" }} />
</div>
<div><span className="font-bold text-base" style={{ color: "var(--text-primary)" }}>LeaveFlow</span></div>
</div>
<div className="flex items-center gap-3">
<div className="hidden sm:flex items-center gap-2">
<span className="text-sm font-medium" style={{ color: "var(--text-secondary)" }}>{user.name}</span>
</div>
<Button size="sm" onClick={logout} className="text-xs font-semibold px-4" style={{ background: "linear-gradient(135deg, var(--brand-600), var(--brand-500))", color: "var(--text-on-brand)", border: "none", borderRadius: "var(--radius-sm)" }}>Logout</Button>
</div>
</div>
</header>
<main className="max-w-6xl mx-auto px-4 py-8 space-y-8">
<div>
<h1 className="text-2xl font-bold" style={{ color: "var(--text-primary)" }}>Manager Dashboard</h1>
<p className="text-sm mt-1" style={{ color: "var(--text-muted)" }}>Review and manage all employee leave requests</p>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{stats.map((s) => {
const Icon = s.icon;
return (
<Card key={s.label} style={{ border: `1px solid ${s.border}`, background: s.bg, borderRadius: "var(--radius-md)", boxShadow: "none" }}>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<Icon className="w-4 h-4" style={{ color: s.color }} />
<span className="text-xs font-medium" style={{ color: s.color }}>{s.label}</span>
</div>
<p className="text-2xl font-bold" style={{ color: s.color }}>{s.value}</p>
</CardContent>
</Card>
);
})}
</div>
<Card style={{ border: "1px solid var(--surface-border)", borderRadius: "var(--radius-lg)", background: "var(--surface-card)", boxShadow: "var(--shadow-card)" }}>
<CardHeader style={{ borderBottom: "1px solid var(--surface-border)", paddingBottom: "1rem" }}>
<div className="flex items-center justify-between flex-wrap gap-3">
<CardTitle className="text-base font-semibold" style={{ color: "var(--text-primary)" }}>Leave Requests</CardTitle>
<div className="flex gap-1 p-1 rounded-lg" style={{ background: "var(--surface-base)", border: "1px solid var(--surface-border)" }}>
{filters.map((f) => (
<button key={f} onClick={() => setFilter(f)} className="text-xs font-medium px-3 py-1.5 rounded-md transition-all flex items-center gap-1.5" style={filter === f ? { background: "var(--surface-card)", color: "var(--brand-600)", boxShadow: "var(--shadow-card)" } : { color: "var(--text-muted)" }}>
{f}
{filterCounts[f] > 0 && (
<span className="inline-flex items-center justify-center w-4 h-4 rounded-full text-[10px] font-bold" style={{ background: filter === f ? "var(--brand-100)" : "var(--surface-border)", color: filter === f ? "var(--brand-700)" : "var(--text-muted)" }}>{filterCounts[f]}</span>
)}
</button>
))}
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<LeaveTable leaves={filtered} isManager onApprove={handleApprove} onReject={handleReject} loadingId={loadingId} />
</CardContent>
</Card>
</main>
</div>
);
}