+ {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 })}
+
+
+
setViewSnapshot({
+ data: version.snapshotData,
+ type: 'PROPOSAL',
+ title: `Proposal (v${version.version})`
+ })}
+ >
+
+ View Details
+
+
+
+ {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 })}
+
+
+
setViewSnapshot({
+ data: version.snapshotData,
+ type: 'COMPLETION',
+ title: `Completion (v${version.version})`
+ })}
+ >
+
+ View Details
+
+
+
+ {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
-
-
+
+ setViewSnapshot({
+ data: item.snapshotData,
+ type: 'PROPOSAL',
+ title: `Historical Proposal (Version ${item.version})`
+ })}
+ >
+ View Details
+
+
+
)}
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
-
-
+
+ setViewSnapshot({
+ data: item.snapshotData,
+ type: 'COMPLETION',
+ title: `Historical Completion (Version ${item.version})`
+ })}
+ >
+ View Details
+
+
+
)}
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 ? : }
+
+
+
+ {showPreviousProposal && (
+
+ {/* Header Info: Date & Document */}
+
+ {previousProposalData.expectedCompletionDate && (
+
+
+ Expected Completion:
+ {new Date(previousProposalData.expectedCompletionDate).toLocaleDateString('en-IN')}
+
+ )}
+
+ {previousProposalData.documentUrl && (
+
+ )}
+
+ {/* 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:
+
+
+
+
+ Description
+ Amount
+
+
+
+ {(previousProposalData.costItems || previousProposalData.costBreakup).map((item: any, idx: number) => (
+
+ {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
-
-
-
-
- handleDownloadFile(previewFile.file)}
- className="gap-2 h-9"
- >
-
- Download
-
-
-
-
-
-
- {previewFile.file.type?.includes('image') ? (
-
-
-
- ) : previewFile.file.type?.includes('pdf') || previewFile.file.name.toLowerCase().endsWith('.pdf') ? (
-
-
-
- ) : (
-
-
-
-
-
Preview Not Available
-
- This file type cannot be previewed. Please download to view.
-
-
handleDownloadFile(previewFile.file)}
- className="gap-2"
- >
-
- Download {previewFile.file.name}
-
-
- )}
-
-
-
-
+ {/* Standardized File Preview */}
+ {previewDoc && (
+ setPreviewDoc(null)}
+ />
)}
);
diff --git a/src/dealer-claim/components/request-detail/modals/InitiatorProposalApprovalModal.tsx b/src/dealer-claim/components/request-detail/modals/InitiatorProposalApprovalModal.tsx
index 88b6198..2e83b3e 100644
--- a/src/dealer-claim/components/request-detail/modals/InitiatorProposalApprovalModal.tsx
+++ b/src/dealer-claim/components/request-detail/modals/InitiatorProposalApprovalModal.tsx
@@ -25,10 +25,13 @@ import {
MessageSquare,
Download,
Eye,
- Loader2,
+ Plus,
+ Minus,
} from 'lucide-react';
import { toast } from 'sonner';
import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi';
+import { DocumentCard } from '@/components/workflow/DocumentUpload/DocumentCard';
+import { FilePreview } from '@/components/common/FilePreview/FilePreview';
import '@/components/common/FilePreview/FilePreview.css';
import './DealerProposalModal.css';
@@ -60,11 +63,13 @@ interface InitiatorProposalApprovalModalProps {
onClose: () => void;
onApprove: (comments: string) => Promise;
onReject: (comments: string) => Promise;
+ onRequestRevision?: (comments: string) => Promise;
proposalData: ProposalData | null;
dealerName?: string;
activityName?: string;
requestId?: string;
request?: any; // Request object to check IO blocking status
+ previousProposalData?: any;
}
export function InitiatorProposalApprovalModal({
@@ -72,27 +77,30 @@ export function InitiatorProposalApprovalModal({
onClose,
onApprove,
onReject,
+ onRequestRevision,
proposalData,
dealerName = 'Dealer',
activityName = 'Activity',
requestId: _requestId, // Prefix with _ to indicate intentionally unused
request,
+ previousProposalData,
}: InitiatorProposalApprovalModalProps) {
const [comments, setComments] = useState('');
const [submitting, setSubmitting] = useState(false);
- const [actionType, setActionType] = useState<'approve' | 'reject' | null>(null);
+ const [actionType, setActionType] = useState<'approve' | 'reject' | 'revision' | null>(null);
+ const [showPreviousProposal, setShowPreviousProposal] = useState(false);
// Check if IO is blocked (IO blocking moved to Requestor Evaluation level)
const internalOrder = request?.internalOrder || request?.internal_order;
const ioBlockedAmount = internalOrder?.ioBlockedAmount || internalOrder?.io_blocked_amount || 0;
const isIOBlocked = ioBlockedAmount > 0;
- const [previewDocument, setPreviewDocument] = useState<{
+ const [previewDoc, setPreviewDoc] = useState<{
name: string;
url: string;
type?: string;
size?: number;
+ id?: string;
} | null>(null);
- const [previewLoading, setPreviewLoading] = useState(false);
// Calculate total budget
const totalBudget = useMemo(() => {
@@ -140,75 +148,40 @@ export function InitiatorProposalApprovalModal({
name.endsWith('.webp');
};
- // Handle document preview - fetch as blob to avoid CSP issues
- const handlePreviewDocument = async (doc: { name: string; url?: string; id?: string }) => {
- if (!doc.id) {
- toast.error('Document preview not available - document ID missing');
+ // Handle document preview - leverage FilePreview's internal fetching
+ const handlePreviewDocument = (doc: { name: string; url?: string; id?: string; storageUrl?: string; documentId?: string }) => {
+ let fileUrl = doc.url || doc.storageUrl || '';
+ const documentId = doc.id || doc.documentId || '';
+
+ if (!documentId && !fileUrl) {
+ toast.error('Document preview not available');
return;
}
- setPreviewLoading(true);
- try {
- const previewUrl = getDocumentPreviewUrl(doc.id);
-
- // Determine file type from name
- const fileName = doc.name.toLowerCase();
- const isPDF = fileName.endsWith('.pdf');
- const isImage = fileName.match(/\.(jpg|jpeg|png|gif|webp)$/i);
-
- // Fetch the document as a blob to create a blob URL (CSP compliant)
- const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
- const token = isProduction ? null : localStorage.getItem('accessToken');
-
- const headers: HeadersInit = {
- 'Accept': isPDF ? 'application/pdf' : '*/*'
- };
-
- if (!isProduction && token) {
- headers['Authorization'] = `Bearer ${token}`;
- }
-
- const response = await fetch(previewUrl, {
- headers,
- credentials: 'include',
- mode: 'cors'
- });
-
- if (!response.ok) {
- throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
- }
-
- const blob = await response.blob();
-
- if (blob.size === 0) {
- throw new Error('File is empty or could not be loaded');
- }
-
- // Create blob URL (CSP compliant - uses 'blob:' protocol)
- const blobUrl = window.URL.createObjectURL(blob);
-
- setPreviewDocument({
- name: doc.name,
- url: blobUrl,
- type: blob.type || (isPDF ? 'application/pdf' : isImage ? 'image' : undefined),
- size: blob.size,
- });
- } catch (error) {
- console.error('Failed to load document preview:', error);
- toast.error('Failed to load document preview');
- } finally {
- setPreviewLoading(false);
+ // Handle relative URLs for snapshots
+ if (fileUrl && !fileUrl.startsWith('http') && !fileUrl.startsWith('blob:')) {
+ 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({
+ name: doc.name || 'Document',
+ url: fileUrl || (documentId ? getDocumentPreviewUrl(documentId) : ''),
+ type: (doc.name || '').toLowerCase().endsWith('.pdf') ? 'application/pdf' : 'image/jpeg',
+ id: documentId
+ });
};
// Cleanup blob URLs on unmount
useEffect(() => {
return () => {
- if (previewDocument?.url && previewDocument.url.startsWith('blob:')) {
- window.URL.revokeObjectURL(previewDocument.url);
+ if (previewDoc?.url && previewDoc.url.startsWith('blob:')) {
+ window.URL.revokeObjectURL(previewDoc.url);
}
};
- }, [previewDocument]);
+ }, [previewDoc]);
const handleApprove = async () => {
if (!comments.trim()) {
@@ -252,6 +225,32 @@ export function InitiatorProposalApprovalModal({
}
};
+ const handleRequestRevision = async () => {
+ if (!comments.trim()) {
+ toast.error('Please provide reasons for requesting a revision');
+ return;
+ }
+
+ if (!onRequestRevision) {
+ toast.error('Revision feature is not available');
+ return;
+ }
+
+ try {
+ setSubmitting(true);
+ setActionType('revision');
+ await onRequestRevision(comments);
+ handleReset();
+ onClose();
+ } catch (error) {
+ console.error('Failed to request revision:', error);
+ toast.error('Failed to request revision. Please try again.');
+ } finally {
+ setSubmitting(false);
+ setActionType(null);
+ }
+ };
+
const handleReset = () => {
setComments('');
setActionType(null);
@@ -296,7 +295,163 @@ export function InitiatorProposalApprovalModal({
-
+
+
+ {/* Previous Proposal Reference Section */}
+ {previousProposalData && (
+
+
setShowPreviousProposal(!showPreviousProposal)}
+ >
+
+
+
+ Reference: Previous Proposal Details (last revision)
+
+ ₹{Number(previousProposalData.totalEstimatedBudget || previousProposalData.totalBudget || 0).toLocaleString('en-IN')}
+
+
+
+ {showPreviousProposal ? : }
+
+
+
+ {showPreviousProposal && (
+
+ {/* Header Info: Date & Document */}
+
+ {previousProposalData.expectedCompletionDate && (
+
+
+ Expected Completion:
+ {new Date(previousProposalData.expectedCompletionDate).toLocaleDateString('en-IN')}
+
+ )}
+
+ {previousProposalData.documentUrl && (
+
+ )}
+
+
+ {/* Cost Breakdown */}
+ {(previousProposalData.costItems || previousProposalData.costBreakup) && (previousProposalData.costItems || previousProposalData.costBreakup).length > 0 && (
+
+
+
+ Previous Cost Breakdown
+
+
+
+
+
+ Description
+ Amount
+
+
+
+ {(previousProposalData.costItems || previousProposalData.costBreakup).map((item: any, idx: number) => (
+
+ {item.description}
+
+ ₹{Number(item.amount).toLocaleString('en-IN')}
+
+
+ ))}
+
+ Total
+
+ ₹{Number(previousProposalData.totalEstimatedBudget || previousProposalData.totalBudget || 0).toLocaleString('en-IN')}
+
+
+
+
+
+
+ )}
+
+ {/* Additional/Supporting Documents */}
+ {previousProposalData.otherDocuments && previousProposalData.otherDocuments.length > 0 && (
+
+
+
+ Supporting Documents
+
+
+ {previousProposalData.otherDocuments.map((doc: any, idx: number) => (
+ handlePreviewDocument(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');
+ }
+ }}
+ />
+ ))}
+
+
+ )}
+
+ {/* Comments */}
+ {(previousProposalData.comments || previousProposalData.dealerComments) && (
+
+
+
+ Previous Comments
+
+
+ "{previousProposalData.comments || previousProposalData.dealerComments}"
+
+
+ )}
+
+ )}
+
+
+ )}
+
{/* Left Column - Documents */}
@@ -330,15 +485,10 @@ export function InitiatorProposalApprovalModal({
handlePreviewDocument(proposalData.proposalDocument!)}
- disabled={previewLoading}
- className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+ className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
title="Preview document"
>
- {previewLoading ? (
-
- ) : (
-
- )}
+
)}
handlePreviewDocument(doc)}
- disabled={previewLoading}
- className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+ className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
title="Preview document"
>
- {previewLoading ? (
-
- ) : (
-
- )}
+
)}
+
+ {submitting && actionType === 'revision' ? (
+ 'Requesting...'
+ ) : (
+ <>
+
+ Request Revised Quotation
+ >
+ )}
+
- {/* File Preview Modal - Matching DocumentsTab style */}
- {previewDocument && (
- setPreviewDocument(null)}
- >
-
-
-
-
-
-
-
-
- {previewDocument.name}
-
- {previewDocument.type && (
-
- {previewDocument.type} {previewDocument.size && `• ${(previewDocument.size / 1024).toFixed(1)} KB`}
-
- )}
-
-
-
- {
- const link = document.createElement('a');
- link.href = previewDocument.url;
- link.download = previewDocument.name;
- link.click();
- }}
- className="gap-2 h-9"
- >
-
- Download
-
-
-
-
-
-
- {previewLoading ? (
-
- ) : previewDocument.name.toLowerCase().endsWith('.pdf') || previewDocument.type?.includes('pdf') ? (
-
-
-
- ) : previewDocument.name.match(/\.(jpg|jpeg|png|gif|webp)$/i) || previewDocument.type?.includes('image') ? (
-
-
-
- ) : (
-
-
-
-
-
Preview Not Available
-
- This file type cannot be previewed. Please download to view.
-
-
{
- const link = document.createElement('a');
- link.href = previewDocument.url;
- link.download = previewDocument.name;
- link.click();
- }}
- className="gap-2"
- >
-
- Download {previewDocument.name}
-
-
- )}
-
-
-
-
+ {/* Standardized File Preview */}
+ {previewDoc && (
+ setPreviewDoc(null)}
+ />
)}
);
diff --git a/src/dealer-claim/components/request-detail/modals/SnapshotDetailsModal.tsx b/src/dealer-claim/components/request-detail/modals/SnapshotDetailsModal.tsx
new file mode 100644
index 0000000..074e64a
--- /dev/null
+++ b/src/dealer-claim/components/request-detail/modals/SnapshotDetailsModal.tsx
@@ -0,0 +1,307 @@
+import { useState } from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import {
+ FileText,
+ Calendar,
+ Receipt,
+ AlignLeft
+} from "lucide-react";
+import { DocumentCard } from '@/components/workflow/DocumentUpload/DocumentCard';
+import { FilePreview } from '@/components/common/FilePreview/FilePreview';
+import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi';
+
+interface SnapshotDetailsModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ snapshot: any;
+ type: 'PROPOSAL' | 'COMPLETION';
+ title?: string;
+}
+
+export function SnapshotDetailsModal({
+ isOpen,
+ onClose,
+ snapshot,
+ type,
+ title
+}: SnapshotDetailsModalProps) {
+ // State for preview
+ const [previewDoc, setPreviewDoc] = useState<{
+ fileName: string;
+ fileType: string;
+ documentId: string;
+ fileUrl?: string;
+ fileSize?: number;
+ } | null>(null);
+
+ if (!snapshot) return null;
+
+ const isProposal = type === 'PROPOSAL';
+
+ // Helper to format currency
+ const formatCurrency = (amount: number | string) => {
+ return Number(amount || 0).toLocaleString('en-IN', {
+ maximumFractionDigits: 2,
+ style: 'currency',
+ currency: 'INR'
+ });
+ };
+
+ // Helper to format date
+ const formatDate = (dateString: string) => {
+ if (!dateString) return null;
+ try {
+ return new Date(dateString).toLocaleDateString('en-IN', {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric'
+ });
+ } catch {
+ return dateString;
+ }
+ };
+
+ // Helper to check if file is previewable
+ 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);
+ };
+
+ // Helper to get file type for DocumentCard
+ const getFileType = (fileName: string) => {
+ const ext = (fileName || '').split('.').pop()?.toLowerCase();
+ if (ext === 'pdf') return 'pdf';
+ if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext || '')) return 'image';
+ return 'file';
+ };
+
+ // Handle document preview click
+ const handlePreview = (doc: any) => {
+ const fileName = doc.fileName || doc.originalFileName || (isProposal ? 'Proposal Document' : 'Completion Document');
+ const documentId = doc.documentId || '';
+ const fileType = fileName.toLowerCase().endsWith('.pdf') ? 'application/pdf' : 'image/jpeg';
+
+ let fileUrl = '';
+ if (documentId) {
+ fileUrl = getDocumentPreviewUrl(documentId);
+ } else {
+ // Fallback for documents without ID (using direct storageUrl)
+ 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,
+ fileSize: doc.sizeBytes
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+ {isProposal ? (
+
+ ) : (
+
+ )}
+ {title || (isProposal ? 'Proposal Snapshot Details' : 'Completion Snapshot Details')}
+
+
+ View detailed snapshot of the {isProposal ? 'proposal' : 'completion request'} at this version.
+
+
+
+
+ {/* Header Stats */}
+
+
+
+ {isProposal ? 'Total Budget' : 'Total Expenses'}
+
+
+ {formatCurrency(snapshot.totalBudget || snapshot.totalExpenses)}
+
+
+
+ {isProposal && snapshot.expectedCompletionDate && (
+
+
+
+ Expected Completion
+
+
+ {formatDate(snapshot.expectedCompletionDate)}
+
+
+ )}
+
+
+ {/* Main Document */}
+ {snapshot.documentUrl && (
+
+
+ Primary Document
+
+ handlePreview({
+ fileName: isProposal ? 'Proposal Document' : 'Completion Document',
+ documentUrl: snapshot.documentUrl
+ }) : undefined}
+ onDownload={async () => {
+ // Handle download for document without ID
+ let downloadUrl = snapshot.documentUrl;
+ if (!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}`;
+ }
+ window.open(downloadUrl, '_blank');
+ }}
+ />
+
+ )}
+
+ {/* Supporting Documents */}
+ {snapshot.otherDocuments && snapshot.otherDocuments.length > 0 && (
+
+
+ Supporting Documents
+
+ {snapshot.otherDocuments.length} Files
+
+
+
+ {snapshot.otherDocuments.map((doc: any, idx: number) => (
+ handlePreview(doc) : undefined}
+ onDownload={doc.documentId ? downloadDocument : async () => {
+ 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');
+ }}
+ />
+ ))}
+
+
+ )}
+
+ {/* Cost Breakup / Expenses */}
+ {(snapshot.costItems || snapshot.expenses) && (
+
+
+ {isProposal ? 'Cost Breakdown' : 'Expenses Breakdown'}
+
+
+
+
+
+ Description
+ Amount
+
+
+
+ {(snapshot.costItems || snapshot.expenses).length > 0 ? (
+ (snapshot.costItems || snapshot.expenses).map((item: any, idx: number) => (
+
+ {item.description}
+
+ {formatCurrency(item.amount)}
+
+
+ ))
+ ) : (
+
+
+ No breakdown items available
+
+
+ )}
+
+ Total
+
+ {formatCurrency(snapshot.totalBudget || snapshot.totalExpenses)}
+
+
+
+
+
+
+ )}
+
+ {/* Comments */}
+ {snapshot.comments && (
+
+
+
+ Comments
+
+
+ {snapshot.comments}
+
+
+ )}
+
+
+
+
+ Close
+
+
+
+
+ {/* File Preview */}
+ {previewDoc && (
+ setPreviewDoc(null)}
+ />
+ )}
+ >
+ );
+}
diff --git a/src/dealer-claim/components/request-detail/modals/index.ts b/src/dealer-claim/components/request-detail/modals/index.ts
index 02f9725..e9610df 100644
--- a/src/dealer-claim/components/request-detail/modals/index.ts
+++ b/src/dealer-claim/components/request-detail/modals/index.ts
@@ -13,5 +13,6 @@ export { DeptLeadIOApprovalModal } from './DeptLeadIOApprovalModal';
export { DMSPushModal } from './DMSPushModal';
export { EditClaimAmountModal } from './EditClaimAmountModal';
export { EmailNotificationTemplateModal } from './EmailNotificationTemplateModal';
-export { InitiatorProposalApprovalModal } from './InitiatorProposalApprovalModal';
export { InitiatorActionModal } from './InitiatorActionModal';
+export { InitiatorProposalApprovalModal } from './InitiatorProposalApprovalModal';
+export { SnapshotDetailsModal } from './SnapshotDetailsModal';
diff --git a/src/dealer-claim/pages/RequestDetail.tsx b/src/dealer-claim/pages/RequestDetail.tsx
index 57f38e0..50025e0 100644
--- a/src/dealer-claim/pages/RequestDetail.tsx
+++ b/src/dealer-claim/pages/RequestDetail.tsx
@@ -621,6 +621,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
setShowSkipApproverModal(true);
}}
onRefresh={refreshDetails}
+ documentPolicy={documentPolicy}
/>