import { ArrowLeft, CheckCircle2, Clock, AlertCircle, Upload, Download, Eye, Navigation, MapPin, MessageSquare, Loader2, Calendar, Reply, Ban } from 'lucide-react'; import { DocumentPreviewModal } from '@/components/ui/DocumentPreviewModal'; import { formatDateTime } from '@/components/ui/utils'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { User as UserType } from '@/lib/mock-data'; import { toast } from 'sonner'; import { API } from '@/api/API'; import { SlaBadge } from '@/components/sla/SlaBadge'; import { useSlaBatchStatus } from '@/hooks/useSlaBatchStatus'; import { getCurrentStageBadgeClass, getOffboardingRequestStatusBadgeClass, getStatusLabelBadgeClass, getStatusProgressBarClass, isOffboardingTerminalNegative, WORKFLOW_IN_PROGRESS_ACCENT, } from '@/lib/offboardingDisplay'; interface RelocationRequestDetailsProps { requestId: string; onBack: () => void; currentUser: UserType | null; } // Workflow stages configuration const workflowStages = [ { id: 1, name: 'ASM Review', key: 'ASM_REVIEW', role: 'ASM' }, { id: 2, name: 'RBM Review', key: 'RBM_REVIEW', role: 'RBM' }, { id: 3, name: 'DD ZM Review', key: 'DD_ZM_REVIEW', role: 'DD-ZM' }, { id: 4, name: 'ZBH Review', key: 'ZBH_REVIEW', role: 'ZBH' }, { id: 5, name: 'DD Lead Review', key: 'DD_LEAD_REVIEW', role: 'DD Lead' }, { id: 6, name: 'NBH Approval', key: 'NBH_APPROVAL', role: 'NBH' }, { id: 7, name: 'Legal Clearance', key: 'LEGAL_CLEARANCE', role: 'Legal Admin' }, { id: 8, name: 'Relocation Complete', key: 'COMPLETED', role: 'System' } ]; /** Map API stage / status label to 1-based workflow row index; 0 = unknown; length+1 = finished */ function relocationStageLabelToOrdinal(label: string | null | undefined): number { const raw = String(label || '') .trim() .replace(/\u00a0/g, ' '); if (!raw || raw === 'Submitted') return 0; const lower = raw.toLowerCase(); if (['completed', 'relocation complete', 'closed'].includes(lower)) { return workflowStages.length + 1; } const pendingMatch = raw.match(/^pending\s+(.+)$/i); const core = (pendingMatch ? pendingMatch[1] : raw).trim(); let idx = workflowStages.findIndex( (s) => s.name === core || s.name === raw || s.key === core || s.key === raw ); if (idx < 0) { idx = workflowStages.findIndex( (s) => core.toLowerCase().includes(s.name.toLowerCase()) || s.name.toLowerCase().includes(core.toLowerCase()) ); } return idx >= 0 ? idx + 1 : 0; } function relocationIsSendBackAction(action: string): boolean { const a = String(action || '').toLowerCase(); if (a.includes('sent back')) return true; if (a.includes('send') && a.includes('back')) return true; return false; } function relocationIsDocumentOnlyTimelineEntry(e: any): boolean { const action = String(e?.action || ''); if (/document\s*verified/i.test(action)) return true; if (/document/i.test(action) && /upload/i.test(action)) return true; return false; } /** * Applies one timeline/audit workflow row to a running 1-based stage ordinal. * Send-back moves the workflow backward to `targetStage`; forward actions advance using stage/target labels. */ function relocationApplyWorkflowEntry( currentOrdinal: number, e: { action?: string; stage?: string; targetStage?: string } ): number { if (relocationIsDocumentOnlyTimelineEntry(e)) return currentOrdinal; const action = String(e?.action || ''); if (relocationIsSendBackAction(action)) { const t = relocationStageLabelToOrdinal(String(e?.targetStage || '')); return t > 0 ? t : currentOrdinal; } let next = currentOrdinal; for (const lab of [e?.targetStage, e?.stage].filter(Boolean)) { const o = relocationStageLabelToOrdinal(String(lab)); if (o > next) next = o; } return next; } /** Effective workflow step from timeline order (send-back lowers the active stage; max-only logic would not). */ function relocationTimelineResolvedOrdinal(entries: any[]): number { const sorted = [...(entries || [])] .filter(Boolean) .sort( (a, b) => new Date(a?.timestamp || a?.createdAt || 0).getTime() - new Date(b?.timestamp || b?.createdAt || 0).getTime() ); let resolved = 0; for (const e of sorted) { resolved = relocationApplyWorkflowEntry(resolved, e); } return resolved; } /** Same resolution rules as {@link relocationTimelineResolvedOrdinal}, using audit API rows. */ function relocationAuditResolvedOrdinal(logs: any[]): number { const sorted = [...(logs || [])] .filter(Boolean) .sort( (a, b) => new Date(a?.createdAt || a?.timestamp || 0).getTime() - new Date(b?.createdAt || b?.timestamp || 0).getTime() ); let resolved = 0; for (const log of sorted) { const action = String(log?.action || ''); if (action === 'DOCUMENT_UPLOADED' || action === 'DOCUMENT_VERIFIED') continue; const d = log?.details || log?.newData || {}; const syntheticAction = [action, d?.action, String(d?.type || '')].filter(Boolean).join(' '); resolved = relocationApplyWorkflowEntry(resolved, { action: syntheticAction, stage: d?.stage !== undefined ? String(d.stage) : log?.stage !== undefined ? String(log.stage) : undefined, targetStage: d?.targetStage !== undefined ? String(d.targetStage) : undefined }); } return resolved; } /** Timeline rows for a workflow stage (matches resignation: filter by `stage` at action time). */ function getRelocationTimelineEntriesForStage( entries: any[], stage: { name: string; key: string }, stageIndex: number ): any[] { const list = Array.isArray(entries) ? [...entries] : []; const filtered = list.filter((t: any) => { const src = String(t?.stage || '').trim(); if (src === stage.name || src === stage.key) return true; if (stageIndex === 0 && (src === 'Submitted' || src === 'Request submitted')) return true; return false; }); filtered.sort((a, b) => { const ta = new Date(a?.timestamp || a?.createdAt || 0).getTime(); const tb = new Date(b?.timestamp || b?.createdAt || 0).getTime(); return ta - tb; }); return filtered; } // Required documents configuration const requiredDocuments = [ 'Property documents for new location', 'Lease/Rental agreement for new location', 'NOC from current landlord', 'Municipal approvals', 'Fire safety certificate', 'Pollution clearance', 'Layout/Floor plan of new location', 'Photos of new location', 'Locality map', 'Building plan approval', 'Electricity connection documents', 'Water supply documents' ]; const getStatusColor = (status: string) => getStatusLabelBadgeClass(status); const getDocChecklistUploadButtonClass = (isRejected: boolean) => isRejected ? 'h-7 px-2 text-red-700 hover:bg-red-50 hover:text-red-800 flex-shrink-0' : 'h-7 px-2 text-slate-700 hover:bg-slate-50 flex-shrink-0'; const getApiErrorMessage = (error: any, fallback: string) => { const responseData = error?.response?.data || error?.data; if (responseData?.readiness) { const missing = responseData.readiness?.missingUploads || []; const pending = responseData.readiness?.pendingVerification || []; const details = [ missing.length ? `Missing: ${missing.join(', ')}` : '', pending.length ? `Pending verification: ${pending.join(', ')}` : '' ] .filter(Boolean) .join(' | '); return details ? `${responseData.message || fallback} (${details})` : (responseData.message || fallback); } return responseData?.message || error?.message || fallback; }; export function RelocationRequestDetails({ requestId, onBack, currentUser }: RelocationRequestDetailsProps) { const { get: getSla } = useSlaBatchStatus( requestId ? [{ entityType: 'relocation', entityId: requestId }] : [], Boolean(requestId) ); const navigate = useNavigate(); const [request, setRequest] = useState(null); const [auditLogs, setAuditLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [isActionDialogOpen, setIsActionDialogOpen] = useState(false); const [actionType, setActionType] = useState<'approve' | 'reject' | 'hold' | 'sendBack' | 'revoke'>('approve'); const [comments, setComments] = useState(''); const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false); const [eorChecklist, setEorChecklist] = useState(null); const [isEorLoading, setIsEorLoading] = useState(false); const [isSubmittingEor, setIsSubmittingEor] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [selectedDocType, setSelectedDocType] = useState(requiredDocuments[0]); // True when the dialog was opened from a checklist row -> doc type is implicit, // so we hide the dropdown and show the doc name as a read-only badge instead. const [docTypeLocked, setDocTypeLocked] = useState(false); const [isUploading, setIsUploading] = useState(false); const [activeTab, setActiveTab] = useState('workflow'); const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [selectedDoc, setSelectedDoc] = useState(null); const [rejectDocDialogOpen, setRejectDocDialogOpen] = useState(false); const [rejectDocId, setRejectDocId] = useState(null); const [rejectDocReason, setRejectDocReason] = useState(''); const [isRejectingDoc, setIsRejectingDoc] = useState(false); useEffect(() => { fetchRequestDetails(); fetchAuditLogs(); }, [requestId]); const fetchAuditLogs = async () => { try { const response: any = await API.getAuditLogs('relocation', requestId); if (response.data && response.data.success) { setAuditLogs(response.data.data || []); } } catch (error) { console.error('Error fetching audit logs:', error); } }; const fetchEorChecklist = async (relocationUuid?: string) => { try { setIsEorLoading(true); const id = relocationUuid || request?.id || requestId; const response = await API.getEorChecklistForRelocation(id) as any; if (response.data.success) { setEorChecklist(response.data.data); } } catch (error) { console.error('Fetch EOR checklist error:', error); // Don't toast error here as it might not be created yet } finally { setIsEorLoading(false); } }; const handleUpdateEorItem = async (description: string, isCompliant: boolean, itemType: string) => { if (!eorChecklist) return; try { const response = await API.updateEorChecklistItem(eorChecklist.id, { description, isCompliant, itemType, remarks: '' }) as any; if (response.data.success) { fetchEorChecklist(); } } catch (error) { console.error('Update EOR item error:', error); toast.error(getApiErrorMessage(error, 'Failed to update item')); } }; const handleSubmitEorAudit = async () => { if (!eorChecklist) return; try { setIsSubmittingEor(true); const response = await API.submitEorAudit(eorChecklist.id, { status: 'Completed', overallComments: 'Relocation EOR Audit Completed' }) as any; if (response.data.success) { toast.success('EOR Audit submitted successfully'); fetchRequestDetails(); fetchEorChecklist(); } } catch (error) { console.error('Submit EOR audit error:', error); toast.error(getApiErrorMessage(error, 'Failed to submit EOR audit')); } finally { setIsSubmittingEor(false); } }; const fetchRequestDetails = async (isSilent = false) => { try { if (!isSilent) setIsLoading(true); const response = await API.getRelocationRequestById(requestId) as any; if (response.data.success) { const req = response.data.request; setRequest(req); } } catch (error) { console.error('Fetch relocation request details error:', error); toast.error(getApiErrorMessage(error, 'Failed to fetch request details')); } finally { setIsLoading(false); } }; /** * 1-based ordinal from persisted record (currentStage, else status) — drives timeline highlight and progress bar. */ const getDbStageOrdinal = () => { if (!request) return 1; if (request.status === 'Completed' || request.currentStage === 'Completed') { return workflowStages.length + 1; } if (request.status === 'Rejected' || request.status === 'Revoked' || request.status === 'Withdrawn') { const tl = [...(request.timeline || [])].filter(Boolean).reverse(); for (const e of tl) { const stageLabel = String(e.stage || '').trim(); const stageKey = String(e.targetStage || '').trim(); const idx = workflowStages.findIndex((s) => s.name === stageLabel || s.key === stageKey || s.key === stageLabel || s.name === stageKey ); if (idx >= 0) return idx + 1; } return 1; } const stageName = request.currentStage; const idx = workflowStages.findIndex( (s) => s.name === stageName || s.key === stageName || s.name.replace(/\s+/g, ' ') === String(stageName || '').replace(/\s+/g, ' ') ); if (idx >= 0) return idx + 1; const fromStatus = relocationStageLabelToOrdinal(String(request.status || '')); if (fromStatus > 0) return fromStatus; return 1; }; const timelineEntries = Array.isArray(request?.timeline) ? request.timeline : []; const timelineResolvedOrdinal = relocationTimelineResolvedOrdinal(timelineEntries); const auditResolvedOrdinal = relocationAuditResolvedOrdinal(auditLogs); const dbOrdinal = request ? getDbStageOrdinal() : 1; /** Audit/timeline can reference later steps while the request still sits in a prior stage — do not use that to drive the active step. */ const workflowProgressMismatch = Boolean(request) && Math.max(timelineResolvedOrdinal, auditResolvedOrdinal) > dbOrdinal && (timelineEntries.length > 0 || auditLogs.length > 0); const currentStageConfig = request ? workflowStages[Math.min(Math.max(dbOrdinal, 1), workflowStages.length) - 1] : undefined; const allWorkflowComplete = request?.status === 'Completed' || request?.currentStage === 'Completed' || dbOrdinal >= workflowStages.length + 1; /** Match backend: N/(pipeline+1) while in flight; 100% only when completed. */ const timelineProgressPct = allWorkflowComplete ? 100 : Math.min(100, Math.round((dbOrdinal / workflowStages.length) * 100)); const displayProgressPct = allWorkflowComplete ? 100 : timelineProgressPct; const workflowTerminalNegative = request ? isOffboardingTerminalNegative(request.status, request.currentStage) : false; const statusProgressBarClass = request ? getStatusProgressBarClass(request.status, request.currentStage) : 'bg-status-progress'; const requestStatusBadgeClass = request ? getOffboardingRequestStatusBadgeClass(request.status, request.currentStage) : 'bg-re-red hover:bg-re-red-hover text-white border-transparent'; const missingRequiredDocs = request ? requiredDocuments.filter((doc) => !request.documents?.some((d: any) => d.type === doc || (d.name && d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0])) )) : []; const pendingVerificationDocs = request ? requiredDocuments.filter((doc) => { const matched = request.documents?.filter((d: any) => d.type === doc || (d.name && d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0])) ) || []; return matched.length > 0 && !matched.some((d: any) => d.status === 'Verified'); }) : []; // Helper to find assigned reviewer for a stage const getAssignedReviewer = (stageName: string) => { if (!request || !request.participants || request.participants.length === 0) return null; // The backend stores the stage string directly in metadata (e.g. "ASM Review") const participant = request.participants.find((p: any) => p.metadata?.stage === stageName || p.metadata?.stage === stageName.toUpperCase().replace(/ /g, '_') ); if (!participant) return null; return participant.user?.fullName || participant.user?.name || participant.user?.role || null; }; // Visibility logic for Approve/Reject buttons const canUserAction = () => { if (!request || !currentUser) return false; // Check for Super Admin bypass const isAdmin = (currentUser?.role as any) === 'Super Admin' || currentUser.role === 'Super Admin'; if (isAdmin) return true; // Check if user's role matches the role required for the current stage return Boolean(currentStageConfig?.role && currentUser.role === currentStageConfig.role); }; const showActions = canUserAction() && request.status !== 'Completed' && request.status !== 'Rejected' && request.status !== 'Revoked'; const canSendBack = showActions && request.currentStage && request.currentStage !== 'ASM Review' && request.currentStage !== 'Rejected'; const canRevoke = showActions && ['ZBH', 'DD Lead', 'NBH', 'Legal Admin', 'Super Admin'].includes(currentUser?.role || ''); const requiresDocGate = request?.currentStage === 'NBH Approval' || request?.currentStage === 'Legal Clearance'; const canApprove = showActions && (!requiresDocGate || (missingRequiredDocs.length === 0 && pendingVerificationDocs.length === 0)); const handleAction = (type: 'approve' | 'reject' | 'hold' | 'sendBack' | 'revoke') => { setActionType(type); setIsActionDialogOpen(true); }; const handleSubmitAction = async (e: React.FormEvent) => { e.preventDefault(); const remarksTrimmed = String(comments || '').trim(); const isSendBack = actionType === 'sendBack'; const isRevoke = actionType === 'revoke'; if ((isSendBack || isRevoke) && remarksTrimmed.length < 5) { toast.error(`Remarks are required for ${actionType === 'sendBack' ? 'Send Back' : 'Revoke'} (minimum 5 characters).`); return; } try { setIsSubmitting(true); const actionMap: Record = { approve: 'approve', reject: 'reject', hold: 'hold', sendBack: 'sendBack', revoke: 'revoke' }; const action = actionMap[actionType] || 'approve'; const response = await API.updateRelocationRequest(requestId, action, { remarks: remarksTrimmed }) as any; if (response.data.success) { const verb = actionType === 'sendBack' ? 'sent back' : actionType === 'revoke' ? 'revoked' : `${actionType}ed`; toast.success(`Request ${verb} successfully`); setIsActionDialogOpen(false); setComments(''); fetchRequestDetails(); fetchAuditLogs(); // If moving to NBH Clearance EOR, fetch the checklist if (actionType === 'approve' && currentStageConfig?.key === 'LEGAL_CLEARANCE' && request?.id) { fetchEorChecklist(request.id); } } } catch (error) { console.error('Submit action error:', error); toast.error(getApiErrorMessage(error, 'Failed to submit action')); } finally { setIsSubmitting(false); } }; const handleUploadDocument = async () => { if (!selectedFile || !selectedDocType) { toast.error('Please select both a document type and a file'); return; } try { setIsUploading(true); const formData = new FormData(); formData.append('file', selectedFile); formData.append('documentType', selectedDocType); formData.append('stage', request.currentStage); const response = await API.uploadRelocationDocument(requestId, formData) as any; if (response.data.success) { toast.success('Document uploaded successfully'); setIsUploadDialogOpen(false); setSelectedFile(null); fetchRequestDetails(true); fetchAuditLogs(); } } catch (error) { console.error('Upload document error:', error); toast.error(getApiErrorMessage(error, 'Failed to upload document')); } finally { setIsUploading(false); } }; const handleVerifyDocument = async (documentId: string) => { try { const response = await API.verifyRelocationDocument(requestId, documentId) as any; if (response.data.success) { toast.success('Document verified successfully'); fetchRequestDetails(true); // Silent refresh fetchAuditLogs(); } } catch (error) { console.error('Verify document error:', error); toast.error(getApiErrorMessage(error, 'Failed to verify document')); } }; const submitRejectDocument = async () => { if (!rejectDocId || !String(rejectDocReason).trim()) { toast.error('Please enter a rejection reason.'); return; } try { setIsRejectingDoc(true); const response = await API.rejectRelocationDocument(requestId, rejectDocId, { remarks: rejectDocReason.trim() }) as any; if (response.data?.success) { toast.success('Document rejected successfully'); setRejectDocDialogOpen(false); setRejectDocId(null); setRejectDocReason(''); fetchRequestDetails(true); fetchAuditLogs(); } else { toast.error(response.data?.message || 'Failed to reject document'); } } catch (error) { console.error('Reject document error:', error); toast.error(getApiErrorMessage(error, 'Failed to reject document')); } finally { setIsRejectingDoc(false); } }; const handlePreviewDocument = (doc: any) => { setSelectedDoc({ fileName: doc.name, filePath: doc.url, documentType: doc.type, createdAt: doc.uploadedOn, mimeType: doc.mimeType }); setIsPreviewOpen(true); }; if (isLoading) { return (

Loading request details...

); } if (!request) { return (

Request Not Found

The relocation request you're looking for doesn't exist.

); } return (
{/* Header */}

{request.requestId} - Relocation Request Details

{request.outlet?.name} ({request.outlet?.code})

{request.status}
{/* Request Overview */} Relocation Overview

Dealer Details

{request.outlet?.name}

{request.outlet?.code}

{request.dealer?.fullName}

Relocation Route

From (Current)

{request.currentLocation || (request.outlet ? `${request.outlet.address}, ${request.outlet.city}` : 'N/A')}

To (Proposed)

{request.proposedLocation || `${request.newAddress}, ${request.newCity}`}

Type: {request.relocationType} {request.distance && ( Distance: {request.distance} )} {request.propertyType && ( Property: {request.propertyType} )} {request.expectedRelocationDate && ( Expected Date: {request.expectedRelocationDate} )}

Request Information

Submitted: {formatDateTime(request.createdAt)}

By: {request.dealer?.fullName}

Current Stage: {String(request.currentStage || '').replace(/_/g, ' ')}

Reason for Relocation

{request.reason}

{/* Main Content */}
Workflow Progress Documents History & Audit Trail
{/* Workflow Progress Tab */} {/* Progress Bar */}
Overall Progress {displayProgressPct}% Complete
{workflowTerminalNegative && (
This request is closed as {String(request.status)}. The approval path below is for reference only.
)} {workflowProgressMismatch && !workflowTerminalNegative && (
Activity ahead of current stage: Some timeline or audit entries reference steps after the official current stage ({String(request.currentStage)}). Per-step history below may include future steps; the highlighted step and approvals follow the server current stage only.
)}

Progress Timeline

Track the relocation approval process — activity recorded at each stage appears below that step.
{/* Workflow stages + per-stage timeline (same pattern as ResignationDetails progress tab) */}
{workflowTerminalNegative ? (
    {workflowStages.map((stage: any) => (
  • {stage.name} — {stage.role}
  • ))}
) : ( workflowStages.map((stage: any, index: number) => { const isCompleted = allWorkflowComplete || index < dbOrdinal - 1; const isCurrent = !allWorkflowComplete && index === dbOrdinal - 1; const stageTimelineEntries = getRelocationTimelineEntriesForStage( timelineEntries, stage, index ); const timelineEntry = stageTimelineEntries.length > 0 ? stageTimelineEntries[stageTimelineEntries.length - 1] : null; return (
{isCompleted ? ( ) : isCurrent ? ( ) : ( {stage.id} )}
{index < workflowStages.length - 1 && (
)}

{stage.name}

Responsible: {stage.role}

{getAssignedReviewer(stage.name) && (

Assigned: {getAssignedReviewer(stage.name)}

)}
{isCompleted ? 'Completed' : isCurrent ? 'In Progress' : 'Pending'} {timelineEntry && (
{formatDateTime(timelineEntry.timestamp || timelineEntry.createdAt)}
)}
{timelineEntry && (
{timelineEntry.user || 'System'} {timelineEntry.action || 'Update'} {timelineEntry.targetStage && timelineEntry.targetStage !== timelineEntry.stage && ( → {String(timelineEntry.targetStage).replace(/_/g, ' ')} )}
{timelineEntry.remarks || timelineEntry.comments || 'No remarks provided.'}
{stageTimelineEntries.length > 1 && (

{stageTimelineEntries.length} events at this stage; showing the latest.

)}
)}
); }) )}
{/* Documents Tab */} Required for Process Existing Documents {/* Required Documents Sub-tab */}
{/* Upload Button */}

Required Documents

{ setIsUploadDialogOpen(open); if (!open) { setDocTypeLocked(false); setSelectedFile(null); } }} > Upload Document {docTypeLocked ? 'Pick a file for the selected document.' : 'Select the document type and upload the file.'}
{docTypeLocked ? (
{selectedDocType}
) : (
)}
setSelectedFile(e.target.files?.[0] || null)} />
{/* Required Documents Checklist */}
{requiredDocuments.map((doc: string, index: number) => { const uploaded = request.documents?.find((d: any) => d.type === doc || d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0]) ); const isRejected = uploaded && String(uploaded.status) === 'Rejected'; const ok = uploaded && !isRejected; return (
{isRejected ? ( ) : ok ? ( ) : ( )} {doc}
{!ok && ( )}
); })}
{/* Existing Documents Sub-tab */} {request.documents && request.documents.length > 0 ? (

All Uploaded Documents

Document Name Category Uploaded On Uploaded By Status Actions {request.documents.map((doc: any) => ( {doc.name} {doc.category || 'General'} {formatDateTime(doc.uploadedOn)} {doc.uploadedBy} {doc.status}
{doc.status === 'Pending Verification' && (() => { const role = currentUser?.role || currentUser?.roleCode || ''; // SRS — only authorized review roles can verify relocation documents return ['DD Lead', 'NBH', 'Legal Admin', 'DD Admin', 'Super Admin', 'SUPER_ADMIN', 'DD_ADMIN'].includes(role); })() && ( <> )}
))}
) : (
No documents uploaded yet
)}
{/* EOR Checklist Tab */}

EOR Readiness Checklist

Verify new location infrastructure and statutory compliances

When document types match a checklist line, proofs from the Documents tab are linked here automatically (same files; no separate EOR upload required).

{eorChecklist && ( {eorChecklist.status} )}
{!eorChecklist ? (
{isEorLoading ? (

Fetching checklist...

) : ( <>
No Checklist Found

The EOR checklist will be automatically initiated once the request reaches the final clearance stage.

)}
) : (
Category Checklist Item Proof & Documents {(!eorChecklist.items || eorChecklist.items.length === 0) ? ( No checklist rows returned. Use "Try Refreshing" above or reload the page. ) : ( eorChecklist.items.map((item: any) => ( handleUpdateEorItem(item.description, e.target.checked, item.itemType)} disabled={eorChecklist.status === 'Completed' || (currentUser?.role !== 'NBH' && currentUser?.role !== 'Super Admin')} className="w-4 h-4 rounded border-slate-300 text-re-red" /> {item.itemType} {item.description}
{item.proofDocumentId && item.proofDocument ? ( <> {item.proofDocument.fileName} ) : item.proofDocumentId ? ( Proof linked (refresh if file details are missing) ) : ( )}
)) )}
{/* Audit Submission Button */} {(currentUser?.role === 'NBH' || currentUser?.role === 'Super Admin') && eorChecklist.status !== 'Completed' && (
)} {!eorChecklist.items?.every((i: any) => i.isCompliant) && (

All items must be marked as compliant before final submission.

)}
)}
{/* History Tab */}
{auditLogs.length > 0 ? ( auditLogs.map((entry: any, index: number) => (
{entry.action} by {entry.actor?.name || entry.userName || 'System'} {entry.details?.stage && ( at {entry.details.stage} )}
{formatDateTime(entry.timestamp || entry.createdAt)}
{(entry.remarks || entry.description || entry.newData?.remarks || entry.details?.remarks) && (
{entry.remarks || entry.description || entry.newData?.remarks || entry.details?.remarks}
)}
)) ) : (
No history found
)}
{/* Right Sidebar - Actions */}
{/* Current Status Card */} Current Status

Current Stage

{request.currentStage}

Progress

{displayProgressPct}%

Distance

{request.distance}

{/* Actions Card */} Actions {showActions && ( <> {!canApprove && (
Approval is blocked until mandatory documents are uploaded and verified for this stage.
)} {canSendBack && ( )} {canRevoke && ( )}
)}
{/* Action Dialog */} {actionType === 'approve' ? 'Approve Request' : actionType === 'reject' ? 'Reject Request' : actionType === 'sendBack' ? 'Send Back Request' : actionType === 'revoke' ? 'Revoke Request' : 'Put Request on Hold'} {actionType === 'sendBack' || actionType === 'revoke' ? 'Remarks are required and will be recorded in Work Notes and the audit trail.' : 'Please provide comments for this action. This will be recorded in the audit trail.'}