pmtool / src /pages /employees.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
71.6 kB
import React, { useState, useMemo, useEffect, useCallback, useRef } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useQuery, useQueryClient, useMutation } 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 { UserPlus, Search, Loader2, Eye, Edit, Trash2, Filter, Download, MoreHorizontal, Upload, FileText, Check, X, ChevronDown, Phone, Mail, User, Briefcase } from "lucide-react";
import { toast } from "@/lib/custom-toast";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { jsPDF } from 'jspdf';
import autoTable from 'jspdf-autotable';
import { roleService } from "@/lib/api";
import { Employee as EmployeeType, Role } from "@/types";
import { Employee, employeeApi } from "@/services/employeeApi";
import Header from "@/components/layout/header";
import Sidebar from "@/components/layout/sidebar";
import { useIsMobile } from "@/hooks/use-mobile";
import ImportEmployeesDialog from "@/components/employee/import-employees-dialog";
import DownloadTemplateButton from "@/components/employee/download-template-button";
// Update the type declaration
declare module 'jspdf' {
interface jsPDF {
autoTable: (options: any) => jsPDF;
}
}
// Mobile Employee Card Component
interface MobileEmployeeCardProps {
employee: Employee;
getRoleName: (roleId: number) => string;
getRoleBadgeVariant: (roleId: number) => "default" | "secondary" | "outline" | "destructive";
onView: (id: number) => void;
onEdit: (id: number) => void;
onDelete: (id: number) => void;
onStatusChange: (employee: Employee, status: string) => void;
onReportingChange: (employeeId: number, reportingToId: number | null) => void;
isUpdating: boolean;
getInitials: (firstName: string, lastName: string) => string;
employees: Employee[];
}
const MobileEmployeeCard = ({
employee,
getRoleName,
getRoleBadgeVariant,
onView,
onEdit,
onDelete,
onStatusChange,
onReportingChange,
isUpdating,
getInitials,
employees
}: MobileEmployeeCardProps) => {
const [isActionsOpen, setIsActionsOpen] = useState(false);
const actionsRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (actionsRef.current && !actionsRef.current.contains(event.target as Node)) {
setIsActionsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case 'Active': return 'default';
case 'Inactive': return 'outline';
case 'On Leave': return 'secondary';
case 'Terminated': return 'destructive';
default: return 'outline';
}
};
return (
<Card className="w-full hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3 flex-1 min-w-0">
<Avatar className="h-12 w-12 flex-shrink-0">
<AvatarImage src={null} alt={`${employee.firstName} ${employee.lastName}`} />
<AvatarFallback className="bg-primary/10 text-primary text-sm font-medium">
{getInitials(employee.firstName, employee.lastName)}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-base truncate">
{employee.firstName} {employee.lastName}
</h3>
<p className="text-sm text-muted-foreground truncate">
{employee.empCode}
</p>
</div>
</div>
{/* Actions Dropdown */}
<div className="relative flex-shrink-0" ref={actionsRef}>
<Button
variant="ghost"
size="sm"
onClick={() => setIsActionsOpen(!isActionsOpen)}
className="h-8 w-8 p-0"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
{isActionsOpen && (
<div className="absolute z-50 top-full right-0 mt-1 w-40 bg-white rounded-md shadow-lg border py-1 animate-in fade-in-0 zoom-in-95">
<button
type="button"
className="flex items-center w-full text-left px-3 py-2 text-sm hover:bg-gray-100"
onClick={() => {
onView(employee.id);
setIsActionsOpen(false);
}}
>
<Eye className="h-4 w-4 mr-2" />
View
</button>
<button
type="button"
className="flex items-center w-full text-left px-3 py-2 text-sm hover:bg-gray-100"
onClick={() => {
onEdit(employee.id);
setIsActionsOpen(false);
}}
>
<Edit className="h-4 w-4 mr-2" />
Edit
</button>
</div>
)}
</div>
</div>
{/* Contact Information */}
<div className="space-y-2 mb-3">
<div className="flex items-center gap-2 text-sm">
<Mail className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<span className="truncate">{employee.email}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Phone className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<span>{employee.mobile}</span>
</div>
</div>
{/* Job Information */}
<div className="flex flex-wrap gap-2 mb-3">
<div className="flex items-center gap-1 text-xs bg-gray-50 px-2 py-1 rounded">
<Briefcase className="h-3 w-3 text-muted-foreground" />
<span>{employee.type}</span>
</div>
<Badge variant={getRoleBadgeVariant(employee.roleId)} className="text-xs">
{getRoleName(employee.roleId)}
</Badge>
</div>
{/* Status */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Status:</span>
<StatusDropdown
employee={employee}
onStatusChange={onStatusChange}
isUpdating={isUpdating}
/>
</div>
</div>
{/* Reporting */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Reporting To:</span>
<ReportingDropdown
employee={employee}
employees={employees}
onReportingChange={onReportingChange}
isUpdating={isUpdating}
/>
</div>
</div>
</CardContent>
</Card>
);
};
// Custom status dropdown component
interface StatusDropdownProps {
employee: Employee;
onStatusChange: (employee: Employee, status: string) => void;
isUpdating: boolean;
}
const StatusDropdown = ({ employee, onStatusChange, isUpdating }: StatusDropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const statusOptions = ['Active', 'Inactive', 'On Leave', 'Terminated', 'Unknown'];
// Close the dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case 'Active': return 'default';
case 'Inactive': return 'outline';
case 'On Leave': return 'secondary';
case 'Terminated': return 'destructive';
default: return 'outline';
}
};
return (
<div className="relative" ref={dropdownRef}>
{/* Status Badge with custom dropdown */}
<div
className="cursor-pointer flex items-center"
onClick={() => !isUpdating && setIsOpen(!isOpen)}
>
<Badge
variant={getStatusBadgeVariant(employee.status || 'Unknown')}
className="mr-1 text-xs"
>
{employee.status || 'Unknown'}
</Badge>
<ChevronDown className={`h-3 w-3 text-muted-foreground transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</div>
{/* Custom dropdown content */}
{isOpen && (
<div className="absolute z-50 top-full left-0 mt-1 w-[180px] bg-white rounded-md shadow-lg border py-1 animate-in fade-in-0 zoom-in-95">
<div className="p-2 font-medium text-sm border-b">Update Status</div>
<div className="py-1">
{statusOptions.map(status => (
<button
key={status}
type="button"
className={`flex items-center w-full text-left px-3 py-2 text-sm hover:bg-gray-100
${employee.status === status ? "bg-primary/10 font-medium" : ""}`}
onClick={() => {
onStatusChange(employee, status);
setIsOpen(false);
}}
>
<Badge
variant={getStatusBadgeVariant(status)}
className="mr-2 text-xs"
>
{status}
</Badge>
{employee.status === status && (
<Check className="ml-auto h-4 w-4" />
)}
</button>
))}
</div>
</div>
)}
{/* Loading indicator during update */}
{isUpdating && (
<div className="absolute inset-0 bg-background/80 flex items-center justify-center">
<Loader2 className="h-4 w-4 animate-spin text-primary" />
</div>
)}
</div>
);
};
// Custom reporting dropdown component
interface ReportingDropdownProps {
employee: Employee;
employees: Employee[];
onReportingChange: (employeeId: number, reportingToId: number | null) => void;
isUpdating: boolean;
}
const ReportingDropdown = ({ employee, employees, onReportingChange, isUpdating }: ReportingDropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
const dropdownRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
setSearchQuery('');
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
useEffect(() => {
if (isOpen && searchInputRef.current) {
searchInputRef.current.focus();
}
if (isOpen && triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
setDropdownPosition({
top: rect.bottom + 4,
left: rect.right - 250 // 250px is the dropdown width
});
}
}, [isOpen]);
const filteredEmployees = employees
.filter(emp => emp.id !== employee.id)
.filter(emp => {
if (!searchQuery) return true;
const fullName = `${emp.firstName} ${emp.lastName}`.toLowerCase();
return fullName.includes(searchQuery.toLowerCase());
});
const getReportingName = (id: number | null | undefined) => {
if (!id) return 'None';
const reportingTo = employees.find(emp => emp.id === id);
return reportingTo ? `${reportingTo.firstName} ${reportingTo.lastName}` : 'Unknown';
};
return (
<div className="relative" ref={dropdownRef}>
<div
ref={triggerRef}
className="cursor-pointer flex items-center"
onClick={() => !isUpdating && setIsOpen(!isOpen)}
>
<span className="mr-1 text-sm">{getReportingName(employee.reportingToId)}</span>
<ChevronDown className={`h-3 w-3 text-muted-foreground transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</div>
{isOpen && (
<div
className="fixed z-50 w-[250px] bg-white rounded-md shadow-lg border py-1 animate-in fade-in-0 zoom-in-95"
style={{ top: `${dropdownPosition.top}px`, left: `${dropdownPosition.left}px` }}
>
<div className="p-2 font-medium text-sm border-b">Select Reporting To</div>
<div className="p-2 border-b">
<div className="relative">
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
ref={searchInputRef}
type="text"
placeholder="Search employees..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-8 pr-3 py-1.5 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/20"
onClick={(e) => e.stopPropagation()}
/>
</div>
</div>
<div className="py-1 max-h-60 overflow-y-auto">
<button
type="button"
className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-100 ${!employee.reportingToId ? 'bg-primary/10 font-medium' : ''}`}
onClick={() => {
onReportingChange(employee.id, null);
setIsOpen(false);
setSearchQuery('');
}}
>
None
{!employee.reportingToId && <Check className="ml-auto h-4 w-4" />}
</button>
{filteredEmployees.length > 0 ? (
filteredEmployees.map(reporting => (
<button
key={reporting.id}
type="button"
className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-100 ${employee.reportingToId === reporting.id ? 'bg-primary/10 font-medium' : ''}`}
onClick={() => {
onReportingChange(employee.id, reporting.id);
setIsOpen(false);
setSearchQuery('');
}}
>
{reporting.firstName} {reporting.lastName}
{employee.reportingToId === reporting.id && <Check className="ml-auto h-4 w-4" />}
</button>
))
) : (
<div className="px-3 py-2 text-sm text-muted-foreground text-center">
No employees found
</div>
)}
</div>
</div>
)}
{isUpdating && (
<div className="absolute inset-0 bg-background/80 flex items-center justify-center">
<Loader2 className="h-4 w-4 animate-spin text-primary" />
</div>
)}
</div>
);
};
// Custom export modal component
interface CustomExportModalProps {
isOpen: boolean;
onClose: () => void;
employees: Employee[];
getRoleName: (roleId: number) => string;
}
const CustomExportModal = ({ isOpen, onClose, employees, getRoleName }: CustomExportModalProps) => {
const [exportFormat, setExportFormat] = useState<'excel' | 'pdf'>('excel');
const [selectedColumns, setSelectedColumns] = useState({
empCode: true,
name: true,
email: true,
mobile: true,
type: true,
role: true,
status: true
});
// Export to Excel
const exportToExcel = () => {
if (!employees || employees.length === 0) {
toast.error("No data to export");
return;
}
try {
// Build headers based on selected columns
const headers = [];
if (selectedColumns.empCode) headers.push("Employee Code");
if (selectedColumns.name) headers.push("Name");
if (selectedColumns.email) headers.push("Email");
if (selectedColumns.mobile) headers.push("Mobile");
if (selectedColumns.type) headers.push("Type");
if (selectedColumns.role) headers.push("Role");
if (selectedColumns.status) headers.push("Status");
// Check if any columns are selected
if (headers.length === 0) {
toast.error("Please select at least one column to export");
return;
}
// Create HTML table content for proper Excel formatting
let htmlTable = '<table border="1">';
// Headers row
htmlTable += '<tr>';
headers.forEach(header => {
htmlTable += `<th>${header}</th>`;
});
htmlTable += '</tr>';
// Data rows
employees.forEach(employee => {
htmlTable += '<tr>';
if (selectedColumns.empCode) htmlTable += `<td>${employee.empCode}</td>`;
if (selectedColumns.name) htmlTable += `<td>${employee.firstName} ${employee.middleName || ''} ${employee.lastName}</td>`;
if (selectedColumns.email) htmlTable += `<td>${employee.email}</td>`;
if (selectedColumns.mobile) htmlTable += `<td>${employee.mobile}</td>`;
if (selectedColumns.type) htmlTable += `<td>${employee.type}</td>`;
if (selectedColumns.role) htmlTable += `<td>${getRoleName(employee.roleId)}</td>`;
if (selectedColumns.status) htmlTable += `<td>${employee.status || 'Unknown'}</td>`;
htmlTable += '</tr>';
});
htmlTable += '</table>';
// Prepare the file
const excelTemplate = `
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">
<head>
<!--[if gte mso 9]>
<xml>
<x:ExcelWorkbook>
<x:ExcelWorksheets>
<x:ExcelWorksheet>
<x:Name>Employee Data</x:Name>
<x:WorksheetOptions>
<x:DisplayGridlines/>
</x:WorksheetOptions>
</x:ExcelWorksheet>
</x:ExcelWorksheets>
</x:ExcelWorkbook>
</xml>
<![endif]-->
<style>
table, td, th {
border: 1px solid black;
border-collapse: collapse;
}
th {
background-color: #f2f2f2;
font-weight: bold;
}
</style>
</head>
<body>
${htmlTable}
</body>
</html>
`;
// Create download
const blob = new Blob([excelTemplate], { type: 'application/vnd.ms-excel' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', 'employees.xls');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success("Employee data exported to Excel successfully");
onClose();
} catch (error) {
console.error("Excel export error:", error);
toast.error("Failed to export data");
}
};
// Export to PDF
const exportToPDF = () => {
if (!employees || employees.length === 0) {
toast.error("No data to export");
return;
}
try {
const doc = new jsPDF();
// Add title
doc.setFontSize(16);
doc.text('Employee Directory', 14, 15);
doc.setFontSize(10);
doc.text(`Generated on: ${new Date().toLocaleDateString()}`, 14, 22);
// Prepare columns based on selection
const columns = [];
if (selectedColumns.empCode) columns.push('Employee Code');
if (selectedColumns.name) columns.push('Name');
if (selectedColumns.email) columns.push('Email');
if (selectedColumns.mobile) columns.push('Mobile');
if (selectedColumns.type) columns.push('Type');
if (selectedColumns.role) columns.push('Role');
if (selectedColumns.status) columns.push('Status');
// Check if any columns are selected
if (columns.length === 0) {
toast.error("Please select at least one column to export");
return;
}
// Prepare data rows
const tableData = employees.map(employee => {
const row = [];
if (selectedColumns.empCode) row.push(employee.empCode);
if (selectedColumns.name) row.push(`${employee.firstName} ${employee.middleName || ''} ${employee.lastName}`);
if (selectedColumns.email) row.push(employee.email);
if (selectedColumns.mobile) row.push(employee.mobile);
if (selectedColumns.type) row.push(employee.type);
if (selectedColumns.role) row.push(getRoleName(employee.roleId));
if (selectedColumns.status) row.push(employee.status || 'Unknown');
return row;
});
// Generate table
autoTable(doc, {
head: [columns],
body: tableData,
startY: 30,
theme: 'grid',
styles: {
fontSize: 8,
cellPadding: 2,
overflow: 'linebreak'
},
headStyles: {
fillColor: [41, 128, 185],
textColor: 255,
fontSize: 9,
fontStyle: 'bold'
},
alternateRowStyles: {
fillColor: [245, 245, 245]
}
});
// Save the PDF
doc.save('employees.pdf');
toast.success("Employee data exported to PDF successfully");
onClose();
} catch (error) {
console.error('PDF export error:', error);
toast.error("Failed to export data to PDF");
}
};
// Toggle all columns
const toggleAllColumns = (checked: boolean) => {
setSelectedColumns({
empCode: checked,
name: checked,
email: checked,
mobile: checked,
type: checked,
role: checked,
status: checked
});
};
// Check if any columns are selected
const hasSelectedColumns = Object.values(selectedColumns).some(v => v);
// Prevent export if no columns are selected
const handleExport = () => {
if (!employees || employees.length === 0) {
toast.error("No data to export");
return;
}
// Validate at least one column is selected
if (!hasSelectedColumns) {
toast.error("Please select at least one column to export");
return;
}
if (exportFormat === 'excel') {
exportToExcel();
} else {
exportToPDF();
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="bg-white rounded-lg shadow-lg w-full max-w-md max-h-[90vh] overflow-hidden">
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg sm:text-xl font-semibold">Export Employee Data</h2>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="p-4 overflow-y-auto max-h-[calc(90vh-120px)]">
<div className="mb-6">
<p className="font-medium mb-3">Export Format</p>
<div className="grid grid-cols-2 gap-3">
<label className={`flex items-center space-x-2 cursor-pointer p-3 rounded border ${exportFormat === 'excel' ? 'bg-green-50 border-green-500 ring-1 ring-green-500' : 'bg-white border-gray-300'}`}>
<input
type="radio"
name="exportFormat"
checked={exportFormat === 'excel'}
onChange={() => setExportFormat('excel')}
className="h-4 w-4 text-green-600 border-gray-300 focus:ring-green-500"
/>
<span className="flex items-center">
<div className="h-5 w-5 bg-green-600 text-white flex items-center justify-center mr-2 text-[10px] font-bold rounded">
X
</div>
<span className="font-medium">Excel</span>
</span>
</label>
<label className={`flex items-center space-x-2 cursor-pointer p-3 rounded border ${exportFormat === 'pdf' ? 'bg-red-50 border-red-500 ring-1 ring-red-500' : 'bg-white border-gray-300'}`}>
<input
type="radio"
name="exportFormat"
checked={exportFormat === 'pdf'}
onChange={() => setExportFormat('pdf')}
className="h-4 w-4 text-red-600 border-gray-300 focus:ring-red-500"
/>
<span className="flex items-center">
<FileText className="h-5 w-5 mr-2 text-red-600" />
<span className="font-medium">PDF</span>
</span>
</label>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-3">
<p className="font-medium">Columns to Include</p>
<button
type="button"
onClick={() => toggleAllColumns(!Object.values(selectedColumns).every(v => v))}
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 py-1 px-2 rounded border flex items-center"
>
{Object.values(selectedColumns).every(v => v) ? (
<>
<X className="h-3 w-3 mr-1" />
Uncheck All
</>
) : (
<>
<Check className="h-3 w-3 mr-1" />
Check All
</>
)}
</button>
</div>
<div className="bg-gray-50 rounded-md p-3 border">
<div className="space-y-3">
{/* Group 1: Identity */}
<div className="bg-white rounded p-3 border shadow-sm">
<div className="text-sm font-medium text-gray-700 mb-2">Identity Information</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<label className="flex items-center space-x-2 cursor-pointer">
<div className="relative flex items-center justify-center h-5 w-5">
<input
type="checkbox"
checked={selectedColumns.empCode}
onChange={(e) => setSelectedColumns({...selectedColumns, empCode: e.target.checked})}
className="absolute opacity-0 h-0 w-0"
id="col-empCode"
/>
<div className={`h-4 w-4 border rounded-sm ${selectedColumns.empCode ? 'bg-blue-600 border-blue-600' : 'bg-white border-gray-300'} flex items-center justify-center`}>
{selectedColumns.empCode && <Check className="h-3 w-3 text-white" />}
</div>
</div>
<span className="text-sm">Employee Code</span>
</label>
<label className="flex items-center space-x-2 cursor-pointer">
<div className="relative flex items-center justify-center h-5 w-5">
<input
type="checkbox"
checked={selectedColumns.name}
onChange={(e) => setSelectedColumns({...selectedColumns, name: e.target.checked})}
className="absolute opacity-0 h-0 w-0"
id="col-name"
/>
<div className={`h-4 w-4 border rounded-sm ${selectedColumns.name ? 'bg-blue-600 border-blue-600' : 'bg-white border-gray-300'} flex items-center justify-center`}>
{selectedColumns.name && <Check className="h-3 w-3 text-white" />}
</div>
</div>
<span className="text-sm">Name</span>
</label>
</div>
</div>
{/* Group 2: Contact */}
<div className="bg-white rounded p-3 border shadow-sm">
<div className="text-sm font-medium text-gray-700 mb-2">Contact Information</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<label className="flex items-center space-x-2 cursor-pointer">
<div className="relative flex items-center justify-center h-5 w-5">
<input
type="checkbox"
checked={selectedColumns.email}
onChange={(e) => setSelectedColumns({...selectedColumns, email: e.target.checked})}
className="absolute opacity-0 h-0 w-0"
id="col-email"
/>
<div className={`h-4 w-4 border rounded-sm ${selectedColumns.email ? 'bg-blue-600 border-blue-600' : 'bg-white border-gray-300'} flex items-center justify-center`}>
{selectedColumns.email && <Check className="h-3 w-3 text-white" />}
</div>
</div>
<span className="text-sm">Email</span>
</label>
<label className="flex items-center space-x-2 cursor-pointer">
<div className="relative flex items-center justify-center h-5 w-5">
<input
type="checkbox"
checked={selectedColumns.mobile}
onChange={(e) => setSelectedColumns({...selectedColumns, mobile: e.target.checked})}
className="absolute opacity-0 h-0 w-0"
id="col-mobile"
/>
<div className={`h-4 w-4 border rounded-sm ${selectedColumns.mobile ? 'bg-blue-600 border-blue-600' : 'bg-white border-gray-300'} flex items-center justify-center`}>
{selectedColumns.mobile && <Check className="h-3 w-3 text-white" />}
</div>
</div>
<span className="text-sm">Mobile</span>
</label>
</div>
</div>
{/* Group 3: Job */}
<div className="bg-white rounded p-3 border shadow-sm">
<div className="text-sm font-medium text-gray-700 mb-2">Job Information</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<label className="flex items-center space-x-2 cursor-pointer">
<div className="relative flex items-center justify-center h-5 w-5">
<input
type="checkbox"
checked={selectedColumns.type}
onChange={(e) => setSelectedColumns({...selectedColumns, type: e.target.checked})}
className="absolute opacity-0 h-0 w-0"
id="col-type"
/>
<div className={`h-4 w-4 border rounded-sm ${selectedColumns.type ? 'bg-blue-600 border-blue-600' : 'bg-white border-gray-300'} flex items-center justify-center`}>
{selectedColumns.type && <Check className="h-3 w-3 text-white" />}
</div>
</div>
<span className="text-sm">Type</span>
</label>
<label className="flex items-center space-x-2 cursor-pointer">
<div className="relative flex items-center justify-center h-5 w-5">
<input
type="checkbox"
checked={selectedColumns.role}
onChange={(e) => setSelectedColumns({...selectedColumns, role: e.target.checked})}
className="absolute opacity-0 h-0 w-0"
id="col-role"
/>
<div className={`h-4 w-4 border rounded-sm ${selectedColumns.role ? 'bg-blue-600 border-blue-600' : 'bg-white border-gray-300'} flex items-center justify-center`}>
{selectedColumns.role && <Check className="h-3 w-3 text-white" />}
</div>
</div>
<span className="text-sm">Role</span>
</label>
<label className="flex items-center space-x-2 cursor-pointer">
<div className="relative flex items-center justify-center h-5 w-5">
<input
type="checkbox"
checked={selectedColumns.status}
onChange={(e) => setSelectedColumns({...selectedColumns, status: e.target.checked})}
className="absolute opacity-0 h-0 w-0"
id="col-status"
/>
<div className={`h-4 w-4 border rounded-sm ${selectedColumns.status ? 'bg-blue-600 border-blue-600' : 'bg-white border-gray-300'} flex items-center justify-center`}>
{selectedColumns.status && <Check className="h-3 w-3 text-white" />}
</div>
</div>
<span className="text-sm">Status</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row justify-end p-4 border-t gap-2">
<Button variant="outline" onClick={onClose} className="w-full sm:w-auto">
Cancel
</Button>
<Button onClick={handleExport} className="w-full sm:w-auto">
<Download className="mr-2 h-4 w-4" />
Export
</Button>
</div>
</div>
</div>
);
};
// Custom mobile actions dropdown component
interface MobileActionsDropdownProps {
employee: Employee;
onView: (id: number) => void;
onEdit: (id: number) => void;
onDelete: (id: number) => void;
}
const MobileActionsDropdown = ({ employee, onView, onEdit, onDelete }: MobileActionsDropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// Close the dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative" ref={dropdownRef}>
<Button
variant="ghost"
size="sm"
className="sm:hidden"
onClick={() => setIsOpen(!isOpen)}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
{isOpen && (
<div className="absolute z-50 top-full right-0 mt-1 w-48 bg-white rounded-md shadow-lg border py-1 animate-in fade-in-0 zoom-in-95">
<button
type="button"
className="flex items-center w-full text-left px-3 py-2 text-sm hover:bg-gray-100"
onClick={() => {
onView(employee.id);
setIsOpen(false);
}}
>
<Eye className="h-4 w-4 mr-2" />
View
</button>
<button
type="button"
className="flex items-center w-full text-left px-3 py-2 text-sm hover:bg-gray-100"
onClick={() => {
onEdit(employee.id);
setIsOpen(false);
}}
>
<Edit className="h-4 w-4 mr-2" />
Edit
</button>
</div>
)}
</div>
);
};
// Custom filter dropdown component - reusable base
interface FilterDropdownProps {
isOpen: boolean;
title: string;
children: React.ReactNode;
dropdownRef?: React.RefObject<HTMLDivElement>;
}
const FilterDropdown = ({ isOpen, title, children, dropdownRef }: FilterDropdownProps) => {
if (!isOpen) return null;
return (
<div ref={dropdownRef} className="absolute top-full left-0 mt-1 w-[240px] bg-white rounded-md shadow-lg border z-50 py-1 animate-in fade-in-0 zoom-in-95">
<div className="p-2 font-medium border-b mb-1">{title}</div>
<div className="p-1 max-h-[300px] overflow-auto">
{children}
</div>
</div>
);
};
const Employees = () => {
const navigate = useNavigate();
const location = useLocation();
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const [isSidebarOpen, setIsSidebarOpen] = useState(!isMobile);
const [searchQuery, setSearchQuery] = useState("");
const [roleFilter, setRoleFilter] = useState<number | null>(null);
const [statusFilter, setStatusFilter] = useState<string | null>(null);
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
const [isRoleFilterOpen, setIsRoleFilterOpen] = useState(false);
const [isStatusFilterOpen, setIsStatusFilterOpen] = useState(false);
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
// Status update popup state
const [statusPopupOpen, setStatusPopupOpen] = useState<number | null>(null);
const statusBadgeRefs = useRef<{[key: number]: HTMLDivElement | null}>({});
// References for filter buttons and popups
const roleFilterButtonRef = useRef<HTMLButtonElement>(null);
const statusFilterButtonRef = useRef<HTMLButtonElement>(null);
const roleFilterDropdownRef = useRef<HTMLDivElement>(null);
const statusFilterDropdownRef = useRef<HTMLDivElement>(null);
// Status options for filter
const statusOptions = ['Active', 'Inactive', 'On Leave', 'Terminated', 'Unknown'];
// Auto-switch to cards view on mobile
useEffect(() => {
if (isMobile && viewMode === 'table') {
setViewMode('cards');
} else if (!isMobile && viewMode === 'cards') {
setViewMode('table');
}
}, [isMobile, viewMode]);
// Setup status update mutation
const statusUpdateMutation = useMutation({
mutationFn: ({ id, status }: { id: number, status: string }) => {
return employeeApi.updateStatus(id, status);
},
onSuccess: (data) => {
if (data.success) {
toast.success(data.message || "Status updated successfully");
// Refetch employee data to reflect the changes
refetch();
} else {
toast.error(data.message || "Failed to update status");
}
},
onError: (error) => {
console.error("Status update error:", error);
toast.error("An error occurred while updating the status");
}
});
// Handle clicks outside the filter popups
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
// Close role filter if clicked outside both button and dropdown
if (
isRoleFilterOpen &&
roleFilterButtonRef.current &&
!roleFilterButtonRef.current.contains(event.target as Node) &&
roleFilterDropdownRef.current &&
!roleFilterDropdownRef.current.contains(event.target as Node)
) {
setIsRoleFilterOpen(false);
}
// Close status filter if clicked outside both button and dropdown
if (
isStatusFilterOpen &&
statusFilterButtonRef.current &&
!statusFilterButtonRef.current.contains(event.target as Node) &&
statusFilterDropdownRef.current &&
!statusFilterDropdownRef.current.contains(event.target as Node)
) {
setIsStatusFilterOpen(false);
}
};
// Add event listener
document.addEventListener('mousedown', handleClickOutside);
// Clean up
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isRoleFilterOpen, isStatusFilterOpen]);
// Update sidebar state when screen size changes
useEffect(() => {
setIsSidebarOpen(!isMobile);
}, [isMobile]);
// Close sidebar on mobile when route changes
useEffect(() => {
if (isMobile) {
setIsSidebarOpen(false);
}
}, [location, isMobile]);
// Fetch employees
const { data: employees, isLoading, error, refetch } = useQuery({
queryKey: ["employees"],
queryFn: () => employeeApi.getAllForAdmin(),
});
// Debug logging to see employee data structure
useEffect(() => {
if (employees && employees.length > 0) {
console.log("Employee data structure:", employees[0]);
console.log("All employees:", employees);
// Ensure all employees have status values
const hasStatusField = employees.some(emp => emp.status !== undefined);
if (!hasStatusField) {
console.log("Employees missing status field, normalizing data...");
// Add status field to all employees if missing
const normalizedEmployees = employees.map(emp => ({
...emp,
status: emp.status || 'Unknown'
}));
// Update query cache with normalized employees
queryClient.setQueryData(['employees'], normalizedEmployees);
}
}
}, [employees, queryClient]);
// Fetch roles
const { data: rolesData, isLoading: rolesLoading } = useQuery({
queryKey: ["roles"],
queryFn: roleService.getAll,
});
// Normalize roles data to ensure consistent structure
const roles = useMemo(() => {
if (!rolesData) return null;
return rolesData.map((role: any) => {
// Ensure both id and roleId are available
if (role.id === undefined && role.roleId !== undefined) {
return { ...role, id: role.roleId };
}
if (role.roleId === undefined && role.id !== undefined) {
return { ...role, roleId: role.id };
}
return role;
});
}, [rolesData]);
// Toggle sidebar
const toggleSidebar = () => {
setIsSidebarOpen(!isSidebarOpen);
};
// Get role name from role ID using fetched roles
const getRoleName = (roleId: number) => {
if (!roles) return "Loading...";
// Find role by ID (should work with normalized data)
const role = roles.find(r => r.roleId === roleId || r.id === roleId);
if (role) {
return role.roleName;
}
console.log(`Role with ID ${roleId} not found in`, roles);
return `Role ${roleId}`;
};
// Get role badge color based on role ID
const getRoleBadgeVariant = (roleId: number) => {
if (!roles) return "outline";
// Create a mapping of roleId to variants
const roleVariants: Record<number, "default" | "secondary" | "outline" | "destructive"> = {};
// Assign variants based on role index
roles.forEach((role: any, index: number) => {
const variants: Array<"default" | "secondary" | "outline" | "destructive"> = ["default", "secondary", "outline"];
// Support both roleId and id property names
const id = role.roleId !== undefined ? role.roleId : role.id;
if (id !== undefined) {
roleVariants[id] = variants[index % variants.length];
}
});
return roleVariants[roleId] || "outline";
};
// Delete employee handler
const handleDeleteEmployee = async (id: number) => {
if (window.confirm("Are you sure you want to delete this employee?")) {
try {
// Actual API call would go here
// await employeeService.delete(id);
toast.success("Employee has been successfully deleted");
// Refresh the employee list
refetch();
} catch (error) {
toast.error("Failed to delete employee");
}
}
};
// Helper function to get role ID regardless of property name
const getRoleId = (role: any): number => {
return role.roleId !== undefined ? role.roleId : (role.id !== undefined ? role.id : 0);
};
// Filter employees based on search query, role filter, and status filter
const filteredEmployees = useMemo(() => {
if (!employees) return [];
console.log("Filtering employees: ", {
total: employees.length,
roleFilter,
statusFilter,
searchQuery
});
return employees.filter((employee: Employee) => {
// Apply role filter
if (roleFilter !== null && employee.roleId !== roleFilter) {
return false;
}
// Apply status filter with better handling of undefined values
if (statusFilter !== null) {
const employeeStatus = employee.status || 'Unknown';
console.log(`Employee ${employee.id} (${employee.firstName}): comparing status '${employeeStatus}' with filter '${statusFilter}'`);
if (employeeStatus !== statusFilter) {
return false;
}
}
// Apply search filter
if (!searchQuery) return true;
const fullName = `${employee.firstName} ${employee.middleName || ''} ${employee.lastName}`.toLowerCase();
const query = searchQuery.toLowerCase();
return (
fullName.includes(query) ||
employee.email.toLowerCase().includes(query) ||
employee.empCode.toLowerCase().includes(query) ||
employee.mobile.includes(query) ||
employee.type.toLowerCase().includes(query)
);
});
}, [employees, searchQuery, roleFilter, statusFilter]);
// Get initials for avatar
const getInitials = (firstName: string, lastName: string) => {
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
};
// Reset all filters
const resetFilters = useCallback(() => {
console.log("Resetting all filters");
setSearchQuery("");
setRoleFilter(null);
setStatusFilter(null);
setIsRoleFilterOpen(false);
setIsStatusFilterOpen(false);
}, []);
// Add debug information for filtered results
useEffect(() => {
if (filteredEmployees && filteredEmployees.length > 0) {
console.log(`Filter results: ${filteredEmployees.length} employees matched criteria`);
console.log("First filtered employee:", filteredEmployees[0]);
} else if (filteredEmployees && filteredEmployees.length === 0 && employees && employees.length > 0) {
console.log("Filter returned zero results. Current filters:", { roleFilter, statusFilter, searchQuery });
}
}, [filteredEmployees, employees, roleFilter, statusFilter, searchQuery]);
// Set role filter with callback for reliability
const updateRoleFilter = useCallback((roleId: number | null) => {
console.log(`Setting role filter to: ${roleId === null ? 'null' : roleId}`);
setRoleFilter(roleId);
setIsRoleFilterOpen(false);
}, []);
// Set status filter with callback for reliability
const updateStatusFilter = useCallback((status: string | null) => {
console.log(`Setting status filter to: ${status === null ? 'null' : `"${status}"`}`);
setStatusFilter(status);
setIsStatusFilterOpen(false);
}, []);
// Handler for status update with confirmation
const handleStatusUpdate = useCallback((employee: Employee, newStatus: string) => {
// Skip if status is already the same
if (employee.status === newStatus) return;
// Ask for confirmation for Terminated status
if (newStatus === 'Terminated') {
if (window.confirm(`Are you sure you want to mark ${employee.firstName} ${employee.lastName} as Terminated? This action may affect their access to systems.`)) {
statusUpdateMutation.mutate({ id: employee.id, status: newStatus });
}
} else {
// For other statuses, no confirmation needed
statusUpdateMutation.mutate({ id: employee.id, status: newStatus });
}
}, [statusUpdateMutation]);
// Reporting update mutation
const reportingUpdateMutation = useMutation({
mutationFn: ({ id, reportingToId }: { id: number, reportingToId: number | null }) => {
return employeeApi.updateEmployeeReporting(id, reportingToId);
},
onSuccess: (data) => {
if (data.success) {
toast.success(data.message || "Reporting updated successfully");
// Refetch employee data to reflect the changes
refetch();
} else {
toast.error(data.message || "Failed to update reporting");
}
},
onError: (error) => {
console.error("Reporting update error:", error);
toast.error("An error occurred while updating the reporting");
}
});
// Handler for reporting update
const handleReportingUpdate = useCallback(
(employeeId: number, reportingToId: number | null) => {
reportingUpdateMutation.mutate({ id: employeeId, reportingToId });
},
[reportingUpdateMutation]
);
return (
<div className="flex min-h-screen flex-col">
<Header toggleSidebar={toggleSidebar} isSidebarOpen={isSidebarOpen} />
<Sidebar isOpen={isSidebarOpen} setIsOpen={setIsSidebarOpen} />
<main className={`flex-1 p-3 pt-16 sm:p-6 sm:pt-20 md:p-10 md:pt-24 transition-all duration-300 ${isSidebarOpen && !isMobile ? "md:ml-64" : ""}`}>
<div className="mx-auto max-w-7xl">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-4 sm:mb-6 gap-4">
<div>
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight">Employees</h2>
<p className="text-muted-foreground text-sm sm:text-base">
Manage your organization's employees
</p>
</div>
<Button onClick={() => navigate("/employee-register")} className="w-full sm:w-auto">
<UserPlus className="mr-2 h-4 w-4" />
Add Employee
</Button>
</div>
{/* Import/Export actions - Compact design */}
<div className="mb-3">
<div className="flex items-center justify-between gap-2 px-1">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<FileText className="h-3.5 w-3.5" />
<span>{filteredEmployees?.length || 0} employees</span>
</div>
<div className="flex flex-wrap gap-1.5">
<ImportEmployeesDialog
onImportComplete={() => {
refetch();
toast.success("Employee list refreshed");
}}
/>
<DownloadTemplateButton size="sm" variant="outline" />
<Button
variant="outline"
size="sm"
onClick={() => setIsExportModalOpen(true)}
>
<Download className="h-4 w-4 mr-2" />
<span className="hidden sm:inline">Export</span>
<span className="sm:hidden">Export</span>
</Button>
<CustomExportModal
isOpen={isExportModalOpen}
onClose={() => setIsExportModalOpen(false)}
employees={filteredEmployees || []}
getRoleName={getRoleName}
/>
</div>
</div>
</div>
<Card className="h-[calc(100vh-280px)] flex flex-col">
<CardHeader className="pb-3 flex-shrink-0">
{/* Single Row Header with Title, Search, and Filters */}
<div className="flex flex-col lg:flex-row items-start lg:items-center gap-3">
{/* Title Section */}
<div className="flex-shrink-0">
<CardTitle className="text-lg">Employee Directory</CardTitle>
<CardDescription className="text-xs">
{filteredEmployees?.length || 0} of {employees?.length || 0}
</CardDescription>
</div>
{/* Search Bar */}
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search employees..."
className="pl-9 h-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* Filters and View Toggle */}
<div className="flex items-center gap-2 flex-wrap">
{/* Role Filter */}
<div className="relative">
<Button
variant="outline"
size="sm"
className={`${roleFilter !== null ? 'border-primary bg-primary/5' : ''}`}
onClick={() => {
setIsRoleFilterOpen(!isRoleFilterOpen);
setIsStatusFilterOpen(false);
}}
ref={roleFilterButtonRef}
>
<Filter className="mr-2 h-4 w-4" />
<span className="hidden sm:inline">Role Filter</span>
<span className="sm:hidden">Role</span>
{roleFilter !== null && (
<Badge variant="secondary" className="ml-2 px-1.5 py-0 text-xs">
<span className="hidden sm:inline">{getRoleName(roleFilter)}</span>
<span className="sm:hidden">1</span>
</Badge>
)}
</Button>
{/* Custom Role Filter Popup */}
{isRoleFilterOpen && (
<FilterDropdown
isOpen={isRoleFilterOpen}
title="Filter by Role"
dropdownRef={roleFilterDropdownRef}
children={
<>
<button
type="button"
className={`w-full text-left px-3 py-2 rounded-sm text-sm hover:bg-gray-100 ${roleFilter === null ? 'bg-primary/10 font-medium' : ''}`}
onClick={() => updateRoleFilter(null)}
>
<div className="flex items-center">
{roleFilter === null && <Check className="h-4 w-4 mr-2" />}
<span>All Roles</span>
</div>
</button>
{!rolesLoading && roles?.map((role: any) => (
<button
key={getRoleId(role)}
type="button"
className={`w-full text-left px-3 py-2 rounded-sm text-sm hover:bg-gray-100 ${roleFilter === getRoleId(role) ? 'bg-primary/10 font-medium' : ''}`}
onClick={() => updateRoleFilter(getRoleId(role))}
>
<div className="flex items-center">
{roleFilter === getRoleId(role) && <Check className="h-4 w-4 mr-2" />}
<span>{role.roleName}</span>
</div>
</button>
))}
</>
}
/>
)}
</div>
{/* Status Filter */}
<div className="relative">
<Button
variant="outline"
size="sm"
className={`${statusFilter !== null ? 'border-primary bg-primary/5' : ''}`}
onClick={() => {
setIsStatusFilterOpen(!isStatusFilterOpen);
setIsRoleFilterOpen(false);
}}
ref={statusFilterButtonRef}
>
<Filter className="mr-2 h-4 w-4" />
<span className="hidden sm:inline">Status Filter</span>
<span className="sm:hidden">Status</span>
{statusFilter !== null && (
<Badge variant="secondary" className="ml-2 px-1.5 py-0 text-xs">
<span className="hidden sm:inline">{statusFilter}</span>
<span className="sm:hidden">1</span>
</Badge>
)}
</Button>
{/* Custom Status Filter Popup */}
{isStatusFilterOpen && (
<FilterDropdown
isOpen={isStatusFilterOpen}
title="Filter by Status"
dropdownRef={statusFilterDropdownRef}
children={
<>
<button
type="button"
className={`w-full text-left px-3 py-2 rounded-sm text-sm hover:bg-gray-100 ${statusFilter === null ? 'bg-primary/10 font-medium' : ''}`}
onClick={() => updateStatusFilter(null)}
>
<div className="flex items-center">
{statusFilter === null && <Check className="h-4 w-4 mr-2" />}
<span>All Statuses</span>
</div>
</button>
{statusOptions.map((status) => (
<button
key={status}
type="button"
className={`w-full text-left px-3 py-2 rounded-sm text-sm hover:bg-gray-100 ${statusFilter === status ? 'bg-primary/10 font-medium' : ''}`}
onClick={() => updateStatusFilter(status)}
>
<div className="flex items-center">
{statusFilter === status && <Check className="h-4 w-4 mr-2" />}
<span>{status}</span>
</div>
</button>
))}
</>
}
/>
)}
</div>
{/* Reset Button */}
{(searchQuery || roleFilter !== null || statusFilter !== null) && (
<Button
variant="ghost"
size="sm"
onClick={resetFilters}
className="flex-shrink-0"
title="Reset all filters"
>
<X className="h-4 w-4 mr-1" />
<span className="hidden sm:inline">Reset</span>
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent className="p-3 sm:p-6 pt-0 flex-1 overflow-x-hidden overflow-y-auto">
{isLoading || rolesLoading ? (
<div className="flex justify-center items-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : error ? (
<div className="flex justify-center items-center h-64 text-destructive">
<p>Error loading employees. Please try again later.</p>
</div>
) : filteredEmployees?.length === 0 ? (
<div className="flex flex-col justify-center items-center h-64 text-muted-foreground">
<p>No employees found matching your filters.</p>
{(searchQuery || roleFilter !== null) && (
<Button
variant="link"
onClick={resetFilters}
className="mt-2"
>
Reset Filters
</Button>
)}
</div>
) : (
<>
{/* Mobile/Cards View */}
{(isMobile || viewMode === 'cards') && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredEmployees?.map((employee: Employee) => (
<MobileEmployeeCard
key={employee.id}
employee={employee}
getRoleName={getRoleName}
getRoleBadgeVariant={getRoleBadgeVariant}
onView={(id) => navigate(`/employee/${id}`)}
onEdit={(id) => navigate(`/employee-edit/${id}`)}
onDelete={(id) => handleDeleteEmployee(id)}
onStatusChange={handleStatusUpdate}
onReportingChange={handleReportingUpdate}
isUpdating={statusUpdateMutation.isPending && statusUpdateMutation.variables?.id === employee.id}
getInitials={getInitials}
employees={employees || []}
/>
))}
</div>
)}
{/* Desktop Table View */}
{!isMobile && viewMode === 'table' && (
<div className="rounded-md border overflow-x-auto overflow-y-visible">
<Table className="min-w-[800px]">
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow>
<TableHead>Employee</TableHead>
<TableHead>Contact</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead>Reporting To</TableHead>
<TableHead className="text-right sticky right-0 bg-background">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredEmployees?.map((employee: Employee) => (
<TableRow key={employee.id} className="group">
<TableCell>
<div className="flex items-center gap-3">
<Avatar className="h-9 w-9">
<AvatarImage src={null} alt={`${employee.firstName} ${employee.lastName}`} />
<AvatarFallback className="bg-primary/10 text-primary">
{getInitials(employee.firstName, employee.lastName)}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">
{employee.firstName} {employee.lastName}
</div>
<div className="text-sm text-muted-foreground">
{employee.empCode}
</div>
</div>
</div>
</TableCell>
<TableCell>
<div>
<div className="text-sm">{employee.email}</div>
<div className="text-sm text-muted-foreground">{employee.mobile}</div>
</div>
</TableCell>
<TableCell>
<Badge variant={getRoleBadgeVariant(employee.roleId)}>
{getRoleName(employee.roleId)}
</Badge>
</TableCell>
<TableCell>
<StatusDropdown
employee={employee}
onStatusChange={handleStatusUpdate}
isUpdating={statusUpdateMutation.isPending && statusUpdateMutation.variables?.id === employee.id}
/>
</TableCell>
<TableCell>
<ReportingDropdown
employee={employee}
employees={employees || []}
onReportingChange={handleReportingUpdate}
isUpdating={reportingUpdateMutation.isPending && reportingUpdateMutation.variables?.id === employee.id}
/>
</TableCell>
<TableCell className="text-right sticky right-0 bg-background">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => navigate(`/employee/${employee.id}`)}
className="hidden sm:flex items-center group-hover:flex"
>
<Eye className="h-4 w-4 mr-1" />
View
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => navigate(`/employee-edit/${employee.id}`)}
className="hidden sm:flex items-center group-hover:flex"
>
<Edit className="h-4 w-4 mr-1" />
Edit
</Button>
<MobileActionsDropdown
employee={employee}
onView={(id) => navigate(`/employee/${id}`)}
onEdit={(id) => navigate(`/employee-edit/${id}`)}
onDelete={(id) => handleDeleteEmployee(id)}
/>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</>
)}
</CardContent>
</Card>
{/* Mobile Add Employee FAB */}
<div className="fixed bottom-4 right-4 sm:bottom-6 sm:right-6 lg:hidden z-10">
<Button
size="lg"
className="h-12 w-12 sm:h-14 sm:w-14 rounded-full shadow-lg"
onClick={() => navigate("/employee-register")}
>
<UserPlus className="h-5 w-5 sm:h-6 sm:w-6" />
<span className="sr-only">Add Employee</span>
</Button>
</div>
</div>
</main>
{/* Overlay for mobile sidebar */}
{isSidebarOpen && isMobile && (
<div
className="fixed inset-0 bg-black/50 z-30"
onClick={() => setIsSidebarOpen(false)}
/>
)}
</div>
);
};
export default Employees;