From 89a882fb149edf5401ed9a5feb29db4b9f2ed5b8 Mon Sep 17 00:00:00 2001 From: dhinesh-m24 Date: Wed, 11 Feb 2026 11:06:19 +0530 Subject: [PATCH 1/5] feat: Implement compliance dashboard for administrators --- .../compliance/ComplianceDashboard.tsx | 316 ++++++++++++++++++ apps/web/src/routes.tsx | 3 + 2 files changed, 319 insertions(+) create mode 100644 apps/web/src/features/workforce/compliance/ComplianceDashboard.tsx diff --git a/apps/web/src/features/workforce/compliance/ComplianceDashboard.tsx b/apps/web/src/features/workforce/compliance/ComplianceDashboard.tsx new file mode 100644 index 00000000..a2e4af32 --- /dev/null +++ b/apps/web/src/features/workforce/compliance/ComplianceDashboard.tsx @@ -0,0 +1,316 @@ +import { useMemo, useState } from "react"; +import { Badge } from "@/common/components/ui/badge"; +import { Button } from "@/common/components/ui/button"; +import { Card, CardContent } from "@/common/components/ui/card"; +import { Input } from "@/common/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/common/components/ui/select"; +import DashboardLayout from "@/features/layouts/DashboardLayout"; +import { useListCertificates, useListStaff, useListTaxForms } from "@/dataconnect-generated/react"; +import { dataConnect } from "@/features/auth/firebase"; +import { format } from "date-fns"; +import { AnimatePresence, motion } from "framer-motion"; +import { + CheckCircle2, + Download, + FileText, + Search, + Users, + XCircle, + Clock +} from "lucide-react"; + + +export default function ComplianceDashboard() { + const [searchTerm, setSearchTerm] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + + const { data: staffData, isLoading: staffLoading } = useListStaff(dataConnect); + const { data: taxFormsData, isLoading: taxFormsLoading } = useListTaxForms(dataConnect); + const { data: certificatesData, isLoading: certificatesLoading } = useListCertificates(dataConnect); + + const staff = staffData?.staffs || []; + const taxForms = taxFormsData?.taxForms || []; + const certificates = certificatesData?.certificates || []; + + const isLoading = staffLoading || taxFormsLoading || certificatesLoading; + + // Build compliance matrix + const complianceMatrix = useMemo(() => { + return staff.map(s => { + const staffTaxForms = taxForms.filter(tf => tf.staffId === s.id); + const i9 = staffTaxForms.find(tf => tf.formType === 'I9'); + const w4 = staffTaxForms.find(tf => tf.formType === 'W4'); + const staffCerts = certificates.filter(c => c.staffId === s.id); + + const hasExpiredCerts = staffCerts.some(c => c.status === 'EXPIRED'); + const hasExpiringCerts = staffCerts.some(c => c.status === 'EXPIRING_SOON' || c.status === 'EXPIRING'); + + let certsStatus = 'MISSING'; + if (staffCerts.length > 0) { + if (hasExpiredCerts) certsStatus = 'EXPIRED'; + else if (hasExpiringCerts) certsStatus = 'EXPIRING'; + else certsStatus = 'CURRENT'; + } + + const isI9Compliant = i9?.status === 'APPROVED'; + const isW4Compliant = w4?.status === 'APPROVED'; + const isCertsCompliant = staffCerts.length > 0 && !hasExpiredCerts; + + const isCompliant = isI9Compliant && isW4Compliant && isCertsCompliant; + + return { + id: s.id, + fullName: s.fullName, + i9Status: i9?.status || 'MISSING', + w4Status: w4?.status || 'MISSING', + certsStatus, + isCompliant, + hasExpiringCerts, + hasExpiredCerts + }; + }); + }, [staff, taxForms, certificates]); + + // Filtered matrix + const filteredMatrix = useMemo(() => { + let result = complianceMatrix; + + if (searchTerm) { + result = result.filter(row => + row.fullName.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + + if (statusFilter === 'compliant') { + result = result.filter(row => row.isCompliant); + } else if (statusFilter === 'non-compliant') { + result = result.filter(row => !row.isCompliant); + } else if (statusFilter === 'expiring') { + result = result.filter(row => row.hasExpiringCerts); + } else if (statusFilter === 'missing') { + result = result.filter(row => row.i9Status === 'MISSING' || row.w4Status === 'MISSING' || row.certsStatus === 'MISSING'); + } + + return result; + }, [complianceMatrix, searchTerm, statusFilter]); + + // Stats + const stats = useMemo(() => { + const total = complianceMatrix.length; + const compliant = complianceMatrix.filter(r => r.isCompliant).length; + const missingDocs = complianceMatrix.reduce((acc, r) => { + if (r.i9Status === 'MISSING') acc++; + if (r.w4Status === 'MISSING') acc++; + return acc; + }, 0); + const expiringCerts = certificates.filter(c => c.status === 'EXPIRING_SOON' || c.status === 'EXPIRING').length; + const complianceRate = total > 0 ? Math.round((compliant / total) * 100) : 0; + + return { total, compliant, missingDocs, expiringCerts, complianceRate }; + }, [complianceMatrix, certificates]); + + const handleExport = () => { + const headers = ["Staff Name", "I-9 Status", "W-4 Status", "Certifications Status", "Overall Compliance"]; + const rows = filteredMatrix.map(r => [ + r.fullName, + r.i9Status, + r.w4Status, + r.certsStatus, + r.isCompliant ? "Compliant" : "Non-Compliant" + ]); + + const csvContent = [headers, ...rows].map(e => e.join(",")).join("\n"); + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement("a"); + const url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute("download", `compliance_report_${format(new Date(), 'yyyy-MM-dd')}.csv`); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const getStatusBadge = (status: string) => { + switch (status) { + case 'APPROVED': + case 'CURRENT': + return Current; + case 'EXPIRING': + case 'EXPIRING_SOON': + return Expiring; + case 'EXPIRED': + return Expired; + case 'MISSING': + case 'NOT_STARTED': + return Missing; + case 'PENDING': + case 'SUBMITTED': + return Pending; + default: + return {status}; + } + }; + + return ( + +
+ {/* Stats Bar */} +
+ + +
+ +
+
+

{stats.complianceRate}%

+

Compliance Rate

+
+
+
+ + + +
+ +
+
+

{stats.missingDocs}

+

Missing Docs

+
+
+
+ + + +
+ +
+
+

{stats.expiringCerts}

+

Expiring Certs

+
+
+
+ + + +
+ +
+
+

{stats.total}

+

Total Staff

+
+
+
+
+ + {/* Filters & Actions */} +
+
+ setSearchTerm(e.target.value)} + leadingIcon={} + className="max-w-xs" + /> + +
+ +
+ + {/* Table */} + + +
+ + + + + + + + + + + + + {filteredMatrix.map((row) => ( + + + + + + + + ))} + + +
Staff NameI-9 StatusW-4 StatusCertificationsOverall
+
+
+ {row.fullName.charAt(0)} +
+ {row.fullName} +
+
+ {getStatusBadge(row.i9Status)} + + {getStatusBadge(row.w4Status)} + + {getStatusBadge(row.certsStatus)} + + {row.isCompliant ? ( +
+ + COMPLIANT +
+ ) : ( +
+ + NON-COMPLIANT +
+ )} +
+ {filteredMatrix.length === 0 && !isLoading && ( +
+ No staff members found matching the criteria. +
+ )} + {isLoading && ( +
+ Loading compliance data... +
+ )} +
+
+
+
+
+ ); +} diff --git a/apps/web/src/routes.tsx b/apps/web/src/routes.tsx index ac32b07a..84aa8276 100644 --- a/apps/web/src/routes.tsx +++ b/apps/web/src/routes.tsx @@ -27,6 +27,8 @@ import TaskBoard from './features/operations/tasks/TaskBoard'; import InvoiceList from './features/finance/invoices/InvoiceList'; import InvoiceDetail from './features/finance/invoices/InvoiceDetail'; import InvoiceEditor from './features/finance/invoices/InvoiceEditor'; +import ComplianceDashboard from './features/workforce/compliance/ComplianceDashboard'; + /** * AppRoutes Component @@ -96,6 +98,7 @@ const AppRoutes: React.FC = () => { } /> } /> } /> + } /> {/* Business Routes */} } /> } /> From 1361253731988f6426a6d3ce44e76902116f669f Mon Sep 17 00:00:00 2001 From: dhinesh-m24 Date: Wed, 11 Feb 2026 15:39:33 +0530 Subject: [PATCH 2/5] feat: Implement document vault for administrators --- apps/web/src/common/config/navigation.ts | 2 +- .../workforce/documents/DocumentVault.tsx | 608 ++++++++++++++++++ apps/web/src/routes.tsx | 3 +- 3 files changed, 611 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/features/workforce/documents/DocumentVault.tsx diff --git a/apps/web/src/common/config/navigation.ts b/apps/web/src/common/config/navigation.ts index c74bd4c2..ede3d92a 100644 --- a/apps/web/src/common/config/navigation.ts +++ b/apps/web/src/common/config/navigation.ts @@ -165,7 +165,7 @@ export const NAV_CONFIG: NavGroup[] = [ label: 'Documents', path: '/documents', icon: FileText, - allowedRoles: ['Vendor', 'Admin'], + allowedRoles: ['Admin','Vendor'], }, ], }, diff --git a/apps/web/src/features/workforce/documents/DocumentVault.tsx b/apps/web/src/features/workforce/documents/DocumentVault.tsx new file mode 100644 index 00000000..fc147dbf --- /dev/null +++ b/apps/web/src/features/workforce/documents/DocumentVault.tsx @@ -0,0 +1,608 @@ +import { Button } from "@/common/components/ui/button"; +import { Card, CardContent } from "@/common/components/ui/card"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/common/components/ui/dialog"; +import { Input } from "@/common/components/ui/input"; +import { Label } from "@/common/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/common/components/ui/select"; +import { Textarea } from "@/common/components/ui/textarea"; +import { Alert, AlertDescription } from "@/common/components/ui/alert"; +import DashboardLayout from "@/features/layouts/DashboardLayout"; +import { useQueryClient } from "@tanstack/react-query"; +import { differenceInDays, format } from "date-fns"; +import { AnimatePresence, motion } from "framer-motion"; +import { + CheckCircle2, + Clock, + FileText, + Loader2, + Plus, + Search, + Upload, + Users, + Download, + ShieldCheck, + Eye, + Calendar, + XCircle, + AlertCircle, + X, + FileCheck, + Flag +} from "lucide-react"; +import { useMemo, useState } from "react"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/store/store"; +import { + useListStaff, + useListDocuments, + useCreateStaffDocument, + useUpdateStaffDocument +} from "@/dataconnect-generated/react"; +import { DocumentStatus, DocumentType } from "@/dataconnect-generated"; +import { dataConnect } from "@/features/auth/firebase"; +import { useToast } from "@/common/components/ui/use-toast"; +import { getStorage, ref, uploadBytesResumable, getDownloadURL } from "firebase/storage"; + +const DOCUMENT_TYPE_LABELS: Record = { + [DocumentType.W4_FORM]: "W-4 FORM", + [DocumentType.I9_FORM]: "I-9 FORM", + [DocumentType.STATE_TAX_FORM]: "STATE TAX FORM", + [DocumentType.DIRECT_DEPOSIT]: "DIRECT DEPOSIT", + [DocumentType.ID_COPY]: "ID COPY", + [DocumentType.SSN_CARD]: "SSN CARD", + [DocumentType.WORK_PERMIT]: "WORK PERMIT", +}; + +export default function DocumentVault() { + const [searchTerm, setSearchTerm] = useState(""); + const [showUploadModal, setShowUploadModal] = useState(false); + const [showVerificationModal, setShowVerificationModal] = useState(false); + const [selectedCell, setSelectedCell] = useState(null); + const [docTypeFilter, setDocTypeFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState("all"); + const [uploadProgress, setUploadProgress] = useState(0); + const [uploading, setUploading] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [expiryDate, setExpiryDate] = useState(""); + const [verificationNotes, setVerificationNotes] = useState(""); + const [flagReason, setFlagReason] = useState(""); + + const { toast } = useToast(); + const queryClient = useQueryClient(); + const user = useSelector((state: RootState) => state.auth.user); + const storage = getStorage(); + + // Data Connect Queries + const { data: staffData, isLoading: staffLoading } = useListStaff(dataConnect); + const { data: docTypesData } = useListDocuments(dataConnect); + + const staff = useMemo(() => staffData?.staffs || [], [staffData]); + const staffDocuments = useMemo(() => { + return staff.flatMap(s => (s as any).staffDocuments_on_staff || []); + }, [staff]); + + const availableDocTypes = useMemo(() => { + const dbDocs = docTypesData?.documents || []; + return Object.entries(DOCUMENT_TYPE_LABELS).map(([type, label]) => { + // Find the template document from DB for this type + const dbDoc = dbDocs.find(d => d.documentType === type); + return { + id: dbDoc?.id || type, + name: label, + documentType: type, + dbDoc: dbDoc + }; + }); + }, [docTypesData]); + + // Mutations + const { mutateAsync: createDoc } = useCreateStaffDocument(dataConnect); + const { mutateAsync: updateDoc } = useUpdateStaffDocument(dataConnect); + + const userRole = user?.userRole?.toLowerCase() || "admin"; + const isVendor = userRole === "vendor"; + const isAdmin = userRole === "admin"; + + // Filter staff by vendor + const filteredStaff = useMemo(() => { + let result = [...staff]; + if (isVendor && user?.uid) { + result = result.filter(s => s.ownerId === user.uid || s.createdBy === user.email); + } + if (searchTerm) { + result = result.filter(s => + s.fullName?.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + return result; + }, [staff, isVendor, user, searchTerm]); + + // Build document matrix + const documentMatrix = useMemo(() => { + const matrix: Record = {}; + + filteredStaff.forEach(emp => { + matrix[emp.id] = { + employee: emp, + documents: {}, + completionRate: 0, + }; + + availableDocTypes.forEach(type => { + matrix[emp.id].documents[type.documentType] = null; + }); + }); + + staffDocuments.forEach(doc => { + if (matrix[doc.staffId]) { + // Calculate status based on expiry + let status = doc.status; + if (doc.expiryDate && status !== DocumentStatus.VERIFIED) { + const days = differenceInDays(new Date(doc.expiryDate), new Date()); + if (days < 0) status = DocumentStatus.EXPIRING; + else if (days <= 30) status = DocumentStatus.EXPIRING; + } + + // Map by documentType if available, otherwise fallback to documentId + const docType = doc.document?.documentType || + availableDocTypes.find(t => t.id === doc.documentId)?.documentType; + + if (docType) { + matrix[doc.staffId].documents[docType] = { ...doc, status }; + } + } + }); + + // Calculate completion rates + Object.values(matrix).forEach(row => { + const uploaded = Object.values(row.documents).filter((d: any) => + d && (d.status === DocumentStatus.UPLOADED || d.status === DocumentStatus.VERIFIED) + ).length; + row.completionRate = availableDocTypes.length > 0 ? Math.round((uploaded / availableDocTypes.length) * 100) : 0; + }); + + return matrix; + }, [filteredStaff, staffDocuments, availableDocTypes]); + + // Stats + const stats = useMemo(() => { + let uploaded = 0, pending = 0, expiring = 0, expired = 0, missing = 0; + + Object.values(documentMatrix).forEach(row => { + Object.values(row.documents).forEach((doc: any) => { + if (!doc) { + missing++; + return; + } + if (doc.status === DocumentStatus.UPLOADED || doc.status === DocumentStatus.VERIFIED) { + uploaded++; + } else if (doc.status === DocumentStatus.PENDING) { + pending++; + } else if (doc.status === DocumentStatus.EXPIRING) { + // Check if actually expired + if (doc.expiryDate && differenceInDays(new Date(doc.expiryDate), new Date()) < 0) { + expired++; + } else { + expiring++; + } + } + }); + }); + + return { uploaded, pending, expiring, expired, missing }; + }, [documentMatrix]); + + // Filter matrix by status + const filteredMatrix = useMemo(() => { + let result = { ...documentMatrix }; + + if (statusFilter !== "all") { + result = Object.fromEntries( + Object.entries(result).filter(([_, row]) => { + const docs = Object.values(row.documents); + if (statusFilter === "uploaded") return docs.some((d: any) => d?.status === DocumentStatus.UPLOADED || d?.status === DocumentStatus.VERIFIED); + if (statusFilter === "pending") return docs.some((d: any) => d?.status === DocumentStatus.PENDING); + if (statusFilter === "expiring") return docs.some((d: any) => { + if (d?.status === DocumentStatus.EXPIRING && d?.expiryDate) { + const days = differenceInDays(new Date(d.expiryDate), new Date()); + return days >= 0; + } + return false; + }); + if (statusFilter === "expired") return docs.some((d: any) => { + if (d?.status === DocumentStatus.EXPIRING && d?.expiryDate) { + const days = differenceInDays(new Date(d.expiryDate), new Date()); + return days < 0; + } + return false; + }); + if (statusFilter === "missing") return docs.some((d: any) => !d || d.status === DocumentStatus.MISSING); + return true; + }) + ); + } + + return result; + }, [documentMatrix, statusFilter]); + + const handleCellClick = (staffId: string, documentId: string, existingDoc: any) => { + const emp = documentMatrix[staffId]?.employee; + const docTypeInfo = availableDocTypes.find(t => t.documentType === documentId || t.id === documentId); + setSelectedCell({ + id: existingDoc?.id, + staffId: staffId, + staffName: emp?.fullName, + documentId: docTypeInfo?.id || documentId, + documentTypeName: docTypeInfo?.name, + ...existingDoc, + }); + setExpiryDate(existingDoc?.expiryDate || ""); + setShowUploadModal(true); + }; + + const handleFileSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png']; + if (!allowedTypes.includes(file.type)) { + toast({ title: "Invalid file type", description: "Please upload PDF, JPG, or PNG files only", variant: "destructive" }); + return; + } + if (file.size > 5 * 1024 * 1024) { + toast({ title: "File too large", description: "Please upload files smaller than 5MB", variant: "destructive" }); + return; + } + setSelectedFile(file); + } + }; + + const handleUpload = async () => { + if (!selectedFile || !selectedCell) return; + setUploading(true); + setUploadProgress(0); + try { + const fileName = `documents/${selectedCell.staffId}/${selectedCell.documentId}/${Date.now()}_${selectedFile.name}`; + const storageRef = ref(storage, fileName); + const uploadTask = uploadBytesResumable(storageRef, selectedFile); + uploadTask.on('state_changed', (snapshot) => { + const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; + setUploadProgress(progress); + }, (error) => { + toast({ title: "Upload failed", description: error.message, variant: "destructive" }); + setUploading(false); + }, async () => { + const downloadURL = await getDownloadURL(uploadTask.snapshot.ref); + if (selectedCell.id) { + await updateDoc({ + staffId: selectedCell.staffId, + documentId: selectedCell.documentId, + status: DocumentStatus.UPLOADED, + documentUrl: downloadURL, + expiryDate: expiryDate || null + }); + toast({ title: "Success", description: "Document updated successfully" }); + } else { + await createDoc({ + staffId: selectedCell.staffId, + staffName: selectedCell.staffName, + documentId: selectedCell.documentId, + status: DocumentStatus.UPLOADED, + documentUrl: downloadURL, + expiryDate: expiryDate || null + }); + toast({ title: "Success", description: "Document uploaded successfully" }); + } + queryClient.invalidateQueries(); + setShowUploadModal(false); + setSelectedCell(null); + setSelectedFile(null); + setExpiryDate(""); + setUploadProgress(0); + setUploading(false); + }); + } catch (error: any) { + toast({ title: "Error", description: error.message, variant: "destructive" }); + setUploading(false); + } + }; + + const handleVerify = async () => { + if (!selectedCell?.id) return; + setShowUploadModal(false); + setShowVerificationModal(true); + }; + + const confirmVerification = async () => { + if (!selectedCell?.id) return; + try { + await updateDoc({ + staffId: selectedCell.staffId, + documentId: selectedCell.documentId, + status: DocumentStatus.VERIFIED + }); + toast({ title: "Document Verified", description: `Document verified. ${verificationNotes ? 'Notes recorded.' : ''}` }); + queryClient.invalidateQueries(); + setShowVerificationModal(false); + setShowUploadModal(false); + setVerificationNotes(""); + setSelectedCell(null); + } catch (error: any) { + toast({ title: "Error", description: error.message, variant: "destructive" }); + } + }; + + const handleFlagIssue = async () => { + if (!selectedCell?.id || !flagReason) { + toast({ title: "Missing Information", description: "Please provide a reason for flagging this document", variant: "destructive" }); + return; + } + try { + await updateDoc({ + staffId: selectedCell.staffId, + documentId: selectedCell.documentId, + status: DocumentStatus.PENDING + }); + toast({ title: "Document Flagged", description: `Document flagged: ${flagReason}.` }); + queryClient.invalidateQueries(); + setShowUploadModal(false); + setFlagReason(""); + setSelectedCell(null); + } catch (error: any) { + toast({ title: "Error", description: error.message, variant: "destructive" }); + } + }; + + const renderCell = (doc: any, staffId: string, documentId: string) => { + const status = doc?.status || DocumentStatus.MISSING; + const daysUntilExpiry = doc?.expiryDate ? differenceInDays(new Date(doc.expiryDate), new Date()) : null; + + return ( + + ); + }; + + if (staffLoading) { + return ( + +
+ +
+
+ ); + } + + return ( + +
+ {/* Stats Bar */} +
+ setStatusFilter('all')}> + +
+
{staff.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

+
+
+
+ + {/* Search & Filter Bar */} +
+
+ + setSearchTerm(e.target.value)} /> +
+ + +
+ + {/* Document Matrix Table */} +
+
+ + + + + {availableDocTypes.map((type, idx) => ( + + ))} + + + + + {Object.entries(filteredMatrix).map(([empId, row]) => ( + + + {availableDocTypes.map(type => ( + + ))} + + ))} + + {Object.keys(filteredMatrix).length === 0 && ( + + )} + +
Employees +
+ {type.name} +
+
+
+
+
{row.employee.fullName?.charAt(0) || '?'}
+
+
+
+ {row.employee.fullName} +
+
+ {row.completionRate}% COMPLETE +
+
+
+
+ {renderCell(row.documents[type.documentType], empId, type.documentType)} +

No employees match your current filters

+
+
+
+ + + + + +
+ {selectedCell?.documentTypeName} +
+

Document management for {selectedCell?.staffName}

+
+
+ {selectedCell?.documentUrl ? ( +
+
+ +

Document Uploaded

{selectedCell.status}

+
+ + +
+
+
+
+ {selectedCell.status === DocumentStatus.VERIFIED ? <>Verified : selectedCell.status === DocumentStatus.UPLOADED ? <>Uploaded : {selectedCell.status}} +
+
+ {selectedCell.expiryDate ? format(new Date(selectedCell.expiryDate), 'MMM d, yyyy') : 'No Expiry'} +
+
+
+ ) : ( +
+
+ +
document.getElementById('file-upload')?.click()}> + {selectedFile ? ( +
+ +

{selectedFile.name}

{(selectedFile.size / 1024 / 1024).toFixed(2)} MB

+ +
+ ) : ( + <>

Click or drag to upload

PDF, JPG or PNG (max 5MB)

+ )} + +
+ {uploading && ( +
+
Uploading...{Math.round(uploadProgress)}%
+
+
+ )} +
+
+ + setExpiryDate(e.target.value)} min={new Date().toISOString().split('T')[0]} /> +
+
+ )} +
+ + {selectedCell?.id ? ( + <> + {isAdmin && selectedCell.status !== DocumentStatus.VERIFIED && ( + + )} + {isAdmin && ( + + )} + + ) : ( + + )} + + +
+ + + +
Verify Document Authenticity
+
+ By verifying this document, you confirm that you have reviewed it for authenticity and validity. +