diff --git a/src/components/workflow/DocumentUpload/DocumentCard.tsx b/src/components/workflow/DocumentUpload/DocumentCard.tsx index d41a4fc..f40617b 100644 --- a/src/components/workflow/DocumentUpload/DocumentCard.tsx +++ b/src/components/workflow/DocumentUpload/DocumentCard.tsx @@ -6,7 +6,7 @@ export interface DocumentData { documentId: string; name: string; fileType: string; - size: string; + size?: string; sizeBytes?: number; uploadedBy?: string; uploadedAt: string; @@ -48,7 +48,9 @@ export function DocumentCard({ {document.name}

- {document.size} • Uploaded by {document.uploadedBy} on {formatDateTime(document.uploadedAt)} + {document.size && {document.size} • } + {document.uploadedBy && Uploaded by {document.uploadedBy} on } + {formatDateTime(document.uploadedAt)}

diff --git a/src/dealer-claim/components/request-detail/WorkflowTab.tsx b/src/dealer-claim/components/request-detail/WorkflowTab.tsx index c27a3aa..4f16a3d 100644 --- a/src/dealer-claim/components/request-detail/WorkflowTab.tsx +++ b/src/dealer-claim/components/request-detail/WorkflowTab.tsx @@ -10,7 +10,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; -import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon, XCircle, History, ChevronDown, ChevronUp, RefreshCw, RotateCw } from 'lucide-react'; +import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon, XCircle, History, ChevronDown, ChevronUp, RefreshCw, RotateCw, Eye } from 'lucide-react'; import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter'; import { formatHoursMinutes } from '@/utils/slaTracker'; import { @@ -21,7 +21,8 @@ import { DealerCompletionDocumentsModal, CreditNoteSAPModal, EmailNotificationTemplateModal, - DMSPushModal + DMSPushModal, + SnapshotDetailsModal // InitiatorActionModal - Removed, using direct buttons instead } from './modals'; import { toast } from 'sonner'; @@ -35,6 +36,10 @@ interface DealerClaimWorkflowTabProps { isInitiator: boolean; onSkipApprover?: (data: any) => void; onRefresh?: () => void; + documentPolicy: { + maxFileSizeMB: number; + allowedFileTypes: string[]; + }; } interface WorkflowStep { @@ -67,6 +72,7 @@ interface WorkflowStep { versionHistory?: { current: any; previous: any; + all?: any[]; }; } @@ -163,7 +169,8 @@ export function DealerClaimWorkflowTab({ user, isInitiator, onSkipApprover: _onSkipApprover, - onRefresh + onRefresh, + documentPolicy }: DealerClaimWorkflowTabProps) { const [showProposalModal, setShowProposalModal] = useState(false); const [showApprovalModal, setShowApprovalModal] = useState(false); @@ -179,6 +186,7 @@ export function DealerClaimWorkflowTab({ const [versionHistory, setVersionHistory] = useState([]); const [showHistory, setShowHistory] = useState(false); const [expandedVersionSteps, setExpandedVersionSteps] = useState>(new Set()); + const [viewSnapshot, setViewSnapshot] = useState<{data: any, type: 'PROPOSAL' | 'COMPLETION', title?: string} | null>(null); // Load approval flows from real API const [approvalFlow, setApprovalFlow] = useState([]); @@ -303,7 +311,11 @@ export function DealerClaimWorkflowTab({ if (request?.id || request?.requestId) { try { const history = await getWorkflowHistory(request.id || request.requestId); - setVersionHistory(history); + // Sort by createdAt descending to ensure most recent is at the top + const sortedHistory = [...(history || [])].sort((a: any, b: any) => { + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + }); + setVersionHistory(sortedHistory); } catch (error) { console.warn('Failed to load version history:', error); } @@ -444,12 +456,40 @@ export function DealerClaimWorkflowTab({ }); // Sort by version descending to get most recent first - const sortedVersions = [...stepVersions].sort((a, b) => b.version - a.version); + // Prioritize APPROVE snapshots over WORKFLOW snapshots for the same version + // This ensures approval comments are shown instead of just workflow movement + const sortedVersions = [...stepVersions].sort((a, b) => { + // First sort by version (descending) + if (b.version !== a.version) { + return b.version - a.version; + } + // If same version, prioritize APPROVE over WORKFLOW + const aPriority = a.snapshotType === 'APPROVE' ? 1 : a.snapshotType === 'PROPOSAL' ? 2 : a.snapshotType === 'COMPLETION' ? 2 : 3; + const bPriority = b.snapshotType === 'APPROVE' ? 1 : b.snapshotType === 'PROPOSAL' ? 2 : b.snapshotType === 'COMPLETION' ? 2 : 3; + return aPriority - bPriority; + }); - const current = sortedVersions.length > 0 ? sortedVersions[0] : null; - const previous = sortedVersions.length > 1 ? sortedVersions[1] : null; + // Filter out WORKFLOW snapshots if there's an APPROVE snapshot for the same level + // This ensures we show APPROVE snapshots (with comments) instead of WORKFLOW snapshots + const filteredVersions = sortedVersions.filter((version, _index, arr) => { + // If this is a WORKFLOW snapshot, check if there's an APPROVE snapshot with same or higher version + if (version.snapshotType === 'WORKFLOW') { + const hasApproveSnapshot = arr.some(v => + v.snapshotType === 'APPROVE' && + v.levelName === version.levelName && + v.version >= version.version + ); + // Only keep WORKFLOW snapshot if there's no APPROVE snapshot + return !hasApproveSnapshot; + } + // Keep all non-WORKFLOW snapshots + return true; + }); - return { current, previous }; + const current = filteredVersions.length > 0 ? filteredVersions[0] : null; + const previous = filteredVersions.length > 1 ? filteredVersions[1] : null; + + return { current, previous, all: filteredVersions }; }; @@ -939,6 +979,62 @@ export function DealerClaimWorkflowTab({ } }; + // Handle proposal revision request + const handleProposalRevision = async (comments: string) => { + try { + if (!request?.id && !request?.requestId) { + throw new Error('Request ID not found'); + } + + const requestId = request.id || request.requestId; + + // Get approval levels to find the initiator's step levelId dynamically + const details = await getWorkflowDetails(requestId); + const approvals = details?.approvalLevels || details?.approvals || []; + + // Find the initiator's step by checking approverEmail or levelName + const initiatorEmail = ( + (request as any)?.initiator?.email?.toLowerCase() || + (request as any)?.initiatorEmail?.toLowerCase() || + '' + ); + + const step2Level = approvals.find((level: any) => { + const levelApproverEmail = (level.approverEmail || level.approver_email || '').toLowerCase(); + const levelName = (level.levelName || level.level_name || '').toLowerCase(); + const levelNumber = level.levelNumber || level.level_number; + + // Check if this is the initiator's step + return (initiatorEmail && levelApproverEmail === initiatorEmail) || + levelName.includes('requestor evaluation') || + (levelName.includes('requestor') && levelName.includes('confirmation')) || + // Fallback: if initiatorStepNumber was found earlier, use it + (levelNumber === initiatorStepNumber); + }) || approvals.find((level: any) => + (level.levelNumber || level.level_number) === 2 + ); // Final fallback to level 2 + + if (!step2Level?.levelId && !step2Level?.level_id) { + throw new Error('Initiator approval level not found'); + } + + const levelId = step2Level.levelId || step2Level.level_id; + + // Reject the initiator's step using real API with Revision Requested reason + // This will trigger the backend to return the workflow to the previous step (Dealer) + await rejectLevel(requestId, levelId, 'Revised Quotation Requested', comments); + + // Activity is logged by backend approval service - no need to create work note + toast.success('Revision requested. Request returned to dealer.'); + handleRefresh(); + } catch (error: any) { + console.error('Failed to request revision:', error); + const errorMessage = error?.response?.data?.message || error?.message || 'Failed to request revision. Please try again.'; + toast.error(errorMessage); + throw error; + } + }; + // Handle IO approval (Department Lead step - found dynamically) const handleIOApproval = async (data: { ioNumber: string; @@ -1440,7 +1536,7 @@ export function DealerClaimWorkflowTab({ )} {/* Version History Section */} - {step.versionHistory && (step.versionHistory.current || step.versionHistory.previous) && ( + {step.versionHistory && step.versionHistory.all && step.versionHistory.all.length > 0 && (
@@ -1477,232 +1568,142 @@ export function DealerClaimWorkflowTab({ )} - {expandedVersionSteps.has(step.step) && ( -
- {/* Current Version */} - {step.versionHistory.current && ( -
+ {expandedVersionSteps.has(step.step) && step.versionHistory.all && ( +
+ {step.versionHistory.all.map((version: any, vIndex: number) => ( +
0 ? 'pt-2 border-t border-amber-200' : ''}`}>
- - Current: v{step.versionHistory.current.version} - - - {formatDateSafe(step.versionHistory.current.createdAt)} - -
-
-

- {step.versionHistory.current.changeReason || 'Version Update'} -

-
-
- - {step.versionHistory.current.changer?.displayName?.charAt(0) || 'U'} - -
- - By {step.versionHistory.current.changer?.displayName || step.versionHistory.current.changer?.email || 'Unknown User'} - -
- {/* Show snapshot data if available - JSONB structure */} - {step.versionHistory.current.snapshotType === 'PROPOSAL' && step.versionHistory.current.snapshotData && ( -
-

Proposal Snapshot:

- {step.versionHistory.current.snapshotData.documentUrl && ( -

- - View Document - -

- )} -

- Budget: ₹{Number(step.versionHistory.current.snapshotData.totalBudget || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -

- {step.versionHistory.current.snapshotData.comments && ( -

- Comments: {step.versionHistory.current.snapshotData.comments.substring(0, 100)} - {step.versionHistory.current.snapshotData.comments.length > 100 ? '...' : ''} -

- )} - {step.versionHistory.current.snapshotData.costItems && step.versionHistory.current.snapshotData.costItems.length > 0 && ( -

- {step.versionHistory.current.snapshotData.costItems.length} cost item(s) -

- )} -
- )} - {step.versionHistory.current.snapshotType === 'INTERNAL_ORDER' && step.versionHistory.current.snapshotData && ( -
-

IO Block Snapshot:

-

- IO Number: {step.versionHistory.current.snapshotData.ioNumber || 'N/A'} -

-

- Blocked Amount: ₹{Number(step.versionHistory.current.snapshotData.blockedAmount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -

- {step.versionHistory.current.snapshotData.sapDocumentNumber && ( -

- SAP Doc: {step.versionHistory.current.snapshotData.sapDocumentNumber} -

- )} -
- )} - {step.versionHistory.current.snapshotType === 'COMPLETION' && step.versionHistory.current.snapshotData && ( -
-

Completion Snapshot:

- {step.versionHistory.current.snapshotData.documentUrl && ( -

- - View Document - -

- )} -

- Total Expenses: ₹{Number(step.versionHistory.current.snapshotData.totalExpenses || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -

- {step.versionHistory.current.snapshotData.comments && ( -

- Comments: {step.versionHistory.current.snapshotData.comments.substring(0, 100)} - {step.versionHistory.current.snapshotData.comments.length > 100 ? '...' : ''} -

- )} - {step.versionHistory.current.snapshotData.expenses && step.versionHistory.current.snapshotData.expenses.length > 0 && ( -

- {step.versionHistory.current.snapshotData.expenses.length} expense item(s) -

- )} -
- )} - {step.versionHistory.current.snapshotType === 'APPROVE' && step.versionHistory.current.snapshotData && ( -
-

- {step.versionHistory.current.snapshotData.action === 'APPROVE' ? 'Approval' : 'Rejection'} Snapshot: -

-

- By: {step.versionHistory.current.snapshotData.approverName || step.versionHistory.current.snapshotData.approverEmail || 'Unknown'} -

- {step.versionHistory.current.snapshotData.comments && ( -

- Comments: {step.versionHistory.current.snapshotData.comments.substring(0, 100)} - {step.versionHistory.current.snapshotData.comments.length > 100 ? '...' : ''} -

- )} - {step.versionHistory.current.snapshotData.rejectionReason && ( -

- Rejection Reason: {step.versionHistory.current.snapshotData.rejectionReason.substring(0, 100)} - {step.versionHistory.current.snapshotData.rejectionReason.length > 100 ? '...' : ''} -

- )} -
- )} -
- )} - - {/* Previous Version */} - {step.versionHistory.previous && ( -
-
-
- - Previous: v{step.versionHistory.previous.version} + + {vIndex === 0 ? 'Current' : 'Previous'}: v{version.version} - {formatDateSafe(step.versionHistory.previous.createdAt)} + {formatDateSafe(version.createdAt)}

- {step.versionHistory.previous.changeReason || 'Version Update'} + {version.changeReason || 'Version Update'}

-
- - {step.versionHistory.previous.changer?.displayName?.charAt(0) || 'U'} +
+ + {version.changer?.displayName?.charAt(0) || 'U'}
- By {step.versionHistory.previous.changer?.displayName || step.versionHistory.previous.changer?.email || 'Unknown User'} + By {version.changer?.displayName || version.changer?.email || 'Unknown User'}
+ {/* Show snapshot data if available - JSONB structure */} - {step.versionHistory.previous.snapshotType === 'PROPOSAL' && step.versionHistory.previous.snapshotData && ( -
-

Proposal Snapshot:

- {step.versionHistory.previous.snapshotData.documentUrl && ( -

- - View Document - -

- )} -

- Budget: ₹{Number(step.versionHistory.previous.snapshotData.totalBudget || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -

- {step.versionHistory.previous.snapshotData.comments && ( -

- Comments: {step.versionHistory.previous.snapshotData.comments.substring(0, 100)} - {step.versionHistory.previous.snapshotData.comments.length > 100 ? '...' : ''} + {version.snapshotType === 'PROPOSAL' && version.snapshotData && ( +

+
+
+

Proposal Snapshot

+

+ Budget: ₹{Number(version.snapshotData.totalBudget || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +

+
+ +
+ + {version.snapshotData.comments && ( +

+ Comments: {version.snapshotData.comments}

)}
)} - {step.versionHistory.previous.snapshotType === 'INTERNAL_ORDER' && step.versionHistory.previous.snapshotData && ( -
+ {version.snapshotType === 'INTERNAL_ORDER' && version.snapshotData && ( +

IO Block Snapshot:

- IO Number: {step.versionHistory.previous.snapshotData.ioNumber || 'N/A'} + IO Number: {version.snapshotData.ioNumber || 'N/A'}

- Blocked Amount: ₹{Number(step.versionHistory.previous.snapshotData.blockedAmount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + Blocked Amount: ₹{Number(version.snapshotData.blockedAmount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

-
- )} - {step.versionHistory.previous.snapshotType === 'COMPLETION' && step.versionHistory.previous.snapshotData && ( -
-

Completion Snapshot:

- {step.versionHistory.previous.snapshotData.documentUrl && ( -

- - View Document - -

- )} -

- Total Expenses: ₹{Number(step.versionHistory.previous.snapshotData.totalExpenses || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -

- {step.versionHistory.previous.snapshotData.comments && ( -

- Comments: {step.versionHistory.previous.snapshotData.comments.substring(0, 100)} - {step.versionHistory.previous.snapshotData.comments.length > 100 ? '...' : ''} + {version.snapshotData.sapDocumentNumber && ( +

+ SAP Doc: {version.snapshotData.sapDocumentNumber}

)}
)} - {step.versionHistory.previous.snapshotType === 'APPROVE' && step.versionHistory.previous.snapshotData && ( -
+ {version.snapshotType === 'COMPLETION' && version.snapshotData && ( +
+
+
+

Completion Snapshot

+

+ Total: ₹{Number(version.snapshotData.totalExpenses || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +

+
+ +
+ + {version.snapshotData.comments && ( +

+ Comments: {version.snapshotData.comments} +

+ )} +
+ )} + {version.snapshotType === 'APPROVE' && version.snapshotData && ( +

- {step.versionHistory.previous.snapshotData.action === 'APPROVE' ? 'Approval' : 'Rejection'} Snapshot: + {version.snapshotData.action === 'APPROVE' ? 'Approval' : 'Rejection'} Snapshot:

- By: {step.versionHistory.previous.snapshotData.approverName || step.versionHistory.previous.snapshotData.approverEmail || 'Unknown'} + By: {version.snapshotData.approverName || version.snapshotData.approverEmail || 'Unknown'}

- {step.versionHistory.previous.snapshotData.comments && ( + {version.snapshotData.comments && (

- Comments: {step.versionHistory.previous.snapshotData.comments.substring(0, 100)} - {step.versionHistory.previous.snapshotData.comments.length > 100 ? '...' : ''} + Comments: {version.snapshotData.comments.substring(0, 100)} + {version.snapshotData.comments.length > 100 ? '...' : ''}

)} - {step.versionHistory.previous.snapshotData.rejectionReason && ( + {version.snapshotData.rejectionReason && (

- Rejection Reason: {step.versionHistory.previous.snapshotData.rejectionReason.substring(0, 100)} - {step.versionHistory.previous.snapshotData.rejectionReason.length > 100 ? '...' : ''} + Rejection Reason: {version.snapshotData.rejectionReason.substring(0, 100)} + {version.snapshotData.rejectionReason.length > 100 ? '...' : ''}

)}
)} + {version.snapshotType === 'WORKFLOW' && version.snapshotData && version.snapshotData.comments && ( +
+

Approval Comment:

+

+ {version.snapshotData.comments.substring(0, 100)} + {version.snapshotData.comments.length > 100 ? '...' : ''} +

+
+ )}
- )} + ))}
)}
@@ -2252,6 +2253,8 @@ export function DealerClaimWorkflowTab({ dealerName={dealerName} activityName={activityName} requestId={request?.id || request?.requestId} + previousProposalData={versionHistory?.find(v => v.snapshotType === 'PROPOSAL')?.snapshotData} + documentPolicy={documentPolicy} /> {/* Initiator Proposal Approval Modal */} @@ -2262,11 +2265,19 @@ export function DealerClaimWorkflowTab({ }} onApprove={handleProposalApprove} onReject={handleProposalReject} + onRequestRevision={handleProposalRevision} proposalData={proposalData} dealerName={dealerName} activityName={activityName} requestId={request?.id || request?.requestId} request={request} + previousProposalData={(() => { + const proposalSnapshots = versionHistory?.filter(v => v.snapshotType === 'PROPOSAL') || []; + // Since history is sorted descending (most recent first): + // proposalSnapshots[0] is the current proposal being reviewed + // proposalSnapshots[1] is the previous proposal (last iteration - 1) + return proposalSnapshots.length > 1 ? proposalSnapshots[1].snapshotData : null; + })()} /> {/* Dept Lead IO Approval Modal */} @@ -2290,6 +2301,7 @@ export function DealerClaimWorkflowTab({ dealerName={dealerName} activityName={activityName} requestId={request?.id || request?.requestId} + documentPolicy={documentPolicy} /> {/* DMS Push Modal */} @@ -2525,11 +2537,21 @@ export function DealerClaimWorkflowTab({

Proposal:

{item.snapshotData.documentUrl && ( -

- - View Document - -

+
+ +
)}

Budget: ₹{Number(item.snapshotData.totalBudget || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

{item.snapshotData.comments && ( @@ -2541,11 +2563,21 @@ export function DealerClaimWorkflowTab({

Completion:

{item.snapshotData.documentUrl && ( -

- - View Document - -

+
+ +
)}

Total Expenses: ₹{Number(item.snapshotData.totalExpenses || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

{item.snapshotData.comments && ( @@ -2597,6 +2629,14 @@ export function DealerClaimWorkflowTab({ )} )} + + setViewSnapshot(null)} + snapshot={viewSnapshot?.data} + type={viewSnapshot?.type || 'PROPOSAL'} + title={viewSnapshot?.title} + /> ); } diff --git a/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx b/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx index 17ef974..cf2b833 100644 --- a/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx +++ b/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx @@ -47,6 +47,10 @@ interface DealerCompletionDocumentsModalProps { dealerName?: string; activityName?: string; requestId?: string; + documentPolicy: { + maxFileSizeMB: number; + allowedFileTypes: string[]; + }; } export function DealerCompletionDocumentsModal({ @@ -56,6 +60,7 @@ export function DealerCompletionDocumentsModal({ dealerName = 'Jaipur Royal Enfield', activityName = 'Activity', requestId: _requestId, + documentPolicy, }: DealerCompletionDocumentsModalProps) { const [activityCompletionDate, setActivityCompletionDate] = useState(''); const [numberOfParticipants, setNumberOfParticipants] = useState(''); @@ -164,16 +169,40 @@ export function DealerCompletionDocumentsModal({ const handleCompletionDocsChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (files.length > 0) { - // Validate file types - const allowedTypes = ['.pdf', '.doc', '.docx', '.zip', '.rar']; - const invalidFiles = files.filter( - (file) => !allowedTypes.some((ext) => file.name.toLowerCase().endsWith(ext)) - ); - if (invalidFiles.length > 0) { - toast.error('Please upload PDF, DOC, DOCX, ZIP, or RAR files only'); - return; + const validFiles: File[] = []; + const maxSizeBytes = documentPolicy.maxFileSizeMB * 1024 * 1024; + const allowedExts = ['.pdf', '.doc', '.docx', '.zip', '.rar']; + + files.forEach(file => { + const fileExt = '.' + file.name.split('.').pop()?.toLowerCase(); + const simpleExt = file.name.split('.').pop()?.toLowerCase() || ''; + + // 1. Check file size + if (file.size > maxSizeBytes) { + toast.error(`"${file.name}" exceeds ${documentPolicy.maxFileSizeMB}MB limit and was not added.`); + return; + } + + // 2. Check field-specific types + if (!allowedExts.includes(fileExt)) { + toast.error(`"${file.name}" is not a supported document type (PDF, DOC, ZIP).`); + return; + } + + // 3. Check system policy types + if (!documentPolicy.allowedFileTypes.includes(simpleExt)) { + toast.error(`"${file.name}" has an unallowed file type according to system policy.`); + return; + } + + validFiles.push(file); + }); + + if (validFiles.length > 0) { + setCompletionDocuments([...completionDocuments, ...validFiles]); } - setCompletionDocuments([...completionDocuments, ...files]); + + if (completionDocsInputRef.current) completionDocsInputRef.current.value = ''; } }; @@ -184,15 +213,38 @@ export function DealerCompletionDocumentsModal({ const handlePhotosChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (files.length > 0) { - // Validate image files - const invalidFiles = files.filter( - (file) => !file.type.startsWith('image/') - ); - if (invalidFiles.length > 0) { - toast.error('Please upload image files only (JPG, PNG, etc.)'); - return; + const validFiles: File[] = []; + const maxSizeBytes = documentPolicy.maxFileSizeMB * 1024 * 1024; + + files.forEach(file => { + const simpleExt = file.name.split('.').pop()?.toLowerCase() || ''; + + // 1. Check file size + if (file.size > maxSizeBytes) { + toast.error(`Photo "${file.name}" exceeds ${documentPolicy.maxFileSizeMB}MB limit.`); + return; + } + + // 2. Check field-specific (Image) + if (!file.type.startsWith('image/')) { + toast.error(`"${file.name}" is not an image file.`); + return; + } + + // 3. Check system policy + if (!documentPolicy.allowedFileTypes.includes(simpleExt)) { + toast.error(`"${file.name}" has an unsupported image format.`); + return; + } + + validFiles.push(file); + }); + + if (validFiles.length > 0) { + setActivityPhotos([...activityPhotos, ...validFiles]); } - setActivityPhotos([...activityPhotos, ...files]); + + if (photosInputRef.current) photosInputRef.current.value = ''; } }; @@ -203,16 +255,40 @@ export function DealerCompletionDocumentsModal({ const handleInvoicesChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (files.length > 0) { - // Validate file types - const allowedTypes = ['.pdf', '.jpg', '.jpeg', '.png']; - const invalidFiles = files.filter( - (file) => !allowedTypes.some((ext) => file.name.toLowerCase().endsWith(ext)) - ); - if (invalidFiles.length > 0) { - toast.error('Please upload PDF, JPG, or PNG files only'); - return; + const validFiles: File[] = []; + const maxSizeBytes = documentPolicy.maxFileSizeMB * 1024 * 1024; + const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png']; + + files.forEach(file => { + const fileExt = '.' + file.name.split('.').pop()?.toLowerCase(); + const simpleExt = file.name.split('.').pop()?.toLowerCase() || ''; + + // 1. Check file size + if (file.size > maxSizeBytes) { + toast.error(`Invoice "${file.name}" exceeds ${documentPolicy.maxFileSizeMB}MB limit.`); + return; + } + + // 2. Check field-specific + if (!allowedExts.includes(fileExt)) { + toast.error(`"${file.name}" is not a supported type (PDF, JPG, PNG).`); + return; + } + + // 3. Check system policy + if (!documentPolicy.allowedFileTypes.includes(simpleExt)) { + toast.error(`"${file.name}" format is not allowed by system policy.`); + return; + } + + validFiles.push(file); + }); + + if (validFiles.length > 0) { + setInvoicesReceipts([...invoicesReceipts, ...validFiles]); } - setInvoicesReceipts([...invoicesReceipts, ...files]); + + if (invoicesInputRef.current) invoicesInputRef.current.value = ''; } }; @@ -223,13 +299,32 @@ export function DealerCompletionDocumentsModal({ const handleAttendanceChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { - // Validate file types - const allowedTypes = ['.pdf', '.xlsx', '.xls', '.csv']; - const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); - if (!allowedTypes.includes(fileExtension)) { - toast.error('Please upload PDF, Excel, or CSV files only'); + const maxSizeBytes = documentPolicy.maxFileSizeMB * 1024 * 1024; + const allowedExts = ['.pdf', '.xlsx', '.xls', '.csv']; + const fileExt = '.' + file.name.split('.').pop()?.toLowerCase(); + const simpleExt = file.name.split('.').pop()?.toLowerCase() || ''; + + // 1. Check file size + if (file.size > maxSizeBytes) { + toast.error(`Attendance file exceeds ${documentPolicy.maxFileSizeMB}MB limit.`); + if (attendanceInputRef.current) attendanceInputRef.current.value = ''; return; } + + // 2. Check field-specific + if (!allowedExts.includes(fileExt)) { + toast.error('Please upload PDF, Excel, or CSV files only'); + if (attendanceInputRef.current) attendanceInputRef.current.value = ''; + return; + } + + // 3. Check system policy + if (!documentPolicy.allowedFileTypes.includes(simpleExt)) { + toast.error(`"${file.name}" format is not allowed by system policy.`); + if (attendanceInputRef.current) attendanceInputRef.current.value = ''; + return; + } + setAttendanceSheet(file); } }; @@ -438,7 +533,7 @@ export function DealerCompletionDocumentsModal({ ref={completionDocsInputRef} type="file" multiple - accept=".pdf,.doc,.docx,.zip,.rar" + accept={['.pdf', '.doc', '.docx', '.zip', '.rar'].filter(ext => documentPolicy.allowedFileTypes.includes(ext.replace('.', ''))).join(',')} className="hidden" id="completionDocs" onChange={handleCompletionDocsChange} @@ -463,8 +558,9 @@ export function DealerCompletionDocumentsModal({ <> - Click to upload documents (PDF, DOC, ZIP - multiple files allowed) + Click to upload documents (Max {documentPolicy.maxFileSizeMB}MB) +

PDF, DOC, ZIP allowed

)} @@ -543,7 +639,7 @@ export function DealerCompletionDocumentsModal({ ref={photosInputRef} type="file" multiple - accept="image/*" + accept={['.jpg', '.jpeg', '.png', '.gif', '.webp'].filter(ext => documentPolicy.allowedFileTypes.includes(ext.replace('.', ''))).join(',')} className="hidden" id="completionPhotos" onChange={handlePhotosChange} @@ -659,7 +755,7 @@ export function DealerCompletionDocumentsModal({ ref={invoicesInputRef} type="file" multiple - accept=".pdf,.jpg,.jpeg,.png" + accept={['.pdf', '.jpg', '.jpeg', '.png'].filter(ext => documentPolicy.allowedFileTypes.includes(ext.replace('.', ''))).join(',')} className="hidden" id="invoiceReceipts" onChange={handleInvoicesChange} @@ -762,7 +858,7 @@ export function DealerCompletionDocumentsModal({ documentPolicy.allowedFileTypes.includes(ext.replace('.', ''))).join(',')} className="hidden" id="attendanceDoc" onChange={handleAttendanceChange} diff --git a/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx b/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx index cb3595d..1c31645 100644 --- a/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx +++ b/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx @@ -19,8 +19,11 @@ import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { CustomDatePicker } from '@/components/ui/date-picker'; -import { Upload, Plus, X, Calendar, IndianRupee, CircleAlert, CheckCircle2, FileText, Eye, Download } from 'lucide-react'; +import { Upload, Plus, Minus, X, Calendar, IndianRupee, CircleAlert, CheckCircle2, FileText, Eye, Download } from 'lucide-react'; import { toast } from 'sonner'; +import { DocumentCard } from '@/components/workflow/DocumentUpload/DocumentCard'; +import { FilePreview } from '@/components/common/FilePreview/FilePreview'; +import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi'; import '@/components/common/FilePreview/FilePreview.css'; import './DealerProposalModal.css'; @@ -43,6 +46,11 @@ interface DealerProposalSubmissionModalProps { dealerName?: string; activityName?: string; requestId?: string; + previousProposalData?: any; + documentPolicy: { + maxFileSizeMB: number; + allowedFileTypes: string[]; + }; } export function DealerProposalSubmissionModal({ @@ -52,6 +60,8 @@ export function DealerProposalSubmissionModal({ dealerName = 'Jaipur Royal Enfield', activityName = 'Activity', requestId: _requestId, + previousProposalData, + documentPolicy, }: DealerProposalSubmissionModalProps) { const [proposalDocument, setProposalDocument] = useState(null); const [costItems, setCostItems] = useState([ @@ -63,49 +73,87 @@ export function DealerProposalSubmissionModal({ const [otherDocuments, setOtherDocuments] = useState([]); const [dealerComments, setDealerComments] = useState(''); const [submitting, setSubmitting] = useState(false); - const [previewFile, setPreviewFile] = useState<{ file: File; url: string } | null>(null); + const [previewDoc, setPreviewDoc] = useState<{ + fileName: string; + fileType: string; + documentId: string; + fileUrl?: string; + fileSize?: number; + } | null>(null); + const [showPreviousProposal, setShowPreviousProposal] = useState(false); const proposalDocInputRef = useRef(null); const otherDocsInputRef = useRef(null); // Helper function to check if file can be previewed + const canPreview = (fileName: string): boolean => { + if (!fileName) return false; + const name = fileName.toLowerCase(); + return name.endsWith('.pdf') || + !!name.match(/\.(jpg|jpeg|png|gif|webp)$/i); + }; + const canPreviewFile = (file: File): boolean => { - const type = file.type.toLowerCase(); - const name = file.name.toLowerCase(); - return type.includes('image') || - type.includes('pdf') || - name.endsWith('.pdf') || - name.endsWith('.jpg') || - name.endsWith('.jpeg') || - name.endsWith('.png') || - name.endsWith('.gif') || - name.endsWith('.webp'); + return canPreview(file.name); }; // Cleanup object URLs when component unmounts or file changes useEffect(() => { return () => { - if (previewFile?.url) { - URL.revokeObjectURL(previewFile.url); + if (previewDoc?.fileUrl && previewDoc.fileUrl.startsWith('blob:')) { + URL.revokeObjectURL(previewDoc.fileUrl); } }; - }, [previewFile]); + }, [previewDoc]); - // Handle file preview - instant preview using object URL + // Handle manual file preview (for local files) const handlePreviewFile = (file: File) => { if (!canPreviewFile(file)) { toast.error('Preview is only available for images and PDF files'); return; } - // Cleanup previous preview URL - if (previewFile?.url) { - URL.revokeObjectURL(previewFile.url); + // Cleanup previous preview URL if it was a blob + if (previewDoc?.fileUrl && previewDoc.fileUrl.startsWith('blob:')) { + URL.revokeObjectURL(previewDoc.fileUrl); } - // Create object URL immediately for instant preview + // Create blob URL for local file const url = URL.createObjectURL(file); - setPreviewFile({ file, url }); + setPreviewDoc({ + fileName: file.name, + fileType: file.type, + documentId: '', + fileUrl: url, + fileSize: file.size + }); + }; + + // Handle preview for existing Documents (with storageUrl/documentId) + const handlePreviewExisting = (doc: any) => { + const fileName = doc.originalFileName || doc.fileName || doc.name || 'Document'; + const documentId = doc.documentId || doc.id || ''; + const fileType = fileName.toLowerCase().endsWith('.pdf') ? 'application/pdf' : 'image/jpeg'; + + let fileUrl = ''; + if (documentId) { + fileUrl = getDocumentPreviewUrl(documentId); + } else { + fileUrl = doc.storageUrl || doc.documentUrl || ''; + if (fileUrl && !fileUrl.startsWith('http')) { + const baseUrl = import.meta.env.VITE_BASE_URL || ''; + const cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; + const cleanFileUrl = fileUrl.startsWith('/') ? fileUrl : `/${fileUrl}`; + fileUrl = `${cleanBaseUrl}${cleanFileUrl}`; + } + } + + setPreviewDoc({ + fileName, + fileType, + documentId, + fileUrl + }); }; // Handle download file (for non-previewable files) @@ -141,20 +189,57 @@ export function DealerProposalSubmissionModal({ const handleProposalDocChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { - // Validate file type - const allowedTypes = ['.pdf', '.doc', '.docx']; - const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); - if (!allowedTypes.includes(fileExtension)) { - toast.error('Please upload a PDF, DOC, or DOCX file'); + // 1. Check file size + const maxSizeBytes = documentPolicy.maxFileSizeMB * 1024 * 1024; + if (file.size > maxSizeBytes) { + toast.error(`File size exceeds the maximum allowed size of ${documentPolicy.maxFileSizeMB}MB. Current size: ${(file.size / (1024 * 1024)).toFixed(2)}MB`); + if (proposalDocInputRef.current) proposalDocInputRef.current.value = ''; return; } + + // 2. Validate file type (User requested: Keep strictly to pdf, doc, docx + Intersection with system policy) + const hardcodedAllowed = ['.pdf', '.doc', '.docx']; + const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); + const simpleExt = file.name.split('.').pop()?.toLowerCase() || ''; + + if (!hardcodedAllowed.includes(fileExtension) || !documentPolicy.allowedFileTypes.includes(simpleExt)) { + toast.error('Please upload a valid PDF, DOC, or DOCX file as per system policy'); + if (proposalDocInputRef.current) proposalDocInputRef.current.value = ''; + return; + } + setProposalDocument(file); } }; const handleOtherDocsChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); - setOtherDocuments(prev => [...prev, ...files]); + const validFiles: File[] = []; + const maxSizeBytes = documentPolicy.maxFileSizeMB * 1024 * 1024; + + files.forEach(file => { + // 1. Check file size + if (file.size > maxSizeBytes) { + toast.error(`"${file.name}" exceeds ${documentPolicy.maxFileSizeMB}MB limit and was not added.`); + return; + } + + // 2. Check file type + const fileExtension = file.name.split('.').pop()?.toLowerCase() || ''; + if (!documentPolicy.allowedFileTypes.includes(fileExtension)) { + toast.error(`"${file.name}" has an unsupported file type and was not added.`); + return; + } + + validFiles.push(file); + }); + + if (validFiles.length > 0) { + setOtherDocuments(prev => [...prev, ...validFiles]); + } + + // Reset input so searching the same file again triggers change event + if (otherDocsInputRef.current) otherDocsInputRef.current.value = ''; }; const handleAddCostItem = () => { @@ -220,11 +305,11 @@ export function DealerProposalSubmissionModal({ }; const handleReset = () => { - // Cleanup preview URL if exists - if (previewFile?.url) { - URL.revokeObjectURL(previewFile.url); + // Cleanup preview URL if exists and it's a blob + if (previewDoc?.fileUrl && previewDoc.fileUrl.startsWith('blob:')) { + URL.revokeObjectURL(previewDoc.fileUrl); } - setPreviewFile(null); + setPreviewDoc(null); setProposalDocument(null); setCostItems([{ id: '1', description: '', amount: 0 }]); setTimelineMode('date'); @@ -245,7 +330,6 @@ export function DealerProposalSubmissionModal({ // Get minimum date (today) const minDate = new Date().toISOString().split('T')[0]; - return ( @@ -272,7 +356,157 @@ export function DealerProposalSubmissionModal({
-
+
+ + {/* Previous Proposal Reference Section */} + {previousProposalData && ( +
+
setShowPreviousProposal(!showPreviousProposal)} + > +
+
+ + Reference: Previous Proposal Details + + ₹{Number(previousProposalData.totalEstimatedBudget || previousProposalData.totalBudget || 0).toLocaleString('en-IN')} + +
+ +
+ + {showPreviousProposal && ( +
+ {/* Header Info: Date & Document */} +
+ {previousProposalData.expectedCompletionDate && ( +
+ + Expected Completion: + {new Date(previousProposalData.expectedCompletionDate).toLocaleDateString('en-IN')} +
+ )} + + {previousProposalData.documentUrl && ( +
+ {canPreview(previousProposalData.documentUrl) ? ( + <> + + + View Previous Document + + + ) : ( + <> + + + Download Previous Document + + + )} +
+ )} + + {/* Additional/Supporting Documents */} + {previousProposalData.otherDocuments && previousProposalData.otherDocuments.length > 0 && ( +
+

+ + Supporting Documents +

+
+ {previousProposalData.otherDocuments.map((doc: any, idx: number) => ( + handlePreviewExisting(doc) : undefined} + onDownload={async (id) => { + if (id) { + await downloadDocument(id); + } else { + let downloadUrl = doc.storageUrl || doc.documentUrl; + if (downloadUrl && !downloadUrl.startsWith('http')) { + const baseUrl = import.meta.env.VITE_BASE_URL || ''; + const cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; + const cleanFileUrl = downloadUrl.startsWith('/') ? downloadUrl : `/${downloadUrl}`; + downloadUrl = `${cleanBaseUrl}${cleanFileUrl}`; + } + if (downloadUrl) window.open(downloadUrl, '_blank'); + } + }} + /> + ))} +
+
+ )} +
+ + {/* Previous Cost Breakup (handling both costBreakup and costItems) */} + {(previousProposalData.costItems || previousProposalData.costBreakup) && (previousProposalData.costItems || previousProposalData.costBreakup).length > 0 && ( +
+

Previous Cost Breakdown:

+
+ + + + + + + + + {(previousProposalData.costItems || previousProposalData.costBreakup).map((item: any, idx: number) => ( + + + + + ))} + + + + + +
DescriptionAmount
{item.description} + ₹{Number(item.amount).toLocaleString('en-IN')} +
Total + ₹{Number(previousProposalData.totalEstimatedBudget || previousProposalData.totalBudget || 0).toLocaleString('en-IN')} +
+
+
+ )} + + {/* Previous Comments */} + {(previousProposalData.comments || previousProposalData.dealerComments) && ( +
+

Previous Comments:

+
+ "{previousProposalData.comments || previousProposalData.dealerComments}" +
+
+ )} +
+ )} +
+
+ )} +
{/* Left Column - Documents */}
@@ -299,7 +533,7 @@ export function DealerProposalSubmissionModal({ documentPolicy.allowedFileTypes.includes(ext.replace('.', ''))).join(',')} className="hidden" id="proposalDoc" onChange={handleProposalDocChange} @@ -381,6 +615,7 @@ export function DealerProposalSubmissionModal({ ref={otherDocsInputRef} type="file" multiple + accept={documentPolicy.allowedFileTypes.map(ext => `.${ext}`).join(',')} className="hidden" id="otherDocs" onChange={handleOtherDocsChange} @@ -404,8 +639,11 @@ export function DealerProposalSubmissionModal({ ) : ( <> - - Click to upload additional documents (multiple files allowed) + + Click to upload additional documents + + + Max {documentPolicy.maxFileSizeMB}MB | {documentPolicy.allowedFileTypes.join(', ').toUpperCase()} )} @@ -658,94 +896,18 @@ export function DealerProposalSubmissionModal({ - {/* File Preview Modal - Matching DocumentsTab style */} - {previewFile && ( - { - if (previewFile?.url) { - URL.revokeObjectURL(previewFile.url); - } - setPreviewFile(null); - }} - > - -
- -
-
- -
- - {previewFile.file.name} - -

- {previewFile.file.type || 'Unknown type'} • {(previewFile.file.size / 1024).toFixed(1)} KB -

-
-
-
- -
-
-
- -
- {previewFile.file.type?.includes('image') ? ( -
- {previewFile.file.name} -
- ) : previewFile.file.type?.includes('pdf') || previewFile.file.name.toLowerCase().endsWith('.pdf') ? ( -
-