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
All Good
Current
Almost Time to Renew
Expiring Soon
Action Required Now
Expired
Compliance Health
Score
{expiringSoonCount} certifications expire in the next 30 days. Would you like to bulk-notify affected workers?
| 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} )} |
|
{getValidationBadge(cert)} | {getStatusBadge(cert.status, cert.days_until_expiry)} |
|
{cert.expert_body || cert.issuer || '—'} |