import React, { useState, useMemo } from "react"; import { base44 } from "@/api/base44Client"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Shield, CheckCircle2, AlertTriangle, XCircle, Plus, Search, Calendar, FileText, Upload, RefreshCw, Download, Award, TrendingUp, Users, Bell, Zap, Loader2, Sparkles, FolderUp, X } from "lucide-react"; import { differenceInDays, format, isBefore } from "date-fns"; import PageHeader from "../components/common/PageHeader"; import { useToast } from "@/components/ui/use-toast"; export default function VendorCompliance() { const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [typeFilter, setTypeFilter] = useState("all"); const [showAddDialog, setShowAddDialog] = useState(false); const [showBulkImportDialog, setShowBulkImportDialog] = useState(false); const [selectedEmployees, setSelectedEmployees] = useState([]); const [employeeSearchTerm, setEmployeeSearchTerm] = useState(""); const [uploading, setUploading] = useState(false); const [validating, setValidating] = useState(false); const [uploadedFileName, setUploadedFileName] = useState(""); const [aiValidationResult, setAiValidationResult] = useState(null); const [bulkProcessing, setBulkProcessing] = useState(false); const [bulkCertificates, setBulkCertificates] = useState([]); const [isDragging, setIsDragging] = useState(false); const [processingProgress, setProcessingProgress] = useState({ current: 0, total: 0 }); const [newCert, setNewCert] = useState({ certification_name: "", certification_type: "Legal", issue_date: "", expiry_date: "", issuer: "", certificate_number: "", document_url: "", owner: "", expert_body: "", is_required_for_role: false }); const queryClient = useQueryClient(); const { toast } = useToast(); const { data: user } = useQuery({ queryKey: ['current-user-compliance'], queryFn: () => base44.auth.me(), }); const { data: certifications = [], isLoading } = useQuery({ queryKey: ['certifications'], queryFn: () => base44.entities.Certification.list('-expiry_date'), initialData: [], }); const { data: staff = [] } = useQuery({ queryKey: ['staff-for-certs'], queryFn: () => base44.entities.Staff.list(), initialData: [], }); const createCertMutation = useMutation({ mutationFn: (certData) => base44.entities.Certification.create(certData), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['certifications'] }); }, }); const resetForm = () => { setNewCert({ certification_name: "", certification_type: "Legal", issue_date: "", expiry_date: "", issuer: "", certificate_number: "", document_url: "", owner: "", expert_body: "", is_required_for_role: false }); setSelectedEmployees([]); setEmployeeSearchTerm(""); setUploadedFileName(""); setAiValidationResult(null); }; const resetBulkImport = () => { setBulkCertificates([]); setBulkProcessing(false); setProcessingProgress({ current: 0, total: 0 }); }; const filteredStaff = useMemo(() => { if (!employeeSearchTerm) return staff; const searchLower = employeeSearchTerm.toLowerCase(); return staff.filter(emp => emp.employee_name?.toLowerCase().includes(searchLower) || emp.position?.toLowerCase().includes(searchLower) || emp.email?.toLowerCase().includes(searchLower) || emp.contact_number?.includes(employeeSearchTerm) ); }, [staff, employeeSearchTerm]); const handleSelectAll = () => { if (selectedEmployees.length === filteredStaff.length && filteredStaff.length > 0) { setSelectedEmployees([]); } else { setSelectedEmployees(filteredStaff.map(s => s.id)); } }; const formatDateForInput = (dateString) => { try { const date = new Date(dateString); if (isNaN(date.getTime())) return ""; return format(date, 'yyyy-MM-dd'); } catch { return ""; } }; const findEmployeeByName = (certificateHolderName) => { if (!certificateHolderName || !staff || staff.length === 0) return null; const nameLower = certificateHolderName.toLowerCase().trim(); let match = staff.find(emp => emp.employee_name?.toLowerCase().trim() === nameLower ); if (match) return { employee: match, confidence: 100 }; const namePartsInput = nameLower.split(' ').filter(p => p.length > 1); match = staff.find(emp => { const empNameLower = emp.employee_name?.toLowerCase().trim(); const empNameParts = empNameLower?.split(' ').filter(p => p.length > 1); if (!empNameParts || empNameParts.length < 2) return false; const firstMatch = namePartsInput[0] === empNameParts[0]; const lastMatch = namePartsInput[namePartsInput.length - 1] === empNameParts[empNameParts.length - 1]; return firstMatch && lastMatch; }); if (match) return { employee: match, confidence: 95 }; if (namePartsInput.length >= 2) { const firstInitial = namePartsInput[0].charAt(0); const lastName = namePartsInput[namePartsInput.length - 1]; match = staff.find(emp => { const empNameLower = emp.employee_name?.toLowerCase().trim(); const empNameParts = empNameLower?.split(' '); if (!empNameParts || empNameParts.length < 2) return false; const empFirstInitial = empNameParts[0].charAt(0); const empLastName = empNameParts[empNameParts.length - 1]; return firstInitial === empFirstInitial && lastName === empLastName; }); if (match) return { employee: match, confidence: 85 }; } if (nameLower.includes(',')) { const parts = nameLower.split(',').map(p => p.trim()); if (parts.length === 2) { const reversedName = `${parts[1]} ${parts[0]}`; match = staff.find(emp => emp.employee_name?.toLowerCase().trim() === reversedName ); if (match) return { employee: match, confidence: 90 }; } } match = staff.find(emp => { const empNameLower = emp.employee_name?.toLowerCase().trim(); if (!empNameLower) return false; return namePartsInput.every(part => empNameLower.includes(part)); }); if (match) return { employee: match, confidence: 75 }; match = staff.find(emp => { const empNameLower = emp.employee_name?.toLowerCase().trim(); return empNameLower?.includes(nameLower) || nameLower.includes(empNameLower || ''); }); if (match) return { employee: match, confidence: 60 }; return null; }; const handleManualEmployeeSelection = (certIndex, employeeId) => { const employee = staff.find(s => s.id === employeeId); if (!employee) return; const updatedCerts = [...bulkCertificates]; updatedCerts[certIndex] = { ...updatedCerts[certIndex], matched_employee: employee, match_confidence: 100, status: 'matched', manual_match: true }; setBulkCertificates(updatedCerts); toast({ title: "Employee Matched!", description: `${employee.employee_name} assigned to ${updatedCerts[certIndex].file_name}`, }); }; const processBulkFiles = async (files) => { setBulkProcessing(true); setProcessingProgress({ current: 0, total: files.length }); toast({ title: `Processing ${files.length} Certificate${files.length > 1 ? 's' : ''}`, description: "AI is analyzing all certificates... This may take a minute.", }); const processedCerts = []; for (let i = 0; i < files.length; i++) { const file = files[i]; setProcessingProgress({ current: i + 1, total: files.length }); try { const { file_url } = await base44.integrations.Core.UploadFile({ file }); const aiResult = await base44.integrations.Core.InvokeLLM({ prompt: `You are a food safety certification expert. Analyze this certificate and extract all information. Extract: 1. Certificate holder name (full name) - BE VERY CAREFUL WITH THE NAME 2. Certificate number or ID 3. Issuing organization 4. Certificate type 5. Issue date 6. Expiration date Validate: - Is this legitimate? - Is it expired? - Confidence score (0-100)`, add_context_from_internet: false, file_urls: file_url, response_json_schema: { type: "object", properties: { certificate_holder_name: { type: "string" }, certificate_number: { type: "string" }, issuing_organization: { type: "string" }, certificate_type: { type: "string" }, issue_date: { type: "string" }, expiration_date: { type: "string" }, is_legitimate: { type: "boolean" }, is_expired: { type: "boolean" }, confidence_score: { type: "number" } } } }); const matchResult = findEmployeeByName(aiResult.certificate_holder_name); processedCerts.push({ file_name: file.name, document_url: file_url, ai_result: aiResult, matched_employee: matchResult?.employee || null, match_confidence: matchResult?.confidence || 0, status: matchResult?.employee ? 'matched' : 'unmatched', certification_name: aiResult.certificate_type || '', certificate_number: aiResult.certificate_number || '', issuer: aiResult.issuing_organization || '', issue_date: aiResult.issue_date ? formatDateForInput(aiResult.issue_date) : '', expiry_date: aiResult.expiration_date ? formatDateForInput(aiResult.expiration_date) : '', confidence_score: aiResult.confidence_score || 0, extracted_name: aiResult.certificate_holder_name || '', manual_match: false }); } catch (error) { processedCerts.push({ file_name: file.name, status: 'error', error_message: error.message || 'Failed to process' }); } setBulkCertificates([...processedCerts]); } setBulkProcessing(false); setProcessingProgress({ current: 0, total: 0 }); const matchedCount = processedCerts.filter(c => c.status === 'matched').length; const unmatchedCount = processedCerts.filter(c => c.status === 'unmatched').length; const errorCount = processedCerts.filter(c => c.status === 'error').length; toast({ title: "Processing Complete!", description: `✅ ${matchedCount} matched • ⚠️ ${unmatchedCount} need review ${errorCount > 0 ? `• ❌ ${errorCount} errors` : ''}`, }); }; const handleBulkFileUpload = async (event) => { const files = Array.from(event.target.files || []); if (files.length === 0) return; const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png']; const invalidFiles = files.filter(f => !allowedTypes.includes(f.type)); if (invalidFiles.length > 0) { toast({ title: "Invalid File Types", description: `${invalidFiles.length} file(s) skipped. Only PDF and images allowed.`, variant: "destructive", }); } const validFiles = files.filter(f => allowedTypes.includes(f.type) && f.size <= 10 * 1024 * 1024); if (validFiles.length === 0) return; await processBulkFiles(validFiles); }; const handleDragOver = (e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }; const handleDragEnter = (e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }; const handleDragLeave = (e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }; const handleDrop = (e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const files = Array.from(e.dataTransfer.files || []); if (files.length === 0) return; processBulkFiles(files); }; const handleImportMatched = async () => { const certsToImport = bulkCertificates.filter(c => c.status === 'matched' && c.matched_employee && c.certification_name && c.expiry_date ); if (certsToImport.length === 0) { toast({ title: "Nothing to Import", description: "No matched certificates found", variant: "destructive", }); return; } const unmatchedCountBeforeImport = bulkCertificates.filter(c => c.status === 'unmatched').length; toast({ title: `Importing ${certsToImport.length} Certificate${certsToImport.length > 1 ? 's' : ''}...`, description: unmatchedCountBeforeImport > 0 ? `Skipping ${unmatchedCountBeforeImport} unmatched certificate${unmatchedCountBeforeImport > 1 ? 's' : ''}` : "Please wait...", }); for (const cert of certsToImport) { try { const validationStatus = cert.confidence_score >= 80 ? "ai_verified" : cert.confidence_score >= 60 ? "manual_review_needed" : "ai_flagged"; await createCertMutation.mutateAsync({ employee_id: cert.matched_employee.id, employee_name: cert.matched_employee.employee_name, certification_name: cert.certification_name, certification_type: "Safety", certificate_number: cert.certificate_number, issuer: cert.issuer, issue_date: cert.issue_date, expiry_date: cert.expiry_date, document_url: cert.document_url, vendor_id: user?.id, vendor_name: user?.company_name, owner: user?.company_name, expert_body: cert.issuer, validation_status: validationStatus, ai_validation_result: { confidence_score: cert.confidence_score, match_confidence: cert.match_confidence, extracted_data: cert.ai_result, manual_match: cert.manual_match || false, flags: [], recommendations: [] } }); } catch (error) { console.error('Failed to import cert:', error); } } queryClient.invalidateQueries({ queryKey: ['certifications'] }); if (unmatchedCountBeforeImport > 0) { const remainingCerts = bulkCertificates.filter(c => c.status === 'unmatched'); setBulkCertificates(remainingCerts); toast({ title: "Import Complete!", description: `Imported ${certsToImport.length} certificate${certsToImport.length > 1 ? 's' : ''}. ${unmatchedCountBeforeImport} still need matching.`, }); } else { setShowBulkImportDialog(false); resetBulkImport(); toast({ title: "Import Complete!", description: `Successfully imported all ${certsToImport.length} certificate${certsToImport.length > 1 ? 's' : ''}`, }); } }; const validateCertificateWithAI = async (fileUrl) => { setValidating(true); try { const response = await base44.integrations.Core.InvokeLLM({ prompt: `You are a food safety certification expert. Analyze this uploaded certificate document and extract the following information. Extract: 1. Certificate holder name 2. Certificate number or ID 3. Issuing organization 4. Certificate type 5. Issue date 6. Expiration date 7. Any special notes Also validate: - Is this legitimate? - Is it expired? - Confidence score (0-100)`, add_context_from_internet: false, file_urls: fileUrl, response_json_schema: { type: "object", properties: { certificate_holder_name: { type: "string" }, certificate_number: { type: "string" }, issuing_organization: { type: "string" }, certificate_type: { type: "string" }, issue_date: { type: "string" }, expiration_date: { type: "string" }, special_notes: { type: "string" }, is_legitimate: { type: "boolean" }, is_expired: { type: "boolean" }, red_flags: { type: "array", items: { type: "string" } }, recommendations: { type: "array", items: { type: "string" } }, confidence_score: { type: "number" } } } }); setAiValidationResult(response); if (response && response.certificate_type) { setNewCert(prev => ({ ...prev, certification_name: response.certificate_type || prev.certification_name, certificate_number: response.certificate_number || prev.certificate_number, issuer: response.issuing_organization || prev.issuer, issue_date: response.issue_date ? formatDateForInput(response.issue_date) : prev.issue_date, expiry_date: response.expiration_date ? formatDateForInput(response.expiration_date) : prev.expiry_date, expert_body: response.issuing_organization || prev.expert_body })); } if (response && response.confidence_score >= 80 && response.is_legitimate) { toast({ title: "✅ Certificate Verified!", description: `AI validated with ${response.confidence_score}% confidence`, }); } else if (response && response.confidence_score >= 60) { toast({ title: "⚠️ Manual Review Recommended", description: `AI confidence: ${response.confidence_score}%`, }); } else { toast({ title: "🚨 Validation Concerns", description: `Low confidence (${response?.confidence_score || 0}%)`, variant: "destructive", }); } return response; } catch (error) { toast({ title: "Validation Error", description: error.message || "Failed to validate certificate", variant: "destructive", }); setAiValidationResult(null); return null; } finally { setValidating(false); } }; const handleFileUpload = async (event) => { const file = event.target.files?.[0]; if (!file) return; const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png']; if (!allowedTypes.includes(file.type)) { toast({ title: "Invalid File Type", description: "Please upload a PDF or image file", variant: "destructive", }); return; } if (file.size > 10 * 1024 * 1024) { toast({ title: "File Too Large", description: "Please upload a file smaller than 10MB", variant: "destructive", }); return; } setUploading(true); setAiValidationResult(null); try { const { file_url } = await base44.integrations.Core.UploadFile({ file }); setNewCert({ ...newCert, document_url: file_url }); setUploadedFileName(file.name); toast({ title: "File Uploaded", description: "Starting AI validation...", }); await validateCertificateWithAI(file_url); } catch (error) { toast({ title: "Upload Failed", description: error.message || "Failed to upload certificate", variant: "destructive", }); setNewCert({ ...newCert, document_url: "" }); setUploadedFileName(""); } finally { setUploading(false); } }; const enrichedCerts = useMemo(() => { const today = new Date(); return certifications.map(cert => { const expiryDate = new Date(cert.expiry_date); const daysUntilExpiry = differenceInDays(expiryDate, today); let status = cert.status; if (isBefore(expiryDate, today)) { status = "expired"; } else if (daysUntilExpiry <= 30) { status = "expiring_soon"; } else { status = "current"; } return { ...cert, status, days_until_expiry: daysUntilExpiry }; }); }, [certifications]); const currentCount = enrichedCerts.filter(c => c.status === "current").length; const expiringSoonCount = enrichedCerts.filter(c => c.status === "expiring_soon").length; const expiredCount = enrichedCerts.filter(c => c.status === "expired").length; const complianceScore = certifications.length > 0 ? Math.round((currentCount / certifications.length) * 100) : 100; const filteredCerts = enrichedCerts.filter(cert => { const matchesSearch = !searchTerm || cert.employee_name?.toLowerCase().includes(searchTerm.toLowerCase()) || cert.certification_name?.toLowerCase().includes(searchTerm.toLowerCase()) || cert.issuer?.toLowerCase().includes(searchTerm.toLowerCase()); const matchesStatus = statusFilter === "all" || cert.status === statusFilter; const matchesType = typeFilter === "all" || cert.certification_type === typeFilter; return matchesSearch && matchesStatus && matchesType; }); const handleAddCertification = () => { if (selectedEmployees.length === 0) { toast({ title: "No Employees Selected", description: "Please select at least one employee", variant: "destructive", }); return; } if (!newCert.certification_name || !newCert.expiry_date) { toast({ title: "Missing Required Fields", description: "Please fill in certification name and expiry date", variant: "destructive", }); return; } selectedEmployees.forEach(empId => { const employee = staff.find(s => s.id === empId); if (employee) { const validationStatus = aiValidationResult ? (aiValidationResult.confidence_score >= 80 ? "ai_verified" : aiValidationResult.confidence_score >= 60 ? "manual_review_needed" : "ai_flagged") : "pending_expert_review"; createCertMutation.mutate({ ...newCert, employee_id: employee.id, employee_name: employee.employee_name, vendor_id: user?.id, vendor_name: user?.company_name, owner: user?.company_name, validation_status: validationStatus, ai_validation_result: aiValidationResult || null }); } }); setShowAddDialog(false); resetForm(); toast({ title: "Certification Added", description: "New certification has been added successfully", }); }; const getStatusBadge = (status, daysUntilExpiry) => { if (status === "current") { return ✅ Current; } if (status === "expiring_soon") { return ⚠️ Expiring in {daysUntilExpiry}d; } if (status === "expired") { return ❌ Expired; } return Unknown; }; const getValidationBadge = (cert) => { if (!cert.validation_status) return null; if (cert.validation_status === "ai_verified") { return ( AI Verified ); } if (cert.validation_status === "approved") { return ✅ Approved; } if (cert.validation_status === "manual_review_needed") { return ⚠️ Review Needed; } if (cert.validation_status === "ai_flagged") { return 🚨 Flagged; } if (cert.validation_status === "pending_expert_review") { return Pending Review; } return null; }; const getStatusColor = (status) => { if (status === "current") return "text-green-700"; if (status === "expiring_soon") return "text-yellow-700"; return "text-red-700"; }; const matchedCertsCount = bulkCertificates.filter(c => c.status === 'matched').length; const unmatchedCertsCount = bulkCertificates.filter(c => c.status === 'unmatched').length; return (
} />
{currentCount}

All Good

Current

{expiringSoonCount}

Almost Time to Renew

Expiring Soon

{expiredCount}

Action Required Now

Expired

= 90 ? 'bg-green-500' : complianceScore >= 70 ? 'bg-yellow-500' : 'bg-red-500' } text-white text-lg px-3 py-1`}> {complianceScore}%

Compliance Health

Score

{expiringSoonCount > 0 && (

🧠 KROW Whisper AI Insight

{expiringSoonCount} certifications expire in the next 30 days. Would you like to bulk-notify affected workers?

)}
setSearchTerm(e.target.value)} className="pl-10" />
Certification Registry
{filteredCerts.length > 0 ? (
{filteredCerts.map((cert) => ( ))}
Employee Certification Type Validation Status Expiry Date Issuer Action
{cert.employee_name?.charAt(0) || '?'}
{cert.employee_name}

{cert.certification_name}

{cert.certificate_number && (

#{cert.certificate_number}

)}
{cert.certification_type} {getValidationBadge(cert)} {getStatusBadge(cert.status, cert.days_until_expiry)}
{format(new Date(cert.expiry_date), 'MMM dd, yyyy')}
{cert.expert_body || cert.issuer || '—'}
{cert.document_url && ( )}
) : (

No certifications found

)}
Bulk Certificate Import Upload multiple certificates at once - AI will extract data and match to employees automatically
{bulkCertificates.length === 0 && !bulkProcessing ? (

{isDragging ? '📂 Drop Files Here!' : 'Drop Multiple Certificates Here'}

{isDragging ? 'Release to upload certificates' : 'Upload PDF or image files • AI will auto-match to employees by name'}

{!isDragging && ( <>

PDF, JPG, PNG • Max 10MB each

)}
) : bulkProcessing ? (

Processing Certificates...

Please wait while AI analyzes each certificate

{processingProgress.total > 0 && (
Progress: {processingProgress.current} / {processingProgress.total} {Math.round((processingProgress.current / processingProgress.total) * 100)}%
)} {bulkCertificates.length > 0 && (

Preview (processing...)

{bulkCertificates.map((cert, idx) => ( ))}
{cert.file_name} {cert.status === 'matched' ? ( ) : cert.status === 'unmatched' ? ( ) : ( )}
)}
) : (

Processed {bulkCertificates.length} Certificate{bulkCertificates.length !== 1 ? 's' : ''}

✅ {matchedCertsCount} ready to import • ⚠️ {unmatchedCertsCount} need employee selection

{bulkCertificates.map((cert, idx) => ( ))}
File Extracted Name Employee Match Certificate Match % Expiry Status
{cert.file_name} {cert.extracted_name || '—'} {cert.matched_employee ? (
{cert.matched_employee.employee_name} {cert.manual_match && ( Manual )}
{cert.matched_employee.position && (

{cert.matched_employee.position}

)}
) : cert.status === 'error' ? (
Processing error
) : (
Select Employee:
)}
{cert.certification_name || '—'} {cert.match_confidence ? ( = 90 ? 'bg-green-100 text-green-700' : cert.match_confidence >= 75 ? 'bg-blue-100 text-blue-700' : cert.match_confidence >= 60 ? 'bg-yellow-100 text-yellow-700' : 'bg-orange-100 text-orange-700' }`}> {cert.match_confidence}% ) : '—'} {cert.expiry_date || '—'} {cert.status === 'matched' ? ( ✅ Ready ) : cert.status === 'unmatched' ? ( ⚠️ Select Employee ) : ( ❌ Error )}
{unmatchedCertsCount > 0 && matchedCertsCount > 0 && (

💡 Import Options: You have {matchedCertsCount} certificate{matchedCertsCount !== 1 ? 's' : ''} ready to import and {unmatchedCertsCount} that need employee selection.

• Import matched now and manually assign unmatched later
• Or assign all employees first, then import everything together

)}
)}
{bulkCertificates.length > 0 && !bulkProcessing && ( <> {unmatchedCertsCount > 0 && matchedCertsCount > 0 && ( )} )}
Add New Certification
setNewCert({ ...newCert, certification_name: e.target.value })} />
setNewCert({ ...newCert, expiry_date: e.target.value })} />
{staff.map(employee => ( ))}
{ if (e.target.checked) { setSelectedEmployees([...selectedEmployees, employee.id]); } else { setSelectedEmployees(selectedEmployees.filter(id => id !== employee.id)); } }} /> {employee.employee_name}
); }