151 lines
7.8 KiB
TypeScript
151 lines
7.8 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";
|
|
|
|
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");
|
|
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) => {
|
|
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>
|
|
);
|
|
}
|