cost item ui enhanced and export csv feture added for ivoice line items and validation added for gst inputs based on inter-state and intra-state

This commit is contained in:
laxmanhalaki 2026-02-20 20:41:30 +05:30
parent 5dce660f05
commit dfe94555ab
7 changed files with 749 additions and 533 deletions

View File

@ -10,7 +10,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon, XCircle, History, ChevronDown, ChevronUp, RefreshCw, RotateCw, Eye } from 'lucide-react';
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon, XCircle, History, ChevronDown, ChevronUp, RefreshCw, RotateCw, Eye, FileSpreadsheet } from 'lucide-react';
import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter';
import { formatHoursMinutes } from '@/utils/slaTracker';
import {
@ -1453,6 +1453,41 @@ export function DealerClaimWorkflowTab({
loadCompletionDocuments();
}, [request]);
const handleDownloadCSV = async () => {
try {
const requestId = request.id || request.requestId;
if (!requestId) {
toast.error('Request ID not found');
return;
}
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1';
const response = await fetch(`${baseUrl}/dealer-claims/${requestId}/e-invoice/csv`, {
headers: {
'Authorization': `Bearer ${TokenManager.getAccessToken()}`
}
});
if (!response.ok) {
throw new Error('Failed to download CSV');
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `Invoice_${request.requestNumber || 'Export'}.csv`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success('CSV downloaded successfully');
} catch (error) {
console.error('Error downloading CSV:', error);
toast.error('Failed to download CSV');
}
};
const handlePreviewInvoice = async () => {
try {
const requestId = request.id || request.requestId;
@ -1482,6 +1517,9 @@ export function DealerClaimWorkflowTab({
const dealerName = request?.claimDetails?.dealerName ||
request?.dealerInfo?.name ||
'Dealer';
const dealerGSTIN = request?.claimDetails?.dealerGstin ||
request?.dealerInfo?.gstin ||
request?.dealerInfo?.dealerGSTIN;
const activityName = request?.claimDetails?.activityName ||
request?.activityInfo?.activityName ||
request?.title ||
@ -1627,6 +1665,23 @@ export function DealerClaimWorkflowTab({
</Button>
);
})()}
{/* CSV Export Button (Requestor Claim Approval) */}
{(() => {
const isRequestorClaimStep = (step.levelName || step.title || '').toLowerCase().includes('requestor claim') ||
(step.levelName || step.title || '').toLowerCase().includes('requestor - claim');
const hasInvoice = request?.invoice || (request?.irn && step.status === 'approved');
return isRequestorClaimStep && hasInvoice && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 hover:bg-emerald-100 ml-1"
title="Export CSV"
onClick={handleDownloadCSV}
>
<FileSpreadsheet className="w-3.5 h-3.5 text-emerald-600" />
</Button>
);
})()}
</div>
<p className="text-sm text-gray-600">{step.approver}</p>
<p className="text-sm text-gray-500 mt-2 italic">{step.description}</p>
@ -2374,6 +2429,7 @@ export function DealerClaimWorkflowTab({
onClose={() => setShowProposalModal(false)}
onSubmit={handleProposalSubmit}
dealerName={dealerName}
dealerGSTIN={dealerGSTIN}
activityName={activityName}
defaultGstRate={request?.claimDetails?.defaultGstRate}
requestId={request?.id || request?.requestId}
@ -2423,6 +2479,7 @@ export function DealerClaimWorkflowTab({
onClose={() => setShowCompletionModal(false)}
onSubmit={handleCompletionSubmit}
dealerName={dealerName}
dealerGSTIN={dealerGSTIN}
activityName={activityName}
defaultGstRate={request?.claimDetails?.defaultGstRate}
requestId={request?.id || request?.requestId}

View File

@ -125,7 +125,7 @@ export function ActivityInformationCard({
<Receipt className="w-4 h-4 text-blue-600" />
{formatCurrency(
activityInfo.closedExpensesBreakdown && activityInfo.closedExpensesBreakdown.length > 0
? activityInfo.closedExpensesBreakdown.reduce((sum, item: any) => sum + (item.totalAmt || ((item.amount * (item.quantity || 1)) + (item.gstAmt || 0))), 0)
? activityInfo.closedExpensesBreakdown.reduce((sum, item: any) => sum + (item.totalAmt || (Number(item.amount) + Number(item.gstAmt || 0))), 0)
: activityInfo.closedExpenses
)}
</p>
@ -156,7 +156,6 @@ export function ActivityInformationCard({
<thead className="bg-blue-100/50">
<tr>
<th className="px-3 py-2 text-left font-semibold text-blue-900">Description</th>
<th className="px-3 py-2 text-right font-semibold text-blue-900 w-16">Qty</th>
<th className="px-3 py-2 text-right font-semibold text-blue-900 w-24">Base</th>
<th className="px-3 py-2 text-right font-semibold text-blue-900 w-24">GST</th>
<th className="px-3 py-2 text-right font-semibold text-blue-900 w-28">Total</th>
@ -169,19 +168,18 @@ export function ActivityInformationCard({
{item.description}
{item.gstRate ? <span className="text-[10px] text-gray-400 block">{item.gstRate}% GST</span> : null}
</td>
<td className="px-3 py-2 text-right text-gray-900">{item.quantity || 1}</td>
<td className="px-3 py-2 text-right text-gray-900">{formatCurrency(item.amount)}</td>
<td className="px-3 py-2 text-right text-gray-900">{formatCurrency(item.gstAmt || 0)}</td>
<td className="px-3 py-2 text-right font-medium text-gray-900">
{formatCurrency(item.totalAmt || ((item.amount * (item.quantity || 1)) + (item.gstAmt || 0)))}
{formatCurrency(item.totalAmt || (Number(item.amount) + Number(item.gstAmt || 0)))}
</td>
</tr>
))}
<tr className="bg-blue-100/50 font-bold">
<td colSpan={4} className="px-3 py-2 text-blue-900">Final Claim Amount</td>
<td colSpan={3} className="px-3 py-2 text-blue-900">Final Claim Amount</td>
<td className="px-3 py-2 text-right text-blue-700">
{formatCurrency(
activityInfo.closedExpensesBreakdown.reduce((sum: number, item: any) => sum + (item.totalAmt || ((item.amount * (item.quantity || 1)) + (item.gstAmt || 0))), 0)
activityInfo.closedExpensesBreakdown.reduce((sum: number, item: any) => sum + (item.totalAmt || (Number(item.amount) + Number(item.gstAmt || 0))), 0)
)}
</td>
</tr>

View File

@ -46,10 +46,8 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
if (proposalDetails.costBreakup && proposalDetails.costBreakup.length > 0) {
const total = proposalDetails.costBreakup.reduce((sum, item) => {
const amount = item.amount || 0;
const quantity = item.quantity || 1;
const baseTotal = amount * quantity;
const gst = item.gstAmt || 0;
const lineTotal = item.totalAmt || (baseTotal + gst);
const lineTotal = item.totalAmt || (Number(amount) + Number(gst));
return sum + (Number.isNaN(lineTotal) ? 0 : lineTotal);
}, 0);
return total;
@ -111,9 +109,6 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wide">
Item Description
</th>
<th className="px-4 py-2 text-right text-xs font-semibold text-gray-700 uppercase tracking-wide">
Qty
</th>
<th className="px-4 py-2 text-right text-xs font-semibold text-gray-700 uppercase tracking-wide">
Base Amount
</th>
@ -136,9 +131,6 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
</div>
) : null}
</td>
<td className="px-4 py-3 text-sm text-gray-900 text-right">
{item.quantity || 1}
</td>
<td className="px-4 py-3 text-sm text-gray-900 text-right">
{formatCurrency(item.amount)}
</td>
@ -146,12 +138,12 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
{formatCurrency(item.gstAmt)}
</td>
<td className="px-4 py-3 text-sm text-gray-900 text-right font-medium">
{formatCurrency(item.totalAmt || ((item.amount || 0) * (item.quantity || 1)) + (item.gstAmt || 0))}
{formatCurrency(item.totalAmt || (Number(item.amount || 0) + Number(item.gstAmt || 0)))}
</td>
</tr>
))}
<tr className="bg-green-50 font-semibold">
<td colSpan={4} className="px-4 py-3 text-sm text-gray-900">
<td colSpan={3} className="px-4 py-3 text-sm text-gray-900">
Estimated Budget (Total Inclusive of GST)
</td>
<td className="px-4 py-3 text-sm text-green-700 text-right">

View File

@ -24,6 +24,7 @@ import { toast } from 'sonner';
import '@/components/common/FilePreview/FilePreview.css';
import './DealerCompletionDocumentsModal.css';
import { validateHSNSAC } from '@/utils/validationUtils';
import { getStateCodeFromGSTIN, getActiveTaxComponents } from '@/utils/gstUtils';
interface ExpenseItem {
id: string;
@ -62,6 +63,7 @@ interface DealerCompletionDocumentsModalProps {
completionDescription: string;
}) => Promise<void>;
dealerName?: string;
dealerGSTIN?: string;
activityName?: string;
requestId?: string;
defaultGstRate?: number;
@ -76,6 +78,7 @@ export function DealerCompletionDocumentsModal({
onClose,
onSubmit,
dealerName = 'Jaipur Royal Enfield',
dealerGSTIN,
activityName = 'Activity',
requestId: _requestId,
defaultGstRate = 18,
@ -83,6 +86,13 @@ export function DealerCompletionDocumentsModal({
}: 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 [expenseItems, setExpenseItems] = useState<ExpenseItem[]>([]);
const [completionDocuments, setCompletionDocuments] = useState<File[]>([]);
const [activityPhotos, setActivityPhotos] = useState<File[]>([]);
@ -181,36 +191,28 @@ export function DealerCompletionDocumentsModal({
}, [expenseItems]);
// GST Calculation Helper
const calculateGST = (amount: number, rate: number, quantity: number = 1, type: 'IGST' | 'CGST_SGST' = 'CGST_SGST') => {
const calculateGST = (amount: number, rates: { cgstRate: number; sgstRate: number; igstRate: number; utgstRate: number }, quantity: number = 1) => {
const baseTotal = amount * quantity;
const gstAmt = (baseTotal * rate) / 100;
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;
if (type === 'IGST') {
return {
gstAmt,
igstRate: rate,
igstAmt: gstAmt,
cgstRate: 0,
cgstAmt: 0,
sgstRate: 0,
sgstAmt: 0,
totalAmt
};
} else {
const halfRate = rate / 2;
const halfAmt = gstAmt / 2;
return {
gstAmt,
igstRate: 0,
igstAmt: 0,
cgstRate: halfRate,
cgstAmt: halfAmt,
sgstRate: halfRate,
sgstAmt: halfAmt,
totalAmt
};
}
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
@ -262,21 +264,75 @@ export function DealerCompletionDocumentsModal({
setExpenseItems(
expenseItems.map((item) => {
if (item.id === id) {
const updatedItem = { ...item, [field]: value };
let updatedItem = { ...item, [field]: value };
// Re-calculate GST if amount or rate changes
if (field === 'amount' || field === 'gstRate') {
// Re-calculate GST if relevant fields change
if (['amount', 'gstRate', 'cgstRate', 'sgstRate', 'utgstRate', 'igstRate', 'quantity'].includes(field)) {
const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount;
const rate = field === 'gstRate' ? parseFloat(value) || 0 : item.gstRate;
const quantity = 1;
const gst = calculateGST(amount, rate, quantity);
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,
gstRate: rate,
quantity,
...gst
...calculation
};
}
@ -576,7 +632,7 @@ export function DealerCompletionDocumentsModal({
</DialogHeader>
<div className="flex-1 overflow-y-auto overflow-x-hidden px-6 py-3">
<div className="space-y-5 sm:space-y-6 max-w-4xl mx-auto">
<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">
@ -600,6 +656,9 @@ export function DealerCompletionDocumentsModal({
<h3 className="font-semibold text-base sm:text-lg">Closed Expenses</h3>
<Badge className="bg-secondary text-secondary-foreground text-xs">Optional</Badge>
</div>
<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}
@ -671,19 +730,68 @@ export function DealerCompletionDocumentsModal({
<option value="SAC">SAC (Service)</option>
</select>
</div>
<div className="w-20 sm:w-24 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">GST %</Label>
<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.gstRate || ''}
value={item.cgstRate || ''}
onChange={(e) =>
handleExpenseChange(item.id, 'gstRate', e.target.value)
handleExpenseChange(item.id, 'cgstRate', e.target.value)
}
className="w-full bg-white text-sm"
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
@ -697,26 +805,36 @@ export function DealerCompletionDocumentsModal({
</Button>
</div>
{item.gstAmt > 0 && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 pt-3 border-t border-dashed border-gray-200">
<div className="flex flex-col">
<span className="text-[10px] text-gray-500 uppercase">CGST ({item.cgstRate}%)</span>
<span className="text-xs font-semibold">{item.cgstAmt.toLocaleString('en-IN')}</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] text-gray-500 uppercase">SGST ({item.sgstRate}%)</span>
<span className="text-xs font-semibold">{item.sgstAmt.toLocaleString('en-IN')}</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] text-gray-500 uppercase">GST Total</span>
<span className="text-xs font-semibold">{item.gstAmt.toLocaleString('en-IN')}</span>
</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.totalAmt.toLocaleString('en-IN')}</span>
</div>
<div className="grid grid-cols-2 sm:grid-cols-5 gap-3 pt-3 border-t border-dashed border-gray-200">
<div className="flex flex-col">
<span className="text-[10px] text-gray-500 uppercase">CGST</span>
<span className="text-xs font-semibold">{item.cgstAmt.toLocaleString('en-IN', { minimumFractionDigits: 2 })}</span>
</div>
)}
{item.sgstAmt > 0 && (
<div className="flex flex-col">
<span className="text-[10px] text-gray-500 uppercase">SGST</span>
<span className="text-xs font-semibold">{item.sgstAmt.toLocaleString('en-IN', { minimumFractionDigits: 2 })}</span>
</div>
)}
{item.utgstAmt > 0 && (
<div className="flex flex-col">
<span className="text-[10px] text-gray-500 uppercase">UTGST</span>
<span className="text-xs font-semibold">{item.utgstAmt.toLocaleString('en-IN', { minimumFractionDigits: 2 })}</span>
</div>
)}
<div className="flex flex-col">
<span className="text-[10px] text-gray-500 uppercase">IGST</span>
<span className="text-xs font-semibold">{item.igstAmt.toLocaleString('en-IN', { minimumFractionDigits: 2 })}</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] text-gray-500 uppercase">GST Total</span>
<span className="text-xs font-semibold">{item.gstAmt.toLocaleString('en-IN', { minimumFractionDigits: 2 })}</span>
</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.totalAmt.toLocaleString('en-IN', { minimumFractionDigits: 2 })}</span>
</div>
</div>
</div>
))}
</div>

View File

@ -27,6 +27,7 @@ import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi'
import '@/components/common/FilePreview/FilePreview.css';
import './DealerProposalModal.css';
import { validateHSNSAC } from '@/utils/validationUtils';
import { getStateCodeFromGSTIN, getActiveTaxComponents } from '@/utils/gstUtils';
interface CostItem {
id: string;
@ -61,6 +62,7 @@ interface DealerProposalSubmissionModalProps {
dealerComments: string;
}) => Promise<void>;
dealerName?: string;
dealerGSTIN?: string;
activityName?: string;
requestId?: string;
previousProposalData?: any;
@ -76,6 +78,7 @@ export function DealerProposalSubmissionModal({
onClose,
onSubmit,
dealerName = 'Jaipur Royal Enfield',
dealerGSTIN,
activityName = 'Activity',
requestId: _requestId,
previousProposalData,
@ -83,6 +86,13 @@ export function DealerProposalSubmissionModal({
documentPolicy,
}: DealerProposalSubmissionModalProps) {
const [proposalDocument, setProposalDocument] = useState<File | null>(null);
// Determine active tax components based on dealer GSTIN
const taxConfig = useMemo(() => {
const stateCode = getStateCodeFromGSTIN(dealerGSTIN);
return getActiveTaxComponents(stateCode);
}, [dealerGSTIN]);
const [costItems, setCostItems] = useState<CostItem[]>([
{
id: '1',
@ -108,36 +118,28 @@ export function DealerProposalSubmissionModal({
]);
// GST Calculation Helper
const calculateGST = (amount: number, rate: number, quantity: number = 1, type: 'IGST' | 'CGST_SGST' = 'CGST_SGST') => {
const calculateGST = (amount: number, rates: { cgstRate: number; sgstRate: number; igstRate: number; utgstRate: number }, quantity: number = 1) => {
const baseTotal = amount * quantity;
const gstAmt = (baseTotal * rate) / 100;
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;
if (type === 'IGST') {
return {
gstAmt,
igstRate: rate,
igstAmt: gstAmt,
cgstRate: 0,
cgstAmt: 0,
sgstRate: 0,
sgstAmt: 0,
totalAmt
};
} else {
const halfRate = rate / 2;
const halfAmt = gstAmt / 2;
return {
gstAmt,
igstRate: 0,
igstAmt: 0,
cgstRate: halfRate,
cgstAmt: halfAmt,
sgstRate: halfRate,
sgstAmt: halfAmt,
totalAmt
};
}
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
};
};
const [timelineMode, setTimelineMode] = useState<'date' | 'days'>('date');
const [expectedCompletionDate, setExpectedCompletionDate] = useState('');
@ -356,21 +358,75 @@ export function DealerProposalSubmissionModal({
setCostItems(prev =>
prev.map(item => {
if (item.id === id) {
const updatedItem = { ...item, [field]: value };
let updatedItem = { ...item, [field]: value };
// Re-calculate GST if amount or rate changes
if (field === 'amount' || field === 'gstRate') {
// Re-calculate GST if relevant fields change
if (['amount', 'gstRate', 'cgstRate', 'sgstRate', 'utgstRate', 'igstRate', 'quantity'].includes(field)) {
const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount;
const rate = field === 'gstRate' ? parseFloat(value) || 0 : item.gstRate;
const quantity = 1;
const gst = calculateGST(amount, rate, quantity);
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,
gstRate: rate,
quantity,
...gst
...calculation
};
}
@ -662,448 +718,325 @@ export function DealerProposalSubmissionModal({
</div>
)}
<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'].filter(ext => documentPolicy.allowedFileTypes.includes(ext.replace('.', ''))).join(',')}
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 className="space-y-6 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-x-8 lg:gap-y-6 lg:items-start">
{/* 1. Proposal Document - Column 1 */}
<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>
{/* 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
accept={documentPolicy.allowedFileTypes.map(ext => `.${ext}`).join(',')}
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 text-center">
Click to upload additional documents
</span>
<span className="text-[10px] text-gray-400">
Max {documentPolicy.maxFileSizeMB}MB | {documentPolicy.allowedFileTypes.join(', ').toUpperCase()}
</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
className={`border-2 border-dashed rounded-lg p-3 lg:p-4 transition-all duration-200 ${proposalDocument
? 'border-green-500 bg-green-50'
: 'border-gray-300 bg-white'
}`}
>
<input
ref={proposalDocInputRef}
type="file"
accept={['.pdf', '.doc', '.docx'].filter(ext => documentPolicy.allowedFileTypes.includes(ext.replace('.', ''))).join(',')}
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 text-center">
<CheckCircle2 className="w-8 h-8 text-green-600" />
<span className="text-sm font-semibold text-green-700 truncate w-full px-2">{proposalDocument.name}</span>
<div className="flex gap-2">
{canPreviewFile(proposalDocument) && (
<Button type="button" variant="outline" size="sm" onClick={() => handlePreviewFile(proposalDocument)} className="h-7 text-[10px]">Preview</Button>
)}
<Button type="button" variant="outline" size="sm" onClick={() => handleDownloadFile(proposalDocument)} className="h-7 text-[10px]">Download</Button>
</div>
</div>
) : (
<>
<Upload className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600">Upload Proposal (PDF, DOC)</span>
</>
)}
</div>
</label>
</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>
{/* 2. Timeline for Closure - Column 2 */}
<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-3">
<div className="flex gap-2">
<Button
type="button"
onClick={handleAddCostItem}
className="bg-[#2d4a3e] hover:bg-[#1f3329] text-white"
onClick={() => setTimelineMode('date')}
className={timelineMode === 'date' ? 'bg-[#2d4a3e] text-white flex-1' : 'border flex-1'}
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>
Date
</Button>
<Button
type="button"
onClick={() => setTimelineMode('days')}
className={timelineMode === 'days' ? 'bg-[#2d4a3e] text-white flex-1' : 'border flex-1'}
size="sm"
>
Days
</Button>
</div>
<div className="space-y-3 lg:space-y-4 max-h-[300px] lg:max-h-[280px] overflow-y-auto pr-1">
{costItems.map((item) => (
<div key={item.id} className="p-3 border rounded-lg bg-gray-50/50 space-y-3 relative group">
<div className="flex flex-col gap-3 w-full relative">
{/* Row 1: Description and Close Button */}
<div className="flex gap-2 items-start w-full">
<div className="flex-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Description</Label>
<Input
placeholder="Item description"
value={item.description}
onChange={(e) =>
handleCostItemChange(item.id, 'description', e.target.value)
}
className="w-full bg-white"
/>
</div>
<Button
type="button"
variant="ghost"
size="sm"
className="mt-6 hover:bg-red-100 hover:text-red-700 flex-shrink-0 h-9 w-9 p-0"
onClick={() => handleRemoveCostItem(item.id)}
disabled={costItems.length === 1}
>
<X className="w-4 h-4" />
</Button>
</div>
{timelineMode === 'date' ? (
<CustomDatePicker
value={expectedCompletionDate || null}
onChange={(date) => setExpectedCompletionDate(date || '')}
minDate={minDate}
placeholderText="dd/mm/yyyy"
className="w-full"
/>
) : (
<Input
type="number"
placeholder="Enter number of days"
min="1"
value={numberOfDays}
onChange={(e) => setNumberOfDays(e.target.value)}
className="h-10 w-full"
/>
)}
</div>
</div>
{/* Row 2: Numeric Fields */}
<div className="flex gap-2 w-full">
<div className="flex-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Amount (Base)</Label>
{/* 3. Cost Breakup Section - Full Width (Row 2) */}
<div className="lg:col-span-2 space-y-3 py-4 border-y border-gray-100 mt-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-lg text-[#2d4a3e]">Cost Breakup</h3>
<Badge variant="outline" className="text-xs border-[#2d4a3e] text-[#2d4a3e] bg-green-50 font-medium">Required</Badge>
</div>
<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={handleAddCostItem}
className="bg-[#2d4a3e] hover:bg-[#1f3329] text-white"
size="sm"
>
<Plus className="w-4 h-4 mr-1" />
Add Item
</Button>
</div>
<div className="space-y-4 max-h-[400px] overflow-y-auto pr-1">
{costItems.map((item) => (
<div key={item.id} className="p-4 border rounded-xl bg-gray-50/50 hover:bg-gray-50 transition-colors shadow-sm relative group">
<div className="flex flex-col gap-4">
{/* Row 1: Description and Close */}
<div className="flex gap-3 items-start">
<div className="flex-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Item Description</Label>
<Input
placeholder="e.g., Venue branding, Logistics, etc."
value={item.description}
onChange={(e) => handleCostItemChange(item.id, 'description', e.target.value)}
className="w-full bg-white shadow-sm"
/>
</div>
<Button
type="button"
variant="ghost"
size="sm"
className="mt-6 hover:bg-red-50 hover:text-red-600 h-9 w-9 p-0 rounded-full"
onClick={() => handleRemoveCostItem(item.id)}
disabled={costItems.length === 1}
>
<X className="w-4 h-4" />
</Button>
</div>
{/* Row 2: Numeric Calculations - Optimization: Narrower widths for GST to save space */}
<div className="flex flex-wrap gap-x-4 gap-y-3 items-start">
<div className="flex-1 min-w-[140px]">
<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="Amount"
min="0"
step="0.01"
value={item.amount || ''}
onChange={(e) =>
handleCostItemChange(item.id, 'amount', e.target.value)
}
className="w-full bg-white"
/>
</div>
<div className="flex-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">HSN Code</Label>
<Input
placeholder="HSN"
value={item.hsnCode || ''}
onChange={(e) =>
handleCostItemChange(item.id, 'hsnCode', e.target.value)
}
className={`w-full bg-white ${!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="flex-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Item Type</Label>
<select
value={item.isService ? 'SAC' : 'HSN'}
onChange={(e) =>
handleCostItemChange(item.id, 'isService', e.target.value === 'SAC')
}
className="flex h-10 w-full rounded-md border border-input bg-white px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="HSN">HSN (Goods)</option>
<option value="SAC">SAC (Serv.)</option>
</select>
</div>
<div className="flex-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">GST %</Label>
<Input
type="number"
placeholder="%"
min="0"
max="100"
step="0.1"
value={item.gstRate || ''}
onChange={(e) =>
handleCostItemChange(item.id, 'gstRate', e.target.value)
}
className="w-full bg-white"
onChange={(e) => handleCostItemChange(item.id, 'amount', e.target.value)}
className="pl-8 bg-white shadow-sm"
/>
</div>
</div>
<div className="w-28 sm:w-32">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">HSN/SAC</Label>
<Input
value={item.hsnCode || ''}
onChange={(e) => handleCostItemChange(item.id, 'hsnCode', e.target.value)}
className={`bg-white shadow-sm ${!validateHSNSAC(item.hsnCode, item.isService).isValid ? 'border-red-500' : ''}`}
/>
</div>
<div className="w-28 sm:w-32">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Type</Label>
<select
value={item.isService ? 'SAC' : 'HSN'}
onChange={(e) => handleCostItemChange(item.id, 'isService', e.target.value === 'SAC')}
className="flex h-10 w-full rounded-md border border-input bg-white px-3 py-2 text-sm shadow-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-[#2d4a3e]"
>
<option value="HSN">HSN (Goods)</option>
<option value="SAC">SAC (Service)</option>
</select>
</div>
<div className="w-16">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">CGST %</Label>
<Input
type="number"
value={item.cgstRate || ''}
onChange={(e) => handleCostItemChange(item.id, 'cgstRate', e.target.value)}
disabled={!taxConfig.isCGST}
className="bg-white shadow-sm text-center px-1 disabled:bg-gray-100 disabled:text-gray-400"
/>
</div>
<div className="w-16">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">SGST %</Label>
<Input
type="number"
value={item.sgstRate || ''}
onChange={(e) => handleCostItemChange(item.id, 'sgstRate', e.target.value)}
disabled={!taxConfig.isSGST}
className="bg-white shadow-sm text-center px-1 disabled:bg-gray-100 disabled:text-gray-400"
/>
</div>
<div className="w-16">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">UTGST %</Label>
<Input
type="number"
value={item.utgstRate || ''}
onChange={(e) => handleCostItemChange(item.id, 'utgstRate', e.target.value)}
disabled={!taxConfig.isUTGST}
className="bg-white shadow-sm text-center px-1 disabled:bg-gray-100 disabled:text-gray-400"
/>
</div>
<div className="w-16">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">IGST %</Label>
<Input
type="number"
value={item.igstRate || ''}
onChange={(e) => handleCostItemChange(item.id, 'igstRate', e.target.value)}
disabled={!taxConfig.isIGST}
className="bg-white shadow-sm text-center px-1 disabled:bg-gray-100 disabled:text-gray-400"
/>
</div>
</div>
{item.gstAmt > 0 && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 pt-1 border-t border-dashed border-gray-200">
<div className="text-[10px]">
<span className="text-gray-500">CGST ({item.cgstRate}%):</span>
<span className="ml-1 font-semibold">{item.cgstAmt.toLocaleString('en-IN')}</span>
</div>
<div className="text-[10px]">
<span className="text-gray-500">SGST ({item.sgstRate}%):</span>
<span className="ml-1 font-semibold">{item.sgstAmt.toLocaleString('en-IN')}</span>
</div>
<div className="text-[10px]">
<span className="text-gray-500">GST Total:</span>
<span className="ml-1 font-semibold">{item.gstAmt.toLocaleString('en-IN')}</span>
</div>
<div className="text-[10px] text-right">
<span className="text-gray-500">Item Total:</span>
<span className="ml-1 font-bold text-gray-900">{item.totalAmt.toLocaleString('en-IN')}</span>
</div>
{/* Item Summary Row */}
<div className="flex flex-wrap gap-4 pt-3 border-t border-dashed items-center justify-between text-xs">
<div className="flex gap-4 text-gray-500 font-medium">
<span>CGST: <span className="text-gray-900">{item.cgstAmt.toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>
{item.sgstAmt > 0 && <span>SGST: <span className="text-gray-900">{item.sgstAmt.toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>}
{item.utgstAmt > 0 && <span>UTGST: <span className="text-gray-900">{item.utgstAmt.toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>}
<span>IGST: <span className="text-gray-900">{item.igstAmt.toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>
</div>
)}
</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 className="flex gap-4 items-center">
<span className="text-gray-500">GST Total: <span className="text-gray-900 font-semibold">{item.gstAmt.toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>
<span className="text-sm font-bold text-[#2d4a3e] bg-green-50 px-3 py-1 rounded-lg">Item Total: {item.totalAmt.toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span>
</div>
</div>
</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>
{/* Total Budget Card */}
<div className="bg-[#2d4a3e] rounded-xl p-5 text-white flex items-center justify-between shadow-lg">
<div className="flex items-center gap-3">
<div className="p-2 bg-white/20 rounded-lg">
<IndianRupee className="w-6 h-6" />
</div>
<div>
<p className="text-xs text-white/70 uppercase font-bold tracking-wider">Estimated Total Budget</p>
<p className="text-sm text-white/90">Inclusive of all applicable taxes</p>
</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 className="text-3xl font-bold border-l border-white/20 pl-6">
{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2 })}
</div>
</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>
{/* 4. Other Supporting Docs - Column 1 */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-base lg:text-lg">Other Documents</h3>
<Badge variant="outline" className="text-xs border-gray-300 text-gray-500 bg-gray-50 font-medium">Optional</Badge>
</div>
)}
<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' : 'border-gray-300 bg-white'}`}
>
<input
ref={otherDocsInputRef}
type="file"
multiple
accept={documentPolicy.allowedFileTypes.map(ext => `.${ext}`).join(',')}
className="hidden"
id="otherDocs"
onChange={handleOtherDocsChange}
/>
<label htmlFor="otherDocs" className="cursor-pointer flex flex-col items-center gap-2">
<Upload className="w-8 h-8 text-gray-400" />
<span className="text-sm text-gray-600">
{otherDocuments.length > 0 ? `${otherDocuments.length} files selected` : 'Upload supporting files'}
</span>
<span className="text-[10px] text-gray-400">Click to add documents</span>
</label>
</div>
{otherDocuments.length > 0 && (
<div className="mt-2 space-y-2 max-h-[120px] overflow-y-auto">
{otherDocuments.map((file, index) => (
<div key={index} className="flex items-center justify-between bg-blue-50 border border-blue-100 p-2 rounded-lg text-xs">
<div className="flex items-center gap-2 flex-1 min-w-0 pr-2">
<FileText className="w-3.5 h-3.5 text-blue-600 flex-shrink-0" />
<span className="text-gray-700 font-medium truncate">{file.name}</span>
</div>
<div className="flex items-center gap-1">
{canPreviewFile(file) && (
<Button type="button" variant="ghost" size="sm" className="h-6 w-6 p-0 hover:bg-blue-100" onClick={() => handlePreviewFile(file)}><Eye className="w-3 h-3" /></Button>
)}
<Button type="button" variant="ghost" size="sm" className="h-6 w-6 p-0 hover:bg-green-100" onClick={() => handleDownloadFile(file)}><Download className="w-3 h-3" /></Button>
<Button type="button" variant="ghost" size="sm" className="h-6 w-6 p-0 hover:bg-red-100 hover:text-red-600" onClick={() => handleRemoveOtherDoc(index)}><X className="w-3 h-3" /></Button>
</div>
</div>
))}
</div>
)}
</div>
{/* 5. Dealer Comments - Column 2 */}
<div className="space-y-2">
<Label className="font-semibold text-base lg:text-lg">Dealer Comments *</Label>
<Textarea
placeholder="Details about execution, special requirements, etc."
value={dealerComments}
onChange={(e) => setDealerComments(e.target.value)}
className="min-h-[100px] bg-white shadow-sm"
/>
</div>
</div>
{/* 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 mt-4">
<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>
<DialogFooter className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end flex-shrink-0 pt-3 lg:pt-4 border-t">
@ -1128,11 +1061,11 @@ export function DealerProposalSubmissionModal({
{/* Standardized File Preview */}
{previewDoc && (
<FilePreview
fileName={previewDoc.fileName}
fileType={previewDoc.fileType}
fileUrl={previewDoc.fileUrl}
fileSize={previewDoc.fileSize}
attachmentId={previewDoc.documentId}
fileName={previewDoc.fileName || ''}
fileType={previewDoc.fileType || ''}
fileUrl={previewDoc.fileUrl || ''}
fileSize={previewDoc.fileSize || 0}
attachmentId={previewDoc.documentId || ''}
onDownload={downloadDocument}
open={!!previewDoc}
onClose={() => setPreviewDoc(null)}

View File

@ -608,9 +608,8 @@ export function InitiatorProposalApprovalModal({
<>
<div className="border rounded-lg overflow-hidden max-h-[200px] lg:max-h-[180px] overflow-y-auto">
<div className="bg-gray-50 px-3 lg:px-4 py-2 border-b sticky top-0">
<div className="grid grid-cols-5 gap-4 text-xs lg:text-sm font-semibold text-gray-700">
<div className="grid grid-cols-4 gap-4 text-xs lg:text-sm font-semibold text-gray-700">
<div className="col-span-1">Item Description</div>
<div className="text-right">Qty</div>
<div className="text-right">Base</div>
<div className="text-right">GST</div>
<div className="text-right">Total</div>
@ -618,14 +617,11 @@ export function InitiatorProposalApprovalModal({
</div>
<div className="divide-y">
{costBreakup.map((item: any, index: number) => (
<div key={item?.id || item?.description || index} className="px-3 lg:px-4 py-2 lg:py-3 grid grid-cols-5 gap-4">
<div key={item?.id || item?.description || index} className="px-3 lg:px-4 py-2 lg:py-3 grid grid-cols-4 gap-4">
<div className="col-span-1 text-xs lg:text-sm text-gray-700">
{item?.description || 'N/A'}
{item?.gstRate ? <span className="block text-[10px] text-gray-400">{item.gstRate}% GST</span> : null}
</div>
<div className="text-xs lg:text-sm text-gray-900 text-right">
{item?.quantity || 1}
</div>
<div className="text-xs lg:text-sm text-gray-900 text-right">
{(Number(item?.amount) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>

122
src/utils/gstUtils.ts Normal file
View File

@ -0,0 +1,122 @@
/**
* GST Utility for state validation and tax calculations
* Contains state codes and helper functions for determining GST components
*/
export const STATE_CODES: Record<string, string> = {
'01': 'Jammu and Kashmir',
'02': 'Himachal Pradesh',
'03': 'Punjab',
'04': 'Chandigarh',
'05': 'Uttarakhand',
'06': 'Haryana',
'07': 'Delhi',
'08': 'Rajasthan',
'09': 'Uttar Pradesh',
'10': 'Bihar',
'11': 'Sikkim',
'12': 'Arunachal Pradesh',
'13': 'Nagaland',
'14': 'Manipur',
'15': 'Mizoram',
'16': 'Tripura',
'17': 'Meghalaya',
'18': 'Assam',
'19': 'West Bengal',
'20': 'Jharkhand',
'21': 'Odisha',
'22': 'Chhattisgarh',
'23': 'Madhya Pradesh',
'24': 'Gujarat',
'25': 'Daman and Diu',
'26': 'Dadra and Nagar Haveli',
'27': 'Maharashtra',
'29': 'Karnataka',
'30': 'Goa',
'31': 'Lakshadweep Islands',
'32': 'Kerala',
'33': 'Tamil Nadu',
'34': 'Pondicherry',
'35': 'Andaman and Nicobar',
'36': 'Telangana',
'37': 'Andhra Pradesh',
'38': 'Ladakh',
'97': 'Others',
};
/**
* Royal Enfield State Code (Tamil Nadu)
*/
export const COMPANY_STATE_CODE = '33';
/**
* State codes that use UTGST instead of SGST
* Andaman and Nicobar Islands, Chandigarh, Dadra and Nagar Haveli and Daman and Diu, Ladakh, Lakshadweep
*/
export const UT_STATE_CODES = new Set(['04', '25', '26', '31', '35', '38']);
/**
* Extracts state code from GSTIN
* @param gstin The 15-digit GSTIN string
* @returns 2-digit state code or null
*/
export const getStateCodeFromGSTIN = (gstin: string | undefined | null): string | null => {
if (!gstin || gstin.length < 2) return null;
const stateCode = gstin.substring(0, 2);
return STATE_CODES[stateCode] ? stateCode : null;
};
/**
* Checks if a state code corresponds to a Union Territory (requiring UTGST)
* @param stateCode 2-digit state code
* @returns boolean
*/
export const isUnionTerritory = (stateCode: string | undefined | null): boolean => {
if (!stateCode) return false;
return UT_STATE_CODES.has(stateCode);
};
/**
* Determines if a transaction is Inter-state (IGST) or Intra-state (CGST+SGST/UTGST)
* @param dealerStateCode 2-digit state code of the dealer
* @returns true if IGST, false if CGST+SGST/UTGST
*/
export const isInterState = (dealerStateCode: string | undefined | null): boolean => {
if (!dealerStateCode) return false; // Default to intra-state if unknown
return dealerStateCode !== COMPANY_STATE_CODE;
};
/**
* Gets the tax components for a given dealer state
* @param dealerStateCode 2-digit state code of the dealer
* @returns Object indicating which tax components are active
*/
export const getActiveTaxComponents = (dealerStateCode: string | undefined | null) => {
if (!dealerStateCode) {
return {
isIGST: false,
isCGST: true,
isSGST: true,
isUTGST: false,
};
}
const isInter = isInterState(dealerStateCode);
if (isInter) {
return {
isIGST: true,
isCGST: false,
isSGST: false,
isUTGST: false,
};
}
const isUT = isUnionTerritory(dealerStateCode);
return {
isIGST: false,
isCGST: true,
isSGST: !isUT,
isUTGST: isUT,
};
};