Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from 'react'; | |
| import { | |
| Box, | |
| Card, | |
| CardContent, | |
| Typography, | |
| Grid, | |
| Divider, | |
| Chip, | |
| Stack, | |
| Pagination, | |
| CircularProgress, | |
| Select, | |
| MenuItem, | |
| FormControl, | |
| InputLabel, | |
| TextField, | |
| Button, | |
| IconButton, | |
| SelectChangeEvent, | |
| Alert, | |
| } from '@mui/material'; | |
| import FilterListIcon from '@mui/icons-material/FilterList'; | |
| import VisibilityIcon from '@mui/icons-material/Visibility'; | |
| import { subYears, getYear } from 'date-fns'; | |
| import { Leave, LeaveStatus } from '../../types/Leave'; | |
| import LeaveCard from './LeaveCard'; | |
| import LeaveDetail from './LeaveDetail'; | |
| import LeaveStatusUpdate from './LeaveStatusUpdate'; | |
| // Extend the Leave type to include additional properties needed | |
| interface ExtendedLeave extends Leave { | |
| employeeName?: string; | |
| leaveMasterId?: number; | |
| appliedOn?: string; // To match with imported type | |
| } | |
| export interface LeaveListProps { | |
| leaves: ExtendedLeave[]; | |
| isLoading?: boolean; | |
| error?: string | null; | |
| onStatusUpdate?: (leaveId: string, status: LeaveStatus, comment?: string) => Promise<void>; | |
| canApprove?: boolean; | |
| title?: string; | |
| showFilters?: boolean; | |
| leaveTypes?: string[]; | |
| employees?: { id: string; firstName: string; lastName: string }[]; | |
| getLeaveTypeName?: (id?: number) => string; | |
| } | |
| const LeaveList: React.FC<LeaveListProps> = ({ | |
| leaves, | |
| isLoading = false, | |
| error = null, | |
| onStatusUpdate, | |
| canApprove = false, | |
| title = "Leave Requests", | |
| showFilters = true, | |
| leaveTypes = [], | |
| employees = [], | |
| getLeaveTypeName, | |
| }) => { | |
| const [filteredLeaves, setFilteredLeaves] = useState<ExtendedLeave[]>(leaves); | |
| const [statusFilter, setStatusFilter] = useState<string>("All"); | |
| const [typeFilter, setTypeFilter] = useState<string>("All"); | |
| const [employeeFilter, setEmployeeFilter] = useState<string>("All"); | |
| const [yearFilter, setYearFilter] = useState<number>(getYear(new Date())); | |
| const [searchTerm, setSearchTerm] = useState<string>(""); | |
| const [displayFilters, setDisplayFilters] = useState<boolean>(false); | |
| const [page, setPage] = useState<number>(1); | |
| const [selectedLeave, setSelectedLeave] = useState<ExtendedLeave | null>(null); | |
| const [showDetailDialog, setShowDetailDialog] = useState<boolean>(false); | |
| const [showApproveDialog, setShowApproveDialog] = useState<boolean>(false); | |
| const [showRejectDialog, setShowRejectDialog] = useState<boolean>(false); | |
| const [isSubmitting, setIsSubmitting] = useState<boolean>(false); | |
| const itemsPerPage = 6; | |
| const years = Array.from({ length: 5 }, (_, i) => getYear(subYears(new Date(), i))); | |
| // Apply filters whenever filter values change | |
| useEffect(() => { | |
| if (!leaves) 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 not "All" | |
| if (statusFilter !== "All") { | |
| filtered = filtered.filter(leave => leave.status === statusFilter); | |
| } | |
| // Filter by type if not "All" | |
| if (typeFilter !== "All") { | |
| filtered = filtered.filter(leave => { | |
| if (leave.type) return leave.type === typeFilter; | |
| return false; | |
| }); | |
| } | |
| // Filter by employee if not "All" | |
| if (employeeFilter !== "All") { | |
| filtered = filtered.filter(leave => leave.employeeId === employeeFilter); | |
| } | |
| // Filter by search term | |
| if (searchTerm) { | |
| filtered = filtered.filter(leave => | |
| leave.reason.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| leave.employeeName?.toLowerCase().includes(searchTerm.toLowerCase()) | |
| ); | |
| } | |
| setFilteredLeaves(filtered); | |
| setPage(1); // Reset to first page whenever filters change | |
| }, [leaves, statusFilter, typeFilter, employeeFilter, yearFilter, searchTerm]); | |
| const handlePageChange = (event: React.ChangeEvent<unknown>, value: number) => { | |
| setPage(value); | |
| }; | |
| const handleStatusFilterChange = (event: SelectChangeEvent) => { | |
| setStatusFilter(event.target.value); | |
| }; | |
| const handleTypeFilterChange = (event: SelectChangeEvent) => { | |
| setTypeFilter(event.target.value); | |
| }; | |
| const handleEmployeeFilterChange = (event: SelectChangeEvent) => { | |
| setEmployeeFilter(event.target.value); | |
| }; | |
| const handleYearFilterChange = (event: SelectChangeEvent) => { | |
| setYearFilter(Number(event.target.value)); | |
| }; | |
| const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |
| setSearchTerm(event.target.value); | |
| }; | |
| const handleViewLeave = (leave: ExtendedLeave) => { | |
| setSelectedLeave(leave); | |
| setShowDetailDialog(true); | |
| }; | |
| const handleApproveLeave = (leave: ExtendedLeave) => { | |
| setSelectedLeave(leave); | |
| setShowApproveDialog(true); | |
| }; | |
| const handleRejectLeave = (leave: ExtendedLeave) => { | |
| setSelectedLeave(leave); | |
| setShowRejectDialog(true); | |
| }; | |
| const handleSubmitStatusUpdate = async (status: LeaveStatus, comment?: string) => { | |
| if (!selectedLeave || !onStatusUpdate) return; | |
| setIsSubmitting(true); | |
| try { | |
| await onStatusUpdate(selectedLeave.id, status, comment); | |
| // Close dialogs | |
| setShowApproveDialog(false); | |
| setShowRejectDialog(false); | |
| setSelectedLeave(null); | |
| } catch (error) { | |
| console.error("Error in status update:", error); | |
| } finally { | |
| setIsSubmitting(false); | |
| } | |
| }; | |
| const paginatedLeaves = filteredLeaves.slice( | |
| (page - 1) * itemsPerPage, | |
| page * itemsPerPage | |
| ); | |
| // Helper function to get leave type name if getLeaveTypeName is not provided | |
| const getLeaveTypeDisplay = (leave: ExtendedLeave): string => { | |
| if (getLeaveTypeName && leave.leaveMasterId) { | |
| return getLeaveTypeName(leave.leaveMasterId); | |
| } | |
| return leave.type; | |
| }; | |
| return ( | |
| <Card> | |
| <CardContent> | |
| <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> | |
| <Typography variant="h6" component="h2"> | |
| {title} {filteredLeaves.length > 0 && `(${filteredLeaves.length})`} | |
| </Typography> | |
| {showFilters && ( | |
| <IconButton | |
| onClick={() => setDisplayFilters(!displayFilters)} | |
| color={displayFilters ? "primary" : "default"} | |
| > | |
| <FilterListIcon /> | |
| </IconButton> | |
| )} | |
| </Box> | |
| {displayFilters && ( | |
| <Box sx={{ mb: 2 }}> | |
| <Grid container spacing={2}> | |
| <Grid item xs={12} sm={6} md={2}> | |
| <FormControl fullWidth size="small"> | |
| <InputLabel>Status</InputLabel> | |
| <Select | |
| value={statusFilter} | |
| label="Status" | |
| onChange={handleStatusFilterChange} | |
| > | |
| <MenuItem value="All">All</MenuItem> | |
| <MenuItem value="Pending">Pending</MenuItem> | |
| <MenuItem value="Approved">Approved</MenuItem> | |
| <MenuItem value="Rejected">Rejected</MenuItem> | |
| <MenuItem value="Cancelled">Cancelled</MenuItem> | |
| </Select> | |
| </FormControl> | |
| </Grid> | |
| <Grid item xs={12} sm={6} md={2}> | |
| <FormControl fullWidth size="small"> | |
| <InputLabel>Year</InputLabel> | |
| <Select | |
| value={yearFilter.toString()} | |
| label="Year" | |
| onChange={handleYearFilterChange} | |
| > | |
| {years.map(year => ( | |
| <MenuItem key={year} value={year}>{year}</MenuItem> | |
| ))} | |
| </Select> | |
| </FormControl> | |
| </Grid> | |
| {leaveTypes.length > 0 && ( | |
| <Grid item xs={12} sm={6} md={2}> | |
| <FormControl fullWidth size="small"> | |
| <InputLabel>Type</InputLabel> | |
| <Select | |
| value={typeFilter} | |
| label="Type" | |
| onChange={handleTypeFilterChange} | |
| > | |
| <MenuItem value="All">All</MenuItem> | |
| {leaveTypes.map(type => ( | |
| <MenuItem key={type} value={type}>{type}</MenuItem> | |
| ))} | |
| </Select> | |
| </FormControl> | |
| </Grid> | |
| )} | |
| {employees.length > 0 && ( | |
| <Grid item xs={12} sm={6} md={2}> | |
| <FormControl fullWidth size="small"> | |
| <InputLabel>Employee</InputLabel> | |
| <Select | |
| value={employeeFilter} | |
| label="Employee" | |
| onChange={handleEmployeeFilterChange} | |
| > | |
| <MenuItem value="All">All</MenuItem> | |
| {employees.map(emp => ( | |
| <MenuItem key={emp.id} value={emp.id}> | |
| {`${emp.firstName} ${emp.lastName}`.trim()} | |
| </MenuItem> | |
| ))} | |
| </Select> | |
| </FormControl> | |
| </Grid> | |
| )} | |
| <Grid item xs={12} sm={6} md={4}> | |
| <TextField | |
| fullWidth | |
| size="small" | |
| label="Search" | |
| variant="outlined" | |
| value={searchTerm} | |
| onChange={handleSearchChange} | |
| placeholder="Search by reason or employee name" | |
| /> | |
| </Grid> | |
| </Grid> | |
| </Box> | |
| )} | |
| {error && ( | |
| <Alert severity="error" sx={{ mb: 2 }}> | |
| {error} | |
| </Alert> | |
| )} | |
| {isLoading ? ( | |
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}> | |
| <CircularProgress /> | |
| </Box> | |
| ) : filteredLeaves.length === 0 ? ( | |
| <Box sx={{ p: 4, textAlign: 'center' }}> | |
| <Typography variant="body1" color="textSecondary"> | |
| No leave requests found | |
| </Typography> | |
| </Box> | |
| ) : ( | |
| <> | |
| <Grid container spacing={2}> | |
| {paginatedLeaves.map(leave => ( | |
| <Grid item xs={12} sm={6} md={4} key={leave.id}> | |
| <LeaveCard | |
| leave={leave} | |
| employeeName={leave.employeeName || 'Unknown'} | |
| leaveType={getLeaveTypeDisplay(leave)} | |
| onView={() => handleViewLeave(leave)} | |
| onApprove={canApprove && leave.status === 'Pending' ? () => handleApproveLeave(leave) : undefined} | |
| onReject={canApprove && leave.status === 'Pending' ? () => handleRejectLeave(leave) : undefined} | |
| /> | |
| </Grid> | |
| ))} | |
| </Grid> | |
| <Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}> | |
| <Pagination | |
| count={Math.ceil(filteredLeaves.length / itemsPerPage)} | |
| page={page} | |
| onChange={handlePageChange} | |
| color="primary" | |
| /> | |
| </Box> | |
| </> | |
| )} | |
| </CardContent> | |
| {selectedLeave && ( | |
| <> | |
| <LeaveDetail | |
| open={showDetailDialog} | |
| leave={selectedLeave} | |
| onClose={() => setShowDetailDialog(false)} | |
| /> | |
| {canApprove && ( | |
| <> | |
| <LeaveStatusUpdate | |
| open={showApproveDialog} | |
| leave={selectedLeave} | |
| onClose={() => setShowApproveDialog(false)} | |
| onSubmit={(comment) => handleSubmitStatusUpdate("Approved", comment)} | |
| isSubmitting={isSubmitting} | |
| title="Approve Leave Request" | |
| actionLabel="Approve" | |
| /> | |
| <LeaveStatusUpdate | |
| open={showRejectDialog} | |
| leave={selectedLeave} | |
| onClose={() => setShowRejectDialog(false)} | |
| onSubmit={(comment) => handleSubmitStatusUpdate("Rejected", comment)} | |
| isSubmitting={isSubmitting} | |
| title="Reject Leave Request" | |
| actionLabel="Reject" | |
| /> | |
| </> | |
| )} | |
| </> | |
| )} | |
| </Card> | |
| ); | |
| }; | |
| export default LeaveList; |