/** * 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, DollarSign, Calendar, MessageSquare, Download, Eye, Loader2, } from 'lucide-react'; import { toast } from 'sonner'; import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi'; import '@/components/common/FilePreview/FilePreview.css'; interface CostItem { id: string; description: string; amount: 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; proposalData: ProposalData | null; dealerName?: string; activityName?: string; requestId?: string; } export function InitiatorProposalApprovalModal({ isOpen, onClose, onApprove, onReject, proposalData, dealerName = 'Dealer', activityName = 'Activity', requestId: _requestId, // Prefix with _ to indicate intentionally unused }: InitiatorProposalApprovalModalProps) { const [comments, setComments] = useState(''); const [submitting, setSubmitting] = useState(false); const [actionType, setActionType] = useState<'approve' | 'reject' | null>(null); const [previewDocument, setPreviewDocument] = useState<{ name: string; url: string; type?: string; size?: number; } | null>(null); const [previewLoading, setPreviewLoading] = useState(false); // 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; return sum + (Number(amount) || 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 - 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'); 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); } }; // Cleanup blob URLs on unmount useEffect(() => { return () => { if (previewDocument?.url && previewDocument.url.startsWith('blob:')) { window.URL.revokeObjectURL(previewDocument.url); } }; }, [previewDocument]); 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 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 Step 2: Review dealer proposal and make a decision
Dealer: {dealerName}
Activity: {activityName}
Decision: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)
{/* 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

)}
{/* 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
Amount
{costBreakup.map((item: any, index: number) => (
{item?.description || 'N/A'}
₹{(Number(item?.amount) || 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'}

{/* 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) && ( )}
)}
))}
)} {/* Dealer Comments */}

Dealer Comments

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

{/* Decision Section */}

Your Decision & Comments