import React, { useState, useEffect, useRef } from "react";
import { useAuth } from "@/lib/auth-context";
import { format, getYear, parseISO, differenceInCalendarDays, getMonth } from "date-fns";
import * as XLSX from 'xlsx';
import {
Calendar as CalendarIcon,
CheckCircle,
XCircle,
Loader2,
Filter,
SearchIcon,
X,
Download,
Users,
UserPlus,
FileText,
BarChart,
ChevronDown,
Check,
Clock,
User,
AlertCircle,
File,
ChevronLeft,
ChevronRight,
ListFilter,
SlidersHorizontal,
Table as TableIcon,
UserIcon,
Info,
FileSpreadsheet,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator";
import { toast } from "sonner";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
import {
Leave,
LeaveStatus,
LeaveType,
LeaveStatusUpdateRequest,
Employee,
LeaveMaster,
LeaveBalance
} from "@/types";
import { leaveApi } from "@/services/leaveApi";
import { employeeApi } from "@/services/employeeApi";
import { leaveMasterApi } from "@/services/leaveMasterApi";
import { leaveBalanceApi } from "@/services/leaveBalanceApi";
import LeaveCard from "@/components/leaves/LeaveCard";
import Header from "@/components/layout/header";
import Sidebar from "@/components/layout/sidebar";
import { useIsMobile } from "@/hooks/use-mobile";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import CustomDialog from "@/components/CustomDialog";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useToast } from "@/components/ui/use-toast";
import Layout from "@/components/layout/layout";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import LeaveList from "@/components/leaves/LeaveList";
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
import MonthlyReportTab from "./leaves-admin-monthly-report-tab";
import CalendarTab from "./leaves-admin-calendar-tab";
// Custom Dropdown Component
interface CustomDropdownProps {
value: string;
onChange: (value: string) => void;
options: { value: string; label: string; color?: string }[];
placeholder: string;
showBadge?: boolean;
disabled?: boolean;
searchable?: boolean;
}
function CustomDropdown({ value, onChange, options, placeholder, showBadge = false, disabled = false, searchable = false }: CustomDropdownProps) {
const [open, setOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const getLabel = () => {
return options.find(option => option.value === value)?.label || placeholder;
};
const getColor = () => {
return options.find(option => option.value === value)?.color || "";
};
const handleButtonClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!disabled) {
setOpen(!open);
}
};
const handleOptionClick = (optionValue: string) => {
onChange(optionValue);
setOpen(false);
setSearchQuery("");
};
const filteredOptions = searchable && searchQuery
? options.filter(option =>
option.label.toLowerCase().includes(searchQuery.toLowerCase())
)
: options;
return (
{showBadge && value ? (
{getLabel()}
) : (
{getLabel()}
)}
{open && (
{searchable && (
)}
{filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
handleOptionClick(option.value)}
>
{showBadge ? (
{option.label}
) : (
option.label
)}
{value === option.value && }
))
) : (
No results found
)}
)}
);
}
// Type for leave with employee info
interface LeaveWithEmployee extends Leave {
employeeName?: string;
}
// Type for monthly leave report
interface EmployeeLeaveReport {
employeeId: number;
employeeName: string;
employeeCode: string;
halfDay: { count: number; dates: number[] };
annualLeave: { count: number; dates: number[] };
compensatoryOff: { count: number; dates: number[] };
workFromHome: { count: number; dates: number[] };
sickLeave: { count: number; dates: number[] };
casualLeave: { count: number; dates: number[] };
other: { count: number; dates: number[] };
}
// Define the form schema for status update
const statusUpdateSchema = z.object({
comments: z.string().optional(),
});
type StatusUpdateFormValues = z.infer;
// Custom LeaveDetailDialog component
function CustomLeaveDetailDialog({
leave,
employee,
approver,
isOpen,
onClose,
onDownloadDocument,
leaveMasters,
getLeaveTypeName,
}: {
leave: Leave | null;
employee: Employee | null;
approver: Employee | null;
isOpen: boolean;
onClose: () => void;
onDownloadDocument?: (leave: Leave) => void;
leaveMasters: LeaveMaster[];
getLeaveTypeName: (id?: number) => string;
}) {
if (!leave) return null;
const [leaveBalances, setLeaveBalances] = useState([]);
const [loadingBalances, setLoadingBalances] = useState(false);
useEffect(() => {
const fetchBalances = async () => {
if (leave && isOpen && leave.employeeId) {
setLoadingBalances(true);
try {
const year = getYear(new Date(leave.startDate));
const balances = await leaveBalanceApi.getByEmployeeIdAndYear(leave.employeeId, year);
setLeaveBalances(balances);
} catch (error) {
console.error("Failed to fetch leave balances", error);
} finally {
setLoadingBalances(false);
}
}
};
fetchBalances();
}, [leave, isOpen]);
const startDate = new Date(leave.startDate);
const endDate = new Date(leave.endDate);
const durationDays = differenceInCalendarDays(endDate, startDate) + 1;
// Find the associated leave master if available
const leaveMaster = leave.leaveMasterId ? leaveMasters.find(m => m.id === leave.leaveMasterId) : null;
// Helper function to get status icon
const getStatusIcon = (status: LeaveStatus) => {
switch (status) {
case "Approved":
return ;
case "Rejected":
return ;
case "Cancelled":
return ;
case "Pending":
default:
return ;
}
};
// Helper function to get badge variant
const getStatusBadgeVariant = (status: LeaveStatus) => {
switch (status) {
case "Approved":
return "bg-green-500 hover:bg-green-600";
case "Rejected":
return "bg-red-500 hover:bg-red-600";
case "Cancelled":
return "bg-gray-500 hover:bg-gray-600";
case "Pending":
default:
return "bg-yellow-500 hover:bg-yellow-600";
}
};
return (
{getStatusIcon(leave.status)}
{leave.status}
Employee:
{employee ? `${employee.firstName} ${employee.lastName}` : `ID: ${leave.employeeId}`}
Period:
{format(startDate, "PPP")} to {format(endDate, "PPP")}
{" "}({durationDays} {durationDays === 1 ? "day" : "days"})
Type:
{leave.leaveMasterId ? getLeaveTypeName(leave.leaveMasterId) : leave.type}
{leaveMaster && (
Leave Policy:
{leaveMaster.description && (
{leaveMaster.description}
)}
{leaveMaster.requiresDocumentation && (
Documentation required
)}
{leaveMaster.minDaysBeforeApply > 0 && (
Must apply {leaveMaster.minDaysBeforeApply} days in advance
)}
)}
{leave.appliedOn && (
Applied On:
{format(new Date(leave.appliedOn), "PPP")}
)}
{leave.halfDay && (
)}
{leave.documents && (
Documents:
onDownloadDocument?.(leave)}
>
View Documents
)}
{(leave.status === "Approved" || leave.status === "Rejected") && (
<>
{approver && (
Actioned By:
{approver.firstName} {approver.lastName}
)}
{leave.actionDate && (
Action Date:
{format(new Date(leave.actionDate), "PPP")}
)}
{leave.comments && (
Comments:
{leave.comments}
)}
>
)}
Leave Balances ({leave ? getYear(new Date(leave.startDate)) : ''})
{loadingBalances ? (
) : leaveBalances.length > 0 ? (
Type
Allocated
Used
Balance
{leaveBalances.map((balance) => (
{balance.leaveType}
{balance.allocated}
{balance.used}
{balance.pending}
))}
) : (
No leave balance data available.
)}
Close
);
}
function CustomLeaveStatusUpdateDialog({
leave,
action,
isOpen,
onClose,
onSubmit,
employeeName,
approverId,
isSubmitting,
leaveMasters,
getLeaveTypeName,
}: {
leave: Leave | null;
action: "approve" | "reject";
isOpen: boolean;
onClose: () => void;
onSubmit: (data: LeaveStatusUpdateRequest) => Promise;
employeeName?: string;
approverId: number;
isSubmitting: boolean;
leaveMasters: LeaveMaster[];
getLeaveTypeName: (id?: number) => string;
}) {
const form = useForm({
resolver: zodResolver(statusUpdateSchema),
defaultValues: {
comments: "",
},
});
if (!leave) return null;
const handleSubmit = async (values: StatusUpdateFormValues) => {
try {
const updateData: LeaveStatusUpdateRequest = {
id: leave.id,
status: action === "approve" ? "Approved" : "Rejected",
approvedById: approverId,
actionDate: format(new Date(), "yyyy-MM-dd'T'HH:mm:ss"),
comments: values.comments,
};
await onSubmit(updateData);
form.reset();
onClose();
} catch (error) {
console.error(`Error ${action} leave request:`, error);
toast.error(`Failed to ${action} leave request`);
}
};
// Only treat as Comp Off Request if compOff flag is explicitly true
// If leave type is Compensatory but compOff is false, it's a regular leave request
const isCompOff = leave.compOff === true;
const requestType = isCompOff ? "Comp Off Request" : "Leave Request";
return (
Employee:
{employeeName ? employeeName : `ID: ${leave.employeeId}`}
Date:
{format(new Date(leave.startDate), "PPP")} to {format(new Date(leave.endDate), "PPP")}
Type:
{leave.leaveMasterId ? getLeaveTypeName(leave.leaveMasterId) : leave.type}
);
}
const LeavesAdminPage: React.FC = () => {
const { userData } = useAuth();
const [leaves, setLeaves] = useState([]);
const [filteredLeaves, setFilteredLeaves] = useState([]);
const [employees, setEmployees] = useState([]);
const [teamMembers, setTeamMembers] = useState([]);
const [leaveMasters, setLeaveMasters] = useState([]);
const [stats, setStats] = useState({
total: 0,
pending: 0,
approved: 0,
rejected: 0,
cancelled: 0,
});
// Dialog states
const [selectedLeave, setSelectedLeave] = useState(null);
const [selectedEmployee, setSelectedEmployee] = useState(null);
const [showLeaveDetailDialog, setShowLeaveDetailDialog] = useState(false);
const [showApproveDialog, setShowApproveDialog] = useState(false);
const [showRejectDialog, setShowRejectDialog] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
// Filter states
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("Pending");
const [typeFilter, setTypeFilter] = useState("All");
const [employeeFilter, setEmployeeFilter] = useState("All");
const [yearFilter, setYearFilter] = useState(getYear(new Date()));
const [viewMode, setViewMode] = useState<"card" | "table">("card");
const [apiError, setApiError] = useState(null);
// Monthly report states
const currentDate = new Date();
const [selectedMonth, setSelectedMonth] = useState(getMonth(currentDate));
const [selectedReportYear, setSelectedReportYear] = useState(getYear(currentDate));
const [reportData, setReportData] = useState([]);
const [isExporting, setIsExporting] = useState(false);
const months = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
// Date range filter state
const [dateRange, setDateRange] = useState<{ from: Date | null; to: Date | null }>({ from: null, to: null });
const [showFromCalendar, setShowFromCalendar] = useState(false);
const [showToCalendar, setShowToCalendar] = useState(false);
const fromDateRef = useRef(null);
const toDateRef = useRef(null);
// Handle click outside to close calendars
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (fromDateRef.current && !fromDateRef.current.contains(event.target as Node)) {
setShowFromCalendar(false);
}
if (toDateRef.current && !toDateRef.current.contains(event.target as Node)) {
setShowToCalendar(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
// Get array of years for filtering (current year and 2 years back)
const currentYear = getYear(new Date());
const years = [currentYear, currentYear - 1, currentYear - 2];
const [leaveMastersLoading, setLeaveMastersLoading] = useState(false);
console.log(userData?.roleName);
const isAdmin = userData?.roleName === 'Admin' || userData?.roleName === 'CEO' || userData?.roleName === 'COO' || userData?.roleName === 'Senior Project Manager' || userData?.roleName === 'HR' || userData?.roleName === 'Sr. HR Manager';
const isProjectManager = userData?.roleName === 'Project Manager';
// Fetch leave masters (leave types)
useEffect(() => {
const fetchLeaveMasters = async () => {
try {
const masters = await leaveMasterApi.getAll();
setLeaveMasters(masters);
} catch (error) {
console.error("Error fetching leave masters:", error);
}
};
fetchLeaveMasters();
}, []);
// Fetch all employees
useEffect(() => {
const fetchEmployees = async () => {
try {
setIsLoading(true);
let employeeData: Employee[] = [];
if (isAdmin) {
// Admin can see all employees
employeeData = await employeeApi.getAll();
} else if (isProjectManager && userData?.employeeId) {
// Project Manager can only see team members
employeeData = await employeeApi.getByManagerId(userData.employeeId);
} else if (userData?.employeeId) {
// Other roles can only see themselves
const response = await employeeApi.getById(userData.employeeId);
if (response.success && response.data) {
employeeData = [response.data];
}
}
setEmployees(employeeData);
setTeamMembers(employeeData);
} catch (error) {
console.error("Error fetching employees:", error);
toast.error("Failed to fetch employee data");
}
};
fetchEmployees();
}, [userData, isAdmin, toast]);
// Fetch leaves based on the employees
useEffect(() => {
const fetchLeaves = async () => {
try {
setApiError(null);
setIsLoading(true);
if (employees.length === 0) {
console.log('No employees to fetch leaves for');
setIsLoading(false);
return;
}
// Get all leave records
let allLeaves: Leave[] = [];
if (isAdmin) {
// Admin can see all leaves
console.log('Fetching all leave records for admin');
allLeaves = await leaveApi.getAll();
} else {
// For project managers or other roles, only fetch leaves for the employees they have access to
console.log(`Fetching leaves for ${employees.length} team members`);
// Create an array of promises to fetch leaves for each employee
const promises = employees.map(emp => {
console.log(`Fetching leaves for employee ID: ${emp.id}`);
return leaveApi.getByEmployeeId(emp.id);
});
try {
const results = await Promise.all(promises);
// Combine all leaves into one array
allLeaves = results.flat();
console.log(`Successfully fetched ${allLeaves.length} leaves for team members`);
} catch (error) {
console.error("Error fetching leaves for team members:", error);
toast.error("Error fetching some team member leave records");
}
}
if (allLeaves.length === 0) {
console.warn('No leaves fetched - either no leaves exist or API connection issue');
}
// Enrich leaves with employee names
const leavesWithEmployee = allLeaves.map((leave) => {
let employeeName = "Unknown";
const employee = employees.find(emp => emp.id === leave.employeeId);
if (employee) {
// Format name properly with a single space
employeeName = `${employee.firstName || ""} ${employee.lastName || ""}`.trim();
}
return {
...leave,
employeeName
};
});
setLeaves(leavesWithEmployee);
// Calculate stats
const total = allLeaves.length;
const pending = allLeaves.filter(l => l.status === "Pending").length;
const approved = allLeaves.filter(l => l.status === "Approved").length;
const rejected = allLeaves.filter(l => l.status === "Rejected").length;
const cancelled = allLeaves.filter(l => l.status === "Cancelled").length;
setStats({
total,
pending,
approved,
rejected,
cancelled
});
} catch (error) {
console.error("Error fetching leaves:", error);
setApiError("Failed to connect to leaves API. Please check network connection and API availability.");
toast.error("Failed to fetch leave records");
} finally {
setIsLoading(false);
}
};
if (employees.length > 0) {
fetchLeaves();
} else if (!isLoading && employees.length === 0) {
setIsLoading(false);
}
}, [employees, isAdmin, toast]);
// Get leave type name by ID
const getLeaveTypeName = (id?: number): string => {
if (!id) return "General";
const found = leaveMasters.find(m => m.id === id);
return found ? found.leaveType : "Unknown";
};
// Create a mapping to get leave master ID from type string
const getLeaveTypeId = (type: string): number | undefined => {
const master = leaveMasters.find(m =>
m.leaveType.toLowerCase() === type.toLowerCase()
);
return master?.id;
};
// Helper function to get color based on leave type
const getLeaveTypeColor = (type: string): string => {
switch (type.toLowerCase()) {
case "annual":
return "bg-blue-500 hover:bg-blue-600";
case "sick":
return "bg-red-500 hover:bg-red-600";
case "personal":
return "bg-green-500 hover:bg-green-600";
case "maternity":
return "bg-purple-500 hover:bg-purple-600";
case "paternity":
return "bg-indigo-500 hover:bg-indigo-600";
case "unpaid":
return "bg-gray-500 hover:bg-gray-600";
case "compensatory":
return "bg-orange-500 hover:bg-orange-600";
case "bereavement":
return "bg-slate-500 hover:bg-slate-600";
case "half day":
return "bg-amber-500 hover:bg-amber-600";
case "casual":
return "bg-emerald-500 hover:bg-emerald-600";
default:
return "bg-neutral-500 hover:bg-neutral-600";
}
};
// Apply filters whenever filter states change
useEffect(() => {
if (!leaves.length) return;
let filtered = [...leaves];
// Filter by year
filtered = filtered.filter(leave => {
const leaveYear = getYear(new Date(leave.startDate));
return leaveYear === yearFilter;
});
// Filter by status
if (statusFilter !== "All") {
filtered = filtered.filter(leave => leave.status === statusFilter);
}
// Filter by type
if (typeFilter !== "All") {
filtered = filtered.filter(leave => {
// Check both type and leaveMasterId
if (leave.leaveMasterId) {
const master = leaveMasters.find(m => m.id === leave.leaveMasterId);
return master?.leaveType === typeFilter;
} else {
return leave.type === typeFilter;
}
});
}
// Filter by employee
if (employeeFilter !== "All") {
filtered = filtered.filter(leave => leave.employeeId === employeeFilter);
}
// Filter by search term (reason or employee name)
if (searchTerm) {
filtered = filtered.filter(leave =>
leave.reason.toLowerCase().includes(searchTerm.toLowerCase()) ||
leave.employeeName?.toLowerCase().includes(searchTerm.toLowerCase())
);
}
// Apply date range filter
if (dateRange.from && dateRange.to) {
filtered = filtered.filter(leave => {
const leaveStartDate = parseISO(leave.startDate);
const leaveEndDate = parseISO(leave.endDate);
const fromDate = dateRange.from!;
const toDate = dateRange.to!;
// Set the time of toDate to the end of the day for inclusivity
const toDateEndOfDay = new Date(toDate);
toDateEndOfDay.setHours(23, 59, 59, 999);
// Check if the leave overlaps with the selected range
return (
(leaveStartDate <= toDateEndOfDay && leaveEndDate >= fromDate) // Leave overlaps with the range
);
});
} else if (dateRange.from) {
filtered = filtered.filter(leave => {
const leaveStartDate = parseISO(leave.startDate);
return leaveStartDate >= dateRange.from!;
});
} else if (dateRange.to) {
filtered = filtered.filter(leave => {
const leaveEndDate = parseISO(leave.endDate);
const toDate = dateRange.to!;
// Set the time of toDate to the end of the day for inclusivity
const toDateEndOfDay = new Date(toDate);
toDateEndOfDay.setHours(23, 59, 59, 999);
return leaveEndDate <= toDateEndOfDay;
});
}
setFilteredLeaves(filtered);
}, [leaves, searchTerm, statusFilter, typeFilter, employeeFilter, yearFilter, dateRange]);
const handleViewLeaveDetails = (leave: Leave) => {
setSelectedLeave(leave);
setSelectedEmployee(employees.find(emp => emp.id === leave.employeeId) || null);
setShowLeaveDetailDialog(true);
};
const handleApproveLeave = (leave: Leave) => {
setSelectedLeave(leave);
setShowApproveDialog(true);
};
const handleRejectLeave = (leave: Leave) => {
setSelectedLeave(leave);
setShowRejectDialog(true);
};
const handleCancelLeave = async (leave: Leave) => {
if (!userData?.employeeId || !leave) return;
if (window.confirm("Are you sure you want to cancel this approved leave?")) {
try {
const updateData: LeaveStatusUpdateRequest = {
id: leave.id,
status: "Cancelled",
approvedById: userData.employeeId,
actionDate: format(new Date(), "yyyy-MM-dd'T'HH:mm:ss"),
comments: "Cancelled by admin",
};
await leaveApi.updateStatus(updateData);
// Refresh leaves list
setLeaves(prev => prev.map(l => l.id === leave.id ? { ...l, status: "Cancelled" } : l));
// Update stats
setStats(prev => ({
...prev,
approved: prev.approved - (leave.status === "Approved" ? 1 : 0),
pending: prev.pending - (leave.status === "Pending" ? 1 : 0),
cancelled: prev.cancelled + 1
}));
toast.success("Leave request cancelled successfully");
} catch (error) {
console.error("Error cancelling leave request:", error);
toast.error("Failed to cancel leave request");
}
}
};
const handleStatusUpdate = async (data: LeaveStatusUpdateRequest) => {
setIsSubmitting(true);
try {
await leaveApi.updateStatus(data);
setLeaves((prev) => {
const updated = prev.map((leave) => {
if (leave.id !== data.id) return leave;
return {
...leave,
status: data.status,
approvedById: data.approvedById,
actionDate: data.actionDate,
comments: data.comments,
};
});
// Update stats based on the updated list
const total = updated.length;
const pending = updated.filter(l => l.status === "Pending").length;
const approved = updated.filter(l => l.status === "Approved").length;
const rejected = updated.filter(l => l.status === "Rejected").length;
const cancelled = updated.filter(l => l.status === "Cancelled").length;
setStats({ total, pending, approved, rejected, cancelled });
return updated;
});
// Close dialogs
setShowApproveDialog(false);
setShowRejectDialog(false);
setSelectedLeave(null);
// Show success message
toast.success(`Leave request ${data.status.toLowerCase()}`);
} catch (error) {
console.error("Error updating leave status:", error);
toast.error("Failed to update leave status");
} finally {
setIsSubmitting(false);
}
};
const resetFilters = () => {
setSearchTerm("");
setStatusFilter("Pending");
setTypeFilter("All");
setEmployeeFilter("All");
setYearFilter(currentYear);
setDateRange({ from: null, to: null });
};
const exportToCSV = () => {
if (!filteredLeaves.length) return;
// Create CSV content
const headers = ['ID', 'Employee', 'Type', 'Start Date', 'End Date', 'Status', 'Applied On', 'Reason'];
const rows = filteredLeaves.map(leave => [
leave.id,
leave.employeeName || `Employee ID: ${leave.employeeId}`,
leave.type,
format(new Date(leave.startDate), 'yyyy-MM-dd'),
format(new Date(leave.endDate), 'yyyy-MM-dd'),
leave.status,
leave.appliedOn ? format(new Date(leave.appliedOn), 'yyyy-MM-dd') : '',
leave.reason.replace(/,/g, ';') // Replace commas to avoid CSV issues
]);
const csvContent = [
headers.join(','),
...rows.map(row => row.join(','))
].join('\n');
// Create and download file
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `leaves-report-${format(new Date(), 'yyyy-MM-dd')}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const selectedLeaveEmployeeName = selectedLeave && employees.find(e => e.id === selectedLeave.employeeId)
? `${employees.find(e => e.id === selectedLeave.employeeId)?.firstName || ""} ${employees.find(e => e.id === selectedLeave.employeeId)?.lastName || ""}`.trim()
: undefined;
return (
{/* Page title */}
{isAdmin ? "Leave Management" : "Team Leave Requests"}
{isAdmin
? "View and manage leave requests from all employees"
: isProjectManager
? "View and manage leave requests from your team members"
: "View and manage your leave requests"
}
{isAdmin ? (
Admin access to all leave requests
) : isProjectManager ? (
Viewing team members' leave requests
) : (
Restricted view
)}
{isProjectManager && (
Team View
You are viewing leave requests for your team members only.
{employees.length === 0 ? (
No team members found. Please contact an administrator if this is incorrect.
) : (
Managing {employees.length} team member{employees.length !== 1 ? 's' : ''}.
)}
)}
All Leaves
Calendar
Monthly Report
setViewMode(viewMode === "table" ? "card" : "table")}
>
{viewMode === "table" ? "Card View" : "Table View"}
Export
Leave Requests
Manage employee leave requests
{/* Filter section */}
Filters
{(searchTerm || statusFilter !== "Pending" || typeFilter !== "All" || employeeFilter !== "All" || yearFilter !== currentYear || dateRange.from || dateRange.to) && (
Reset All
)}
Status
setStatusFilter(value as LeaveStatus | "All")}
options={[
{ value: "All", label: "All Statuses" },
{ value: "Pending", label: "Pending", color: "bg-yellow-500 hover:bg-yellow-600" },
{ value: "Approved", label: "Approved", color: "bg-green-500 hover:bg-green-600" },
{ value: "Rejected", label: "Rejected", color: "bg-red-500 hover:bg-red-600" },
{ value: "Cancelled", label: "Cancelled", color: "bg-gray-500 hover:bg-gray-600" }
]}
placeholder="Status"
showBadge={true}
/>
Type
setTypeFilter(value as LeaveType | "All")}
options={
leaveMastersLoading
? [{ value: "All", label: "Loading..." }]
: [
{ value: "All", label: "All Types" },
...leaveMasters.map(master => ({
value: master.leaveType,
label: master.leaveType,
color: getLeaveTypeColor(master.leaveType)
}))
]
}
placeholder="Type"
showBadge={true}
disabled={leaveMastersLoading}
/>
Employee
setEmployeeFilter(value === "All" ? "All" : parseInt(value))}
options={[
{ value: "All", label: "All Employees" },
...employees.map(employee => ({
value: employee.id.toString(),
label: `${employee.firstName} ${employee.lastName}`
}))
]}
placeholder="Filter by employee"
searchable={true}
/>
Year
setYearFilter(parseInt(value))}
options={years.map(year => ({
value: year.toString(),
label: year.toString()
}))}
placeholder="Year"
/>
Date Range
{
setShowToCalendar(false);
setShowFromCalendar(!showFromCalendar);
}}
>
{dateRange.from ? format(dateRange.from, "PPP") : "From"}
{showFromCalendar && (
{
setDateRange(prev => ({ ...prev, from: date }));
setShowFromCalendar(false);
}}
initialFocus
/>
)}
to
{
setShowFromCalendar(false);
setShowToCalendar(!showToCalendar);
}}
>
{dateRange.to ? format(dateRange.to, "PPP") : "To"}
{showToCalendar && (
{
setDateRange(prev => ({ ...prev, to: date }));
setShowToCalendar(false);
}}
disabled={(date) => {
if (dateRange.from) {
const normalizedFromDate = new Date(dateRange.from);
normalizedFromDate.setHours(0, 0, 0, 0);
const normalizedDate = new Date(date);
normalizedDate.setHours(0, 0, 0, 0);
return normalizedDate < normalizedFromDate;
}
return false;
}}
initialFocus
/>
)}
{(dateRange.from || dateRange.to) && (
setDateRange({ from: null, to: null })}
className="p-1 h-9"
>
)}
{renderLeavesList(filteredLeaves)}
{/* Dialogs */}
e.id === selectedLeave.approvedById) || null : null}
isOpen={showLeaveDetailDialog}
onClose={() => setShowLeaveDetailDialog(false)}
onDownloadDocument={leave => {
if (leave.documents) {
window.open(leave.documents, '_blank');
}
}}
leaveMasters={leaveMasters}
getLeaveTypeName={getLeaveTypeName}
/>
setShowApproveDialog(false)}
onSubmit={handleStatusUpdate}
employeeName={selectedLeaveEmployeeName}
approverId={userData?.employeeId || 0}
isSubmitting={isSubmitting}
leaveMasters={leaveMasters}
getLeaveTypeName={getLeaveTypeName}
/>
setShowRejectDialog(false)}
onSubmit={handleStatusUpdate}
employeeName={selectedLeaveEmployeeName}
approverId={userData?.employeeId || 0}
isSubmitting={isSubmitting}
leaveMasters={leaveMasters}
getLeaveTypeName={getLeaveTypeName}
/>
);
function renderLeavesList(leavesToRender: LeaveWithEmployee[]) {
if (isLoading) {
return (
);
}
if (apiError) {
return (
API Connection Error
{apiError}
window.location.reload()}
>
Retry Connection
);
}
if (leavesToRender.length === 0) {
return (
No leaves found
Try adjusting your filters
);
}
if (viewMode === "card") {
return (
{leavesToRender.map(leave => (
))}
);
}
return (
List of leave requests
Employee
Type
Duration
Status
Applied On
Actions
{leavesToRender.map(leave => (
{leave.employeeName || `Employee ID: ${leave.employeeId}`}
{leave.leaveMasterId ? getLeaveTypeName(leave.leaveMasterId) : leave.type}
{format(new Date(leave.startDate), "MMM d, yyyy")} -
{format(new Date(leave.endDate), "MMM d, yyyy")}
{leave.status}
{leave.appliedOn ? format(new Date(leave.appliedOn), "MMM d, yyyy") : "N/A"}
handleViewLeaveDetails(leave)}
>
View
{leave.status === "Pending" && (
<>
handleApproveLeave(leave)}
>
Approve
handleRejectLeave(leave)}
>
Reject
>
)}
{leave.status === "Approved" && (
handleCancelLeave(leave)}
>
Cancel Approved
)}
))}
);
}
};
export default LeavesAdminPage;