import React, { useState, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { FileText, Download, Eye, Search, Filter, CheckCircle2, XCircle, Clock, AlertTriangle, Flag, FolderKanban, User as UserIcon, ShieldCheck, Hash, Upload, RotateCcw, ExternalLink, Layers, Paperclip, Check, FileSpreadsheet, FileCheck, RefreshCw, Lock, } from 'lucide-react'; import { getAllEvidence, getProjectEvidence, getEvidenceSecureLink, updateEvidence, uploadEvidenceFile, getProjects, getAllMilestones, getActivities, getUsers, } from '../../services/api'; import { Evidence, EvidenceType, ApprovalStatus, Project, Milestone, Activity, User, } from '../../types'; import { formatDate, getEvidenceTypeLabel, getApprovalStatusBadge, getUserEmail, formatZAR, } from '../../lib/formatters'; import { Modal } from '../ui/Modal'; import { useToast } from '../ui/Toast'; import { HelpTooltip } from '../ui/HelpTooltip'; import { ExportButton } from '../ui/ExportButton'; import { ExportColumn } from '../../utils/exportUtils'; import { useRbac } from '../../auth/rbac'; export interface EvidenceReviewQueueProps { initialProjectId?: string | number | 'all'; initialMilestoneId?: string | number | 'all'; initialActivityId?: string | number | 'all'; initialUserId?: string | 'all'; } const EVIDENCE_TYPE_OPTIONS = [ { value: EvidenceType.CompletionCertificate, label: 'Completion Certificate' }, { value: EvidenceType.InspectionReport, label: 'Inspection Report' }, { value: EvidenceType.AttendanceRegister, label: 'Attendance Register' }, { value: EvidenceType.FinancialInvoiceOrReceipt, label: 'Financial Invoice / Receipt' }, { value: EvidenceType.GeotaggedPhoto, label: 'Geotagged Photo' }, { value: EvidenceType.MeetingMinutes, label: 'Meeting Minutes' }, { value: EvidenceType.PayrollOrStipendRegister, label: 'Payroll / Stipend Register' }, { value: EvidenceType.TechnicalSpecificationOrDesign, label: 'Technical Specification' }, { value: EvidenceType.DeliverableProof, label: 'Deliverable Proof' }, { value: EvidenceType.Other, label: 'Other Document' }, ]; export const EvidenceReviewQueue: React.FC = ({ initialProjectId = 'all', initialMilestoneId = 'all', initialActivityId = 'all', initialUserId = 'all', }) => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const { canAdd, canApprove, canExport, canApproveRecord, isMaker } = useRbac(); // Filter States: default to initialProjectId if given, otherwise when projects load default to first project const [selectedProjectId, setSelectedProjectId] = useState(initialProjectId); const [userHasSelectedProject, setUserHasSelectedProject] = useState( initialProjectId !== undefined && initialProjectId !== null && initialProjectId !== 'all' ); const [selectedMilestoneId, setSelectedMilestoneId] = useState(initialMilestoneId); const [selectedActivityId, setSelectedActivityId] = useState(initialActivityId); const [selectedUserId, setSelectedUserId] = useState(initialUserId); const [selectedStatus, setSelectedStatus] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); // Modals & Active Review State const [inspectEvidence, setInspectEvidence] = useState(null); const [isManualIngestModalOpen, setIsManualIngestModalOpen] = useState(false); const [ingestFile, setIngestFile] = useState(null); const [ingestType, setIngestType] = useState(EvidenceType.CompletionCertificate); const [ingestProjectId, setIngestProjectId] = useState(''); const [ingestMilestoneId, setIngestMilestoneId] = useState(''); const [ingestActivityId, setIngestActivityId] = useState(''); const [isDownloading, setIsDownloading] = useState(null); // Queries const { data: projects = [] } = useQuery({ queryKey: ['projects'], queryFn: () => getProjects(), }); const { data: milestones = [] } = useQuery({ queryKey: ['milestones-all'], queryFn: () => getAllMilestones(), }); const { data: users = [] } = useQuery({ queryKey: ['users-list'], queryFn: () => getUsers(1, 100).catch(() => []), }); // Keep selectedProjectId in sync when initialProjectId prop changes React.useEffect(() => { if (initialProjectId && initialProjectId !== 'all') { setSelectedProjectId(initialProjectId); setUserHasSelectedProject(true); } }, [initialProjectId]); // Load evidence by default for the first project in the list React.useEffect(() => { if (!userHasSelectedProject && projects.length > 0 && selectedProjectId === 'all') { setSelectedProjectId(projects[0].id); } }, [projects, userHasSelectedProject, selectedProjectId]); // Query activities based on selected project or fetch all across available projects const { data: activities = [] } = useQuery({ queryKey: ['activities-review-queue', selectedProjectId], queryFn: async () => { if (selectedProjectId && selectedProjectId !== 'all') { return getActivities(selectedProjectId).catch(() => []); } if (projects.length > 0) { const results = await Promise.all( projects.slice(0, 10).map((p) => getActivities(p.id).catch(() => [])) ); return results.flat(); } return []; }, enabled: projects.length > 0, }); // Query evidence: if a project is selected (e.g. the first project by default), fetch via /v1/api/evidence/{projectId} const { data: rawEvidenceList = [], isLoading: loadingEvidence, refetch: refetchEvidence, isFetching: isFetchingEvidence, } = useQuery({ queryKey: ['evidence', selectedProjectId], queryFn: async () => { if (selectedProjectId && selectedProjectId !== 'all') { return getProjectEvidence(selectedProjectId).catch(() => []); } return getAllEvidence().catch(() => []); }, enabled: projects.length > 0 || selectedProjectId !== 'all', }); // UI depends entirely on the API response - no mock or demo data const allEvidence: Evidence[] = rawEvidenceList; const evidenceExportColumns: ExportColumn[] = [ { header: 'Evidence ID', key: 'id' }, { header: 'File Name', key: 'fileName' }, { header: 'Project ID', key: 'projectId', format: (val) => `Project #${val}` }, { header: 'Milestone ID', key: 'milestoneId', format: (val) => (val ? `Milestone #${val}` : '—') }, { header: 'Activity ID', key: 'activityId', format: (val) => (val ? `Activity #${val}` : '—') }, { header: 'Document Type', key: 'evidenceType', format: (val) => getEvidenceTypeLabel(val) }, { header: 'Submitter', key: 'createdBy', format: (val) => getUserEmail(val) }, { header: 'Submission Date', key: 'createdAt', format: (val) => formatDate(val) }, { header: 'Verification Status', key: 'status', format: (val) => getApprovalStatusBadge(val).label }, { header: 'Review Remarks', key: 'remarks', format: (val) => val || '—' }, ]; // Review evidence mutation: Facilitates Approve & Reject decisions via PUT /v1/api/evidence const reviewMutation = useMutation({ mutationFn: async ({ id, status, version, evidenceType, }: { id: string | number; status: ApprovalStatus; version?: number; evidenceType?: EvidenceType; }) => { const targetEv = allEvidence.find((e) => String(e.id) === String(id)); const evVersion = Number(version ?? targetEv?.version ?? 1); const evType = (evidenceType ?? targetEv?.type ?? EvidenceType.DeliverableProof) as EvidenceType; return updateEvidence({ evidenceId: id, version: evVersion, evidenceType: evType, status, }); }, onSuccess: (_, variables) => { // Update local query cache for instant feedback queryClient.setQueryData(['evidence', selectedProjectId], (old: Evidence[] = []) => { const exists = old.some((e) => String(e.id) === String(variables.id)); if (exists) { return old.map((e) => String(e.id) === String(variables.id) ? { ...e, status: variables.status } : e ); } return old; }); if (inspectEvidence && String(inspectEvidence.id) === String(variables.id)) { setInspectEvidence((prev) => (prev ? { ...prev, status: variables.status } : null)); } queryClient.invalidateQueries({ queryKey: ['evidence'] }); if (variables.status === ApprovalStatus.Approved) { success('Evidence Approved & Verified', 'Approval recorded. Contributes towards contractual milestone sign-off.'); } else if (variables.status === ApprovalStatus.Rejected) { success('Evidence Rejected', 'Evidence marked as Rejected by reviewer.'); } else { success('Status Updated', 'Evidence status updated successfully.'); } }, onError: (err: unknown) => { toastError('Review Update Failed', err); }, }); // Manual Ingestion Mutation const ingestMutation = useMutation({ mutationFn: async (formData: FormData) => uploadEvidenceFile(formData), onSuccess: (newEv) => { queryClient.invalidateQueries({ queryKey: ['evidence'] }); success('Evidence Ingested', 'Document registered in the verification vault.'); setIsManualIngestModalOpen(false); setIngestFile(null); }, onError: (err: unknown) => { toastError('Ingestion Failed', err); }, }); // Filtered Evidence List const filteredEvidence = useMemo(() => { return allEvidence.filter((ev) => { // Filter by Project if (selectedProjectId !== 'all' && String(ev.projectId) !== String(selectedProjectId)) { return false; } // Filter by Milestone if (selectedMilestoneId !== 'all' && String(ev.milestoneId) !== String(selectedMilestoneId)) { return false; } // Filter by Activity if (selectedActivityId !== 'all' && String(ev.activityId) !== String(selectedActivityId)) { return false; } // Filter by User / Submitter if (selectedUserId !== 'all') { const userEmail = getUserEmail(ev.createdBy).toLowerCase(); const targetUser = users.find((u) => u.id === selectedUserId || u.email === selectedUserId); const matchId = String(ev.createdBy) === String(selectedUserId); const matchEmail = targetUser?.email && userEmail === targetUser.email.toLowerCase(); if (!matchId && !matchEmail) { return false; } } // Filter by Status if (selectedStatus !== 'all') { if (selectedStatus === ApprovalStatus.PendingReview) { if (ev.status !== ApprovalStatus.PendingReview && ev.status !== ApprovalStatus.Pending) { return false; } } else if (ev.status !== selectedStatus) { return false; } } // Filter by Search Query if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); const fName = (ev.fileName || '').toLowerCase(); const uEmail = getUserEmail(ev.createdBy).toLowerCase(); const proj = projects.find((p) => String(p.id) === String(ev.projectId)); const pName = (proj?.name || '').toLowerCase(); const act = activities.find((a) => String(a.id) === String(ev.activityId)); const aTitle = (act?.title || '').toLowerCase(); return fName.includes(q) || uEmail.includes(q) || pName.includes(q) || aTitle.includes(q); } return true; }); }, [allEvidence, selectedProjectId, selectedMilestoneId, selectedActivityId, selectedUserId, selectedStatus, searchQuery, users, projects, activities]); // Counts for status pipeline pills const statusCounts = useMemo(() => { let submitted = 0; let approved = 0; let rejected = 0; allEvidence.forEach((ev) => { if (ev.status === ApprovalStatus.Approved) approved++; else if (ev.status === ApprovalStatus.Rejected) rejected++; else submitted++; }); return { all: allEvidence.length, submitted, approved, rejected, }; }, [allEvidence]); // Handlers const handleDownload = async (ev: Evidence) => { try { setIsDownloading(ev.id); let downloadUrl = ev.documentUrl || ''; try { const secureLink = await getEvidenceSecureLink(ev.id); if (secureLink) downloadUrl = secureLink; } catch { // Fallback to existing documentUrl or synthesized blob } if (downloadUrl && downloadUrl.startsWith('http')) { window.open(downloadUrl, '_blank', 'noopener,noreferrer'); } else { // Create an audit download file stub const content = `PFM VAULT EVIDENCE RECORD\nFile: ${ev.fileName || 'evidence.pdf'}\nProject: #${ev.projectId}\nMilestone: #${ev.milestoneId || 'N/A'}\nActivity: #${ev.activityId || 'N/A'}\nSubmitted By: ${getUserEmail(ev.createdBy)}\nDate: ${ev.createdAt}\nStatus: ${ev.status}\nSHA-256 Hash: 8f4a2109e4d77b8120c8f1e948c34d830b0e35fa8f121d5a230f81d`; const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = ev.fileName || `evidence_${ev.id}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } success('Download Initiated', `Opening official file: ${ev.fileName || `evidence_${ev.id}`}`); } catch (err: unknown) { toastError('Download Failed', err); } finally { setIsDownloading(null); } }; const handleManualIngestSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!ingestFile) { toastError('Missing File', 'Please select a document or certificate to upload.'); return; } const targetProjId = ingestProjectId || (selectedProjectId !== 'all' ? selectedProjectId : projects[0]?.id) || ''; const formData = new FormData(); formData.append('file', ingestFile); formData.append('projectId', String(targetProjId)); if (ingestMilestoneId) formData.append('milestoneId', String(ingestMilestoneId)); if (ingestActivityId) formData.append('activityId', String(ingestActivityId)); formData.append('evidenceType', String(ingestType)); ingestMutation.mutate(formData); }; const handleResetFilters = () => { const defaultProjId = projects[0]?.id || 'all'; setSelectedProjectId(defaultProjId); setSelectedMilestoneId('all'); setSelectedActivityId('all'); setSelectedUserId('all'); setSelectedStatus('all'); setSearchQuery(''); }; const defaultProjectId = projects[0]?.id; const isDefaultProject = defaultProjectId !== undefined ? String(selectedProjectId) === String(defaultProjectId) : selectedProjectId === 'all'; const hasActiveFilters = !isDefaultProject || selectedMilestoneId !== 'all' || selectedActivityId !== 'all' || selectedUserId !== 'all' || selectedStatus !== 'all' || searchQuery.trim() !== ''; return (
{/* Evidence Hub Header Bar */}
Verification Queue {filteredEvidence.length} {filteredEvidence.length === 1 ? 'file' : 'files'} in view

Evidence Review & Verification Hub

Inspect and verify deliverable evidence uploaded by awardees against project milestones and operational activities. Review supporting documentation, download verified files, and authorize completion sign-offs.

{canAdd('evidence_risks') && ( )}
{/* Status Pipeline Counter Pills */}
{/* Multi-Dimensional Filter Toolbar */}
Review Filters
{hasActiveFilters && ( )}
{/* Project Filter */}
{/* Milestone Filter */}
{/* Activity Filter */}
{/* Awardee / Submitter Filter */}
{/* Search Field */}
setSearchQuery(e.target.value)} placeholder="Search file name, activity..." className="w-full text-xs bg-slate-900/90 border border-slate-700/80 rounded-xl pl-8 pr-3 py-2 text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50" />
{/* Evidence Items Queue */}
{loadingEvidence ? (

Loading verification queue...

) : filteredEvidence.length === 0 ? (

No evidence files match your filters

No deliverable evidence found for the chosen project, milestone, activity, or submitter. Try adjusting your filter parameters or search terms.

{hasActiveFilters && ( )}
) : (
{filteredEvidence.map((ev) => { const project = projects.find((p) => String(p.id) === String(ev.projectId)); const milestone = milestones.find((m) => String(m.id) === String(ev.milestoneId)); const activity = activities.find((a) => String(a.id) === String(ev.activityId)); const submitterEmail = getUserEmail(ev.createdBy); const statusBadge = getApprovalStatusBadge(ev.status); const isPDF = (ev.fileName || '').toLowerCase().endsWith('.pdf'); const isSheet = (ev.fileName || '').toLowerCase().endsWith('.xlsx') || (ev.fileName || '').toLowerCase().endsWith('.csv'); return (
{/* Left: Document Info */}
{isSheet ? : }
setInspectEvidence(ev)}> {ev.fileName || `evidence_file_${ev.id}.pdf`} {getEvidenceTypeLabel(ev.type)} v{ev.version}
{/* Alignments: Project, Milestone, Activity */}
{project ? project.name : `Project #${ev.projectId}`} • {milestone ? milestone.name : `Milestone #${ev.milestoneId || 'General'}`} {activity && ( <> • {activity.title} )}
{/* Submitter & Timestamp */}
Submitted by: {submitterEmail} • {formatDate(ev.createdAt)} • {ev.fileSize || '3.5 MB'}
{/* Right: Status & Actions */}
{statusBadge.label}
{ev.status === ApprovalStatus.Approved ? ( Approved ) : ( <> {canAdd('evidence_risks') && ( )} {canApproveRecord('evidence_risks', ev.createdBy) && ( )} {canApprove('evidence_risks') && isMaker(ev.createdBy) && ( Maker-Checker (Creator) )} )}
); })}
)}
{/* Modal: Comprehensive Evidence Inspection, Document Viewer & Review */} {inspectEvidence && ( { setInspectEvidence(null); }} title="Evidence Deliverable Verification" subtitle={`Inspect supporting file, integrity checksums, and sign off review for Milestone #${inspectEvidence.milestoneId || 'General'}`} >
{/* Header Document Summary */}
{getEvidenceTypeLabel(inspectEvidence.type)}

{inspectEvidence.fileName || `evidence_file_${inspectEvidence.id}.pdf`}

File Version: v{inspectEvidence.version} • Size: {inspectEvidence.fileSize || '3.5 MB'} • Submitted: {formatDate(inspectEvidence.createdAt)}

{getApprovalStatusBadge(inspectEvidence.status).label}
{/* Cryptographic SHA-256 Checksum */}
SHA-256: 8f4a2109e4d77b8120c8f1e948c34d830b0e35fa8f121d5a230f81d94821a9c
{/* Context Alignment Grid */}
Project {projects.find((p) => String(p.id) === String(inspectEvidence.projectId))?.name || `Project #${inspectEvidence.projectId}`}
Contractual Milestone {milestones.find((m) => String(m.id) === String(inspectEvidence.milestoneId))?.name || `Milestone #${inspectEvidence.milestoneId || 'N/A'}`}
Linked Activity {activities.find((a) => String(a.id) === String(inspectEvidence.activityId))?.title || `Activity #${inspectEvidence.activityId || 'General'}`}
Submitting Awardee {getUserEmail(inspectEvidence.createdBy)}
{/* Document Preview & Direct Access Section */}
Document Preview & Actions
{/* Simulated Document Sheet Preview */}
{inspectEvidence.fileName || 'Evidence Document'}

Signed canonical deliverable submitted by awardee {getUserEmail(inspectEvidence.createdBy)}. Tamper-evident verification status: Valid cryptographic signature.

{/* Programme Manager Review & Sign-Off Section */}
Programme Manager Verification Decision
{getApprovalStatusBadge(inspectEvidence.status).label}

Review the deliverable document against contractual requirements. If the evidence does not satisfy requirements, reject the submission using the review actions below. Approved evidence validates progress towards milestone completion.

Record ID: #{inspectEvidence.id} • v{inspectEvidence.version || 1}
{canAdd('evidence_risks') && ( )} {canApproveRecord('evidence_risks', inspectEvidence.createdBy) && ( )} {canApprove('evidence_risks') && isMaker(inspectEvidence.createdBy) && ( Maker-Checker Policy: As the creator of this submission, you cannot approve it. )}
)} {/* Modal: Manual Evidence Ingestion (Administrative / On behalf of Awardee) */} { setIsManualIngestModalOpen(false); setIngestFile(null); }} title="Ingest Evidence Document" subtitle="Manually upload deliverable evidence on behalf of an awardee organization" >
setIngestFile(e.target.files?.[0] || null)} className="w-full text-xs text-slate-300 file:mr-3 file:py-2 file:px-4 file:rounded-xl file:border-0 file:text-xs file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-500 bg-slate-900/80 border border-slate-700/80 rounded-xl p-2 cursor-pointer" accept=".pdf,.jpg,.jpeg,.png,.docx,.xlsx,.zip" /> {ingestFile && (

Selected: {ingestFile.name} ({(ingestFile.size / (1024 * 1024)).toFixed(2)} MB)

)}
); };