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:
parent
5dce660f05
commit
dfe94555ab
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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
122
src/utils/gstUtils.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user