From c9de800c470ce7a4444b36d1b371773234bcaa94 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Fri, 3 Apr 2026 02:31:56 +0530 Subject: [PATCH] relocatin flow integrated at cetrtain extent douments mapping , assigns mapping done --- src/api/API.ts | 10 + .../applications/RelocationRequestDetails.tsx | 662 +++++++++++------- src/components/ui/utils.ts | 22 + 3 files changed, 450 insertions(+), 244 deletions(-) diff --git a/src/api/API.ts b/src/api/API.ts index ad176e6..7c6539e 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -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; diff --git a/src/components/applications/RelocationRequestDetails.tsx b/src/components/applications/RelocationRequestDetails.tsx index f8a2280..7cded75 100644 --- a/src/components/applications/RelocationRequestDetails.tsx +++ b/src/components/applications/RelocationRequestDetails.tsx @@ -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([]); const [newWorknote, setNewWorknote] = useState(''); + 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]); + const [isUploading, setIsUploading] = useState(false); + const [activeTab, setActiveTab] = useState('workflow'); + const [isPreviewOpen, setIsPreviewOpen] = useState(false); + const [selectedDoc, setSelectedDoc] = useState(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 = { - '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 = { - '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 ( @@ -186,17 +220,21 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe const handleSubmitAction = async (e: React.FormEvent) => { e.preventDefault(); - + try { setIsSubmitting(true); const action = actionType === 'approve' ? 'APPROVE' : actionType === 'reject' ? 'REJECT' : 'HOLD'; const response = await API.updateRelocationRequest(requestId, action, { remarks: comments }) as any; - + if (response.data.success) { toast.success(`Request ${actionType}d successfully`); 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); @@ -209,10 +247,10 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe const handleAddWorknote = async () => { if (newWorknote.trim()) { try { - const response = await API.addWorknote({ - requestId, - requestType: 'relocation', - message: newWorknote + const response = await API.addWorknote({ + requestId, + requestType: 'relocation', + message: newWorknote }) as any; if (response.data.success) { setNewWorknote(''); @@ -227,10 +265,56 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe }; const handleUploadDocument = async () => { - // In a real app, this would use API.uploadDocument - toast.success('Document uploaded successfully'); - setIsUploadDialogOpen(false); - fetchRequestDetails(); + 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); + } + } 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

From (Current)

-

{request.outlet?.address}, {request.outlet?.city}

+

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

To (Proposed)

-

{request.newAddress}, {request.newCity}

+

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

@@ -314,12 +398,12 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe

Request Information

-

Submitted: {new Date(request.createdAt).toLocaleDateString()}

+

Submitted: {formatDateTime(request.createdAt)}

By: {request.dealer?.fullName}

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

- +

Reason for Relocation

{request.reason}

@@ -331,12 +415,15 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe {/* Main Content */}
- +
Workflow Progress Documents + {(request.currentStage === 'NBH Clearance with EOR' || request.status === 'Completed' || request.currentStage === 'NBH_CLEARANCE_EOR') && ( + EOR Checklist + )} History & Audit Trail
@@ -352,7 +439,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe {request.progressPercentage}%
-
@@ -365,99 +452,14 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe const isCompleted = index < currentStageIndex - 1; const isCurrent = index === currentStageIndex - 1; - // Handle parallel branches - if (stage.isParallel) { - return ( -
- {/* Parallel stage header */} -
-
-
- -
-
-
-
-

{stage.name}

-

Two parallel tracks proceeding simultaneously

-
-
- - {/* Parallel branches */} -
- {stage.branches?.map((branch: any, branchIndex: number) => ( -
-
-
-
- {branch.name} -
-
-
- {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 ( -
-
- {subIsCompleted ? ( - - ) : subIsCurrent ? ( - - ) : ( - - )} -
-
-

- {subStage.name} -

-

{subStage.role}

-
-
- ); - })} -
-
- ))} -
-
-
- ); - } - return (
{/* Status Icon */}
-
+ 'bg-slate-100' + }`}> {isCompleted ? ( ) : isCurrent ? ( @@ -467,9 +469,8 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe )}
{index < workflowStages.length - 1 && ( -
+
)}
@@ -491,8 +492,8 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
{isCompleted ? 'Completed' : isCurrent ? 'In Progress' : 'Pending'} @@ -535,28 +536,49 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
- 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 ( + + ); + })}
- + setSelectedFile(e.target.files?.[0] || null)} + />
- - @@ -566,13 +588,14 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe {/* Required Documents Checklist */}
{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 ( -
{uploaded ? ( @@ -618,7 +641,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe - {new Date(doc.uploadedOn).toLocaleDateString()} + {formatDateTime(doc.uploadedOn)} {doc.uploadedBy} @@ -630,14 +653,37 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
- - + {doc.status === 'Pending Verification' && currentUser?.role !== 'Dealer' && ( + + )}
@@ -655,15 +701,135 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe + {/* EOR Checklist Tab */} + +
+
+
+

EOR Readiness Checklist

+

Verify new location infrastructure and statutory compliances

+
+ {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?.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-amber-600" + /> + + + + {item.itemType} + + + + {item.description} + + +
+ {item.proofDocumentId ? ( + + ) : ( + + )} +
+
+
+ ))} +
+
+
+ + {/* 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 */}
{request.timeline && request.timeline.length > 0 ? request.timeline.map((entry: any, index: number) => (
-
+ }`}> {entry.action.toLowerCase().includes('approve') ? ( ) : entry.action.toLowerCase().includes('submit') ? ( @@ -683,7 +849,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe

{entry.remarks || entry.remarks || 'No remarks provided'}

-

{new Date(entry.timestamp).toLocaleString()}

+

{formatDateTime(entry.timestamp)}

)) : ( @@ -698,8 +864,8 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
- {/* Right Sidebar - Actions */} -
+ {/* Right Sidebar - Actions */} +
{/* Current Status Card */} @@ -715,7 +881,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe

Progress

-
@@ -736,9 +902,9 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe Actions - {currentUser?.role !== 'Dealer' && ( + {showActions && ( <> - - -