added hsn validation and removed quatity part from the cost related items
This commit is contained in:
parent
5e91b85854
commit
5dce660f05
@ -906,7 +906,8 @@ export function DealerClaimWorkflowTab({
|
|||||||
cessAmt: item.cessAmt,
|
cessAmt: item.cessAmt,
|
||||||
totalAmt: item.totalAmt,
|
totalAmt: item.totalAmt,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
hsnCode: item.hsnCode
|
hsnCode: item.hsnCode,
|
||||||
|
isService: item.isService
|
||||||
})),
|
})),
|
||||||
totalEstimatedBudget: totalBudget,
|
totalEstimatedBudget: totalBudget,
|
||||||
expectedCompletionDate: data.expectedCompletionDate,
|
expectedCompletionDate: data.expectedCompletionDate,
|
||||||
@ -1179,7 +1180,8 @@ export function DealerClaimWorkflowTab({
|
|||||||
cessAmt: item.cessAmt,
|
cessAmt: item.cessAmt,
|
||||||
totalAmt: item.totalAmt,
|
totalAmt: item.totalAmt,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
hsnCode: item.hsnCode
|
hsnCode: item.hsnCode,
|
||||||
|
isService: item.isService
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Submit completion documents using dealer claim API
|
// Submit completion documents using dealer claim API
|
||||||
@ -2373,6 +2375,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
onSubmit={handleProposalSubmit}
|
onSubmit={handleProposalSubmit}
|
||||||
dealerName={dealerName}
|
dealerName={dealerName}
|
||||||
activityName={activityName}
|
activityName={activityName}
|
||||||
|
defaultGstRate={request?.claimDetails?.defaultGstRate}
|
||||||
requestId={request?.id || request?.requestId}
|
requestId={request?.id || request?.requestId}
|
||||||
previousProposalData={versionHistory?.find(v => v.snapshotType === 'PROPOSAL')?.snapshotData}
|
previousProposalData={versionHistory?.find(v => v.snapshotType === 'PROPOSAL')?.snapshotData}
|
||||||
documentPolicy={documentPolicy}
|
documentPolicy={documentPolicy}
|
||||||
@ -2421,6 +2424,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
onSubmit={handleCompletionSubmit}
|
onSubmit={handleCompletionSubmit}
|
||||||
dealerName={dealerName}
|
dealerName={dealerName}
|
||||||
activityName={activityName}
|
activityName={activityName}
|
||||||
|
defaultGstRate={request?.claimDetails?.defaultGstRate}
|
||||||
requestId={request?.id || request?.requestId}
|
requestId={request?.id || request?.requestId}
|
||||||
documentPolicy={documentPolicy}
|
documentPolicy={documentPolicy}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -109,7 +109,7 @@ export function ActivityInformationCard({
|
|||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-900 font-medium mt-1 flex items-center gap-2">
|
<p className="text-sm text-gray-900 font-medium mt-1 flex items-center gap-2">
|
||||||
<DollarSign className="w-4 h-4 text-green-600" />
|
<DollarSign className="w-4 h-4 text-green-600" />
|
||||||
{activityInfo.estimatedBudget
|
{activityInfo.estimatedBudget !== undefined && activityInfo.estimatedBudget !== null
|
||||||
? formatCurrency(activityInfo.estimatedBudget)
|
? formatCurrency(activityInfo.estimatedBudget)
|
||||||
: 'TBD'}
|
: 'TBD'}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { Upload, Plus, X, Calendar, FileText, Image, Receipt, CircleAlert, Check
|
|||||||
import { toast } from 'sonner';
|
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';
|
||||||
|
|
||||||
interface ExpenseItem {
|
interface ExpenseItem {
|
||||||
id: string;
|
id: string;
|
||||||
@ -43,6 +44,7 @@ interface ExpenseItem {
|
|||||||
cessRate: number;
|
cessRate: number;
|
||||||
cessAmt: number;
|
cessAmt: number;
|
||||||
totalAmt: number;
|
totalAmt: number;
|
||||||
|
isService: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DealerCompletionDocumentsModalProps {
|
interface DealerCompletionDocumentsModalProps {
|
||||||
@ -62,6 +64,7 @@ interface DealerCompletionDocumentsModalProps {
|
|||||||
dealerName?: string;
|
dealerName?: string;
|
||||||
activityName?: string;
|
activityName?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
|
defaultGstRate?: number;
|
||||||
documentPolicy: {
|
documentPolicy: {
|
||||||
maxFileSizeMB: number;
|
maxFileSizeMB: number;
|
||||||
allowedFileTypes: string[];
|
allowedFileTypes: string[];
|
||||||
@ -75,6 +78,7 @@ export function DealerCompletionDocumentsModal({
|
|||||||
dealerName = 'Jaipur Royal Enfield',
|
dealerName = 'Jaipur Royal Enfield',
|
||||||
activityName = 'Activity',
|
activityName = 'Activity',
|
||||||
requestId: _requestId,
|
requestId: _requestId,
|
||||||
|
defaultGstRate = 18,
|
||||||
documentPolicy,
|
documentPolicy,
|
||||||
}: DealerCompletionDocumentsModalProps) {
|
}: DealerCompletionDocumentsModalProps) {
|
||||||
const [activityCompletionDate, setActivityCompletionDate] = useState('');
|
const [activityCompletionDate, setActivityCompletionDate] = useState('');
|
||||||
@ -116,6 +120,33 @@ export function DealerCompletionDocumentsModal({
|
|||||||
};
|
};
|
||||||
}, [previewFile]);
|
}, [previewFile]);
|
||||||
|
|
||||||
|
// Initialize with one empty row if none exist
|
||||||
|
useEffect(() => {
|
||||||
|
if (expenseItems.length === 0) {
|
||||||
|
setExpenseItems([{
|
||||||
|
id: '1',
|
||||||
|
description: '',
|
||||||
|
amount: 0,
|
||||||
|
gstRate: defaultGstRate || 0,
|
||||||
|
gstAmt: 0,
|
||||||
|
quantity: 1,
|
||||||
|
hsnCode: '',
|
||||||
|
isService: false,
|
||||||
|
cgstRate: 0,
|
||||||
|
cgstAmt: 0,
|
||||||
|
sgstRate: 0,
|
||||||
|
sgstAmt: 0,
|
||||||
|
igstRate: 0,
|
||||||
|
igstAmt: 0,
|
||||||
|
utgstRate: 0,
|
||||||
|
utgstAmt: 0,
|
||||||
|
cessRate: 0,
|
||||||
|
cessAmt: 0,
|
||||||
|
totalAmt: 0
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}, [defaultGstRate]);
|
||||||
|
|
||||||
// Handle file preview
|
// Handle file preview
|
||||||
const handlePreviewFile = (file: File) => {
|
const handlePreviewFile = (file: File) => {
|
||||||
if (!canPreviewFile(file)) {
|
if (!canPreviewFile(file)) {
|
||||||
@ -189,7 +220,12 @@ export function DealerCompletionDocumentsModal({
|
|||||||
const hasPhotos = activityPhotos.length > 0;
|
const hasPhotos = activityPhotos.length > 0;
|
||||||
const hasDescription = completionDescription.trim().length > 0;
|
const hasDescription = completionDescription.trim().length > 0;
|
||||||
|
|
||||||
return hasCompletionDate && hasDocuments && hasPhotos && hasDescription;
|
const hasHSNSACErrors = expenseItems.some(item => {
|
||||||
|
const { isValid } = validateHSNSAC(item.hsnCode, item.isService);
|
||||||
|
return !isValid;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasCompletionDate && hasDocuments && hasPhotos && hasDescription && !hasHSNSACErrors;
|
||||||
}, [activityCompletionDate, completionDocuments, activityPhotos, completionDescription]);
|
}, [activityCompletionDate, completionDocuments, activityPhotos, completionDescription]);
|
||||||
|
|
||||||
// Get today's date in YYYY-MM-DD format for max date
|
// Get today's date in YYYY-MM-DD format for max date
|
||||||
@ -202,10 +238,11 @@ export function DealerCompletionDocumentsModal({
|
|||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
description: '',
|
description: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
gstRate: 0,
|
gstRate: defaultGstRate || 0,
|
||||||
gstAmt: 0,
|
gstAmt: 0,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
hsnCode: '',
|
hsnCode: '',
|
||||||
|
isService: false,
|
||||||
cgstRate: 0,
|
cgstRate: 0,
|
||||||
cgstAmt: 0,
|
cgstAmt: 0,
|
||||||
sgstRate: 0,
|
sgstRate: 0,
|
||||||
@ -227,11 +264,11 @@ export function DealerCompletionDocumentsModal({
|
|||||||
if (item.id === id) {
|
if (item.id === id) {
|
||||||
const updatedItem = { ...item, [field]: value };
|
const updatedItem = { ...item, [field]: value };
|
||||||
|
|
||||||
// Re-calculate GST if amount, rate or quantity changes
|
// Re-calculate GST if amount or rate changes
|
||||||
if (field === 'amount' || field === 'gstRate' || field === 'quantity') {
|
if (field === 'amount' || field === 'gstRate') {
|
||||||
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 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);
|
const gst = calculateGST(amount, rate, quantity);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -428,6 +465,21 @@ export function DealerCompletionDocumentsModal({
|
|||||||
(item) => item.description.trim() !== '' && item.amount > 0
|
(item) => item.description.trim() !== '' && item.amount > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Validation: Alert for 0% GST on taxable items
|
||||||
|
const hasZeroGstItems = validExpenses.some(item =>
|
||||||
|
item.description.trim() !== '' && item.amount > 0 && (item.gstRate === 0 || !item.gstRate)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasZeroGstItems) {
|
||||||
|
const confirmZeroGst = window.confirm(
|
||||||
|
"One or more expenses have 0% GST. Are you sure you want to proceed? \n\nNote: If these items are taxable, please provide a valid GST rate to ensure correct E-Invoice generation."
|
||||||
|
);
|
||||||
|
if (!confirmZeroGst) {
|
||||||
|
setSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
await onSubmit({
|
await onSubmit({
|
||||||
@ -463,10 +515,11 @@ export function DealerCompletionDocumentsModal({
|
|||||||
id: '1',
|
id: '1',
|
||||||
description: '',
|
description: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
gstRate: 0,
|
gstRate: defaultGstRate || 0,
|
||||||
gstAmt: 0,
|
gstAmt: 0,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
hsnCode: '',
|
hsnCode: '',
|
||||||
|
isService: false,
|
||||||
cgstRate: 0,
|
cgstRate: 0,
|
||||||
cgstAmt: 0,
|
cgstAmt: 0,
|
||||||
sgstRate: 0,
|
sgstRate: 0,
|
||||||
@ -597,20 +650,26 @@ export function DealerCompletionDocumentsModal({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleExpenseChange(item.id, 'hsnCode', e.target.value)
|
handleExpenseChange(item.id, 'hsnCode', e.target.value)
|
||||||
}
|
}
|
||||||
className="w-full bg-white text-sm"
|
className={`w-full bg-white text-sm ${!validateHSNSAC(item.hsnCode, item.isService).isValid ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
{!validateHSNSAC(item.hsnCode, item.isService).isValid && (
|
||||||
|
<span className="text-[9px] text-red-500 mt-1 block leading-tight">
|
||||||
|
{validateHSNSAC(item.hsnCode, item.isService).message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-16 sm:w-20 flex-shrink-0">
|
<div className="w-24 sm:w-28 flex-shrink-0">
|
||||||
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Qty</Label>
|
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Item Type</Label>
|
||||||
<Input
|
<select
|
||||||
type="number"
|
value={item.isService ? 'SAC' : 'HSN'}
|
||||||
min="1"
|
|
||||||
value={item.quantity || 1}
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleExpenseChange(item.id, 'quantity', parseInt(e.target.value) || 1)
|
handleExpenseChange(item.id, 'isService', e.target.value === 'SAC')
|
||||||
}
|
}
|
||||||
className="w-full bg-white text-sm"
|
className="flex h-9 w-full rounded-md border border-input bg-white px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
/>
|
>
|
||||||
|
<option value="HSN">HSN (Goods)</option>
|
||||||
|
<option value="SAC">SAC (Service)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-20 sm:w-24 flex-shrink-0">
|
<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>
|
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">GST %</Label>
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import { FilePreview } from '@/components/common/FilePreview/FilePreview';
|
|||||||
import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi';
|
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';
|
||||||
|
|
||||||
interface CostItem {
|
interface CostItem {
|
||||||
id: string;
|
id: string;
|
||||||
@ -46,6 +47,7 @@ interface CostItem {
|
|||||||
cessRate: number;
|
cessRate: number;
|
||||||
cessAmt: number;
|
cessAmt: number;
|
||||||
totalAmt: number;
|
totalAmt: number;
|
||||||
|
isService: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DealerProposalSubmissionModalProps {
|
interface DealerProposalSubmissionModalProps {
|
||||||
@ -62,6 +64,7 @@ interface DealerProposalSubmissionModalProps {
|
|||||||
activityName?: string;
|
activityName?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
previousProposalData?: any;
|
previousProposalData?: any;
|
||||||
|
defaultGstRate?: number;
|
||||||
documentPolicy: {
|
documentPolicy: {
|
||||||
maxFileSizeMB: number;
|
maxFileSizeMB: number;
|
||||||
allowedFileTypes: string[];
|
allowedFileTypes: string[];
|
||||||
@ -76,6 +79,7 @@ export function DealerProposalSubmissionModal({
|
|||||||
activityName = 'Activity',
|
activityName = 'Activity',
|
||||||
requestId: _requestId,
|
requestId: _requestId,
|
||||||
previousProposalData,
|
previousProposalData,
|
||||||
|
defaultGstRate = 18,
|
||||||
documentPolicy,
|
documentPolicy,
|
||||||
}: DealerProposalSubmissionModalProps) {
|
}: DealerProposalSubmissionModalProps) {
|
||||||
const [proposalDocument, setProposalDocument] = useState<File | null>(null);
|
const [proposalDocument, setProposalDocument] = useState<File | null>(null);
|
||||||
@ -84,10 +88,11 @@ export function DealerProposalSubmissionModal({
|
|||||||
id: '1',
|
id: '1',
|
||||||
description: '',
|
description: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
gstRate: 0,
|
gstRate: defaultGstRate || 0,
|
||||||
gstAmt: 0,
|
gstAmt: 0,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
hsnCode: '',
|
hsnCode: '',
|
||||||
|
isService: false,
|
||||||
cgstRate: 0,
|
cgstRate: 0,
|
||||||
cgstAmt: 0,
|
cgstAmt: 0,
|
||||||
sgstRate: 0,
|
sgstRate: 0,
|
||||||
@ -248,9 +253,14 @@ export function DealerProposalSubmissionModal({
|
|||||||
const hasTimeline = timelineMode === 'date'
|
const hasTimeline = timelineMode === 'date'
|
||||||
? expectedCompletionDate !== ''
|
? expectedCompletionDate !== ''
|
||||||
: numberOfDays !== '' && parseInt(numberOfDays) > 0;
|
: numberOfDays !== '' && parseInt(numberOfDays) > 0;
|
||||||
const hasComments = dealerComments.trim().length > 0;
|
const hasValidComments = dealerComments.trim().length > 0;
|
||||||
|
|
||||||
return hasProposalDoc && hasValidCostItems && hasTimeline && hasComments;
|
const hasHSNSACErrors = costItems.some(item => {
|
||||||
|
const { isValid } = validateHSNSAC(item.hsnCode, item.isService);
|
||||||
|
return !isValid;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasProposalDoc && hasValidCostItems && hasTimeline && hasValidComments && !hasHSNSACErrors;
|
||||||
}, [proposalDocument, costItems, timelineMode, expectedCompletionDate, numberOfDays, dealerComments]);
|
}, [proposalDocument, costItems, timelineMode, expectedCompletionDate, numberOfDays, dealerComments]);
|
||||||
|
|
||||||
const handleProposalDocChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleProposalDocChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -316,10 +326,11 @@ export function DealerProposalSubmissionModal({
|
|||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
description: '',
|
description: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
gstRate: 0,
|
gstRate: defaultGstRate || 0,
|
||||||
gstAmt: 0,
|
gstAmt: 0,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
hsnCode: '',
|
hsnCode: '',
|
||||||
|
isService: false,
|
||||||
cgstRate: 0,
|
cgstRate: 0,
|
||||||
cgstAmt: 0,
|
cgstAmt: 0,
|
||||||
sgstRate: 0,
|
sgstRate: 0,
|
||||||
@ -347,11 +358,11 @@ export function DealerProposalSubmissionModal({
|
|||||||
if (item.id === id) {
|
if (item.id === id) {
|
||||||
const updatedItem = { ...item, [field]: value };
|
const updatedItem = { ...item, [field]: value };
|
||||||
|
|
||||||
// Re-calculate GST if amount, rate or quantity changes
|
// Re-calculate GST if amount or rate changes
|
||||||
if (field === 'amount' || field === 'gstRate' || field === 'quantity') {
|
if (field === 'amount' || field === 'gstRate') {
|
||||||
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 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);
|
const gst = calculateGST(amount, rate, quantity);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -392,6 +403,21 @@ export function DealerProposalSubmissionModal({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
// Validation: Alert for 0% GST on taxable items
|
||||||
|
const hasZeroGstItems = costItems.some(item =>
|
||||||
|
item.description.trim() !== '' && item.amount > 0 && (item.gstRate === 0 || !item.gstRate)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasZeroGstItems) {
|
||||||
|
const confirmZeroGst = window.confirm(
|
||||||
|
"One or more items have 0% GST. Are you sure you want to proceed? \n\nNote: If these items are taxable, please provide a valid GST rate to ensure correct E-Invoice generation."
|
||||||
|
);
|
||||||
|
if (!confirmZeroGst) {
|
||||||
|
setSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await onSubmit({
|
await onSubmit({
|
||||||
proposalDocument,
|
proposalDocument,
|
||||||
costBreakup: costItems.filter(item => item.description.trim() !== '' && item.amount > 0),
|
costBreakup: costItems.filter(item => item.description.trim() !== '' && item.amount > 0),
|
||||||
@ -424,6 +450,7 @@ export function DealerProposalSubmissionModal({
|
|||||||
gstAmt: 0,
|
gstAmt: 0,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
hsnCode: '',
|
hsnCode: '',
|
||||||
|
isService: false,
|
||||||
cgstRate: 0,
|
cgstRate: 0,
|
||||||
cgstAmt: 0,
|
cgstAmt: 0,
|
||||||
sgstRate: 0,
|
sgstRate: 0,
|
||||||
@ -906,20 +933,26 @@ export function DealerProposalSubmissionModal({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleCostItemChange(item.id, 'hsnCode', e.target.value)
|
handleCostItemChange(item.id, 'hsnCode', e.target.value)
|
||||||
}
|
}
|
||||||
className="w-full bg-white"
|
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>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Qty</Label>
|
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Item Type</Label>
|
||||||
<Input
|
<select
|
||||||
type="number"
|
value={item.isService ? 'SAC' : 'HSN'}
|
||||||
min="1"
|
|
||||||
value={item.quantity || 1}
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleCostItemChange(item.id, 'quantity', parseInt(e.target.value) || 1)
|
handleCostItemChange(item.id, 'isService', e.target.value === 'SAC')
|
||||||
}
|
}
|
||||||
className="w-full bg-white"
|
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>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<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">GST %</Label>
|
||||||
|
|||||||
@ -78,7 +78,22 @@ export async function submitProposal(
|
|||||||
requestId: string,
|
requestId: string,
|
||||||
proposalData: {
|
proposalData: {
|
||||||
proposalDocument?: File;
|
proposalDocument?: File;
|
||||||
costBreakup?: Array<{ description: string; amount: number }>;
|
costBreakup?: Array<{
|
||||||
|
description: string;
|
||||||
|
amount: number;
|
||||||
|
hsnCode?: string;
|
||||||
|
isService?: boolean;
|
||||||
|
quantity?: number;
|
||||||
|
gstRate?: number;
|
||||||
|
gstAmt?: number;
|
||||||
|
cgstRate?: number;
|
||||||
|
cgstAmt?: number;
|
||||||
|
sgstRate?: number;
|
||||||
|
sgstAmt?: number;
|
||||||
|
igstRate?: number;
|
||||||
|
igstAmt?: number;
|
||||||
|
totalAmt?: number;
|
||||||
|
}>;
|
||||||
totalEstimatedBudget?: number;
|
totalEstimatedBudget?: number;
|
||||||
timelineMode?: 'date' | 'days';
|
timelineMode?: 'date' | 'days';
|
||||||
expectedCompletionDate?: string;
|
expectedCompletionDate?: string;
|
||||||
@ -139,7 +154,16 @@ export async function submitCompletion(
|
|||||||
completionData: {
|
completionData: {
|
||||||
activityCompletionDate: string; // ISO date string
|
activityCompletionDate: string; // ISO date string
|
||||||
numberOfParticipants?: number;
|
numberOfParticipants?: number;
|
||||||
closedExpenses?: Array<{ description: string; amount: number }>;
|
closedExpenses?: Array<{
|
||||||
|
description: string;
|
||||||
|
amount: number;
|
||||||
|
hsnCode?: string;
|
||||||
|
isService?: boolean;
|
||||||
|
quantity?: number;
|
||||||
|
gstRate?: number;
|
||||||
|
gstAmt?: number;
|
||||||
|
totalAmt?: number;
|
||||||
|
}>;
|
||||||
totalClosedExpenses?: number;
|
totalClosedExpenses?: number;
|
||||||
completionDocuments?: File[];
|
completionDocuments?: File[];
|
||||||
activityPhotos?: File[];
|
activityPhotos?: File[];
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export interface ClaimManagementRequest {
|
|||||||
};
|
};
|
||||||
estimatedBudget?: number;
|
estimatedBudget?: number;
|
||||||
closedExpenses?: number;
|
closedExpenses?: number;
|
||||||
|
defaultGstRate?: number;
|
||||||
closedExpensesBreakdown?: Array<{
|
closedExpensesBreakdown?: Array<{
|
||||||
description: string;
|
description: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
@ -153,19 +154,19 @@ export function mapToClaimManagementRequest(
|
|||||||
// Activity fields mapped
|
// Activity fields mapped
|
||||||
|
|
||||||
// Get budget values from budgetTracking table (new source of truth)
|
// Get budget values from budgetTracking table (new source of truth)
|
||||||
const estimatedBudget = budgetTracking.proposalEstimatedBudget ||
|
const estimatedBudget = budgetTracking.proposalEstimatedBudget ??
|
||||||
budgetTracking.proposal_estimated_budget ||
|
budgetTracking.proposal_estimated_budget ??
|
||||||
budgetTracking.initialEstimatedBudget ||
|
budgetTracking.initialEstimatedBudget ??
|
||||||
budgetTracking.initial_estimated_budget ||
|
budgetTracking.initial_estimated_budget ??
|
||||||
claimDetails.estimatedBudget ||
|
claimDetails.estimatedBudget ??
|
||||||
claimDetails.estimated_budget;
|
claimDetails.estimated_budget;
|
||||||
|
|
||||||
// Get closed expenses - check multiple sources with proper number conversion
|
// Get closed expenses - check multiple sources with proper number conversion
|
||||||
const closedExpensesRaw = budgetTracking?.closedExpenses ||
|
const closedExpensesRaw = budgetTracking?.closedExpenses ??
|
||||||
budgetTracking?.closed_expenses ||
|
budgetTracking?.closed_expenses ??
|
||||||
completionDetails?.totalClosedExpenses ||
|
completionDetails?.totalClosedExpenses ??
|
||||||
completionDetails?.total_closed_expenses ||
|
completionDetails?.total_closed_expenses ??
|
||||||
claimDetails?.closedExpenses ||
|
claimDetails?.closedExpenses ??
|
||||||
claimDetails?.closed_expenses;
|
claimDetails?.closed_expenses;
|
||||||
// Convert to number and handle 0 as valid value
|
// Convert to number and handle 0 as valid value
|
||||||
const closedExpenses = closedExpensesRaw !== null && closedExpensesRaw !== undefined
|
const closedExpenses = closedExpensesRaw !== null && closedExpensesRaw !== undefined
|
||||||
@ -192,6 +193,7 @@ export function mapToClaimManagementRequest(
|
|||||||
const activityInfo = {
|
const activityInfo = {
|
||||||
activityName,
|
activityName,
|
||||||
activityType,
|
activityType,
|
||||||
|
defaultGstRate: claimDetails.defaultGstRate || 18,
|
||||||
requestedDate: claimDetails.activityDate || claimDetails.activity_date || apiRequest.createdAt, // Use activityDate as requestedDate, fallback to createdAt
|
requestedDate: claimDetails.activityDate || claimDetails.activity_date || apiRequest.createdAt, // Use activityDate as requestedDate, fallback to createdAt
|
||||||
location,
|
location,
|
||||||
period: (periodStartDate && periodEndDate) ? {
|
period: (periodStartDate && periodEndDate) ? {
|
||||||
|
|||||||
50
src/utils/validationUtils.ts
Normal file
50
src/utils/validationUtils.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Validation utilities for HSN and SAC codes
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ValidationResult {
|
||||||
|
isValid: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates HSN or SAC code based on GST rules
|
||||||
|
* @param code The HSN/SAC code string
|
||||||
|
* @param isService Boolean indicating if it's a Service (SAC) or Goods (HSN)
|
||||||
|
* @returns ValidationResult object
|
||||||
|
*/
|
||||||
|
export const validateHSNSAC = (code: string, isService: boolean): ValidationResult => {
|
||||||
|
if (!code) return { isValid: true, message: '' };
|
||||||
|
|
||||||
|
const cleanCode = code.trim();
|
||||||
|
|
||||||
|
// Basic check for digits only
|
||||||
|
if (!/^\d+$/.test(cleanCode)) {
|
||||||
|
return { isValid: false, message: 'Code must contain only digits' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isService) {
|
||||||
|
// SAC (Services Accounting Code)
|
||||||
|
// Must start with 99 and typically has 6 digits
|
||||||
|
if (!cleanCode.startsWith('99')) {
|
||||||
|
return { isValid: false, message: 'SAC (Service) code must start with 99' };
|
||||||
|
}
|
||||||
|
if (cleanCode.length !== 6) {
|
||||||
|
return { isValid: false, message: 'SAC code must be exactly 6 digits' };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// HSN (Harmonized System of Nomenclature) for Goods
|
||||||
|
// Usually 4, 6, or 8 digits in India
|
||||||
|
const validHSNLengths = [4, 6, 8];
|
||||||
|
if (!validHSNLengths.includes(cleanCode.length)) {
|
||||||
|
return { isValid: false, message: 'HSN code must be 4, 6, or 8 digits' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// HSN codes for goods should generally not start with 99 (that's reserved for SAC)
|
||||||
|
if (cleanCode.startsWith('99')) {
|
||||||
|
return { isValid: false, message: 'HSN code should not start with 99 (use SAC type for services)' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isValid: true, message: '' };
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user