pmtool / src /pages /attendance.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
35.7 kB
import React, { useState, useEffect } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import {
CalendarIcon,
Clock,
Loader2,
Search,
UserIcon,
X,
} from "lucide-react";
import Layout from "@/components/layout/layout";
import { AttendanceRecord } from "@/types";
import { attendanceApi } from "@/services/attendanceApi";
import { employeeService } from "@/lib/api";
import { useAuth } from "@/lib/auth-context";
import { toast } from "sonner";
import { getConfettiOriginFromElement, triggerAttendanceConfetti } from "@/lib/confetti";
import ImportAttendanceDialog from "@/components/attendance/import-attendance-dialog";
import DownloadTemplateButton from "@/components/attendance/download-template-button";
import AttendanceParser from "@/components/attendance/attendance-parser";
import UploadAttendanceButton from "@/components/attendance/upload-attendance-button";
import { CustomSelect } from "@/components/ui/custom-select";
import { format } from "date-fns";
import { useIsMobile } from "@/hooks/use-mobile";
// Mobile Attendance Card Component
interface MobileAttendanceCardProps {
record: AttendanceRecord;
getEmployeeName: (id: number) => string;
formatTime: (time: string | null) => string;
getStatusBadge: (status: string) => JSX.Element;
}
const MobileAttendanceCard = ({
record,
getEmployeeName,
formatTime,
getStatusBadge,
}: MobileAttendanceCardProps) => {
return (
<Card className="w-full">
<CardContent className="p-4">
<div className="flex items-start justify-between mb-3">
<div>
<h3 className="font-medium">{getEmployeeName(record.employeeId)}</h3>
<p className="text-sm text-muted-foreground">
{format(parseAttendanceDate(record.date), "dd MMM yyyy")}
</p>
</div>
<div>{getStatusBadge(record.status)}</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5 text-green-600" />
<span>In Time:</span>
</div>
<span className="font-medium">
{record.inTime ? (
formatTime(record.inTime)
) : (
<span className="text-muted-foreground">Not recorded</span>
)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5 text-red-600" />
<span>Out Time:</span>
</div>
<span className="font-medium">
{record.outTime ? (
formatTime(record.outTime)
) : (
<span className="text-muted-foreground">Not recorded</span>
)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5 text-blue-600" />
<span>Total Time:</span>
</div>
<span className="font-medium">{getOfficeDurationLabel(record.inTime, record.outTime)}</span>
</div>
{record.comments && (
<div className="mt-3 pt-3 border-t text-sm text-muted-foreground">
{record.comments}
</div>
)}
</div>
</CardContent>
</Card>
);
};
const months = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
const years = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - 2 + i);
const parseAttendanceDate = (value: string) => {
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
if (match) {
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
return new Date(year, month - 1, day);
}
return new Date(value);
};
const parseTimeToMinutes = (value: string | null | undefined) => {
if (!value) return null;
const normalized = value.trim();
const match = /^(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(AM|PM|am|pm)?$/.exec(normalized);
if (!match) return null;
let hours = Number(match[1]);
const minutes = Number(match[2]);
const meridiem = match[4]?.toLowerCase();
if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null;
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
if (meridiem) {
if (hours < 1 || hours > 12) return null;
if (meridiem === "am") {
hours = hours === 12 ? 0 : hours;
} else {
hours = hours === 12 ? 12 : hours + 12;
}
}
return hours * 60 + minutes;
};
const formatDurationMinutes = (minutes: number) => {
if (!Number.isFinite(minutes) || minutes < 0) return "-";
const total = Math.floor(minutes);
const h = Math.floor(total / 60);
const m = total % 60;
if (h > 0 && m > 0) return `${h}h ${m}m`;
if (h > 0) return `${h}h`;
return `${m}m`;
};
const getOfficeDurationLabel = (inTime: string | null | undefined, outTime: string | null | undefined) => {
const inMinutes = parseTimeToMinutes(inTime);
const outMinutes = parseTimeToMinutes(outTime);
if (inMinutes == null || outMinutes == null) return "-";
let diff = outMinutes - inMinutes;
if (diff < 0) diff += 24 * 60;
return formatDurationMinutes(diff);
};
const Attendance = () => {
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const { userData } = useAuth();
const checkInBtnRef = React.useRef<HTMLButtonElement>(null);
const checkOutBtnRef = React.useRef<HTMLButtonElement>(null);
const [searchQuery, setSearchQuery] = useState("");
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth().toString());
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear().toString());
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isParsingFile, setIsParsingFile] = useState(false);
const [employeeFilter, setEmployeeFilter] = useState<string>("all");
const [statusFilter, setStatusFilter] = useState<string>("all");
const [viewMode, setViewMode] = useState<'table' | 'cards' | 'matrix'>('table');
const employeeId = userData?.employeeId;
const isAttendanceAdmin =
userData?.roleName === "Admin" ||
userData?.roleName === "CEO" ||
userData?.roleName === "COO" ||
userData?.roleName === "HR" ||
userData?.roleName === "HR Manager";
// Auto-switch to cards view on mobile
useEffect(() => {
if (isMobile && viewMode !== 'cards') {
setViewMode('cards');
}
}, [isMobile, viewMode]);
// Fetch attendance data
const { data: attendanceData, isLoading, refetch } = useQuery({
queryKey: ["attendance", employeeId ?? "all", selectedMonth, selectedYear],
queryFn: async () => {
const month = parseInt(selectedMonth);
const year = parseInt(selectedYear);
if (!isAttendanceAdmin && employeeId) {
const all = await attendanceApi.getByEmployeeId(employeeId);
return all.filter(r => {
const d = parseAttendanceDate(r.date);
return d.getFullYear() === year && d.getMonth() === month;
});
}
return attendanceApi.getByMonth(month, year);
},
enabled: !isParsingFile,
});
// Fetch employees
const { data: employees, isLoading: employeesLoading } = useQuery({
queryKey: ["employees"],
queryFn: () => employeeService.getAll(),
enabled: isAttendanceAdmin,
});
const { data: todayAttendance, isLoading: todayAttendanceLoading } = useQuery({
queryKey: ["attendance-today", employeeId ?? 0],
queryFn: () => {
if (!employeeId) return Promise.resolve(null);
return attendanceApi.getToday(employeeId);
},
enabled: !!employeeId,
});
const checkInMutation = useMutation({
mutationFn: async () => {
if (!employeeId) throw new Error("Employee ID not found");
return attendanceApi.checkIn(employeeId);
},
onSuccess: async () => {
toast.success("Checked in successfully");
triggerAttendanceConfetti("checkIn", getConfettiOriginFromElement(checkInBtnRef.current));
await queryClient.invalidateQueries({ queryKey: ["attendance-today", employeeId ?? 0] });
await queryClient.invalidateQueries({ queryKey: ["attendance", employeeId ?? "all", selectedMonth, selectedYear] });
},
onError: (error: any) => {
toast.error(error?.response?.data || error?.message || "Failed to check in");
}
});
const checkOutMutation = useMutation({
mutationFn: async () => {
if (!employeeId) throw new Error("Employee ID not found");
return attendanceApi.checkOut(employeeId);
},
onSuccess: async () => {
toast.success("Checked out successfully");
triggerAttendanceConfetti("checkOut", getConfettiOriginFromElement(checkOutBtnRef.current));
await queryClient.invalidateQueries({ queryKey: ["attendance-today", employeeId ?? 0] });
await queryClient.invalidateQueries({ queryKey: ["attendance", employeeId ?? "all", selectedMonth, selectedYear] });
},
onError: (error: any) => {
toast.error(error?.response?.data || error?.message || "Failed to check out");
}
});
// Handle file upload
const handleFileUpload = (file: File) => {
if (file) {
setSelectedFile(file);
setIsParsingFile(true);
}
};
// Cancel file parsing
const handleCancelParsing = () => {
setSelectedFile(null);
setIsParsingFile(false);
};
// Handle import complete
const handleImportComplete = () => {
setSelectedFile(null);
setIsParsingFile(false);
refetch();
toast.success("Attendance data imported successfully");
};
// Filter attendance records
const filteredAttendance = React.useMemo(() => {
if (!attendanceData) return [];
return attendanceData.filter(record => {
// Filter by employee
if (employeeFilter !== "all" && record.employeeId !== parseInt(employeeFilter)) {
return false;
}
// Filter by status
if (statusFilter !== "all" && record.status !== statusFilter) {
return false;
}
// Filter by search query (if any)
if (searchQuery) {
const employee = employees?.find(emp => emp.id === record.employeeId);
const employeeName = employee
? `${employee.firstName} ${employee.lastName}`
: `Employee ${record.employeeId}`;
return employeeName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.date.includes(searchQuery);
}
return true;
});
}, [attendanceData, employeeFilter, statusFilter, searchQuery, employees]);
// Get employee name by ID
const getEmployeeName = (employeeId: number) => {
if (!isAttendanceAdmin) {
return userData ? `${userData.firstName} ${userData.lastName}` : `Employee ${employeeId}`;
}
const employee = employees?.find(emp => emp.id === employeeId);
return employee ? `${employee.firstName} ${employee.lastName}` : `Employee ${employeeId}`;
};
// Format time for display
const formatTime = (time: string | null) => {
if (!time) return "-";
return time;
};
// Get status badge
const getStatusBadge = (status: string) => {
const variants: Record<string, string> = {
"Present": "bg-green-100 text-green-700 border-green-200",
"Absent": "bg-red-100 text-red-700 border-red-200",
"Half Day": "bg-yellow-100 text-yellow-700 border-yellow-200",
"Leave": "bg-blue-100 text-blue-700 border-blue-200"
};
return (
<Badge variant="outline" className={variants[status] || ""}>
{status}
</Badge>
);
};
const matrixYear = parseInt(selectedYear);
const matrixMonth = parseInt(selectedMonth);
const daysInSelectedMonth = new Date(matrixYear, matrixMonth + 1, 0).getDate();
const matrixDays = React.useMemo(
() => Array.from({ length: daysInSelectedMonth }, (_, i) => i + 1),
[daysInSelectedMonth]
);
const recordByEmployeeDay = React.useMemo(() => {
const map = new Map<string, AttendanceRecord>();
for (const record of attendanceData ?? []) {
const d = parseAttendanceDate(record.date);
if (d.getFullYear() !== matrixYear || d.getMonth() !== matrixMonth) continue;
const day = d.getDate();
map.set(`${record.employeeId}-${day}`, record);
}
return map;
}, [attendanceData, matrixYear, matrixMonth]);
const matrixEmployees = React.useMemo(() => {
if (!employees) return [];
let list = employees;
if (employeeFilter !== "all") {
const selectedId = parseInt(employeeFilter);
list = list.filter(e => e.id === selectedId);
}
if (searchQuery) {
const q = searchQuery.toLowerCase();
list = list.filter(e => `${e.firstName} ${e.lastName}`.toLowerCase().includes(q));
}
if (statusFilter !== "all") {
const allowed = new Set<number>();
for (const record of attendanceData ?? []) {
if (record.status === statusFilter) {
allowed.add(record.employeeId);
}
}
list = list.filter(e => allowed.has(e.id));
}
return list;
}, [employees, employeeFilter, searchQuery, statusFilter, attendanceData]);
const getMatrixCellClass = (status: AttendanceRecord["status"]) => {
const base = "inline-block h-2.5 w-2.5 rounded-full";
if (status === "Present") return `${base} bg-green-500`;
if (status === "Absent") return `${base} bg-red-500`;
if (status === "Half Day") return `${base} bg-yellow-500`;
return `${base} bg-blue-500`;
};
const showEmployeeFilter = isAttendanceAdmin && !employeesLoading && !!employees && employees.length > 0;
const filterGridCols = showEmployeeFilter ? "lg:grid-cols-12" : "lg:grid-cols-10";
return (
<Layout>
<div className="space-y-4 sm:space-y-6">
<div className="flex flex-col sm:flex-row justify-between gap-4">
<div>
<h1 className="text-xl sm:text-2xl font-bold tracking-tight">Attendance Management</h1>
<p className="text-sm sm:text-base text-muted-foreground">
Track and manage employee attendance records
</p>
</div>
{isAttendanceAdmin && (
<div className="flex flex-wrap gap-2">
<DownloadTemplateButton />
<ImportAttendanceDialog onImportComplete={refetch} />
<UploadAttendanceButton onFileSelected={handleFileUpload} />
</div>
)}
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base">My Office Timing</CardTitle>
<CardDescription className="text-xs">
{format(new Date(), "dd MMM yyyy")}
</CardDescription>
</CardHeader>
<CardContent className="pt-0">
{todayAttendanceLoading ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
Loading today's status...
</div>
) : (
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-3">
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
<div>
<div className="text-[11px] text-muted-foreground">In Time</div>
<div className="text-sm font-medium">{todayAttendance?.inTime || "-"}</div>
</div>
<div>
<div className="text-[11px] text-muted-foreground">Out Time</div>
<div className="text-sm font-medium">{todayAttendance?.outTime || "-"}</div>
</div>
<div className="col-span-2 sm:col-span-1">
<div className="text-[11px] text-muted-foreground">Status</div>
<div className="text-sm font-medium">
{todayAttendance?.outTime
? "Checked Out"
: todayAttendance?.inTime
? "Checked In"
: "Not Checked In"}
</div>
</div>
</div>
<div className="flex flex-wrap gap-2">
<Button
ref={checkInBtnRef}
size="sm"
onClick={() => checkInMutation.mutate()}
disabled={
!employeeId ||
checkInMutation.isPending ||
!!todayAttendance?.inTime
}
className="h-8 px-3 text-xs"
>
{checkInMutation.isPending ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
Checking In...
</>
) : (
"Check In"
)}
</Button>
<Button
ref={checkOutBtnRef}
variant="outline"
size="sm"
onClick={() => checkOutMutation.mutate()}
disabled={
!employeeId ||
checkOutMutation.isPending ||
!todayAttendance?.inTime ||
!!todayAttendance?.outTime
}
className="h-8 px-3 text-xs"
>
{checkOutMutation.isPending ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
Checking Out...
</>
) : (
"Check Out"
)}
</Button>
</div>
</div>
)}
</CardContent>
</Card>
{isParsingFile && selectedFile && (
<AttendanceParser
file={selectedFile}
onComplete={handleImportComplete}
onCancel={handleCancelParsing}
/>
)}
{!isParsingFile && (
<Card>
<CardHeader>
<CardTitle>Attendance Records</CardTitle>
<CardDescription>
View and filter employee attendance records
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className={`grid grid-cols-1 sm:grid-cols-2 ${filterGridCols} gap-2 items-end`}>
<div className="lg:col-span-4 flex items-center gap-2">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="flex-1 h-8 text-xs"
/>
</div>
<div className="lg:col-span-2">
<label className="sr-only">Month</label>
<CustomSelect
value={selectedMonth}
onChange={setSelectedMonth}
placeholder="Month"
options={months.map((month, index) => ({ value: index.toString(), label: month }))}
triggerClassName="h-8 px-2 py-1 text-xs"
/>
</div>
<div className="lg:col-span-2">
<label className="sr-only">Year</label>
<CustomSelect
value={selectedYear}
onChange={setSelectedYear}
placeholder="Year"
options={years.map((year) => ({ value: year.toString(), label: year.toString() }))}
triggerClassName="h-8 px-2 py-1 text-xs"
/>
</div>
<div className="lg:col-span-2">
<label className="sr-only">Status</label>
<CustomSelect
value={statusFilter}
onChange={setStatusFilter}
placeholder="Status"
options={[
{ value: "all", label: "All Statuses" },
{ value: "Present", label: "Present" },
{ value: "Absent", label: "Absent" },
{ value: "Half Day", label: "Half Day" },
{ value: "Leave", label: "Leave" },
]}
triggerClassName="h-8 px-2 py-1 text-xs"
/>
</div>
{showEmployeeFilter && (
<div className="lg:col-span-2">
<label className="sr-only">Employee</label>
<CustomSelect
value={employeeFilter}
onChange={setEmployeeFilter}
placeholder="Employee"
options={[
{ value: "all", label: "All Employees" },
...employees!.map((employee) => ({
value: employee.id.toString(),
label: `${employee.firstName} ${employee.lastName}`,
})),
]}
triggerClassName="h-8 px-2 py-1 text-xs"
/>
</div>
)}
</div>
{!isMobile && (
<div className="flex justify-end">
<div className="flex items-center gap-2 bg-gray-100 rounded-lg p-1">
<Button
variant={viewMode === 'table' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('table')}
className="h-8 px-3"
>
Table
</Button>
<Button
variant={viewMode === 'cards' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('cards')}
className="h-8 px-3"
>
Cards
</Button>
{isAttendanceAdmin && (
<Button
variant={viewMode === 'matrix' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('matrix')}
className="h-8 px-3"
>
Matrix
</Button>
)}
</div>
</div>
)}
<div className="rounded-md border overflow-hidden">
{isLoading ? (
<div className="flex justify-center items-center h-24 sm:h-32">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
<span className="ml-2">Loading attendance data...</span>
</div>
) : (!isMobile && viewMode === 'matrix' && isAttendanceAdmin) ? (
<div className="max-h-[600px] overflow-auto">
{employeesLoading ? (
<div className="flex justify-center items-center h-24 sm:h-32">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
<span className="ml-2">Loading employees...</span>
</div>
) : !employees || employees.length === 0 ? (
<div className="flex flex-col items-center justify-center h-32 sm:h-48">
<UserIcon className="h-10 w-10 text-muted-foreground/50 mb-2" />
<p className="text-muted-foreground">No employees found</p>
</div>
) : (
<>
<div className="flex items-center gap-4 px-4 py-2 border-b text-xs text-muted-foreground">
<div className="flex items-center gap-2">
<span className={getMatrixCellClass("Present")} />
<span>Present</span>
</div>
<div className="flex items-center gap-2">
<span className={getMatrixCellClass("Absent")} />
<span>Absent</span>
</div>
<div className="flex items-center gap-2">
<span className={getMatrixCellClass("Half Day")} />
<span>Half Day</span>
</div>
<div className="flex items-center gap-2">
<span className={getMatrixCellClass("Leave")} />
<span>Leave</span>
</div>
</div>
<table className="min-w-max w-full border-collapse">
<thead className="sticky top-0 z-30 bg-background">
<tr>
<th className="sticky left-0 z-40 bg-background border-b border-r px-3 py-2 text-left text-sm font-medium min-w-[220px]">
Employee
</th>
{matrixDays.map(day => (
<th
key={day}
className="border-b px-2 py-2 text-xs font-medium text-muted-foreground text-center min-w-[34px]"
>
{day}
</th>
))}
</tr>
</thead>
<tbody>
{matrixEmployees.map(emp => (
<tr key={emp.id} className="hover:bg-muted/30">
<td className="sticky left-0 z-20 bg-background border-r px-3 py-2 text-sm font-medium whitespace-nowrap">
{emp.firstName} {emp.lastName}
</td>
{matrixDays.map(day => {
const record = recordByEmployeeDay.get(`${emp.id}-${day}`);
const cellRecord = record && (statusFilter === "all" || record.status === statusFilter) ? record : null;
const title = cellRecord
? `${cellRecord.status}${cellRecord.inTime ? ` | In: ${cellRecord.inTime}` : ""}${cellRecord.outTime ? ` | Out: ${cellRecord.outTime}` : ""} | Total: ${getOfficeDurationLabel(cellRecord.inTime, cellRecord.outTime)}`
: "";
return (
<td key={day} className="border-b px-2 py-2 text-center">
{cellRecord ? (
<span className={getMatrixCellClass(cellRecord.status)} title={title} />
) : (
<span className="text-muted-foreground/40">·</span>
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</>
)}
</div>
) : filteredAttendance.length > 0 ? (
<>
{(isMobile || viewMode === 'cards') && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
{filteredAttendance.map((record, index) => (
<MobileAttendanceCard
key={`${record.employeeId}-${record.date}-${index}`}
record={record}
getEmployeeName={getEmployeeName}
formatTime={formatTime}
getStatusBadge={getStatusBadge}
/>
))}
</div>
)}
{!isMobile && viewMode === 'table' && (
<div className="max-h-[600px] overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Employee</TableHead>
<TableHead>Date</TableHead>
<TableHead>In Time</TableHead>
<TableHead>Out Time</TableHead>
<TableHead>Total Time</TableHead>
<TableHead>Status</TableHead>
<TableHead>Comments</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredAttendance.map((record, index) => (
<TableRow key={`${record.employeeId}-${record.date}-${index}`}>
<TableCell className="font-medium">
{getEmployeeName(record.employeeId)}
</TableCell>
<TableCell>
{format(parseAttendanceDate(record.date), "dd MMM yyyy")}
</TableCell>
<TableCell>
{record.inTime ? (
<div className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5 text-green-600" />
{formatTime(record.inTime)}
</div>
) : (
<div className="flex items-center gap-1.5 text-muted-foreground">
<X className="h-3.5 w-3.5" />
Not recorded
</div>
)}
</TableCell>
<TableCell>
{record.outTime ? (
<div className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5 text-red-600" />
{formatTime(record.outTime)}
</div>
) : (
<div className="flex items-center gap-1.5 text-muted-foreground">
<X className="h-3.5 w-3.5" />
Not recorded
</div>
)}
</TableCell>
<TableCell>
{getOfficeDurationLabel(record.inTime, record.outTime)}
</TableCell>
<TableCell>
{getStatusBadge(record.status)}
</TableCell>
<TableCell className="text-muted-foreground text-sm">
{record.comments || "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</>
) : (
<div className="flex flex-col items-center justify-center h-32 sm:h-48">
<CalendarIcon className="h-10 w-10 text-muted-foreground/50 mb-2" />
<p className="text-muted-foreground">No attendance records found</p>
<p className="text-sm text-muted-foreground/80 mt-1">
Try changing filters or importing attendance data
</p>
</div>
)}
</div>
{viewMode !== 'matrix' && filteredAttendance.length > 0 && (
<div className="flex items-center justify-between text-sm text-muted-foreground pt-2 border-t">
<div>
<span className="font-medium">{filteredAttendance.length}</span> records found
</div>
<div>
<Button variant="ghost" size="sm" onClick={() => {
setSearchQuery("");
setEmployeeFilter("all");
setStatusFilter("all");
}}>
Clear filters
</Button>
</div>
</div>
)}
</div>
</CardContent>
</Card>
)}
</div>
</Layout>
);
};
export default Attendance;