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.
148 lines
7.7 KiB
TypeScript
148 lines
7.7 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";
|
|
|
|
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("/");
|
|
else if (!isLoading && user?.role === "employee") router.replace("/employee");
|
|
else if (!isLoading && user) loadData();
|
|
}, [user, isLoading, router, loadData]);
|
|
|
|
const handleApprove = async (id: number) => {
|
|
setLoadingId(id);
|
|
await updateLeaveStatus(id, "Approved");
|
|
await loadData();
|
|
setLoadingId(null);
|
|
};
|
|
|
|
const handleReject = async (id: number) => {
|
|
setLoadingId(id);
|
|
await updateLeaveStatus(id, "Rejected");
|
|
await loadData();
|
|
setLoadingId(null);
|
|
};
|
|
|
|
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>
|
|
);
|
|
}
|