/** * InitiatorProposalApprovalModal Component * Modal for Step 2: Requestor Evaluation & Confirmation * Allows initiator to review dealer's proposal and approve/reject */ import { useState, useMemo, useEffect } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { CheckCircle, XCircle, FileText, IndianRupee, Calendar, MessageSquare, Download, Eye, 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'; interface CostItem { id: string; description: string; amount: number; quantity?: number; } interface ProposalData { proposalDocument?: { name: string; url?: string; id?: string; }; costBreakup: CostItem[]; expectedCompletionDate: string; otherDocuments?: Array<{ name: string; url?: string; id?: string; }>; dealerComments: string; submittedAt?: string; } interface InitiatorProposalApprovalModalProps { isOpen: boolean; 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; taxationType?: string | null; } export function InitiatorProposalApprovalModal({ isOpen, onClose, onApprove, onReject, onRequestRevision, proposalData, dealerName = 'Dealer', activityName = 'Activity', requestId: _requestId, // Prefix with _ to indicate intentionally unused request, previousProposalData, taxationType, }: InitiatorProposalApprovalModalProps) { const isNonGst = useMemo(() => { return taxationType === 'Non GST' || taxationType === 'Non-GST'; }, [taxationType]); const [comments, setComments] = useState(''); const [submitting, setSubmitting] = useState(false); 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 [previewDoc, setPreviewDoc] = useState<{ name: string; url: string; type?: string; size?: number; id?: string; } | null>(null); // Calculate total budget const totalBudget = useMemo(() => { if (!proposalData?.costBreakup) return 0; // Ensure costBreakup is an array const costBreakup = Array.isArray(proposalData.costBreakup) ? proposalData.costBreakup : (typeof proposalData.costBreakup === 'string' ? JSON.parse(proposalData.costBreakup) : []); if (!Array.isArray(costBreakup)) return 0; return costBreakup.reduce((sum: number, item: any) => { const amount = typeof item === 'object' ? (item.amount || 0) : 0; const quantity = typeof item === 'object' ? (item.quantity || 1) : 1; const baseTotal = amount * quantity; const gst = typeof item === 'object' ? (item.gstAmt || 0) : 0; const total = item.totalAmt || (baseTotal + gst); return sum + (Number(total) || 0); }, 0); }, [proposalData]); // Format date const formatDate = (dateString: string) => { if (!dateString) return '—'; try { const date = new Date(dateString); return date.toLocaleDateString('en-IN', { year: 'numeric', month: 'long', day: 'numeric', }); } catch { return dateString; } }; // Check if document can be previewed const canPreviewDocument = (doc: { name: string; url?: string; id?: string }): boolean => { if (!doc.name) return false; const name = doc.name.toLowerCase(); return name.endsWith('.pdf') || name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') || name.endsWith('.gif') || name.endsWith('.webp'); }; // 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; } // 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 (previewDoc?.url && previewDoc.url.startsWith('blob:')) { window.URL.revokeObjectURL(previewDoc.url); } }; }, [previewDoc]); const handleApprove = async () => { if (!comments.trim()) { toast.error('Please provide approval comments'); return; } try { setSubmitting(true); setActionType('approve'); await onApprove(comments); handleReset(); onClose(); } catch (error) { console.error('Failed to approve proposal:', error); toast.error('Failed to approve proposal. Please try again.'); } finally { setSubmitting(false); setActionType(null); } }; const handleReject = async () => { if (!comments.trim()) { toast.error('Please provide rejection reason'); return; } try { setSubmitting(true); setActionType('reject'); await onReject(comments); handleReset(); onClose(); } catch (error) { console.error('Failed to reject proposal:', error); toast.error('Failed to reject proposal. Please try again.'); } finally { setSubmitting(false); setActionType(null); } }; 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); }; const handleClose = () => { if (!submitting) { handleReset(); onClose(); } }; // Don't return null - show modal even if proposalData is not loaded yet // This allows the modal to open and show a loading/empty state if (!isOpen) { return null; } return (
Requestor Evaluation & Confirmation
{taxationType && ( {!isNonGst ? 'GST Claim' : 'Non-GST Claim'} )}
Step 2: Review dealer proposal and make a decision
Dealer: {dealerName}
Activity: {activityName}
Decision: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)
{/* Previous Proposal Reference Section */} {previousProposalData && (
setShowPreviousProposal(!showPreviousProposal)} >
Reference: Previous Proposal Details (last revision) ₹{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 && (
{canPreviewDocument({ name: previousProposalData.documentUrl }) ? ( <> View Previous Document ) : ( <> Download Previous Document )}
)}
{/* Cost Breakdown */} {(previousProposalData.costItems || previousProposalData.costBreakup) && (previousProposalData.costItems || previousProposalData.costBreakup).length > 0 && (

Previous Cost Breakdown

{(previousProposalData.costItems || previousProposalData.costBreakup).map((item: any, idx: number) => ( ))}
Description Amount
{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 */}
{/* Proposal Document Section */}

Proposal Document

{proposalData?.proposalDocument ? (

{proposalData.proposalDocument.name}

{proposalData?.submittedAt && (

Submitted on {formatDate(proposalData.submittedAt)}

)}
{proposalData.proposalDocument.id && ( <> {canPreviewDocument(proposalData.proposalDocument) && ( )} )}
) : (

No proposal document available

)}
{/* Other Supporting Documents */} {proposalData?.otherDocuments && proposalData.otherDocuments.length > 0 && (

Other Supporting Documents

{proposalData.otherDocuments.length} file(s)
{proposalData.otherDocuments.map((doc, index) => (

{doc.name}

{doc.id && (
{canPreviewDocument(doc) && ( )}
)}
))}
)}
{/* Right Column - Planning & Details */}
{/* Cost Breakup Section */}

Cost Breakup

{(() => { // Ensure costBreakup is an array const costBreakup = proposalData?.costBreakup ? (Array.isArray(proposalData.costBreakup) ? proposalData.costBreakup : (typeof proposalData.costBreakup === 'string' ? JSON.parse(proposalData.costBreakup) : [])) : []; return costBreakup && Array.isArray(costBreakup) && costBreakup.length > 0 ? ( <>
Item Description
Base
{!isNonGst &&
GST
}
Total
{costBreakup.map((item: any, index: number) => (
{item?.description || 'N/A'} {!isNonGst && item?.gstRate ? {item.gstRate}% GST : null}
₹{(Number(item?.amount) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{!isNonGst && (
₹{(Number(item?.gstAmt) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
)}
₹{(Number(item?.totalAmt || ((item?.amount || 0) * (item?.quantity || 1) + (item?.gstAmt || 0))) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
))}
Total Estimated Budget
₹{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
) : (

No cost breakdown available

); })()}
{/* Timeline Section */}

Expected Completion Date

{proposalData?.expectedCompletionDate ? formatDate(proposalData.expectedCompletionDate) : 'Not specified'}

{/* Comments Section - Side by Side */}
{/* Dealer Comments */}

Dealer Comments

{proposalData?.dealerComments || 'No comments provided'}

{/* Your Decision & Comments */}

Your Decision & Comments