Re_Figma_Code/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx

753 lines
30 KiB
TypeScript

/**
* DealerProposalSubmissionModal Component
* Modal for Step 1: Dealer Proposal Submission
* Allows dealers to upload proposal documents, provide cost breakdown, timeline, and comments
*/
import { useState, useRef, useMemo, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
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 { toast } from 'sonner';
import '@/components/common/FilePreview/FilePreview.css';
import './DealerProposalModal.css';
interface CostItem {
id: string;
description: string;
amount: number;
}
interface DealerProposalSubmissionModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (data: {
proposalDocument: File | null;
costBreakup: CostItem[];
expectedCompletionDate: string;
otherDocuments: File[];
dealerComments: string;
}) => Promise<void>;
dealerName?: string;
activityName?: string;
requestId?: string;
}
export function DealerProposalSubmissionModal({
isOpen,
onClose,
onSubmit,
dealerName = 'Jaipur Royal Enfield',
activityName = 'Activity',
requestId: _requestId,
}: DealerProposalSubmissionModalProps) {
const [proposalDocument, setProposalDocument] = useState<File | null>(null);
const [costItems, setCostItems] = useState<CostItem[]>([
{ id: '1', description: '', amount: 0 },
]);
const [timelineMode, setTimelineMode] = useState<'date' | 'days'>('date');
const [expectedCompletionDate, setExpectedCompletionDate] = useState('');
const [numberOfDays, setNumberOfDays] = useState('');
const [otherDocuments, setOtherDocuments] = useState<File[]>([]);
const [dealerComments, setDealerComments] = useState('');
const [submitting, setSubmitting] = useState(false);
const [previewFile, setPreviewFile] = useState<{ file: File; url: string } | null>(null);
const proposalDocInputRef = useRef<HTMLInputElement>(null);
const otherDocsInputRef = useRef<HTMLInputElement>(null);
// Helper function to check if file can be previewed
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');
};
// Cleanup object URLs when component unmounts or file changes
useEffect(() => {
return () => {
if (previewFile?.url) {
URL.revokeObjectURL(previewFile.url);
}
};
}, [previewFile]);
// Handle file preview - instant preview using object URL
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);
}
// Create object URL immediately for instant preview
const url = URL.createObjectURL(file);
setPreviewFile({ file, url });
};
// Handle download file (for non-previewable files)
const handleDownloadFile = (file: File) => {
const url = URL.createObjectURL(file);
const a = document.createElement('a');
a.href = url;
a.download = file.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Calculate total estimated budget
const totalBudget = useMemo(() => {
return costItems.reduce((sum, item) => sum + (item.amount || 0), 0);
}, [costItems]);
// Check if all required fields are filled
const isFormValid = useMemo(() => {
const hasProposalDoc = proposalDocument !== null;
const hasValidCostItems = costItems.length > 0 &&
costItems.every(item => item.description.trim() !== '' && item.amount > 0);
const hasTimeline = timelineMode === 'date'
? expectedCompletionDate !== ''
: numberOfDays !== '' && parseInt(numberOfDays) > 0;
const hasComments = dealerComments.trim().length > 0;
return hasProposalDoc && hasValidCostItems && hasTimeline && hasComments;
}, [proposalDocument, costItems, timelineMode, expectedCompletionDate, numberOfDays, dealerComments]);
const handleProposalDocChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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');
return;
}
setProposalDocument(file);
}
};
const handleOtherDocsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
setOtherDocuments(prev => [...prev, ...files]);
};
const handleAddCostItem = () => {
setCostItems(prev => [
...prev,
{ id: Date.now().toString(), description: '', amount: 0 },
]);
};
const handleRemoveCostItem = (id: string) => {
if (costItems.length > 1) {
setCostItems(prev => prev.filter(item => item.id !== id));
}
};
const handleCostItemChange = (id: string, field: 'description' | 'amount', value: string | number) => {
setCostItems(prev =>
prev.map(item =>
item.id === id
? { ...item, [field]: field === 'amount' ? parseFloat(value.toString()) || 0 : value }
: item
)
);
};
const handleRemoveOtherDoc = (index: number) => {
setOtherDocuments(prev => prev.filter((_, i) => i !== index));
};
const handleSubmit = async () => {
if (!isFormValid) {
toast.error('Please fill all required fields');
return;
}
// Calculate final completion date if using days mode
let finalCompletionDate: string = expectedCompletionDate || '';
if (timelineMode === 'days' && numberOfDays) {
const days = parseInt(numberOfDays);
const date = new Date();
date.setDate(date.getDate() + days);
const isoString = date.toISOString();
finalCompletionDate = isoString.split('T')[0] as string;
}
try {
setSubmitting(true);
await onSubmit({
proposalDocument,
costBreakup: costItems.filter(item => item.description.trim() !== '' && item.amount > 0),
expectedCompletionDate: finalCompletionDate,
otherDocuments,
dealerComments,
});
handleReset();
onClose();
} catch (error) {
console.error('Failed to submit proposal:', error);
toast.error('Failed to submit proposal. Please try again.');
} finally {
setSubmitting(false);
}
};
const handleReset = () => {
// Cleanup preview URL if exists
if (previewFile?.url) {
URL.revokeObjectURL(previewFile.url);
}
setPreviewFile(null);
setProposalDocument(null);
setCostItems([{ id: '1', description: '', amount: 0 }]);
setTimelineMode('date');
setExpectedCompletionDate('');
setNumberOfDays('');
setOtherDocuments([]);
setDealerComments('');
if (proposalDocInputRef.current) proposalDocInputRef.current.value = '';
if (otherDocsInputRef.current) otherDocsInputRef.current.value = '';
};
const handleClose = () => {
if (!submitting) {
handleReset();
onClose();
}
};
// Get minimum date (today)
const minDate = new Date().toISOString().split('T')[0];
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="dealer-proposal-modal overflow-hidden flex flex-col">
<DialogHeader className="flex-shrink-0 pb-3 lg:pb-4">
<DialogTitle className="flex items-center gap-2 text-xl lg:text-2xl">
<Upload className="w-5 h-5 lg:w-6 lg:h-6 text-[--re-green]" />
Dealer Proposal Submission
</DialogTitle>
<DialogDescription className="text-sm lg:text-base">
Step 1: Upload proposal and planning details
</DialogDescription>
<div className="space-y-1 mt-2 text-xs lg:text-sm text-gray-600">
<div className="flex flex-wrap gap-x-4 gap-y-1">
<div>
<strong>Dealer:</strong> {dealerName}
</div>
<div>
<strong>Activity:</strong> {activityName}
</div>
</div>
<div className="mt-1">
Please upload proposal document, provide cost breakdown, timeline, and detailed comments.
</div>
</div>
</DialogHeader>
<div className="flex-1 overflow-y-auto overflow-x-hidden py-3 lg:py-4">
<div className="space-y-4 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-6 lg:items-start lg:content-start">
{/* Left Column - Documents */}
<div className="space-y-4 lg:space-y-4 flex flex-col">
{/* Proposal Document Section */}
<div className="space-y-2 lg:space-y-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-base lg:text-lg">Proposal Document</h3>
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
</div>
<div>
<Label className="text-sm lg:text-base font-semibold flex items-center gap-2">
Proposal Document *
</Label>
<p className="text-xs lg:text-sm text-gray-600 mb-2">
Detailed proposal with activity details and requested information
</p>
<div
className={`border-2 border-dashed rounded-lg p-3 lg:p-4 transition-all duration-200 ${
proposalDocument
? 'border-green-500 bg-green-50 hover:border-green-600'
: 'border-gray-300 hover:border-blue-500 bg-white'
}`}
>
<input
ref={proposalDocInputRef}
type="file"
accept=".pdf,.doc,.docx"
className="hidden"
id="proposalDoc"
onChange={handleProposalDocChange}
/>
<label
htmlFor="proposalDoc"
className="cursor-pointer flex flex-col items-center gap-2"
>
{proposalDocument ? (
<div className="flex flex-col items-center gap-2 w-full">
<CheckCircle2 className="w-8 h-8 text-green-600" />
<div className="flex flex-col items-center gap-1 w-full max-w-full px-2">
<span className="text-sm font-semibold text-green-700 break-words text-center w-full max-w-full">
{proposalDocument.name}
</span>
<span className="text-xs text-green-600 mb-2">
Document selected
</span>
</div>
<div className="flex items-center gap-2">
{canPreviewFile(proposalDocument) && (
<Button
type="button"
variant="outline"
size="sm"
onClick={() => handlePreviewFile(proposalDocument)}
className="h-8 text-xs"
>
<Eye className="w-3.5 h-3.5 mr-1" />
Preview
</Button>
)}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => handleDownloadFile(proposalDocument)}
className="h-8 text-xs"
>
<Download className="w-3.5 h-3.5 mr-1" />
Download
</Button>
</div>
</div>
) : (
<>
<Upload className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600">
Click to upload proposal (PDF, DOC, DOCX)
</span>
</>
)}
</label>
</div>
</div>
</div>
{/* Other Supporting Documents Section */}
<div className="space-y-2 lg:space-y-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-base lg:text-lg">Other Supporting Documents</h3>
<Badge variant="outline" className="text-xs border-gray-300 text-gray-600 bg-gray-50 font-medium">Optional</Badge>
</div>
<div>
<Label className="flex items-center gap-2 text-sm lg:text-base font-semibold">
Additional Documents
</Label>
<p className="text-xs lg:text-sm text-gray-600 mb-2">
Any other supporting documents (invoices, receipts, photos, etc.)
</p>
<div
className={`border-2 border-dashed rounded-lg p-3 lg:p-4 transition-all duration-200 ${
otherDocuments.length > 0
? 'border-blue-500 bg-blue-50 hover:border-blue-600'
: 'border-gray-300 hover:border-blue-500 bg-white'
}`}
>
<input
ref={otherDocsInputRef}
type="file"
multiple
className="hidden"
id="otherDocs"
onChange={handleOtherDocsChange}
/>
<label
htmlFor="otherDocs"
className="cursor-pointer flex flex-col items-center gap-2"
>
{otherDocuments.length > 0 ? (
<>
<CheckCircle2 className="w-8 h-8 text-blue-600" />
<div className="flex flex-col items-center gap-1">
<span className="text-sm font-semibold text-blue-700">
{otherDocuments.length} document{otherDocuments.length !== 1 ? 's' : ''} selected
</span>
<span className="text-xs text-blue-600">
Click to add more documents
</span>
</div>
</>
) : (
<>
<Upload className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600">
Click to upload additional documents (multiple files allowed)
</span>
</>
)}
</label>
</div>
{otherDocuments.length > 0 && (
<div className="mt-2 lg:mt-3 space-y-2 max-h-[150px] lg:max-h-[140px] overflow-y-auto">
<p className="text-xs font-medium text-gray-600 mb-1">
Selected Documents ({otherDocuments.length}):
</p>
{otherDocuments.map((file, index) => (
<div
key={index}
className="flex items-start justify-between bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 p-2 lg:p-3 rounded-lg text-xs lg:text-sm shadow-sm hover:shadow-md transition-shadow w-full"
>
<div className="flex items-start gap-2 flex-1 min-w-0 pr-2">
<FileText className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
<span className="text-gray-800 font-medium break-words break-all">
{file.name}
</span>
</div>
<div className="flex items-center gap-1 flex-shrink-0 ml-2">
{canPreviewFile(file) && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 w-7 p-0 hover:bg-blue-100 hover:text-blue-700"
onClick={() => handlePreviewFile(file)}
title="Preview file"
>
<Eye className="w-3.5 h-3.5" />
</Button>
)}
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 w-7 p-0 hover:bg-green-100 hover:text-green-700"
onClick={() => handleDownloadFile(file)}
title="Download file"
>
<Download className="w-3.5 h-3.5" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 w-7 p-0 hover:bg-red-100 hover:text-red-700"
onClick={() => handleRemoveOtherDoc(index)}
title="Remove document"
>
<X className="w-4 h-4" />
</Button>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* Right Column - Planning & Budget */}
<div className="space-y-4 lg:space-y-4 flex flex-col">
{/* Cost Breakup Section */}
<div className="space-y-2 lg:space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-base lg:text-lg">Cost Breakup</h3>
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
</div>
<Button
type="button"
onClick={handleAddCostItem}
className="bg-[#2d4a3e] hover:bg-[#1f3329] text-white"
size="sm"
>
<Plus className="w-3 h-3 lg:w-4 lg:h-4 mr-1" />
<span className="hidden sm:inline">Add Item</span>
<span className="sm:hidden">Add</span>
</Button>
</div>
<div className="space-y-2 lg:space-y-2 max-h-[200px] lg:max-h-[180px] overflow-y-auto">
{costItems.map((item) => (
<div key={item.id} className="flex gap-2 items-start w-full">
<div className="flex-1 min-w-0">
<Input
placeholder="Item description (e.g., Banner printing, Event setup)"
value={item.description}
onChange={(e) =>
handleCostItemChange(item.id, 'description', e.target.value)
}
className="w-full"
/>
</div>
<div className="w-32 lg:w-36 flex-shrink-0">
<Input
type="number"
placeholder="Amount"
min="0"
step="0.01"
value={item.amount || ''}
onChange={(e) =>
handleCostItemChange(item.id, 'amount', e.target.value)
}
className="w-full"
/>
</div>
<Button
type="button"
variant="ghost"
size="sm"
className="mt-0.5 hover:bg-red-100 hover:text-red-700 flex-shrink-0"
onClick={() => handleRemoveCostItem(item.id)}
disabled={costItems.length === 1}
>
<X className="w-4 h-4" />
</Button>
</div>
))}
</div>
<div className="border-2 border-gray-300 rounded-lg p-3 lg:p-4 bg-white">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<IndianRupee className="w-4 h-4 lg:w-5 lg:h-5 text-gray-700" />
<span className="font-semibold text-sm lg:text-base text-gray-900">Estimated Budget</span>
</div>
<div className="text-xl lg:text-2xl font-bold text-gray-900">
{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
</div>
</div>
</div>
{/* Timeline for Closure Section */}
<div className="space-y-2 lg:space-y-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-base lg:text-lg">Timeline for Closure</h3>
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Button
type="button"
onClick={() => setTimelineMode('date')}
className={
timelineMode === 'date'
? 'bg-[#2d4a3e] hover:bg-[#1f3329] text-white'
: 'border-2 hover:bg-gray-50'
}
size="sm"
>
<Calendar className="w-4 h-4 mr-1" />
Specific Date
</Button>
<Button
type="button"
onClick={() => setTimelineMode('days')}
className={
timelineMode === 'days'
? 'bg-[#2d4a3e] hover:bg-[#1f3329] text-white'
: 'border-2 hover:bg-gray-50'
}
size="sm"
>
Number of Days
</Button>
</div>
{timelineMode === 'date' ? (
<div className="w-full">
<Label className="text-xs lg:text-sm font-medium mb-1.5 lg:mb-2 block">
Expected Completion Date
</Label>
<CustomDatePicker
value={expectedCompletionDate || null}
onChange={(date) => setExpectedCompletionDate(date || '')}
minDate={minDate}
placeholderText="dd/mm/yyyy"
className="w-full"
/>
</div>
) : (
<div className="w-full">
<Label className="text-xs lg:text-sm font-medium mb-1.5 lg:mb-2 block">
Number of Days
</Label>
<Input
type="number"
placeholder="Enter number of days"
min="1"
value={numberOfDays}
onChange={(e) => setNumberOfDays(e.target.value)}
className="h-9 lg:h-10 w-full"
/>
</div>
)}
</div>
</div>
{/* Dealer Comments Section */}
<div className="space-y-2">
<Label htmlFor="dealerComments" className="text-sm lg:text-base font-semibold flex items-center gap-2">
Dealer Comments / Details *
</Label>
<Textarea
id="dealerComments"
placeholder="Provide detailed comments about this claim request, including any special considerations, execution details, or additional information..."
value={dealerComments}
onChange={(e) => setDealerComments(e.target.value)}
className="min-h-[80px] lg:min-h-[100px] text-sm w-full"
/>
<p className="text-xs text-gray-500">{dealerComments.length} characters</p>
</div>
</div>
{/* Full Width Sections */}
{/* Warning Message */}
{!isFormValid && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 lg:p-4 flex items-start gap-2 lg:gap-3 lg:col-span-2">
<CircleAlert className="w-4 h-4 lg:w-5 lg:h-5 text-amber-600 flex-shrink-0 mt-0.5" />
<div className="text-xs lg:text-sm text-amber-800">
<p className="font-semibold mb-1">Missing Required Information</p>
<p>
Please ensure proposal document, cost breakup, timeline, and dealer comments are provided before submitting.
</p>
</div>
</div>
)}
</div>
</div>
<DialogFooter className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end flex-shrink-0 pt-3 lg:pt-4 border-t">
<Button
variant="outline"
onClick={handleClose}
disabled={submitting}
className="border-2"
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={!isFormValid || submitting}
className="bg-[#2d4a3e] hover:bg-[#1f3329] text-white disabled:bg-gray-300 disabled:text-gray-500"
>
{submitting ? 'Submitting...' : 'Submit Documents'}
</Button>
</DialogFooter>
</DialogContent>
{/* File Preview Modal - Matching DocumentsTab style */}
{previewFile && (
<Dialog
open={!!previewFile}
onOpenChange={() => {
if (previewFile?.url) {
URL.revokeObjectURL(previewFile.url);
}
setPreviewFile(null);
}}
>
<DialogContent className="file-preview-dialog p-3 sm:p-6">
<div className="file-preview-content">
<DialogHeader className="pb-4 flex-shrink-0 pr-8">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3 flex-1 min-w-0">
<Eye className="w-5 h-5 text-blue-600 flex-shrink-0" />
<div className="flex-1 min-w-0">
<DialogTitle className="text-base sm:text-lg font-bold text-gray-900 truncate pr-2">
{previewFile.file.name}
</DialogTitle>
<p className="text-xs sm:text-sm text-gray-500">
{previewFile.file.type || 'Unknown type'} {(previewFile.file.size / 1024).toFixed(1)} KB
</p>
</div>
</div>
<div className="flex items-center gap-2 flex-wrap mr-2">
<Button
variant="outline"
size="sm"
onClick={() => handleDownloadFile(previewFile.file)}
className="gap-2 h-9"
>
<Download className="h-4 w-4" />
<span className="hidden sm:inline">Download</span>
</Button>
</div>
</div>
</DialogHeader>
<div className="file-preview-body bg-gray-100 rounded-lg p-2 sm:p-4">
{previewFile.file.type?.includes('image') ? (
<div className="flex items-center justify-center h-full">
<img
src={previewFile.url}
alt={previewFile.file.name}
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain'
}}
className="rounded-lg shadow-lg"
/>
</div>
) : previewFile.file.type?.includes('pdf') || previewFile.file.name.toLowerCase().endsWith('.pdf') ? (
<div className="w-full h-full flex items-center justify-center">
<iframe
src={previewFile.url}
className="w-full h-full rounded-lg border-0"
title={previewFile.file.name}
style={{
minHeight: '70vh',
height: '100%'
}}
/>
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-center">
<div className="w-20 h-20 bg-gray-200 rounded-full flex items-center justify-center mb-4">
<Eye className="w-10 h-10 text-gray-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Preview Not Available</h3>
<p className="text-sm text-gray-600 mb-6">
This file type cannot be previewed. Please download to view.
</p>
<Button
onClick={() => handleDownloadFile(previewFile.file)}
className="gap-2"
>
<Download className="h-4 w-4" />
Download {previewFile.file.name}
</Button>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
)}
</Dialog>
);
}