sumona-banerjeee c5946ce95e first commit for the leave management system. built a Leave Management System with role-based access:
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.
2026-04-29 11:22:14 +05:30

189 lines
7.4 KiB
TypeScript

"use client";
import { LeaveRequest, daysBetween } from "@/lib/leavesStore";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { CheckCircle2, XCircle, Clock, CalendarRange } from "lucide-react";
interface LeaveTableProps {
leaves: LeaveRequest[];
isManager?: boolean;
onApprove?: (id: number) => void;
onReject?: (id: number) => void;
loadingId?: number | null;
}
const STATUS_CONFIG = {
Approved: {
className: "status-approved",
icon: CheckCircle2,
},
Pending: {
className: "status-pending",
icon: Clock,
},
Rejected: {
className: "status-rejected",
icon: XCircle,
},
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const day = date.getDate();
const month = date.toLocaleString("en-US", { month: "short" });
const year = date.getFullYear();
return `${day} ${month}, ${year}`;
};
export function LeaveTable({ leaves, isManager, onApprove, onReject, loadingId }: LeaveTableProps) {
if (leaves.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-16 gap-2">
<CalendarRange className="w-10 h-10" style={{ color: "var(--text-muted)" }} />
<p className="font-medium" style={{ color: "var(--text-secondary)" }}>
No leave requests yet
</p>
<p className="text-sm" style={{ color: "var(--text-muted)" }}>
{isManager ? "No requests to review" : "Apply for your first leave"}
</p>
</div>
);
}
return (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow style={{ borderBottom: "1px solid var(--surface-border)" }}>
{isManager && (
<TableHead style={{ color: "var(--text-muted)", fontSize: "0.75rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }}>
Employee
</TableHead>
)}
<TableHead style={{ color: "var(--text-muted)", fontSize: "0.75rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }}>
Type
</TableHead>
<TableHead style={{ color: "var(--text-muted)", fontSize: "0.75rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }}>
Duration
</TableHead>
<TableHead style={{ color: "var(--text-muted)", fontSize: "0.75rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }}>
Days
</TableHead>
<TableHead style={{ color: "var(--text-muted)", fontSize: "0.75rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }}>
Reason
</TableHead>
<TableHead style={{ color: "var(--text-muted)", fontSize: "0.75rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }}>
Status
</TableHead>
{isManager && (
<TableHead style={{ color: "var(--text-muted)", fontSize: "0.75rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }}>
Action
</TableHead>
)}
</TableRow>
</TableHeader>
<TableBody>
{leaves.map((leave) => {
const config = STATUS_CONFIG[leave.status];
const StatusIcon = config.icon;
const days = daysBetween(leave.fromDate, leave.toDate);
const isLoading = loadingId === leave.id;
return (
<TableRow
key={leave.id}
style={{ borderBottom: "1px solid var(--surface-border)" }}
className="hover:bg-[var(--surface-hover)] transition-colors"
>
{isManager && (
<TableCell>
<span className="text-sm font-medium" style={{ color: "var(--text-primary)" }}>
{leave.userName}
</span>
</TableCell>
)}
<TableCell>
<span className="text-sm font-medium" style={{ color: "var(--text-primary)" }}>
{leave.type}
</span>
</TableCell>
<TableCell>
<span className="text-sm" style={{ color: "var(--text-secondary)", fontFamily: "DM Mono, monospace", fontSize: "0.8rem" }}>
{formatDate(leave.fromDate)}-{formatDate(leave.toDate)}
</span>
</TableCell>
<TableCell>
<span
className="inline-flex items-center justify-center w-7 h-7 rounded-full text-xs font-bold"
style={{ background: "var(--brand-50)", color: "var(--brand-700)" }}
>
{days}
</span>
</TableCell>
<TableCell>
<span className="text-sm max-w-[180px] truncate block" style={{ color: "var(--text-secondary)" }}>
{leave.reason}
</span>
</TableCell>
<TableCell>
<span
className={`inline-flex items-center gap-1.5 text-xs font-semibold px-2.5 py-1 rounded-full ${config.className}`}
>
<StatusIcon className="w-3 h-3" />
{leave.status}
</span>
</TableCell>
{isManager && (
<TableCell>
{leave.status === "Pending" ? (
<div className="flex items-center gap-1.5">
<Button
size="sm"
onClick={() => onApprove?.(leave.id)}
disabled={isLoading}
className="h-7 text-xs font-semibold"
style={{
background: "var(--color-success-bg)",
color: "var(--color-success)",
border: "1px solid var(--color-success-border)",
borderRadius: "var(--radius-sm)",
}}
>
{isLoading ? "…" : <><CheckCircle2 className="w-3 h-3 mr-1" />Approve</>}
</Button>
<Button
size="sm"
onClick={() => onReject?.(leave.id)}
disabled={isLoading}
className="h-7 text-xs font-semibold"
style={{
background: "var(--color-danger-bg)",
color: "var(--color-danger)",
border: "1px solid var(--color-danger-border)",
borderRadius: "var(--radius-sm)",
}}
>
<XCircle className="w-3 h-3 mr-1" />Reject
</Button>
</div>
) : (
<span className="text-xs" style={{ color: "var(--text-muted)" }}></span>
)}
</TableCell>
)}
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}