Re_Figma_Code/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx
2026-03-20 10:19:56 +05:30

1487 lines
65 KiB
TypeScript

/**
* DealerCompletionDocumentsModal Component
* Modal for Step 5: Activity Completion Documents
* Allows dealers to upload completion documents, photos, expenses, and provide completion details
*/
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, FileText, Image, Receipt, CircleAlert, CheckCircle2, Eye, Download, IndianRupee } from 'lucide-react';
import { toast } from 'sonner';
import { handleSecurityError } from '@/utils/securityToast';
import '@/components/common/FilePreview/FilePreview.css';
import './DealerCompletionDocumentsModal.css';
import { validateHSNSAC } from '@/utils/validationUtils';
import { getStateCodeFromGSTIN, getActiveTaxComponents } from '@/utils/gstUtils';
interface ExpenseItem {
id: string;
description: string;
amount: number;
gstRate: number;
gstAmt: number;
quantity: number;
hsnCode: string;
cgstRate: number;
cgstAmt: number;
sgstRate: number;
sgstAmt: number;
igstRate: number;
igstAmt: number;
utgstRate: number;
utgstAmt: number;
cessRate: number;
cessAmt: number;
totalAmt: number;
isService: boolean;
}
interface DealerCompletionDocumentsModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (data: {
activityCompletionDate: string;
numberOfParticipants?: number;
closedExpenses: ExpenseItem[];
totalClosedExpenses: number;
completionDocuments: File[];
activityPhotos: File[];
invoicesReceipts?: File[];
attendanceSheet?: File;
completionDescription: string;
}) => Promise<void>;
dealerName?: string;
dealerGSTIN?: string;
activityName?: string;
requestId?: string;
defaultGstRate?: number;
documentPolicy: {
maxFileSizeMB: number;
allowedFileTypes: string[];
};
taxationType?: string | null;
}
export function DealerCompletionDocumentsModal({
isOpen,
onClose,
onSubmit,
dealerName = 'Jaipur Royal Enfield',
dealerGSTIN,
activityName = 'Activity',
requestId: _requestId,
defaultGstRate = 18,
documentPolicy,
taxationType,
}: DealerCompletionDocumentsModalProps) {
const [activityCompletionDate, setActivityCompletionDate] = useState('');
const [numberOfParticipants, setNumberOfParticipants] = useState('');
// Determine active tax components based on dealer GSTIN
const taxConfig = useMemo(() => {
const stateCode = getStateCodeFromGSTIN(dealerGSTIN);
return getActiveTaxComponents(stateCode);
}, [dealerGSTIN]);
const isNonGst = useMemo(() => {
return taxationType === 'Non GST' || taxationType === 'Non-GST';
}, [taxationType]);
const [expenseItems, setExpenseItems] = useState<ExpenseItem[]>([]);
const [completionDocuments, setCompletionDocuments] = useState<File[]>([]);
const [activityPhotos, setActivityPhotos] = useState<File[]>([]);
const [invoicesReceipts, setInvoicesReceipts] = useState<File[]>([]);
const [attendanceSheet, setAttendanceSheet] = useState<File | null>(null);
const [completionDescription, setCompletionDescription] = useState('');
const [submitting, setSubmitting] = useState(false);
const [previewFile, setPreviewFile] = useState<{ file: File; url: string } | null>(null);
const completionDocsInputRef = useRef<HTMLInputElement>(null);
const photosInputRef = useRef<HTMLInputElement>(null);
const invoicesInputRef = useRef<HTMLInputElement>(null);
const attendanceInputRef = 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]);
// Initialize with one empty row if none exist
useEffect(() => {
if (expenseItems.length === 0) {
setExpenseItems([{
id: '1',
description: '',
amount: 0,
gstRate: defaultGstRate || 0,
gstAmt: 0,
quantity: 1,
hsnCode: '',
isService: false,
cgstRate: 0,
cgstAmt: 0,
sgstRate: 0,
sgstAmt: 0,
igstRate: 0,
igstAmt: 0,
utgstRate: 0,
utgstAmt: 0,
cessRate: 0,
cessAmt: 0,
totalAmt: 0
}]);
}
}, [defaultGstRate]);
// Handle file preview
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);
}
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 closed expenses (inclusive of GST)
const totalClosedExpenses = useMemo(() => {
return expenseItems.reduce((sum, item) => sum + (item.totalAmt || item.amount || 0), 0);
}, [expenseItems]);
// GST Calculation Helper
const calculateGST = (amount: number, rates: { cgstRate: number; sgstRate: number; igstRate: number; utgstRate: number }, quantity: number = 1) => {
const baseTotal = amount * quantity;
const cgstAmt = (baseTotal * (rates.cgstRate || 0)) / 100;
const sgstAmt = (baseTotal * (rates.sgstRate || 0)) / 100;
const utgstAmt = (baseTotal * (rates.utgstRate || 0)) / 100;
const igstAmt = (baseTotal * (rates.igstRate || 0)) / 100;
const gstAmt = cgstAmt + sgstAmt + utgstAmt + igstAmt;
const totalAmt = baseTotal + gstAmt;
return {
cgstRate: rates.cgstRate,
cgstAmt,
sgstRate: rates.sgstRate,
sgstAmt,
utgstRate: rates.utgstRate,
utgstAmt,
igstRate: rates.igstRate,
igstAmt,
gstAmt,
gstRate: (rates.cgstRate || 0) + (rates.sgstRate || 0) + (rates.utgstRate || 0) + (rates.igstRate || 0),
totalAmt
};
};
// Check if all required fields are filled
const isFormValid = useMemo(() => {
const hasCompletionDate = activityCompletionDate !== '';
const hasDocuments = completionDocuments.length > 0;
const hasPhotos = activityPhotos.length > 0;
const hasDescription = completionDescription.trim().length > 0;
const hasValidExpenseItems = expenseItems.length > 0 &&
expenseItems.every(item => item.description.trim() !== '' && item.amount > 0);
const hasHSNSACErrors = isNonGst ? false : expenseItems.some(item => {
const { isValid } = validateHSNSAC(item.hsnCode, item.isService);
return !isValid;
});
return hasCompletionDate && hasDocuments && hasPhotos && hasDescription && hasValidExpenseItems && !hasHSNSACErrors;
}, [activityCompletionDate, completionDocuments, activityPhotos, completionDescription, isNonGst, expenseItems]);
// Get today's date in YYYY-MM-DD format for max date
const maxDate = new Date().toISOString().split('T')[0];
const handleAddExpense = () => {
setExpenseItems([
...expenseItems,
{
id: Date.now().toString(),
description: '',
amount: 0,
gstRate: defaultGstRate || 0,
gstAmt: 0,
quantity: 1,
hsnCode: '',
isService: false,
cgstRate: 0,
cgstAmt: 0,
sgstRate: 0,
sgstAmt: 0,
igstRate: 0,
igstAmt: 0,
utgstRate: 0,
utgstAmt: 0,
cessRate: 0,
cessAmt: 0,
totalAmt: 0
},
]);
};
const handleExpenseChange = (id: string, field: string, value: any) => {
setExpenseItems(
expenseItems.map((item) => {
if (item.id === id) {
let updatedItem = { ...item, [field]: value };
if (['amount', 'gstRate', 'cgstRate', 'sgstRate', 'utgstRate', 'igstRate', 'quantity'].includes(field)) {
// Negative amount validation
const numValue = parseFloat(value);
if (!isNaN(numValue) && numValue < 0) {
toast.error('Value cannot be negative');
return item;
}
const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount;
const quantity = field === 'quantity' ? parseInt(value) || 1 : item.quantity;
let cgstRate = item.cgstRate;
let sgstRate = item.sgstRate;
let utgstRate = item.utgstRate;
let igstRate = item.igstRate;
if (field === 'cgstRate') {
if (!taxConfig.isCGST) return item;
cgstRate = parseFloat(value) || 0;
// If UTGST is active for this dealer, sync with it
if (taxConfig.isUTGST) {
utgstRate = cgstRate;
sgstRate = 0;
} else {
sgstRate = cgstRate;
utgstRate = 0;
}
igstRate = 0;
} else if (field === 'sgstRate') {
if (!taxConfig.isSGST) return item;
sgstRate = parseFloat(value) || 0;
cgstRate = sgstRate;
utgstRate = 0;
igstRate = 0;
} else if (field === 'utgstRate') {
if (!taxConfig.isUTGST) return item;
utgstRate = parseFloat(value) || 0;
cgstRate = utgstRate;
sgstRate = 0;
igstRate = 0;
} else if (field === 'igstRate') {
if (!taxConfig.isIGST) return item;
igstRate = parseFloat(value) || 0;
cgstRate = 0;
sgstRate = 0;
utgstRate = 0;
} else if (field === 'gstRate') {
const totalRate = parseFloat(value) || 0;
if (taxConfig.isIGST) {
igstRate = totalRate;
cgstRate = 0;
sgstRate = 0;
utgstRate = 0;
} else {
cgstRate = totalRate / 2;
if (taxConfig.isUTGST) {
utgstRate = totalRate / 2;
sgstRate = 0;
} else {
sgstRate = totalRate / 2;
utgstRate = 0;
}
igstRate = 0;
}
}
const calculation = calculateGST(amount, { cgstRate, sgstRate, igstRate, utgstRate }, quantity);
return {
...updatedItem,
amount,
quantity,
...calculation
};
}
return updatedItem;
}
return item;
})
);
};
const handleRemoveExpense = (id: string) => {
setExpenseItems(expenseItems.filter((item) => item.id !== id));
};
const handleCompletionDocsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
if (files.length > 0) {
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]);
}
if (completionDocsInputRef.current) completionDocsInputRef.current.value = '';
}
};
const handleRemoveCompletionDoc = (index: number) => {
setCompletionDocuments(completionDocuments.filter((_, i) => i !== index));
};
const handlePhotosChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
if (files.length > 0) {
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]);
}
if (photosInputRef.current) photosInputRef.current.value = '';
}
};
const handleRemovePhoto = (index: number) => {
setActivityPhotos(activityPhotos.filter((_, i) => i !== index));
};
const handleInvoicesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
if (files.length > 0) {
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]);
}
if (invoicesInputRef.current) invoicesInputRef.current.value = '';
}
};
const handleRemoveInvoice = (index: number) => {
setInvoicesReceipts(invoicesReceipts.filter((_, i) => i !== index));
};
const handleAttendanceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
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);
}
};
const handleSubmit = async () => {
if (!isFormValid) {
toast.error('Please fill all required fields');
return;
}
// Check for negative amounts
const hasNegativeAmounts = expenseItems.some(item => item.amount < 0 || item.quantity < 1);
if (hasNegativeAmounts) {
toast.error('Please ensure all amounts are non-negative and quantity is at least 1');
return;
}
// Filter valid expense items
const validExpenses = expenseItems.filter(
(item) => item.description.trim() !== '' && item.amount > 0
);
// Validation: Alert for 0% GST on taxable items (Skip for Non-GST)
const hasZeroGstItems = !isNonGst && validExpenses.some(item =>
item.description.trim() !== '' && item.amount > 0 && (item.gstRate === 0 || !item.gstRate)
);
if (hasZeroGstItems) {
const confirmZeroGst = window.confirm(
"One or more expenses have 0% GST. Are you sure you want to proceed? \n\nNote: If these items are taxable, please provide a valid GST rate to ensure correct E-Invoice generation."
);
if (!confirmZeroGst) {
setSubmitting(false);
return;
}
}
try {
setSubmitting(true);
await onSubmit({
activityCompletionDate,
numberOfParticipants: numberOfParticipants ? parseInt(numberOfParticipants) : undefined,
closedExpenses: validExpenses,
totalClosedExpenses,
completionDocuments,
activityPhotos,
invoicesReceipts: invoicesReceipts.length > 0 ? invoicesReceipts : undefined,
attendanceSheet: attendanceSheet || undefined,
completionDescription,
});
handleReset();
onClose();
} catch (error) {
console.error('Failed to submit completion documents:', error);
if (!handleSecurityError(error)) {
toast.error('Failed to submit completion documents. Please try again.');
}
} finally {
setSubmitting(false);
}
};
const handleReset = () => {
// Cleanup preview URL if exists
if (previewFile?.url) {
URL.revokeObjectURL(previewFile.url);
}
setPreviewFile(null);
setActivityCompletionDate('');
setNumberOfParticipants('');
setExpenseItems([{
id: '1',
description: '',
amount: 0,
gstRate: defaultGstRate || 0,
gstAmt: 0,
quantity: 1,
hsnCode: '',
isService: false,
cgstRate: 0,
cgstAmt: 0,
sgstRate: 0,
sgstAmt: 0,
igstRate: 0,
igstAmt: 0,
utgstRate: 0,
utgstAmt: 0,
cessRate: 0,
cessAmt: 0,
totalAmt: 0
}]);
setCompletionDocuments([]);
setActivityPhotos([]);
setInvoicesReceipts([]);
setAttendanceSheet(null);
setCompletionDescription('');
if (completionDocsInputRef.current) completionDocsInputRef.current.value = '';
if (photosInputRef.current) photosInputRef.current.value = '';
if (invoicesInputRef.current) invoicesInputRef.current.value = '';
if (attendanceInputRef.current) attendanceInputRef.current.value = '';
};
const handleClose = () => {
if (!submitting) {
handleReset();
onClose();
}
};
return (
<>
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="dealer-completion-documents-modal overflow-hidden flex flex-col">
<DialogHeader className="px-6 pt-6 pb-3 flex-shrink-0">
<DialogTitle className="font-semibold flex items-center gap-2 text-xl sm:text-2xl flex-wrap">
<div className="flex items-center gap-2">
<Upload className="w-5 h-5 sm:w-6 sm:h-6 text-[--re-green]" />
Activity Completion Documents
</div>
{taxationType && (
<Badge className={`ml-2 border-none shadow-sm ${taxationType === 'GST' ? 'bg-emerald-600 text-white hover:bg-emerald-700' : 'bg-indigo-600 text-white hover:bg-indigo-700'}`}>
{taxationType === 'GST' ? 'GST Claim' : 'Non-GST Claim'}
</Badge>
)}
</DialogTitle>
<DialogDescription className="text-sm sm:text-base">
Step 5: Upload completion proof and final documents
</DialogDescription>
<div className="space-y-1 mt-2 text-xs sm:text-sm text-gray-600">
<div>
<strong>Dealer:</strong> {dealerName}
</div>
<div>
<strong>Activity:</strong> {activityName}
</div>
<div className="mt-2">
Please upload completion documents, photos, and provide details about the completed activity.
</div>
</div>
</DialogHeader>
<div className="flex-1 overflow-y-auto overflow-x-hidden px-6 py-3">
<div className="space-y-5 sm:space-y-6">
{/* Activity Completion Date */}
<div className="space-y-1.5 sm:space-y-2">
<Label className="text-sm sm:text-base font-semibold flex items-center gap-2" htmlFor="completionDate">
<Calendar className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
Activity Completion Date *
</Label>
<CustomDatePicker
value={activityCompletionDate || null}
onChange={(date) => setActivityCompletionDate(date || '')}
maxDate={maxDate}
placeholderText="dd/mm/yyyy"
className="w-full max-w-[280px]"
wrapperClassName="max-w-[280px]"
/>
</div>
{/* Closed Expenses Section */}
<div className="space-y-3 sm:space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-base sm:text-lg">Closed Expenses</h3>
</div>
{!isNonGst && (
<div className="text-[10px] text-gray-500 italic mt-0.5">
Tax fields are automatically toggled based on the dealer's state (Inter-state vs Intra-state).
</div>
)}
<Button
type="button"
onClick={handleAddExpense}
className="bg-[#2d4a3e] hover:bg-[#1f3329] text-white"
size="sm"
>
<Plus className="w-4 h-4 mr-1" />
Add Expense
</Button>
</div>
<div className="space-y-3 sm:space-y-4 max-h-[400px] overflow-y-auto pr-1">
{expenseItems.map((item) => (
<div key={item.id} className="p-4 border rounded-lg bg-gray-50/50 space-y-4 relative group">
<div className="flex gap-3 items-start w-full">
<div className={`${isNonGst ? 'flex-[3]' : 'flex-1'} min-w-0`}>
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Item description</Label>
<Input
placeholder="e.g., Venue rental, Refreshments"
value={item.description}
onChange={(e) =>
handleExpenseChange(item.id, 'description', e.target.value)
}
className="w-full bg-white text-sm"
/>
</div>
{isNonGst && (
<div className="w-28 sm:w-36 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Amount</Label>
<div className="relative">
<IndianRupee className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-400" />
<Input
type="number"
placeholder="0.00"
min="0"
step="0.01"
value={item.amount || ''}
onChange={(e) =>
handleExpenseChange(item.id, 'amount', e.target.value)
}
className="w-full bg-white text-sm pl-8"
/>
</div>
</div>
)}
{!isNonGst && (
<div className="w-28 sm:w-36 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Amount (Base)</Label>
<div className="relative">
<IndianRupee className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-400" />
<Input
type="number"
placeholder="0.00"
min="0"
step="0.01"
value={item.amount || ''}
onChange={(e) =>
handleExpenseChange(item.id, 'amount', e.target.value)
}
className="w-full bg-white text-sm pl-8"
/>
</div>
</div>
)}
{!isNonGst && (
<>
<div className="w-20 sm:w-24 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">HSN/SAC Code</Label>
<Input
placeholder="HSN/SAC Code"
value={item.hsnCode || ''}
onChange={(e) =>
handleExpenseChange(item.id, 'hsnCode', e.target.value)
}
className={`w-full bg-white text-sm ${!validateHSNSAC(item.hsnCode, item.isService).isValid ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
/>
{!validateHSNSAC(item.hsnCode, item.isService).isValid && (
<span className="text-[9px] text-red-500 mt-1 block leading-tight">
{validateHSNSAC(item.hsnCode, item.isService).message}
</span>
)}
</div>
<div className="w-24 sm:w-28 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Item Type</Label>
<select
value={item.isService ? 'SAC' : 'HSN'}
onChange={(e) =>
handleExpenseChange(item.id, 'isService', e.target.value === 'SAC')
}
className="flex h-9 w-full rounded-md border border-input bg-white px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="HSN">HSN (Goods)</option>
<option value="SAC">SAC (Service)</option>
</select>
</div>
<div className="w-14 sm:w-16 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">CGST %</Label>
<Input
type="number"
placeholder="%"
min="0"
max="100"
step="0.1"
value={item.cgstRate || ''}
onChange={(e) =>
handleExpenseChange(item.id, 'cgstRate', e.target.value)
}
disabled={!taxConfig.isCGST}
className="w-full bg-white text-xs px-1 text-center disabled:bg-gray-100 disabled:text-gray-400"
/>
</div>
<div className="w-14 sm:w-16 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">SGST %</Label>
<Input
type="number"
placeholder="%"
min="0"
max="100"
step="0.1"
value={item.sgstRate || ''}
onChange={(e) =>
handleExpenseChange(item.id, 'sgstRate', e.target.value)
}
disabled={!taxConfig.isSGST}
className="w-full bg-white text-xs px-1 text-center disabled:bg-gray-100 disabled:text-gray-400"
/>
</div>
<div className="w-14 sm:w-16 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">UTGST %</Label>
<Input
type="number"
placeholder="%"
min="0"
max="100"
step="0.1"
value={item.utgstRate || ''}
onChange={(e) =>
handleExpenseChange(item.id, 'utgstRate', e.target.value)
}
disabled={!taxConfig.isUTGST}
className="w-full bg-white text-xs px-1 text-center disabled:bg-gray-100 disabled:text-gray-400"
/>
</div>
<div className="w-14 sm:w-16 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">IGST %</Label>
<Input
type="number"
placeholder="%"
min="0"
max="100"
step="0.1"
value={item.igstRate || ''}
onChange={(e) =>
handleExpenseChange(item.id, 'igstRate', e.target.value)
}
disabled={!taxConfig.isIGST}
className="w-full bg-white text-xs px-1 text-center disabled:bg-gray-100 disabled:text-gray-400"
/>
</div>
</>
)}
<Button
type="button"
variant="ghost"
size="sm"
className="mt-5 hover:bg-red-100 hover:text-red-700 flex-shrink-0 h-9 w-9 p-0"
onClick={() => handleRemoveExpense(item.id)}
>
<X className="w-4 h-4" />
</Button>
</div>
<div className={`grid grid-cols-2 sm:grid-cols-5 gap-3 pt-3 border-t border-dashed border-gray-200 ${isNonGst ? 'items-center' : ''}`}>
{!isNonGst ? (
<>
<div className="flex flex-wrap gap-4 text-gray-500 font-medium">
<span>CGST: <span className="text-gray-900 font-semibold">₹{(item.cgstAmt || 0).toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>
{item.sgstAmt > 0 && <span>SGST: <span className="text-gray-900 font-semibold">₹{(item.sgstAmt || 0).toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>}
{item.utgstAmt > 0 && <span>UTGST: <span className="text-gray-900 font-semibold">₹{(item.utgstAmt || 0).toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>}
<span>IGST: <span className="text-gray-900 font-semibold">₹{(item.igstAmt || 0).toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>
</div>
<div className="flex flex-wrap gap-4 items-center sm:justify-end">
<span className="text-gray-500">GST Total: <span className="text-gray-900 font-bold">₹{(item.gstAmt || 0).toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>
<Badge className="bg-[#2d4a3e] text-white px-3 py-1 text-xs">Item Total: ₹{(item.totalAmt || 0).toLocaleString('en-IN', { minimumFractionDigits: 1 })}</Badge>
</div>
</>
) : (
<div className="col-span-4 invisible"></div>
)}
<div className="flex flex-col items-end">
<span className="text-[10px] text-gray-500 uppercase">Item Total</span>
<span className="text-sm font-bold text-[#2d4a3e]">₹{(item.amount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}</span>
</div>
</div>
</div>
))}
</div>
{expenseItems.length === 0 && (
<p className="text-xs sm:text-sm text-gray-500 italic">
No expenses added. Click "Add Expense" to add expense items.
</p>
)}
{expenseItems.length > 0 && totalClosedExpenses > 0 && (
<div className="pt-2 border-t">
<div className="flex justify-between items-center">
<span className="font-semibold text-sm sm:text-base">Total Closed Expenses:</span>
<span className="font-semibold text-base sm:text-lg">
₹{totalClosedExpenses.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>
</div>
)}
</div>
</div>
{/* Completion Evidence Section */}
<div className="space-y-3 sm:space-y-4">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-base sm:text-lg">Completion Evidence</h3>
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
</div>
{/* Grid layout for Completion Documents and Activity Photos */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
{/* Completion Documents */}
<div>
<Label className="text-sm sm:text-base font-semibold flex items-center gap-2">
<FileText className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
Completion Documents *
</Label>
<p className="text-xs sm:text-sm text-gray-600 mb-2">
Upload documents proving activity completion (reports, certificates, etc.) - Can upload multiple files or ZIP folder
</p>
<div
className={`border-2 border-dashed rounded-lg p-3 sm:p-4 transition-all duration-200 ${completionDocuments.length > 0
? 'border-green-500 bg-green-50 hover:border-green-600'
: 'border-gray-300 hover:border-blue-500 bg-white'
}`}
>
<input
ref={completionDocsInputRef}
type="file"
multiple
accept={['.pdf', '.doc', '.docx', '.zip', '.rar'].filter(ext => documentPolicy.allowedFileTypes.includes(ext.replace('.', ''))).join(',')}
className="hidden"
id="completionDocs"
onChange={handleCompletionDocsChange}
/>
<label
htmlFor="completionDocs"
className="cursor-pointer flex flex-col items-center gap-2"
>
{completionDocuments.length > 0 ? (
<>
<CheckCircle2 className="w-8 h-8 text-green-600" />
<div className="flex flex-col items-center gap-1">
<span className="text-sm font-semibold text-green-700">
{completionDocuments.length} document{completionDocuments.length !== 1 ? 's' : ''} selected
</span>
<span className="text-xs text-green-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 documents (Max {documentPolicy.maxFileSizeMB}MB)
</span>
<p className="text-[10px] text-gray-400">PDF, DOC, ZIP allowed</p>
</>
)}
</label>
</div>
{completionDocuments.length > 0 && (
<div className="mt-3 space-y-2">
<p className="text-xs font-medium text-gray-600 mb-1">
Selected Documents ({completionDocuments.length}):
</p>
{completionDocuments.map((file, index) => (
<div
key={index}
className="flex items-start justify-between bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 p-2 sm:p-3 rounded-lg text-xs sm: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-3.5 h-3.5 sm:w-4 sm:h-4 text-green-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-green-100 hover:text-green-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-blue-100 hover:text-blue-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={() => handleRemoveCompletionDoc(index)}
title="Remove document"
>
<X className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
</Button>
</div>
</div>
))}
</div>
)}
</div>
{/* Activity Photos */}
<div>
<Label className="text-sm sm:text-base font-semibold flex items-center gap-2">
<Image className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
Activity Photos *
</Label>
<p className="text-xs sm:text-sm text-gray-600 mb-2">
Upload photos from the completed activity (event photos, installations, etc.)
</p>
<div
className={`border-2 border-dashed rounded-lg p-3 sm:p-4 transition-all duration-200 ${activityPhotos.length > 0
? 'border-green-500 bg-green-50 hover:border-green-600'
: 'border-gray-300 hover:border-blue-500 bg-white'
}`}
>
<input
ref={photosInputRef}
type="file"
multiple
accept={['.jpg', '.jpeg', '.png', '.gif', '.webp'].filter(ext => documentPolicy.allowedFileTypes.includes(ext.replace('.', ''))).join(',')}
className="hidden"
id="completionPhotos"
onChange={handlePhotosChange}
/>
<label
htmlFor="completionPhotos"
className="cursor-pointer flex flex-col items-center gap-2"
>
{activityPhotos.length > 0 ? (
<>
<CheckCircle2 className="w-8 h-8 text-green-600" />
<div className="flex flex-col items-center gap-1">
<span className="text-sm font-semibold text-green-700">
{activityPhotos.length} photo{activityPhotos.length !== 1 ? 's' : ''} selected
</span>
<span className="text-xs text-green-600">
Click to add more photos
</span>
</div>
</>
) : (
<>
<Image className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600">
Click to upload photos (JPG, PNG - multiple files allowed)
</span>
</>
)}
</label>
</div>
{activityPhotos.length > 0 && (
<div className="mt-3 space-y-2">
<p className="text-xs font-medium text-gray-600 mb-1">
Selected Photos ({activityPhotos.length}):
</p>
{activityPhotos.map((file, index) => (
<div
key={index}
className="flex items-start justify-between bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 p-2 sm:p-3 rounded-lg text-xs sm: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">
<Image className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-green-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-green-100 hover:text-green-700"
onClick={() => handlePreviewFile(file)}
title="Preview photo"
>
<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-blue-100 hover:text-blue-700"
onClick={() => handleDownloadFile(file)}
title="Download photo"
>
<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={() => handleRemovePhoto(index)}
title="Remove photo"
>
<X className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
</Button>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* Supporting Documents Section */}
<div className="space-y-3 sm:space-y-4">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-base sm:text-lg">Supporting Documents</h3>
<Badge className="bg-secondary text-secondary-foreground text-xs">Optional</Badge>
</div>
{/* Grid layout for Invoices/Receipts and Attendance Sheet */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
{/* Invoices/Receipts */}
<div>
<Label className="text-sm sm:text-base font-semibold flex items-center gap-2">
<Receipt className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
Invoices / Receipts
</Label>
<p className="text-xs sm:text-sm text-gray-600 mb-2">
Upload invoices and receipts for expenses incurred
</p>
<div
className={`border-2 border-dashed rounded-lg p-3 sm:p-4 transition-all duration-200 ${invoicesReceipts.length > 0
? 'border-blue-500 bg-blue-50 hover:border-blue-600'
: 'border-gray-300 hover:border-blue-500 bg-white'
}`}
>
<input
ref={invoicesInputRef}
type="file"
multiple
accept={['.pdf', '.jpg', '.jpeg', '.png'].filter(ext => documentPolicy.allowedFileTypes.includes(ext.replace('.', ''))).join(',')}
className="hidden"
id="invoiceReceipts"
onChange={handleInvoicesChange}
/>
<label
htmlFor="invoiceReceipts"
className="cursor-pointer flex flex-col items-center gap-2"
>
{invoicesReceipts.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">
{invoicesReceipts.length} document{invoicesReceipts.length !== 1 ? 's' : ''} selected
</span>
<span className="text-xs text-blue-600">
Click to add more documents
</span>
</div>
</>
) : (
<>
<Receipt className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600">
Click to upload invoices/receipts (PDF, JPG, PNG)
</span>
</>
)}
</label>
</div>
{invoicesReceipts.length > 0 && (
<div className="mt-3 space-y-2">
<p className="text-xs font-medium text-gray-600 mb-1">
Selected Documents ({invoicesReceipts.length}):
</p>
{invoicesReceipts.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 sm:p-3 rounded-lg text-xs sm: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">
<Receipt className="w-3.5 h-3.5 sm:w-4 sm: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={() => handleRemoveInvoice(index)}
title="Remove document"
>
<X className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
</Button>
</div>
</div>
))}
</div>
)}
</div>
{/* Attendance Sheet */}
<div>
<Label className="text-sm sm:text-base font-semibold">
Attendance Sheet / Participant List
</Label>
<p className="text-xs sm:text-sm text-gray-600 mb-2">
Upload attendance records or participant lists (if applicable)
</p>
<div
className={`border-2 border-dashed rounded-lg p-3 sm:p-4 transition-all duration-200 ${attendanceSheet
? 'border-blue-500 bg-blue-50 hover:border-blue-600'
: 'border-gray-300 hover:border-blue-500 bg-white'
}`}
>
<input
ref={attendanceInputRef}
type="file"
accept={['.pdf', '.xlsx', '.xls', '.csv'].filter(ext => documentPolicy.allowedFileTypes.includes(ext.replace('.', ''))).join(',')}
className="hidden"
id="attendanceDoc"
onChange={handleAttendanceChange}
/>
<label
htmlFor="attendanceDoc"
className="cursor-pointer flex flex-col items-center gap-2"
>
{attendanceSheet ? (
<>
<CheckCircle2 className="w-8 h-8 text-blue-600" />
<div className="flex flex-col items-center gap-1 w-full max-w-full px-2">
<span className="text-sm font-semibold text-blue-700 break-words text-center w-full max-w-full">
{attendanceSheet.name}
</span>
<span className="text-xs text-blue-600">
Document selected
</span>
</div>
</>
) : (
<>
<Upload className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600">
Click to upload attendance sheet (Excel, PDF, CSV)
</span>
</>
)}
</label>
</div>
{attendanceSheet && (
<div className="mt-3">
<p className="text-xs font-medium text-gray-600 mb-2">
Selected Document:
</p>
<div className="flex items-start justify-between bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 p-2 sm:p-3 rounded-lg text-xs sm: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-3.5 h-3.5 sm:w-4 sm:h-4 text-blue-600 flex-shrink-0 mt-0.5" />
<span className="text-gray-800 font-medium break-words break-all">{attendanceSheet.name}</span>
</div>
<div className="flex items-center gap-1 flex-shrink-0 ml-2">
{canPreviewFile(attendanceSheet) && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 w-7 p-0 hover:bg-blue-100 hover:text-blue-700"
onClick={() => handlePreviewFile(attendanceSheet)}
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(attendanceSheet)}
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={() => {
setAttendanceSheet(null);
if (attendanceInputRef.current) attendanceInputRef.current.value = '';
}}
title="Remove document"
>
<X className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
</Button>
</div>
</div>
</div>
)}
</div>
</div>
</div>
{/* Completion Description */}
<div className="space-y-1.5 sm:space-y-2 max-w-3xl">
<Label className="text-sm sm:text-base font-semibold flex items-center gap-2" htmlFor="completionDescription">
Brief Description of Completion *
</Label>
<Textarea
id="completionDescription"
placeholder="Provide a brief description of the completed activity, including key highlights, outcomes, challenges faced, and any relevant observations..."
value={completionDescription}
onChange={(e) => setCompletionDescription(e.target.value)}
className="min-h-[100px] sm:min-h-[120px] text-sm"
/>
<p className="text-xs text-gray-500">{completionDescription.length} characters</p>
</div>
{/* Warning Message */}
{!isFormValid && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 sm:p-4 flex items-start gap-2 sm:gap-3">
<CircleAlert className="w-4 h-4 sm:w-5 sm:h-5 text-amber-600 flex-shrink-0 mt-0.5" />
<div className="text-xs sm:text-sm text-amber-800">
<p className="font-semibold mb-1">Missing or Invalid Information</p>
<p>
Please ensure completion date, documents/photos, description, and expense details (non-negative amounts and descriptions) are provided before submitting.
</p>
</div>
</div>
)}
</div>
<DialogFooter className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2 px-6 pt-3 pb-6 border-t flex-shrink-0">
<Button
variant="outline"
onClick={handleClose}
disabled={submitting}
className="border-2"
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={submitting || !isFormValid}
className="bg-[#2d4a3e] hover:bg-[#1f3329] text-white disabled:bg-gray-300 disabled:text-gray-500"
>
{submitting ? 'Submitting...' : 'Submit Documents'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog >
{/* 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>
)
}
</>
);
}