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 (
Track and manage all required documents
{filteredStaff.length}
Employees
{stats.uploaded}
Uploaded
{stats.pending}
Pending
{stats.expiring}
Expiring
{stats.expired}
Expired
{stats.missing}
Missing
| Users | {DOCUMENT_TYPES.map(type => (
{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}
{row.completionRate}%
|
{DOCUMENT_TYPES.map(type => (
{renderCell(row.documents[type], empId, type)} | ))}
{row.completionRate === 100 && (
|
No employees found
to make sure everything is always up to date
Document uploaded
W-4 Form • 2 hours ago
Employee
{formData.employee_name}
{formData.document_type}