relocatin flow integrated at cetrtain extent douments mapping , assigns mapping done

This commit is contained in:
laxmanhalaki 2026-04-03 02:31:56 +05:30
parent 830f66b5f7
commit c9de800c47
3 changed files with 450 additions and 244 deletions

View File

@ -152,6 +152,10 @@ export const API = {
getRelocationRequestById: (id: string) => client.get(`/relocation/${id}`),
createRelocationRequest: (data: any) => client.post('/relocation', data),
updateRelocationRequest: (id: string, action: string, data?: any) => client.post(`/relocation/${id}/action`, { action, ...data }),
uploadRelocationDocument: (id: string, data: any) => client.post(`/relocation/${id}/documents`, data, {
headers: { 'Content-Type': 'multipart/form-data' }
}),
verifyRelocationDocument: (id: string, documentId: string) => client.post(`/relocation/${id}/documents/${documentId}/verify`),
getConstitutionalChanges: () => client.get('/constitutional-change'),
getConstitutionalChangeById: (id: string) => client.get(`/constitutional-change/${id}`),
@ -164,6 +168,12 @@ export const API = {
// System Configs
getSystemConfigs: (params?: any) => client.get('/master/system-configs', params),
saveSystemConfig: (data: any) => client.post('/master/system-configs', data),
// EOR Checklist
getEorChecklistForApplication: (applicationId: string) => client.get(`/eor/application/${applicationId}`),
getEorChecklistForRelocation: (relocationId: string) => client.get(`/eor/relocation/${relocationId}`),
updateEorChecklistItem: (checklistId: string, data: any) => client.post(`/eor/item/${checklistId}`, data),
submitEorAudit: (checklistId: string, data: any) => client.post(`/eor/audit/${checklistId}`, data),
};
export default API;

View File

@ -1,4 +1,6 @@
import { ArrowLeft, CheckCircle2, Clock, AlertCircle, Upload, Download, Eye, Navigation, MapPin, GitBranch, MessageSquare, Loader2 } from 'lucide-react';
import { DocumentPreviewModal } from '../ui/DocumentPreviewModal';
import { formatDateTime } from '../ui/utils';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
@ -22,41 +24,16 @@ interface RelocationRequestDetailsProps {
// Workflow stages configuration
const workflowStages = [
{ id: 1, name: 'Request Created', key: 'created', role: 'Dealer' },
{ id: 2, name: 'ASM Review', key: 'asm', role: 'ASM' },
{ id: 3, name: 'RBM Review', key: 'rbm', role: 'RBM' },
{ id: 4, name: 'DD ZM Review', key: 'dd-zm', role: 'DD-ZM' },
{ id: 5, name: 'ZBH Review', key: 'zbh', role: 'ZBH' },
{ id: 6, name: 'DD Lead Review', key: 'dd-lead', role: 'DD Lead' },
{ id: 7, name: 'DD Head Review', key: 'dd-head', role: 'DD Head' },
{ id: 8, name: 'NBH Review', key: 'nbh', role: 'NBH' },
{
id: 9,
name: 'Parallel Processing',
key: 'parallel',
role: 'Multiple Teams',
isParallel: true,
branches: [
{
name: 'Documentation Track',
color: 'blue',
stages: [
{ id: '9a-1', name: 'Documents Collection by H.O', key: 'docs-collection', role: 'DD H.O' },
{ id: '9a-2', name: 'New LOA Issuance', key: 'loa-issuance', role: 'DD Admin' }
]
},
{
name: 'Infrastructure Track',
color: 'green',
stages: [
{ id: '9b-1', name: 'Layout Issuance by Architect', key: 'layout-issuance', role: 'Architect' },
{ id: '9b-2', name: 'Infra Completion', key: 'infra-completion', role: 'Dealer' }
]
}
]
},
{ id: 10, name: 'NBH Clearance with EOR', key: 'nbh-eor', role: 'NBH' },
{ id: 11, name: 'Relocation Complete', key: 'complete', role: 'System' }
{ 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: 'DD Head Approval', key: 'DD_HEAD_APPROVAL', role: 'DD Head' },
{ id: 7, name: 'NBH Approval', key: 'NBH_APPROVAL', role: 'NBH' },
{ id: 8, name: 'Legal Clearance', key: 'LEGAL_CLEARANCE', role: 'Legal Admin' },
{ id: 9, name: 'NBH Clearance with EOR', key: 'NBH_CLEARANCE_EOR', role: 'NBH' },
{ id: 10, name: 'Relocation Complete', key: 'COMPLETED', role: 'System' }
];
// Required documents configuration
@ -94,18 +71,87 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
const [isWorknoteDialogOpen, setIsWorknoteDialogOpen] = useState(false);
const [worknotes, setWorknotes] = useState<any[]>([]);
const [newWorknote, setNewWorknote] = useState('');
const [eorChecklist, setEorChecklist] = useState<any>(null);
const [isEorLoading, setIsEorLoading] = useState(false);
const [isSubmittingEor, setIsSubmittingEor] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedDocType, setSelectedDocType] = useState<string>(requiredDocuments[0]);
const [isUploading, setIsUploading] = useState(false);
const [activeTab, setActiveTab] = useState('workflow');
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [selectedDoc, setSelectedDoc] = useState<any>(null);
useEffect(() => {
fetchRequestDetails();
}, [requestId]);
const fetchRequestDetails = async () => {
const fetchEorChecklist = async () => {
try {
setIsLoading(true);
setIsEorLoading(true);
const response = await API.getEorChecklistForRelocation(requestId) 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('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('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) {
setRequest(response.data.request);
setWorknotes(response.data.request.worknotes || []);
// Auto-fetch EOR checklist if in the correct stage
const currentStage = response.data.request.currentStage;
if (currentStage === 'NBH_CLEARANCE_EOR' || currentStage === 'NBH Clearance with EOR' || response.data.request.status === 'Completed') {
fetchEorChecklist();
}
}
} catch (error) {
console.error('Fetch relocation request details error:', error);
@ -117,57 +163,45 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
// Calculate current stage index based on request data
const getCurrentStageIndex = () => {
if (!request) return 1;
const stageMap: Record<string, number> = {
'Submitted': 1,
'Dealer': 1,
'DD Admin Review': 2,
'ASM': 2,
'ASM Review': 2,
'RBM Review': 3,
'RBM': 3,
'DD ZM Review': 4,
'DD-ZM': 4,
'ZBH Review': 5,
'ZBH': 5,
'DD Lead Review': 6,
'DD Lead': 6,
'DD Head Review': 7,
'DD Head': 7,
'NBH Review': 8,
'NBH Approval': 8,
'NBH': 8,
'DD H.O': 9, // Parallel branch A
'Architect': 9, // Parallel branch B
'Legal Clearance': 10,
'Closed': 11,
'Completed': 11
};
// Map backend stage values to frontend stage indices
const backendStage = request.currentStage?.replace(/_/g, ' ') || 'DD Admin Review';
return stageMap[backendStage] || 2; // Default to stage 2 (DD Admin Review) for new requests
if (!request) return 0;
const stageIndex = workflowStages.findIndex(s =>
s.key === request.currentStage ||
s.name === request.currentStage ||
s.name === (request.currentStage?.replace(/_/g, ' ') || '')
);
return stageIndex !== -1 ? stageIndex + 1 : 1;
};
// Helper to find assigned reviewer for a stage
const getAssignedReviewer = (stageKey: string) => {
const getAssignedReviewer = (stageName: string) => {
if (!request || !request.participants || request.participants.length === 0) return null;
const stageMap: Record<string, string> = {
'ASM Review': 'ASM_REVIEW',
'RBM Review': 'RBM_REVIEW',
'DD ZM Review': 'DD_ZM_REVIEW',
'ZBH Review': 'ZBH_REVIEW',
'DD Lead Review': 'DD_LEAD_REVIEW',
'NBH Review': 'NBH_REVIEW',
'Legal Clearance': 'LEGAL_CLEARANCE'
};
const targetStage = stageMap[stageKey];
if (!targetStage) return null;
const participant = request.participants.find((p: any) => p.metadata?.stage === targetStage);
// 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?.roleCode || null;
return participant.user?.fullName || participant.user?.name || participant.user?.role || null;
};
const currentStageIndex = getCurrentStageIndex();
const currentStageConfig = workflowStages[currentStageIndex - 1];
// Visibility logic for Approve/Reject buttons
const canUserAction = () => {
if (!request || !currentUser) return false;
// Check for Super Admin bypass
const isAdmin = currentUser.role === 'Super Admin' || currentUser.role === 'Super Admin';
if (isAdmin) return true;
// Check if user's role matches the role required for the current stage
return currentUser.role === currentStageConfig?.role || currentUser.role === currentStageConfig?.role;
};
const showActions = canUserAction() && request.status !== 'Completed' && request.status !== 'Rejected';
if (!request) {
return (
@ -197,6 +231,10 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
setIsActionDialogOpen(false);
setComments('');
fetchRequestDetails();
// If moving to NBH Clearance EOR, fetch the checklist
if (actionType === 'approve' && currentStageConfig?.key === 'LEGAL_CLEARANCE') {
fetchEorChecklist();
}
}
} catch (error) {
console.error('Submit action error:', error);
@ -227,10 +265,56 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
};
const handleUploadDocument = async () => {
// In a real app, this would use API.uploadDocument
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);
fetchRequestDetails();
setSelectedFile(null);
fetchRequestDetails(true);
}
} catch (error) {
console.error('Upload document error:', error);
toast.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
}
} catch (error) {
console.error('Verify document error:', error);
toast.error('Failed to verify document');
}
};
const handlePreviewDocument = (doc: any) => {
setSelectedDoc({
fileName: doc.name,
filePath: doc.url,
documentType: doc.type,
createdAt: doc.uploadedOn,
mimeType: doc.mimeType
});
setIsPreviewOpen(true);
};
if (isLoading) {
@ -297,14 +381,14 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
<MapPin className="w-4 h-4 text-slate-400" />
<div>
<p className="text-slate-600 text-xs">From (Current)</p>
<p className="text-slate-900 text-sm">{request.outlet?.address}, {request.outlet?.city}</p>
<p className="text-slate-900 text-sm">{request.currentLocation || (request.outlet ? `${request.outlet.address}, ${request.outlet.city}` : 'N/A')}</p>
</div>
</div>
<div className="flex items-center gap-2">
<Navigation className="w-4 h-4 text-amber-600" />
<div>
<p className="text-slate-600 text-xs">To (Proposed)</p>
<p className="text-slate-900 text-sm">{request.newAddress}, {request.newCity}</p>
<p className="text-slate-900 text-sm">{request.proposedLocation || `${request.newAddress}, ${request.newCity}`}</p>
</div>
</div>
<Badge variant="outline" className="border-slate-300 text-slate-700">
@ -314,7 +398,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
</div>
<div>
<p className="text-slate-600 text-sm mb-1">Request Information</p>
<p className="text-slate-900 text-sm">Submitted: {new Date(request.createdAt).toLocaleDateString()}</p>
<p className="text-slate-900 text-sm">Submitted: {formatDateTime(request.createdAt)}</p>
<p className="text-slate-600 text-sm">By: {request.dealer?.fullName}</p>
<p className="text-slate-900 text-sm mt-2">Current Stage: {request.currentStage.replace(/_/g, ' ')}</p>
</div>
@ -331,12 +415,15 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
{/* Main Content */}
<div className="lg:col-span-2 space-y-6">
<Card>
<Tabs defaultValue="workflow" className="w-full">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<CardHeader className="pb-4">
<div className="overflow-x-auto -mx-6 px-6">
<TabsList className="w-max min-w-full justify-start">
<TabsTrigger value="workflow">Workflow Progress</TabsTrigger>
<TabsTrigger value="documents">Documents</TabsTrigger>
{(request.currentStage === 'NBH Clearance with EOR' || request.status === 'Completed' || request.currentStage === 'NBH_CLEARANCE_EOR') && (
<TabsTrigger value="eor">EOR Checklist</TabsTrigger>
)}
<TabsTrigger value="history">History & Audit Trail</TabsTrigger>
</TabsList>
</div>
@ -365,96 +452,11 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
const isCompleted = index < currentStageIndex - 1;
const isCurrent = index === currentStageIndex - 1;
// Handle parallel branches
if (stage.isParallel) {
return (
<div key={stage.id} className="space-y-4">
{/* Parallel stage header */}
<div className="flex items-start gap-4">
<div className="flex flex-col items-center">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
isCompleted ? 'bg-green-100' :
isCurrent ? 'bg-amber-100' :
'bg-slate-100'
}`}>
<GitBranch className={`w-5 h-5 ${
isCompleted ? 'text-green-600' :
isCurrent ? 'text-amber-600' :
'text-slate-400'
}`} />
</div>
<div className="w-0.5 h-8 bg-slate-200" />
</div>
<div className="flex-1 pb-4">
<h4 className="text-slate-900">{stage.name}</h4>
<p className="text-slate-600 text-sm">Two parallel tracks proceeding simultaneously</p>
</div>
</div>
{/* Parallel branches */}
<div className="ml-14 grid grid-cols-2 gap-4">
{stage.branches?.map((branch: any, branchIndex: number) => (
<div key={branchIndex} className={`border-2 rounded-lg p-4 ${
branch.color === 'blue' ? 'border-blue-200 bg-blue-50' : 'border-green-200 bg-green-50'
}`}>
<div className="flex items-center gap-2 mb-3">
<div className={`w-2 h-2 rounded-full ${
branch.color === 'blue' ? 'bg-blue-600' : 'bg-green-600'
}`} />
<h5 className={branch.color === 'blue' ? 'text-blue-900' : 'text-green-900'}>
{branch.name}
</h5>
</div>
<div className="space-y-3">
{branch.stages.map((subStage: any) => {
const subIsCompleted = currentStageIndex > 9;
const subIsCurrent = currentStageIndex === 9 &&
((branch.color === 'blue' && request.currentStage.includes('H.O')) ||
(branch.color === 'green' && request.currentStage.includes('Arch')));
return (
<div key={subStage.id} className="flex items-start gap-3">
<div className={`w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 ${
subIsCompleted ? 'bg-green-100' :
subIsCurrent ? 'bg-amber-100' :
'bg-white'
}`}>
{subIsCompleted ? (
<CheckCircle2 className="w-4 h-4 text-green-600" />
) : subIsCurrent ? (
<Clock className="w-4 h-4 text-amber-600" />
) : (
<AlertCircle className="w-4 h-4 text-slate-400" />
)}
</div>
<div className="flex-1 min-w-0">
<p className={`text-sm ${
subIsCompleted ? 'text-green-900' :
subIsCurrent ? 'text-amber-900' :
branch.color === 'blue' ? 'text-blue-800' : 'text-green-800'
}`}>
{subStage.name}
</p>
<p className="text-xs text-slate-600">{subStage.role}</p>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
<div className="ml-5 w-0.5 h-8 bg-slate-200" />
</div>
);
}
return (
<div key={stage.id} className="flex items-start gap-4">
{/* Status Icon */}
<div className="flex flex-col items-center">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
isCompleted ? 'bg-green-100' :
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${isCompleted ? 'bg-green-100' :
isCurrent ? 'bg-amber-100' :
'bg-slate-100'
}`}>
@ -467,8 +469,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
)}
</div>
{index < workflowStages.length - 1 && (
<div className={`w-0.5 h-12 ${
isCompleted ? 'bg-green-300' : 'bg-slate-200'
<div className={`w-0.5 h-12 ${isCompleted ? 'bg-green-300' : 'bg-slate-200'
}`} />
)}
</div>
@ -535,28 +536,49 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
<div className="space-y-4">
<div>
<Label>Document Type</Label>
<select className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md">
{requiredDocuments.map((doc, index) => (
<select
className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md"
value={selectedDocType}
onChange={(e) => setSelectedDocType(e.target.value)}
>
{requiredDocuments.map((doc, index) => {
const isAlreadyUploaded = request.documents?.some((d: any) =>
d.type === doc || d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0])
);
return (
<option key={index} value={doc}>
{doc}
{isAlreadyUploaded ? `${doc}` : doc}
</option>
))}
);
})}
</select>
</div>
<div>
<Label>Upload File</Label>
<Input type="file" className="mt-1" />
<Input
type="file"
className="mt-1"
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsUploadDialogOpen(false)}>
<Button variant="outline" onClick={() => setIsUploadDialogOpen(false)} disabled={isUploading}>
Cancel
</Button>
<Button
className="bg-amber-600 hover:bg-amber-700"
onClick={handleUploadDocument}
disabled={isUploading}
>
Upload
{isUploading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Uploading...
</>
) : (
'Upload'
)}
</Button>
</DialogFooter>
</DialogContent>
@ -566,12 +588,13 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
{/* Required Documents Checklist */}
<div className="grid grid-cols-2 gap-2">
{requiredDocuments.map((doc: string, index: number) => {
const uploaded = request.documents?.find((d: any) => d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0]));
const uploaded = request.documents?.find((d: any) =>
d.type === doc || d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0])
);
return (
<div
key={index}
className={`flex items-center gap-2 p-2 rounded border text-sm ${
uploaded ? 'bg-green-50 border-green-200' : 'bg-slate-50 border-slate-200'
className={`flex items-center gap-2 p-2 rounded border text-sm ${uploaded ? 'bg-green-50 border-green-200' : 'bg-slate-50 border-slate-200'
}`}
>
{uploaded ? (
@ -618,7 +641,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
</Badge>
</TableCell>
<TableCell className="text-slate-600">
{new Date(doc.uploadedOn).toLocaleDateString()}
{formatDateTime(doc.uploadedOn)}
</TableCell>
<TableCell className="text-slate-600">
{doc.uploadedBy}
@ -630,14 +653,37 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button size="sm" variant="outline">
<Eye className="w-4 h-4 mr-1" />
View
<Button
size="icon"
variant="outline"
className="h-8 w-8"
onClick={() => doc.url && handlePreviewDocument(doc)}
disabled={!doc.url}
title={doc.url ? "View Document" : "File path not available"}
>
<Eye className="w-4 h-4" />
</Button>
<Button size="sm" variant="outline">
<Download className="w-4 h-4 mr-1" />
Download
<Button
size="icon"
variant="outline"
className="h-8 w-8"
onClick={() => doc.url && window.open(`http://localhost:5000/${doc.url}`, '_blank')}
disabled={!doc.url}
title={doc.url ? "Download Document" : "File path not available"}
>
<Download className="w-4 h-4" />
</Button>
{doc.status === 'Pending Verification' && currentUser?.role !== 'Dealer' && (
<Button
size="sm"
className="h-8 bg-green-600 hover:bg-green-700 text-white gap-1"
onClick={() => handleVerifyDocument(doc.id)}
title="Verify Document"
>
<CheckCircle2 className="w-4 h-4" />
Verify
</Button>
)}
</div>
</TableCell>
</TableRow>
@ -655,13 +701,133 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
</Tabs>
</TabsContent>
{/* EOR Checklist Tab */}
<TabsContent value="eor" className="mt-0">
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h4 className="text-slate-900">EOR Readiness Checklist</h4>
<p className="text-slate-600 text-sm">Verify new location infrastructure and statutory compliances</p>
</div>
{eorChecklist && (
<Badge className={getStatusColor(eorChecklist.status)}>
{eorChecklist.status}
</Badge>
)}
</div>
{!eorChecklist ? (
<div className="bg-slate-50 border border-dashed border-slate-300 rounded-lg p-12 text-center">
{isEorLoading ? (
<div className="flex flex-col items-center gap-2">
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" />
<p className="text-slate-500">Fetching checklist...</p>
</div>
) : (
<>
<AlertCircle className="w-12 h-12 text-slate-300 mx-auto mb-4" />
<h5 className="text-slate-900 mb-1">No Checklist Found</h5>
<p className="text-slate-500 text-sm mb-4">
The EOR checklist will be automatically initiated once the request reaches the final clearance stage.
</p>
<Button variant="outline" onClick={fetchEorChecklist}>
Try Refreshing
</Button>
</>
)}
</div>
) : (
<div className="space-y-4">
<div className="border border-slate-200 rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-slate-50">
<TableHead className="w-[50px]"></TableHead>
<TableHead>Category</TableHead>
<TableHead>Checklist Item</TableHead>
<TableHead>Proof & Documents</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{eorChecklist.items?.map((item: any) => (
<TableRow key={item.id}>
<TableCell>
<input
type="checkbox"
checked={item.isCompliant}
onChange={(e) => 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-amber-600"
/>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 capitalize text-xs">
{item.itemType}
</Badge>
</TableCell>
<TableCell className="text-slate-900 font-medium text-sm">
{item.description}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{item.proofDocumentId ? (
<Button size="sm" variant="ghost" className="h-7 text-blue-600">
<Eye className="w-3.5 h-3.5 mr-1" />
View
</Button>
) : (
<Button
size="sm"
variant="ghost"
className="h-7 text-slate-400"
disabled={eorChecklist.status === 'Completed'}
>
<Upload className="w-3.5 h-3.5 mr-1" />
Upload
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Audit Submission Button */}
{(currentUser?.role === 'NBH' || currentUser?.role === 'Super Admin') && eorChecklist.status !== 'Completed' && (
<div className="flex justify-end pt-4 border-t border-slate-200">
<Button
className="bg-green-600 hover:bg-green-700"
onClick={handleSubmitEorAudit}
disabled={isSubmittingEor || !eorChecklist.items?.every((i: any) => i.isCompliant)}
>
{isSubmittingEor ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<CheckCircle2 className="w-4 h-4 mr-2" />
)}
Finalize EOR & Complete Relocation
</Button>
</div>
)}
{!eorChecklist.items?.every((i: any) => i.isCompliant) && (
<p className="text-right text-xs text-amber-600 italic">
All items must be marked as compliant before final submission.
</p>
)}
</div>
)}
</div>
</TabsContent>
{/* History Tab */}
<TabsContent value="history" className="mt-0">
<div className="space-y-4">
{request.timeline && request.timeline.length > 0 ? request.timeline.map((entry: any, index: number) => (
<div key={index} className="flex items-start gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
entry.action.toLowerCase().includes('approve') || entry.action.toLowerCase().includes('submit') ? 'bg-green-100' :
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${entry.action.toLowerCase().includes('approve') || entry.action.toLowerCase().includes('submit') ? 'bg-green-100' :
'bg-amber-100'
}`}>
{entry.action.toLowerCase().includes('approve') ? (
@ -683,7 +849,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
</Badge>
</div>
<p className="text-slate-600 text-sm mt-2">{entry.remarks || entry.remarks || 'No remarks provided'}</p>
<p className="text-slate-500 text-sm mt-1">{new Date(entry.timestamp).toLocaleString()}</p>
<p className="text-slate-500 text-sm mt-1">{formatDateTime(entry.timestamp)}</p>
</div>
</div>
)) : (
@ -736,7 +902,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
<CardTitle>Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{currentUser?.role !== 'Dealer' && (
{showActions && (
<>
<Button
className="w-full bg-green-600 hover:bg-green-700"
@ -834,6 +1000,9 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
<form onSubmit={handleSubmitAction} className="space-y-4">
<div>
<Label htmlFor="comments" >Comments *</Label>
<div className="space-y-2" />
</div>
<Textarea
id="comments"
value={comments}
@ -842,7 +1011,6 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
rows={4}
required
/>
</div>
<DialogFooter>
<Button
@ -906,7 +1074,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
{note.author?.role || 'User'}
</Badge>
</div>
<span className="text-slate-500 text-xs">{new Date(note.createdAt).toLocaleString()}</span>
<span className="text-slate-500 text-xs">{formatDateTime(note.createdAt)}</span>
</div>
<p className="text-slate-700 text-sm mt-2">{note.noteText}</p>
</div>
@ -960,6 +1128,12 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
</DialogFooter>
</DialogContent>
</Dialog>
<DocumentPreviewModal
isOpen={isPreviewOpen}
onClose={() => setIsPreviewOpen(false)}
document={selectedDoc}
/>
</div>
);
}

View File

@ -4,3 +4,25 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatDateTime(date: string | Date | number, format: 'full' | 'date' | 'time' = 'full') {
const d = new Date(date);
const options: Intl.DateTimeFormatOptions = {
day: '2-digit',
month: 'short',
year: 'numeric',
};
if (format === 'full' || format === 'time') {
options.hour = '2-digit';
options.minute = '2-digit';
options.hour12 = true;
}
if (format === 'time') {
return d.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: true });
}
return d.toLocaleDateString('en-IN', options);
}