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.
This commit is contained in:
1 parent
3fdca895cc
commit
c5946ce95e
33 files changed
+7173
-212
No files matched your search
@@ -0,0 +1,189 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user