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.
136 lines
6.7 KiB
TypeScript
136 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useAuth } from "@/context/AuthContext";
|
|
import {
|
|
getLeavesForUser,
|
|
getUserBalance,
|
|
LeaveRequest,
|
|
} from "@/lib/leavesStore";
|
|
import { LeaveSheet } from "@/components/LeaveSheet";
|
|
import { LeaveTable } from "@/components/LeaveTable";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import {
|
|
CalendarDays,
|
|
PlusCircle,
|
|
Clock,
|
|
CheckCircle2,
|
|
XCircle,
|
|
Wallet,
|
|
} from "lucide-react";
|
|
|
|
export default function EmployeePage() {
|
|
const { user, logout, isLoading } = useAuth();
|
|
const router = useRouter();
|
|
const [leaves, setLeaves] = useState<LeaveRequest[]>([]);
|
|
const [balance, setBalance] = useState(0);
|
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
const [dataLoaded, setDataLoaded] = useState(false);
|
|
|
|
const loadData = useCallback(async () => {
|
|
if (!user) return;
|
|
const [data, bal] = await Promise.all([
|
|
getLeavesForUser(user.id),
|
|
Promise.resolve(getUserBalance(user.id)),
|
|
]);
|
|
setLeaves(data.sort((a, b) => b.appliedAt.localeCompare(a.appliedAt)));
|
|
setBalance(bal);
|
|
setDataLoaded(true);
|
|
}, [user]);
|
|
|
|
useEffect(() => {
|
|
if (!isLoading && !user) router.replace("/");
|
|
else if (!isLoading && user?.role === "manager") router.replace("/manager");
|
|
else if (!isLoading && user) loadData();
|
|
}, [user, isLoading, router, loadData]);
|
|
|
|
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 stats = [
|
|
{ label: "Leave Balance", value: balance, unit: "days", icon: Wallet, 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)" },
|
|
];
|
|
|
|
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-5xl 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-5xl mx-auto px-4 py-8 space-y-8">
|
|
<div className="flex items-start justify-between flex-wrap gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold" style={{ color: "var(--text-primary)" }}>Hello, {user.name.split(" ")[0]}</h1>
|
|
<p className="text-sm mt-1" style={{ color: "var(--text-muted)" }}>Apply for leave from here</p>
|
|
</div>
|
|
<Button onClick={() => setSheetOpen(true)} className="gap-2 font-semibold" style={{ background: "linear-gradient(135deg, var(--brand-600), var(--brand-500))", border: "none", borderRadius: "var(--radius-md)", boxShadow: "0 4px 12px -2px rgb(99 102 241 / 0.35)" }}>
|
|
<PlusCircle className="w-4 h-4" />Apply Leave
|
|
</Button>
|
|
</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}{s.unit && <span className="text-sm font-normal ml-1">{s.unit}</span>}
|
|
</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 className="pb-0" style={{ borderBottom: "1px solid var(--surface-border)", paddingBottom: "1rem", marginBottom: 0 }}>
|
|
<CardTitle className="text-base font-semibold" style={{ color: "var(--text-primary)" }}>Leave History</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<LeaveTable leaves={leaves} />
|
|
</CardContent>
|
|
</Card>
|
|
</main>
|
|
|
|
<LeaveSheet open={sheetOpen} onOpenChange={setSheetOpen} userId={user.id} leaveBalance={balance} onSuccess={loadData} />
|
|
</div>
|
|
);
|
|
}
|