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 { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Progress } from '@/components/ui/progress';
|
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 { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter';
|
||||||
import { formatHoursMinutes } from '@/utils/slaTracker';
|
import { formatHoursMinutes } from '@/utils/slaTracker';
|
||||||
import {
|
import {
|
||||||
@ -1453,6 +1453,41 @@ export function DealerClaimWorkflowTab({
|
|||||||
loadCompletionDocuments();
|
loadCompletionDocuments();
|
||||||
}, [request]);
|
}, [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 () => {
|
const handlePreviewInvoice = async () => {
|
||||||
try {
|
try {
|
||||||
const requestId = request.id || request.requestId;
|
const requestId = request.id || request.requestId;
|
||||||
@ -1482,6 +1517,9 @@ export function DealerClaimWorkflowTab({
|
|||||||
const dealerName = request?.claimDetails?.dealerName ||
|
const dealerName = request?.claimDetails?.dealerName ||
|
||||||
request?.dealerInfo?.name ||
|
request?.dealerInfo?.name ||
|
||||||
'Dealer';
|
'Dealer';
|
||||||
|
const dealerGSTIN = request?.claimDetails?.dealerGstin ||
|
||||||
|
request?.dealerInfo?.gstin ||
|
||||||
|
request?.dealerInfo?.dealerGSTIN;
|
||||||
const activityName = request?.claimDetails?.activityName ||
|
const activityName = request?.claimDetails?.activityName ||
|
||||||
request?.activityInfo?.activityName ||
|
request?.activityInfo?.activityName ||
|
||||||
request?.title ||
|
request?.title ||
|
||||||
@ -1627,6 +1665,23 @@ export function DealerClaimWorkflowTab({
|
|||||||
</Button>
|
</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>
|
</div>
|
||||||
<p className="text-sm text-gray-600">{step.approver}</p>
|
<p className="text-sm text-gray-600">{step.approver}</p>
|
||||||
<p className="text-sm text-gray-500 mt-2 italic">{step.description}</p>
|
<p className="text-sm text-gray-500 mt-2 italic">{step.description}</p>
|
||||||
@ -2374,6 +2429,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
onClose={() => setShowProposalModal(false)}
|
onClose={() => setShowProposalModal(false)}
|
||||||
onSubmit={handleProposalSubmit}
|
onSubmit={handleProposalSubmit}
|
||||||
dealerName={dealerName}
|
dealerName={dealerName}
|
||||||
|
dealerGSTIN={dealerGSTIN}
|
||||||
activityName={activityName}
|
activityName={activityName}
|
||||||
defaultGstRate={request?.claimDetails?.defaultGstRate}
|
defaultGstRate={request?.claimDetails?.defaultGstRate}
|
||||||
requestId={request?.id || request?.requestId}
|
requestId={request?.id || request?.requestId}
|
||||||
@ -2423,6 +2479,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
onClose={() => setShowCompletionModal(false)}
|
onClose={() => setShowCompletionModal(false)}
|
||||||
onSubmit={handleCompletionSubmit}
|
onSubmit={handleCompletionSubmit}
|
||||||
dealerName={dealerName}
|
dealerName={dealerName}
|
||||||
|
dealerGSTIN={dealerGSTIN}
|
||||||
activityName={activityName}
|
activityName={activityName}
|
||||||
defaultGstRate={request?.claimDetails?.defaultGstRate}
|
defaultGstRate={request?.claimDetails?.defaultGstRate}
|
||||||
requestId={request?.id || request?.requestId}
|
requestId={request?.id || request?.requestId}
|
||||||
|
|||||||
@ -125,7 +125,7 @@ export function ActivityInformationCard({
|
|||||||
<Receipt className="w-4 h-4 text-blue-600" />
|
<Receipt className="w-4 h-4 text-blue-600" />
|
||||||
{formatCurrency(
|
{formatCurrency(
|
||||||
activityInfo.closedExpensesBreakdown && activityInfo.closedExpensesBreakdown.length > 0
|
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
|
: activityInfo.closedExpenses
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
@ -156,7 +156,6 @@ export function ActivityInformationCard({
|
|||||||
<thead className="bg-blue-100/50">
|
<thead className="bg-blue-100/50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-2 text-left font-semibold text-blue-900">Description</th>
|
<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">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-24">GST</th>
|
||||||
<th className="px-3 py-2 text-right font-semibold text-blue-900 w-28">Total</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.description}
|
||||||
{item.gstRate ? <span className="text-[10px] text-gray-400 block">{item.gstRate}% GST</span> : null}
|
{item.gstRate ? <span className="text-[10px] text-gray-400 block">{item.gstRate}% GST</span> : null}
|
||||||
</td>
|
</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.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 text-gray-900">{formatCurrency(item.gstAmt || 0)}</td>
|
||||||
<td className="px-3 py-2 text-right font-medium text-gray-900">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
<tr className="bg-blue-100/50 font-bold">
|
<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">
|
<td className="px-3 py-2 text-right text-blue-700">
|
||||||
{formatCurrency(
|
{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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -46,10 +46,8 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
|
|||||||
if (proposalDetails.costBreakup && proposalDetails.costBreakup.length > 0) {
|
if (proposalDetails.costBreakup && proposalDetails.costBreakup.length > 0) {
|
||||||
const total = proposalDetails.costBreakup.reduce((sum, item) => {
|
const total = proposalDetails.costBreakup.reduce((sum, item) => {
|
||||||
const amount = item.amount || 0;
|
const amount = item.amount || 0;
|
||||||
const quantity = item.quantity || 1;
|
|
||||||
const baseTotal = amount * quantity;
|
|
||||||
const gst = item.gstAmt || 0;
|
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);
|
return sum + (Number.isNaN(lineTotal) ? 0 : lineTotal);
|
||||||
}, 0);
|
}, 0);
|
||||||
return total;
|
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">
|
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wide">
|
||||||
Item Description
|
Item Description
|
||||||
</th>
|
</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">
|
<th className="px-4 py-2 text-right text-xs font-semibold text-gray-700 uppercase tracking-wide">
|
||||||
Base Amount
|
Base Amount
|
||||||
</th>
|
</th>
|
||||||
@ -136,9 +131,6 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</td>
|
</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">
|
<td className="px-4 py-3 text-sm text-gray-900 text-right">
|
||||||
{formatCurrency(item.amount)}
|
{formatCurrency(item.amount)}
|
||||||
</td>
|
</td>
|
||||||
@ -146,12 +138,12 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
|
|||||||
{formatCurrency(item.gstAmt)}
|
{formatCurrency(item.gstAmt)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-gray-900 text-right font-medium">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
<tr className="bg-green-50 font-semibold">
|
<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)
|
Estimated Budget (Total Inclusive of GST)
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-green-700 text-right">
|
<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 '@/components/common/FilePreview/FilePreview.css';
|
||||||
import './DealerCompletionDocumentsModal.css';
|
import './DealerCompletionDocumentsModal.css';
|
||||||
import { validateHSNSAC } from '@/utils/validationUtils';
|
import { validateHSNSAC } from '@/utils/validationUtils';
|
||||||
|
import { getStateCodeFromGSTIN, getActiveTaxComponents } from '@/utils/gstUtils';
|
||||||
|
|
||||||
interface ExpenseItem {
|
interface ExpenseItem {
|
||||||
id: string;
|
id: string;
|
||||||
@ -62,6 +63,7 @@ interface DealerCompletionDocumentsModalProps {
|
|||||||
completionDescription: string;
|
completionDescription: string;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
dealerName?: string;
|
dealerName?: string;
|
||||||
|
dealerGSTIN?: string;
|
||||||
activityName?: string;
|
activityName?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
defaultGstRate?: number;
|
defaultGstRate?: number;
|
||||||
@ -76,6 +78,7 @@ export function DealerCompletionDocumentsModal({
|
|||||||
onClose,
|
onClose,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
dealerName = 'Jaipur Royal Enfield',
|
dealerName = 'Jaipur Royal Enfield',
|
||||||
|
dealerGSTIN,
|
||||||
activityName = 'Activity',
|
activityName = 'Activity',
|
||||||
requestId: _requestId,
|
requestId: _requestId,
|
||||||
defaultGstRate = 18,
|
defaultGstRate = 18,
|
||||||
@ -83,6 +86,13 @@ export function DealerCompletionDocumentsModal({
|
|||||||
}: DealerCompletionDocumentsModalProps) {
|
}: DealerCompletionDocumentsModalProps) {
|
||||||
const [activityCompletionDate, setActivityCompletionDate] = useState('');
|
const [activityCompletionDate, setActivityCompletionDate] = useState('');
|
||||||
const [numberOfParticipants, setNumberOfParticipants] = 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 [expenseItems, setExpenseItems] = useState<ExpenseItem[]>([]);
|
||||||
const [completionDocuments, setCompletionDocuments] = useState<File[]>([]);
|
const [completionDocuments, setCompletionDocuments] = useState<File[]>([]);
|
||||||
const [activityPhotos, setActivityPhotos] = useState<File[]>([]);
|
const [activityPhotos, setActivityPhotos] = useState<File[]>([]);
|
||||||
@ -181,36 +191,28 @@ export function DealerCompletionDocumentsModal({
|
|||||||
}, [expenseItems]);
|
}, [expenseItems]);
|
||||||
|
|
||||||
// GST Calculation Helper
|
// 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 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;
|
const totalAmt = baseTotal + gstAmt;
|
||||||
|
|
||||||
if (type === 'IGST') {
|
return {
|
||||||
return {
|
cgstRate: rates.cgstRate,
|
||||||
gstAmt,
|
cgstAmt,
|
||||||
igstRate: rate,
|
sgstRate: rates.sgstRate,
|
||||||
igstAmt: gstAmt,
|
sgstAmt,
|
||||||
cgstRate: 0,
|
utgstRate: rates.utgstRate,
|
||||||
cgstAmt: 0,
|
utgstAmt,
|
||||||
sgstRate: 0,
|
igstRate: rates.igstRate,
|
||||||
sgstAmt: 0,
|
igstAmt,
|
||||||
totalAmt
|
gstAmt,
|
||||||
};
|
gstRate: (rates.cgstRate || 0) + (rates.sgstRate || 0) + (rates.utgstRate || 0) + (rates.igstRate || 0),
|
||||||
} else {
|
totalAmt
|
||||||
const halfRate = rate / 2;
|
};
|
||||||
const halfAmt = gstAmt / 2;
|
|
||||||
return {
|
|
||||||
gstAmt,
|
|
||||||
igstRate: 0,
|
|
||||||
igstAmt: 0,
|
|
||||||
cgstRate: halfRate,
|
|
||||||
cgstAmt: halfAmt,
|
|
||||||
sgstRate: halfRate,
|
|
||||||
sgstAmt: halfAmt,
|
|
||||||
totalAmt
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if all required fields are filled
|
// Check if all required fields are filled
|
||||||
@ -262,21 +264,75 @@ export function DealerCompletionDocumentsModal({
|
|||||||
setExpenseItems(
|
setExpenseItems(
|
||||||
expenseItems.map((item) => {
|
expenseItems.map((item) => {
|
||||||
if (item.id === id) {
|
if (item.id === id) {
|
||||||
const updatedItem = { ...item, [field]: value };
|
let updatedItem = { ...item, [field]: value };
|
||||||
|
|
||||||
// Re-calculate GST if amount or rate changes
|
// Re-calculate GST if relevant fields change
|
||||||
if (field === 'amount' || field === 'gstRate') {
|
if (['amount', 'gstRate', 'cgstRate', 'sgstRate', 'utgstRate', 'igstRate', 'quantity'].includes(field)) {
|
||||||
const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount;
|
const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount;
|
||||||
const rate = field === 'gstRate' ? parseFloat(value) || 0 : item.gstRate;
|
const quantity = field === 'quantity' ? parseInt(value) || 1 : item.quantity;
|
||||||
const quantity = 1;
|
|
||||||
const gst = calculateGST(amount, rate, 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 {
|
return {
|
||||||
...updatedItem,
|
...updatedItem,
|
||||||
amount,
|
amount,
|
||||||
gstRate: rate,
|
|
||||||
quantity,
|
quantity,
|
||||||
...gst
|
...calculation
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,7 +632,7 @@ export function DealerCompletionDocumentsModal({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto overflow-x-hidden px-6 py-3">
|
<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 */}
|
{/* Activity Completion Date */}
|
||||||
<div className="space-y-1.5 sm:space-y-2">
|
<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">
|
<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>
|
<h3 className="font-semibold text-base sm:text-lg">Closed Expenses</h3>
|
||||||
<Badge className="bg-secondary text-secondary-foreground text-xs">Optional</Badge>
|
<Badge className="bg-secondary text-secondary-foreground text-xs">Optional</Badge>
|
||||||
</div>
|
</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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAddExpense}
|
onClick={handleAddExpense}
|
||||||
@ -671,19 +730,68 @@ export function DealerCompletionDocumentsModal({
|
|||||||
<option value="SAC">SAC (Service)</option>
|
<option value="SAC">SAC (Service)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-20 sm:w-24 flex-shrink-0">
|
<div className="w-14 sm:w-16 flex-shrink-0">
|
||||||
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">GST %</Label>
|
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">CGST %</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="%"
|
placeholder="%"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
value={item.gstRate || ''}
|
value={item.cgstRate || ''}
|
||||||
onChange={(e) =>
|
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>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@ -697,26 +805,36 @@ export function DealerCompletionDocumentsModal({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{item.gstAmt > 0 && (
|
<div className="grid grid-cols-2 sm:grid-cols-5 gap-3 pt-3 border-t border-dashed border-gray-200">
|
||||||
<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">
|
||||||
<div className="flex flex-col">
|
<span className="text-[10px] text-gray-500 uppercase">CGST</span>
|
||||||
<span className="text-[10px] text-gray-500 uppercase">CGST ({item.cgstRate}%)</span>
|
<span className="text-xs font-semibold">₹{item.cgstAmt.toLocaleString('en-IN', { minimumFractionDigits: 2 })}</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>
|
</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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi'
|
|||||||
import '@/components/common/FilePreview/FilePreview.css';
|
import '@/components/common/FilePreview/FilePreview.css';
|
||||||
import './DealerProposalModal.css';
|
import './DealerProposalModal.css';
|
||||||
import { validateHSNSAC } from '@/utils/validationUtils';
|
import { validateHSNSAC } from '@/utils/validationUtils';
|
||||||
|
import { getStateCodeFromGSTIN, getActiveTaxComponents } from '@/utils/gstUtils';
|
||||||
|
|
||||||
interface CostItem {
|
interface CostItem {
|
||||||
id: string;
|
id: string;
|
||||||
@ -61,6 +62,7 @@ interface DealerProposalSubmissionModalProps {
|
|||||||
dealerComments: string;
|
dealerComments: string;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
dealerName?: string;
|
dealerName?: string;
|
||||||
|
dealerGSTIN?: string;
|
||||||
activityName?: string;
|
activityName?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
previousProposalData?: any;
|
previousProposalData?: any;
|
||||||
@ -76,6 +78,7 @@ export function DealerProposalSubmissionModal({
|
|||||||
onClose,
|
onClose,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
dealerName = 'Jaipur Royal Enfield',
|
dealerName = 'Jaipur Royal Enfield',
|
||||||
|
dealerGSTIN,
|
||||||
activityName = 'Activity',
|
activityName = 'Activity',
|
||||||
requestId: _requestId,
|
requestId: _requestId,
|
||||||
previousProposalData,
|
previousProposalData,
|
||||||
@ -83,6 +86,13 @@ export function DealerProposalSubmissionModal({
|
|||||||
documentPolicy,
|
documentPolicy,
|
||||||
}: DealerProposalSubmissionModalProps) {
|
}: DealerProposalSubmissionModalProps) {
|
||||||
const [proposalDocument, setProposalDocument] = useState<File | null>(null);
|
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[]>([
|
const [costItems, setCostItems] = useState<CostItem[]>([
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@ -108,36 +118,28 @@ export function DealerProposalSubmissionModal({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// GST Calculation Helper
|
// 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 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;
|
const totalAmt = baseTotal + gstAmt;
|
||||||
|
|
||||||
if (type === 'IGST') {
|
return {
|
||||||
return {
|
cgstRate: rates.cgstRate,
|
||||||
gstAmt,
|
cgstAmt,
|
||||||
igstRate: rate,
|
sgstRate: rates.sgstRate,
|
||||||
igstAmt: gstAmt,
|
sgstAmt,
|
||||||
cgstRate: 0,
|
utgstRate: rates.utgstRate,
|
||||||
cgstAmt: 0,
|
utgstAmt,
|
||||||
sgstRate: 0,
|
igstRate: rates.igstRate,
|
||||||
sgstAmt: 0,
|
igstAmt,
|
||||||
totalAmt
|
gstAmt,
|
||||||
};
|
gstRate: (rates.cgstRate || 0) + (rates.sgstRate || 0) + (rates.utgstRate || 0) + (rates.igstRate || 0),
|
||||||
} else {
|
totalAmt
|
||||||
const halfRate = rate / 2;
|
};
|
||||||
const halfAmt = gstAmt / 2;
|
|
||||||
return {
|
|
||||||
gstAmt,
|
|
||||||
igstRate: 0,
|
|
||||||
igstAmt: 0,
|
|
||||||
cgstRate: halfRate,
|
|
||||||
cgstAmt: halfAmt,
|
|
||||||
sgstRate: halfRate,
|
|
||||||
sgstAmt: halfAmt,
|
|
||||||
totalAmt
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const [timelineMode, setTimelineMode] = useState<'date' | 'days'>('date');
|
const [timelineMode, setTimelineMode] = useState<'date' | 'days'>('date');
|
||||||
const [expectedCompletionDate, setExpectedCompletionDate] = useState('');
|
const [expectedCompletionDate, setExpectedCompletionDate] = useState('');
|
||||||
@ -356,21 +358,75 @@ export function DealerProposalSubmissionModal({
|
|||||||
setCostItems(prev =>
|
setCostItems(prev =>
|
||||||
prev.map(item => {
|
prev.map(item => {
|
||||||
if (item.id === id) {
|
if (item.id === id) {
|
||||||
const updatedItem = { ...item, [field]: value };
|
let updatedItem = { ...item, [field]: value };
|
||||||
|
|
||||||
// Re-calculate GST if amount or rate changes
|
// Re-calculate GST if relevant fields change
|
||||||
if (field === 'amount' || field === 'gstRate') {
|
if (['amount', 'gstRate', 'cgstRate', 'sgstRate', 'utgstRate', 'igstRate', 'quantity'].includes(field)) {
|
||||||
const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount;
|
const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount;
|
||||||
const rate = field === 'gstRate' ? parseFloat(value) || 0 : item.gstRate;
|
const quantity = field === 'quantity' ? parseInt(value) || 1 : item.quantity;
|
||||||
const quantity = 1;
|
|
||||||
const gst = calculateGST(amount, rate, 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 {
|
return {
|
||||||
...updatedItem,
|
...updatedItem,
|
||||||
amount,
|
amount,
|
||||||
gstRate: rate,
|
|
||||||
quantity,
|
quantity,
|
||||||
...gst
|
...calculation
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -662,448 +718,325 @@ export function DealerProposalSubmissionModal({
|
|||||||
</div>
|
</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">
|
<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">
|
||||||
{/* Left Column - Documents */}
|
{/* 1. Proposal Document - Column 1 */}
|
||||||
<div className="space-y-4 lg:space-y-4 flex flex-col">
|
<div className="space-y-2 lg:space-y-2">
|
||||||
{/* Proposal Document Section */}
|
<div className="flex items-center gap-2">
|
||||||
<div className="space-y-2 lg:space-y-2">
|
<h3 className="font-semibold text-base lg:text-lg">Proposal Document</h3>
|
||||||
<div className="flex items-center gap-2">
|
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
||||||
<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>
|
</div>
|
||||||
|
<div
|
||||||
{/* Other Supporting Documents Section */}
|
className={`border-2 border-dashed rounded-lg p-3 lg:p-4 transition-all duration-200 ${proposalDocument
|
||||||
<div className="space-y-2 lg:space-y-2">
|
? 'border-green-500 bg-green-50'
|
||||||
<div className="flex items-center gap-2">
|
: 'border-gray-300 bg-white'
|
||||||
<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>
|
<input
|
||||||
<div>
|
ref={proposalDocInputRef}
|
||||||
<Label className="flex items-center gap-2 text-sm lg:text-base font-semibold">
|
type="file"
|
||||||
Additional Documents
|
accept={['.pdf', '.doc', '.docx'].filter(ext => documentPolicy.allowedFileTypes.includes(ext.replace('.', ''))).join(',')}
|
||||||
</Label>
|
className="hidden"
|
||||||
<p className="text-xs lg:text-sm text-gray-600 mb-2">
|
id="proposalDoc"
|
||||||
Any other supporting documents (invoices, receipts, photos, etc.)
|
onChange={handleProposalDocChange}
|
||||||
</p>
|
/>
|
||||||
<div
|
<label htmlFor="proposalDoc" className="cursor-pointer flex flex-col items-center gap-2">
|
||||||
className={`border-2 border-dashed rounded-lg p-3 lg:p-4 transition-all duration-200 ${otherDocuments.length > 0
|
{proposalDocument ? (
|
||||||
? 'border-blue-500 bg-blue-50 hover:border-blue-600'
|
<div className="flex flex-col items-center gap-2 w-full text-center">
|
||||||
: 'border-gray-300 hover:border-blue-500 bg-white'
|
<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">
|
||||||
<input
|
{canPreviewFile(proposalDocument) && (
|
||||||
ref={otherDocsInputRef}
|
<Button type="button" variant="outline" size="sm" onClick={() => handlePreviewFile(proposalDocument)} className="h-7 text-[10px]">Preview</Button>
|
||||||
type="file"
|
)}
|
||||||
multiple
|
<Button type="button" variant="outline" size="sm" onClick={() => handleDownloadFile(proposalDocument)} className="h-7 text-[10px]">Download</Button>
|
||||||
accept={documentPolicy.allowedFileTypes.map(ext => `.${ext}`).join(',')}
|
</div>
|
||||||
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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - Planning & Budget */}
|
{/* 2. Timeline for Closure - Column 2 */}
|
||||||
<div className="space-y-4 lg:space-y-4 flex flex-col">
|
<div className="space-y-2 lg:space-y-2">
|
||||||
{/* Cost Breakup Section */}
|
<div className="flex items-center gap-2">
|
||||||
<div className="space-y-2 lg:space-y-2">
|
<h3 className="font-semibold text-base lg:text-lg">Timeline for Closure</h3>
|
||||||
<div className="flex items-center justify-between">
|
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
||||||
<div className="flex items-center gap-2">
|
</div>
|
||||||
<h3 className="font-semibold text-base lg:text-lg">Cost Breakup</h3>
|
<div className="space-y-3">
|
||||||
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
<div className="flex gap-2">
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAddCostItem}
|
onClick={() => setTimelineMode('date')}
|
||||||
className="bg-[#2d4a3e] hover:bg-[#1f3329] text-white"
|
className={timelineMode === 'date' ? 'bg-[#2d4a3e] text-white flex-1' : 'border flex-1'}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<Plus className="w-3 h-3 lg:w-4 lg:h-4 mr-1" />
|
Date
|
||||||
<span className="hidden sm:inline">Add Item</span>
|
</Button>
|
||||||
<span className="sm:hidden">Add</span>
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTimelineMode('days')}
|
||||||
|
className={timelineMode === 'days' ? 'bg-[#2d4a3e] text-white flex-1' : 'border flex-1'}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Days
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 lg:space-y-4 max-h-[300px] lg:max-h-[280px] overflow-y-auto pr-1">
|
{timelineMode === 'date' ? (
|
||||||
{costItems.map((item) => (
|
<CustomDatePicker
|
||||||
<div key={item.id} className="p-3 border rounded-lg bg-gray-50/50 space-y-3 relative group">
|
value={expectedCompletionDate || null}
|
||||||
<div className="flex flex-col gap-3 w-full relative">
|
onChange={(date) => setExpectedCompletionDate(date || '')}
|
||||||
{/* Row 1: Description and Close Button */}
|
minDate={minDate}
|
||||||
<div className="flex gap-2 items-start w-full">
|
placeholderText="dd/mm/yyyy"
|
||||||
<div className="flex-1">
|
className="w-full"
|
||||||
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Description</Label>
|
/>
|
||||||
<Input
|
) : (
|
||||||
placeholder="Item description"
|
<Input
|
||||||
value={item.description}
|
type="number"
|
||||||
onChange={(e) =>
|
placeholder="Enter number of days"
|
||||||
handleCostItemChange(item.id, 'description', e.target.value)
|
min="1"
|
||||||
}
|
value={numberOfDays}
|
||||||
className="w-full bg-white"
|
onChange={(e) => setNumberOfDays(e.target.value)}
|
||||||
/>
|
className="h-10 w-full"
|
||||||
</div>
|
/>
|
||||||
<Button
|
)}
|
||||||
type="button"
|
</div>
|
||||||
variant="ghost"
|
</div>
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Row 2: Numeric Fields */}
|
{/* 3. Cost Breakup Section - Full Width (Row 2) */}
|
||||||
<div className="flex gap-2 w-full">
|
<div className="lg:col-span-2 space-y-3 py-4 border-y border-gray-100 mt-2">
|
||||||
<div className="flex-1">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Amount (Base)</Label>
|
<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
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Amount"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
value={item.amount || ''}
|
value={item.amount || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleCostItemChange(item.id, 'amount', e.target.value)}
|
||||||
handleCostItemChange(item.id, 'amount', e.target.value)
|
className="pl-8 bg-white shadow-sm"
|
||||||
}
|
|
||||||
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"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{item.gstAmt > 0 && (
|
{/* Item Summary Row */}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 pt-1 border-t border-dashed border-gray-200">
|
<div className="flex flex-wrap gap-4 pt-3 border-t border-dashed items-center justify-between text-xs">
|
||||||
<div className="text-[10px]">
|
<div className="flex gap-4 text-gray-500 font-medium">
|
||||||
<span className="text-gray-500">CGST ({item.cgstRate}%):</span>
|
<span>CGST: <span className="text-gray-900">₹{item.cgstAmt.toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>
|
||||||
<span className="ml-1 font-semibold">₹{item.cgstAmt.toLocaleString('en-IN')}</span>
|
{item.sgstAmt > 0 && <span>SGST: <span className="text-gray-900">₹{item.sgstAmt.toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>}
|
||||||
</div>
|
{item.utgstAmt > 0 && <span>UTGST: <span className="text-gray-900">₹{item.utgstAmt.toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>}
|
||||||
<div className="text-[10px]">
|
<span>IGST: <span className="text-gray-900">₹{item.igstAmt.toLocaleString('en-IN', { minimumFractionDigits: 1 })}</span></span>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex gap-4 items-center">
|
||||||
</div>
|
<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 className="border-2 border-gray-300 rounded-lg p-3 lg:p-4 bg-white">
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<IndianRupee className="w-4 h-4 lg:w-5 lg:h-5 text-gray-700" />
|
|
||||||
<span className="font-semibold text-sm lg:text-base text-gray-900">Estimated Budget</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xl lg:text-2xl font-bold text-gray-900">
|
|
||||||
₹{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Timeline for Closure Section */}
|
{/* Total Budget Card */}
|
||||||
<div className="space-y-2 lg:space-y-2">
|
<div className="bg-[#2d4a3e] rounded-xl p-5 text-white flex items-center justify-between shadow-lg">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<h3 className="font-semibold text-base lg:text-lg">Timeline for Closure</h3>
|
<div className="p-2 bg-white/20 rounded-lg">
|
||||||
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
<IndianRupee className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div>
|
||||||
<div className="flex gap-2">
|
<p className="text-xs text-white/70 uppercase font-bold tracking-wider">Estimated Total Budget</p>
|
||||||
<Button
|
<p className="text-sm text-white/90">Inclusive of all applicable taxes</p>
|
||||||
type="button"
|
|
||||||
onClick={() => setTimelineMode('date')}
|
|
||||||
className={
|
|
||||||
timelineMode === 'date'
|
|
||||||
? 'bg-[#2d4a3e] hover:bg-[#1f3329] text-white'
|
|
||||||
: 'border-2 hover:bg-gray-50'
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<Calendar className="w-4 h-4 mr-1" />
|
|
||||||
Specific Date
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setTimelineMode('days')}
|
|
||||||
className={
|
|
||||||
timelineMode === 'days'
|
|
||||||
? 'bg-[#2d4a3e] hover:bg-[#1f3329] text-white'
|
|
||||||
: 'border-2 hover:bg-gray-50'
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Number of Days
|
|
||||||
</Button>
|
|
||||||
</div>
|
</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>
|
||||||
</div>
|
<div className="text-3xl font-bold border-l border-white/20 pl-6">
|
||||||
|
₹{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2 })}
|
||||||
{/* Dealer Comments Section */}
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="dealerComments" className="text-sm lg:text-base font-semibold flex items-center gap-2">
|
|
||||||
Dealer Comments / Details *
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
id="dealerComments"
|
|
||||||
placeholder="Provide detailed comments about this claim request, including any special considerations, execution details, or additional information..."
|
|
||||||
value={dealerComments}
|
|
||||||
onChange={(e) => setDealerComments(e.target.value)}
|
|
||||||
className="min-h-[80px] lg:min-h-[100px] text-sm w-full"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500">{dealerComments.length} characters</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Full Width Sections */}
|
{/* 4. Other Supporting Docs - Column 1 */}
|
||||||
|
<div className="space-y-2">
|
||||||
{/* Warning Message */}
|
<div className="flex items-center gap-2">
|
||||||
{!isFormValid && (
|
<h3 className="font-semibold text-base lg:text-lg">Other Documents</h3>
|
||||||
<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">
|
<Badge variant="outline" className="text-xs border-gray-300 text-gray-500 bg-gray-50 font-medium">Optional</Badge>
|
||||||
<CircleAlert className="w-4 h-4 lg:w-5 lg:h-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="text-xs lg:text-sm text-amber-800">
|
|
||||||
<p className="font-semibold mb-1">Missing Required Information</p>
|
|
||||||
<p>
|
|
||||||
Please ensure proposal document, cost breakup, timeline, and dealer comments are provided before submitting.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div
|
||||||
|
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>
|
</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>
|
</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">
|
<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 */}
|
{/* Standardized File Preview */}
|
||||||
{previewDoc && (
|
{previewDoc && (
|
||||||
<FilePreview
|
<FilePreview
|
||||||
fileName={previewDoc.fileName}
|
fileName={previewDoc.fileName || ''}
|
||||||
fileType={previewDoc.fileType}
|
fileType={previewDoc.fileType || ''}
|
||||||
fileUrl={previewDoc.fileUrl}
|
fileUrl={previewDoc.fileUrl || ''}
|
||||||
fileSize={previewDoc.fileSize}
|
fileSize={previewDoc.fileSize || 0}
|
||||||
attachmentId={previewDoc.documentId}
|
attachmentId={previewDoc.documentId || ''}
|
||||||
onDownload={downloadDocument}
|
onDownload={downloadDocument}
|
||||||
open={!!previewDoc}
|
open={!!previewDoc}
|
||||||
onClose={() => setPreviewDoc(null)}
|
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="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="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="col-span-1">Item Description</div>
|
||||||
<div className="text-right">Qty</div>
|
|
||||||
<div className="text-right">Base</div>
|
<div className="text-right">Base</div>
|
||||||
<div className="text-right">GST</div>
|
<div className="text-right">GST</div>
|
||||||
<div className="text-right">Total</div>
|
<div className="text-right">Total</div>
|
||||||
@ -618,14 +617,11 @@ export function InitiatorProposalApprovalModal({
|
|||||||
</div>
|
</div>
|
||||||
<div className="divide-y">
|
<div className="divide-y">
|
||||||
{costBreakup.map((item: any, index: number) => (
|
{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">
|
<div className="col-span-1 text-xs lg:text-sm text-gray-700">
|
||||||
{item?.description || 'N/A'}
|
{item?.description || 'N/A'}
|
||||||
{item?.gstRate ? <span className="block text-[10px] text-gray-400">{item.gstRate}% GST</span> : null}
|
{item?.gstRate ? <span className="block text-[10px] text-gray-400">{item.gstRate}% GST</span> : null}
|
||||||
</div>
|
</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">
|
<div className="text-xs lg:text-sm text-gray-900 text-right">
|
||||||
₹{(Number(item?.amount) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
₹{(Number(item?.amount) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
</div>
|
</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