import React, { useState, useMemo } from "react"; import { base44 } from "@/api/base44Client"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Progress } from "@/components/ui/progress"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { FileText, Search, Plus, Clock, CheckCircle2, XCircle, AlertTriangle, Upload, Eye, Download, Activity, Filter, Users, ChevronRight } from "lucide-react"; import { format, differenceInDays, parseISO } from "date-fns"; import { useToast } from "@/components/ui/use-toast"; const DOCUMENT_TYPES = [ "W-4 Form", "I-9 Form", "State Tax Form", "Direct Deposit", "ID Copy", "SSN Card", "Work Permit" ]; const STATUS_CONFIG = { uploaded: { color: "bg-cyan-400", icon: FileText, label: "Uploaded" }, pending: { color: "bg-amber-300", icon: Clock, label: "Pending" }, expiring: { color: "bg-yellow-400", icon: Clock, label: "Expiring" }, expired: { color: "bg-red-400", icon: XCircle, label: "Expired" }, rejected: { color: "bg-red-500", icon: XCircle, label: "Rejected" }, missing: { color: "bg-slate-200", icon: Plus, label: "Missing" }, }; export default function EmployeeDocuments() { const { toast } = useToast(); const queryClient = useQueryClient(); const [searchTerm, setSearchTerm] = useState(""); const [showUploadModal, setShowUploadModal] = useState(false); const [selectedCell, setSelectedCell] = useState(null); const [showActivityPanel, setShowActivityPanel] = useState(false); const [docTypeFilter, setDocTypeFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all"); const { data: user } = useQuery({ queryKey: ['current-user-docs'], queryFn: () => base44.auth.me(), }); const { data: documents = [] } = useQuery({ queryKey: ['employee-documents'], queryFn: () => base44.entities.EmployeeDocument.list(), initialData: [], }); const { data: staff = [] } = useQuery({ queryKey: ['staff-for-docs'], queryFn: () => base44.entities.Staff.list(), initialData: [], }); const userRole = user?.user_role || user?.role || "admin"; const isVendor = userRole === "vendor"; // Filter staff by vendor const filteredStaff = useMemo(() => { let result = staff; if (isVendor && user?.vendor_id) { result = result.filter(s => s.vendor_id === user.vendor_id); } if (searchTerm) { result = result.filter(s => s.employee_name?.toLowerCase().includes(searchTerm.toLowerCase()) ); } return result; }, [staff, isVendor, user, searchTerm]); // Build document matrix const documentMatrix = useMemo(() => { const matrix = {}; filteredStaff.forEach(emp => { matrix[emp.id] = { employee: emp, documents: {}, completionRate: 0, }; DOCUMENT_TYPES.forEach(type => { matrix[emp.id].documents[type] = null; }); }); documents.forEach(doc => { if (matrix[doc.employee_id]) { // Calculate status based on expiry let status = doc.status || "uploaded"; if (doc.expiry_date) { const days = differenceInDays(parseISO(doc.expiry_date), new Date()); if (days < 0) status = "expired"; else if (days <= 30) status = "expiring"; } matrix[doc.employee_id].documents[doc.document_type] = { ...doc, status }; } }); // Calculate completion rates Object.values(matrix).forEach(row => { const uploaded = Object.values(row.documents).filter(d => d && d.status === "uploaded").length; row.completionRate = Math.round((uploaded / DOCUMENT_TYPES.length) * 100); }); return matrix; }, [filteredStaff, documents]); // Stats const stats = useMemo(() => { let total = 0, uploaded = 0, pending = 0, expiring = 0, expired = 0; Object.values(documentMatrix).forEach(row => { Object.values(row.documents).forEach(doc => { total++; if (!doc) return; if (doc.status === "uploaded") uploaded++; else if (doc.status === "pending") pending++; else if (doc.status === "expiring") expiring++; else if (doc.status === "expired") expired++; }); }); const missing = total - uploaded - pending - expiring - expired; return { total, uploaded, pending, expiring, expired, missing }; }, [documentMatrix]); // Save document const saveMutation = useMutation({ mutationFn: async (data) => { if (data.id) { return base44.entities.EmployeeDocument.update(data.id, data); } return base44.entities.EmployeeDocument.create(data); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['employee-documents'] }); setShowUploadModal(false); setSelectedCell(null); toast({ title: "✅ Document saved" }); }, }); const handleCellClick = (employeeId, docType, existingDoc) => { const emp = documentMatrix[employeeId]?.employee; setSelectedCell({ employee_id: employeeId, employee_name: emp?.employee_name, vendor_id: emp?.vendor_id, vendor_name: emp?.vendor_name, document_type: docType, ...existingDoc, }); setShowUploadModal(true); }; const renderCell = (doc, employeeId, docType) => { const status = doc?.status || "missing"; const config = STATUS_CONFIG[status]; const daysUntilExpiry = doc?.expiry_date ? differenceInDays(parseISO(doc.expiry_date), new Date()) : null; const isExpirableDoc = docType === "Work Permit" || docType === "ID Copy"; return ( ); }; // Calculate column completion percentages const columnStats = useMemo(() => { const stats = {}; DOCUMENT_TYPES.forEach(type => { const total = Object.keys(documentMatrix).length; const uploaded = Object.values(documentMatrix).filter( row => row.documents[type]?.status === "uploaded" ).length; stats[type] = total > 0 ? Math.round((uploaded / total) * 100) : 0; }); return stats; }, [documentMatrix]); // Filter matrix by status and doc type const filteredMatrix = useMemo(() => { let result = { ...documentMatrix }; // Filter by status if (statusFilter !== "all") { result = Object.fromEntries( Object.entries(result).filter(([empId, row]) => { const docs = Object.values(row.documents); if (statusFilter === "uploaded") return docs.some(d => d?.status === "uploaded"); if (statusFilter === "expiring") return docs.some(d => d?.status === "expiring"); if (statusFilter === "expired") return docs.some(d => d?.status === "expired"); if (statusFilter === "missing") return docs.some(d => !d); if (statusFilter === "pending") return docs.some(d => d?.status === "pending"); return true; }) ); } return result; }, [documentMatrix, statusFilter, docTypeFilter]); return (
{/* Header */}

Employee Documents

Track and manage all required documents

{filteredStaff.slice(0, 3).map((emp, i) => ( {emp.employee_name?.charAt(0)} ))} {filteredStaff.length > 3 && (
+{filteredStaff.length - 3}
)}
{/* Stats Bar - Clickable */}
setStatusFilter('all')} >

{filteredStaff.length}

Employees

setStatusFilter('uploaded')} >

{stats.uploaded}

Uploaded

setStatusFilter('pending')} >

{stats.pending}

Pending

setStatusFilter('expiring')} >

{stats.expiring}

Expiring

setStatusFilter('expired')} >

{stats.expired}

Expired

setStatusFilter('missing')} >

{stats.missing}

Missing

{/* Main Document Matrix */} {/* Search & Filter Bar */}
setSearchTerm(e.target.value)} className="pl-10 bg-slate-50 border-0 h-10" />
{/* Document Type Filter */} {/* Status Filter */} {(statusFilter !== "all" || docTypeFilter !== "all") && ( )}
{/* Matrix Table */}
{DOCUMENT_TYPES.map(type => ( ))} {Object.entries(filteredMatrix).map(([empId, row], idx) => ( {DOCUMENT_TYPES.map(type => ( ))} ))}
Users

{type}

= 25 ? 'bg-cyan-400' : 'bg-slate-200'}`} />
= 50 ? 'bg-amber-400' : 'bg-slate-200'}`} />
= 75 ? 'bg-red-400' : 'bg-slate-200'}`} />
= 100 ? 'bg-green-400' : 'bg-slate-200'}`} />
{row.employee.employee_name?.charAt(0)}

{row.employee.employee_name}

{row.completionRate}%
{renderCell(row.documents[type], empId, type)} {row.completionRate === 100 && (
)}
{filteredStaff.length === 0 && (

No employees found

)}
{/* Footer */}

to make sure everything is always up to date

{/* Upload Modal */} {selectedCell?.id ? 'Update' : 'Upload'} Document {selectedCell && ( saveMutation.mutate(data)} onCancel={() => { setShowUploadModal(false); setSelectedCell(null); }} isLoading={saveMutation.isPending} /> )} {/* Activity Panel */} {showActivityPanel && (

Recent Activity

{[1,2,3,4,5].map(i => (

Document uploaded

W-4 Form • 2 hours ago

))}
)}
); } function DocumentUploadForm({ data, onSave, onCancel, isLoading }) { const [formData, setFormData] = useState({ employee_id: data.employee_id || "", employee_name: data.employee_name || "", vendor_id: data.vendor_id || "", vendor_name: data.vendor_name || "", document_type: data.document_type || "", status: data.status || "uploaded", expiry_date: data.expiry_date || "", document_url: data.document_url || "", notes: data.notes || "", ...data, }); const [uploading, setUploading] = useState(false); const handleFileUpload = async (e) => { const file = e.target.files?.[0]; if (!file) return; setUploading(true); try { const result = await base44.integrations.Core.UploadFile({ file }); setFormData(prev => ({ ...prev, document_url: result.file_url, status: "uploaded" })); } catch (error) { console.error("Upload failed:", error); } setUploading(false); }; return (

Employee

{formData.employee_name}

{formData.document_type}

setFormData(prev => ({ ...prev, expiry_date: e.target.value }))} className="mt-1.5" />
); }