relocatin flow integrated at cetrtain extent douments mapping , assigns mapping done
This commit is contained in:
parent
830f66b5f7
commit
c9de800c47
@ -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;
|
||||
|
||||
@ -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"
|
||||
@ -833,7 +999,10 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser, onOpe
|
||||
|
||||
<form onSubmit={handleSubmitAction} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="comments">Comments *</Label>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user