From 5dce660f05cb3d2d021d82b1d8fc60c77f9c644b Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Tue, 17 Feb 2026 20:37:45 +0530 Subject: [PATCH] added hsn validation and removed quatity part from the cost related items --- .../components/request-detail/WorkflowTab.tsx | 8 +- .../claim-cards/ActivityInformationCard.tsx | 2 +- .../modals/DealerCompletionDocumentsModal.tsx | 91 +++++++++++++++---- .../modals/DealerProposalSubmissionModal.tsx | 65 +++++++++---- src/services/dealerClaimApi.ts | 64 +++++++++---- src/utils/claimDataMapper.ts | 22 +++-- src/utils/validationUtils.ts | 50 ++++++++++ 7 files changed, 237 insertions(+), 65 deletions(-) create mode 100644 src/utils/validationUtils.ts diff --git a/src/dealer-claim/components/request-detail/WorkflowTab.tsx b/src/dealer-claim/components/request-detail/WorkflowTab.tsx index e791b01..74223f9 100644 --- a/src/dealer-claim/components/request-detail/WorkflowTab.tsx +++ b/src/dealer-claim/components/request-detail/WorkflowTab.tsx @@ -906,7 +906,8 @@ export function DealerClaimWorkflowTab({ cessAmt: item.cessAmt, totalAmt: item.totalAmt, quantity: item.quantity, - hsnCode: item.hsnCode + hsnCode: item.hsnCode, + isService: item.isService })), totalEstimatedBudget: totalBudget, expectedCompletionDate: data.expectedCompletionDate, @@ -1179,7 +1180,8 @@ export function DealerClaimWorkflowTab({ cessAmt: item.cessAmt, totalAmt: item.totalAmt, quantity: item.quantity, - hsnCode: item.hsnCode + hsnCode: item.hsnCode, + isService: item.isService })); // Submit completion documents using dealer claim API @@ -2373,6 +2375,7 @@ export function DealerClaimWorkflowTab({ onSubmit={handleProposalSubmit} dealerName={dealerName} activityName={activityName} + defaultGstRate={request?.claimDetails?.defaultGstRate} requestId={request?.id || request?.requestId} previousProposalData={versionHistory?.find(v => v.snapshotType === 'PROPOSAL')?.snapshotData} documentPolicy={documentPolicy} @@ -2421,6 +2424,7 @@ export function DealerClaimWorkflowTab({ onSubmit={handleCompletionSubmit} dealerName={dealerName} activityName={activityName} + defaultGstRate={request?.claimDetails?.defaultGstRate} requestId={request?.id || request?.requestId} documentPolicy={documentPolicy} /> diff --git a/src/dealer-claim/components/request-detail/claim-cards/ActivityInformationCard.tsx b/src/dealer-claim/components/request-detail/claim-cards/ActivityInformationCard.tsx index ec6e399..a3a40c1 100644 --- a/src/dealer-claim/components/request-detail/claim-cards/ActivityInformationCard.tsx +++ b/src/dealer-claim/components/request-detail/claim-cards/ActivityInformationCard.tsx @@ -109,7 +109,7 @@ export function ActivityInformationCard({

- {activityInfo.estimatedBudget + {activityInfo.estimatedBudget !== undefined && activityInfo.estimatedBudget !== null ? formatCurrency(activityInfo.estimatedBudget) : 'TBD'}

diff --git a/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx b/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx index dbcc1ca..0d24b34 100644 --- a/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx +++ b/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx @@ -23,6 +23,7 @@ import { Upload, Plus, X, Calendar, FileText, Image, Receipt, CircleAlert, Check import { toast } from 'sonner'; import '@/components/common/FilePreview/FilePreview.css'; import './DealerCompletionDocumentsModal.css'; +import { validateHSNSAC } from '@/utils/validationUtils'; interface ExpenseItem { id: string; @@ -43,6 +44,7 @@ interface ExpenseItem { cessRate: number; cessAmt: number; totalAmt: number; + isService: boolean; } interface DealerCompletionDocumentsModalProps { @@ -62,6 +64,7 @@ interface DealerCompletionDocumentsModalProps { dealerName?: string; activityName?: string; requestId?: string; + defaultGstRate?: number; documentPolicy: { maxFileSizeMB: number; allowedFileTypes: string[]; @@ -75,6 +78,7 @@ export function DealerCompletionDocumentsModal({ dealerName = 'Jaipur Royal Enfield', activityName = 'Activity', requestId: _requestId, + defaultGstRate = 18, documentPolicy, }: DealerCompletionDocumentsModalProps) { const [activityCompletionDate, setActivityCompletionDate] = useState(''); @@ -116,6 +120,33 @@ export function DealerCompletionDocumentsModal({ }; }, [previewFile]); + // Initialize with one empty row if none exist + useEffect(() => { + if (expenseItems.length === 0) { + setExpenseItems([{ + id: '1', + description: '', + amount: 0, + gstRate: defaultGstRate || 0, + gstAmt: 0, + quantity: 1, + hsnCode: '', + isService: false, + cgstRate: 0, + cgstAmt: 0, + sgstRate: 0, + sgstAmt: 0, + igstRate: 0, + igstAmt: 0, + utgstRate: 0, + utgstAmt: 0, + cessRate: 0, + cessAmt: 0, + totalAmt: 0 + }]); + } + }, [defaultGstRate]); + // Handle file preview const handlePreviewFile = (file: File) => { if (!canPreviewFile(file)) { @@ -189,7 +220,12 @@ export function DealerCompletionDocumentsModal({ const hasPhotos = activityPhotos.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]); // Get today's date in YYYY-MM-DD format for max date @@ -202,10 +238,11 @@ export function DealerCompletionDocumentsModal({ id: Date.now().toString(), description: '', amount: 0, - gstRate: 0, + gstRate: defaultGstRate || 0, gstAmt: 0, quantity: 1, hsnCode: '', + isService: false, cgstRate: 0, cgstAmt: 0, sgstRate: 0, @@ -227,11 +264,11 @@ export function DealerCompletionDocumentsModal({ if (item.id === id) { const updatedItem = { ...item, [field]: value }; - // Re-calculate GST if amount, rate or quantity changes - if (field === 'amount' || field === 'gstRate' || field === 'quantity') { + // Re-calculate GST if amount or rate changes + if (field === 'amount' || field === 'gstRate') { 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); return { @@ -428,6 +465,21 @@ export function DealerCompletionDocumentsModal({ (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 { setSubmitting(true); await onSubmit({ @@ -463,10 +515,11 @@ export function DealerCompletionDocumentsModal({ id: '1', description: '', amount: 0, - gstRate: 0, + gstRate: defaultGstRate || 0, gstAmt: 0, quantity: 1, hsnCode: '', + isService: false, cgstRate: 0, cgstAmt: 0, sgstRate: 0, @@ -597,20 +650,26 @@ export function DealerCompletionDocumentsModal({ onChange={(e) => 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 && ( + + {validateHSNSAC(item.hsnCode, item.isService).message} + + )} -
- - + +
diff --git a/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx b/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx index eed4b35..27f610b 100644 --- a/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx +++ b/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx @@ -26,6 +26,7 @@ import { FilePreview } from '@/components/common/FilePreview/FilePreview'; import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi'; import '@/components/common/FilePreview/FilePreview.css'; import './DealerProposalModal.css'; +import { validateHSNSAC } from '@/utils/validationUtils'; interface CostItem { id: string; @@ -46,6 +47,7 @@ interface CostItem { cessRate: number; cessAmt: number; totalAmt: number; + isService: boolean; } interface DealerProposalSubmissionModalProps { @@ -62,6 +64,7 @@ interface DealerProposalSubmissionModalProps { activityName?: string; requestId?: string; previousProposalData?: any; + defaultGstRate?: number; documentPolicy: { maxFileSizeMB: number; allowedFileTypes: string[]; @@ -76,6 +79,7 @@ export function DealerProposalSubmissionModal({ activityName = 'Activity', requestId: _requestId, previousProposalData, + defaultGstRate = 18, documentPolicy, }: DealerProposalSubmissionModalProps) { const [proposalDocument, setProposalDocument] = useState(null); @@ -84,10 +88,11 @@ export function DealerProposalSubmissionModal({ id: '1', description: '', amount: 0, - gstRate: 0, + gstRate: defaultGstRate || 0, gstAmt: 0, quantity: 1, hsnCode: '', + isService: false, cgstRate: 0, cgstAmt: 0, sgstRate: 0, @@ -248,9 +253,14 @@ export function DealerProposalSubmissionModal({ const hasTimeline = timelineMode === 'date' ? expectedCompletionDate !== '' : 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]); const handleProposalDocChange = (e: React.ChangeEvent) => { @@ -316,10 +326,11 @@ export function DealerProposalSubmissionModal({ id: Date.now().toString(), description: '', amount: 0, - gstRate: 0, + gstRate: defaultGstRate || 0, gstAmt: 0, quantity: 1, hsnCode: '', + isService: false, cgstRate: 0, cgstAmt: 0, sgstRate: 0, @@ -347,11 +358,11 @@ export function DealerProposalSubmissionModal({ if (item.id === id) { const updatedItem = { ...item, [field]: value }; - // Re-calculate GST if amount, rate or quantity changes - if (field === 'amount' || field === 'gstRate' || field === 'quantity') { + // Re-calculate GST if amount or rate changes + if (field === 'amount' || field === 'gstRate') { 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); return { @@ -392,6 +403,21 @@ export function DealerProposalSubmissionModal({ try { 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({ proposalDocument, costBreakup: costItems.filter(item => item.description.trim() !== '' && item.amount > 0), @@ -424,6 +450,7 @@ export function DealerProposalSubmissionModal({ gstAmt: 0, quantity: 1, hsnCode: '', + isService: false, cgstRate: 0, cgstAmt: 0, sgstRate: 0, @@ -906,20 +933,26 @@ export function DealerProposalSubmissionModal({ onChange={(e) => 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 && ( + + {validateHSNSAC(item.hsnCode, item.isService).message} + + )}
- - Item Type +
diff --git a/src/services/dealerClaimApi.ts b/src/services/dealerClaimApi.ts index 16ec17b..1a001b2 100644 --- a/src/services/dealerClaimApi.ts +++ b/src/services/dealerClaimApi.ts @@ -78,7 +78,22 @@ export async function submitProposal( requestId: string, proposalData: { 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; timelineMode?: 'date' | 'days'; expectedCompletionDate?: string; @@ -88,31 +103,31 @@ export async function submitProposal( ): Promise { try { const formData = new FormData(); - + if (proposalData.proposalDocument) { formData.append('proposalDocument', proposalData.proposalDocument); } - + if (proposalData.costBreakup) { formData.append('costBreakup', JSON.stringify(proposalData.costBreakup)); } - + if (proposalData.totalEstimatedBudget !== undefined) { formData.append('totalEstimatedBudget', proposalData.totalEstimatedBudget.toString()); } - + if (proposalData.timelineMode) { formData.append('timelineMode', proposalData.timelineMode); } - + if (proposalData.expectedCompletionDate) { formData.append('expectedCompletionDate', proposalData.expectedCompletionDate); } - + if (proposalData.expectedCompletionDays !== undefined) { formData.append('expectedCompletionDays', proposalData.expectedCompletionDays.toString()); } - + if (proposalData.dealerComments) { formData.append('dealerComments', proposalData.dealerComments); } @@ -122,7 +137,7 @@ export async function submitProposal( 'Content-Type': 'multipart/form-data', }, }); - + return response.data?.data || response.data; } catch (error: any) { console.error('[DealerClaimAPI] Error submitting proposal:', error); @@ -139,7 +154,16 @@ export async function submitCompletion( completionData: { activityCompletionDate: string; // ISO date string 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; completionDocuments?: File[]; activityPhotos?: File[]; @@ -148,31 +172,31 @@ export async function submitCompletion( ): Promise { try { const formData = new FormData(); - + formData.append('activityCompletionDate', completionData.activityCompletionDate); - + if (completionData.numberOfParticipants !== undefined) { formData.append('numberOfParticipants', completionData.numberOfParticipants.toString()); } - + if (completionData.closedExpenses) { formData.append('closedExpenses', JSON.stringify(completionData.closedExpenses)); } - + if (completionData.totalClosedExpenses !== undefined) { formData.append('totalClosedExpenses', completionData.totalClosedExpenses.toString()); } - + if (completionData.completionDescription) { formData.append('completionDescription', completionData.completionDescription); } - + if (completionData.completionDocuments) { completionData.completionDocuments.forEach((file) => { formData.append('completionDocuments', file); }); } - + if (completionData.activityPhotos) { completionData.activityPhotos.forEach((file) => { formData.append('activityPhotos', file); @@ -184,7 +208,7 @@ export async function submitCompletion( 'Content-Type': 'multipart/form-data', }, }); - + return response.data?.data || response.data; } catch (error: any) { console.error('[DealerClaimAPI] Error submitting completion:', error); @@ -240,7 +264,7 @@ export async function updateIODetails( ioNumber: ioData.ioNumber, ioRemark: ioData.ioRemark || '', }; - + // Only include balance fields if explicitly provided if (ioData.ioAvailableBalance !== undefined) { payload.availableBalance = ioData.ioAvailableBalance; @@ -251,7 +275,7 @@ export async function updateIODetails( if (ioData.ioRemainingBalance !== undefined) { payload.remainingBalance = ioData.ioRemainingBalance; } - + const response = await apiClient.put(`/dealer-claims/${requestId}/io`, payload); return response.data?.data || response.data; } catch (error) { diff --git a/src/utils/claimDataMapper.ts b/src/utils/claimDataMapper.ts index 4faa5ee..715eeec 100644 --- a/src/utils/claimDataMapper.ts +++ b/src/utils/claimDataMapper.ts @@ -26,6 +26,7 @@ export interface ClaimManagementRequest { }; estimatedBudget?: number; closedExpenses?: number; + defaultGstRate?: number; closedExpensesBreakdown?: Array<{ description: string; amount: number; @@ -153,19 +154,19 @@ export function mapToClaimManagementRequest( // Activity fields mapped // Get budget values from budgetTracking table (new source of truth) - const estimatedBudget = budgetTracking.proposalEstimatedBudget || - budgetTracking.proposal_estimated_budget || - budgetTracking.initialEstimatedBudget || - budgetTracking.initial_estimated_budget || - claimDetails.estimatedBudget || + const estimatedBudget = budgetTracking.proposalEstimatedBudget ?? + budgetTracking.proposal_estimated_budget ?? + budgetTracking.initialEstimatedBudget ?? + budgetTracking.initial_estimated_budget ?? + claimDetails.estimatedBudget ?? claimDetails.estimated_budget; // Get closed expenses - check multiple sources with proper number conversion - const closedExpensesRaw = budgetTracking?.closedExpenses || - budgetTracking?.closed_expenses || - completionDetails?.totalClosedExpenses || - completionDetails?.total_closed_expenses || - claimDetails?.closedExpenses || + const closedExpensesRaw = budgetTracking?.closedExpenses ?? + budgetTracking?.closed_expenses ?? + completionDetails?.totalClosedExpenses ?? + completionDetails?.total_closed_expenses ?? + claimDetails?.closedExpenses ?? claimDetails?.closed_expenses; // Convert to number and handle 0 as valid value const closedExpenses = closedExpensesRaw !== null && closedExpensesRaw !== undefined @@ -192,6 +193,7 @@ export function mapToClaimManagementRequest( const activityInfo = { activityName, activityType, + defaultGstRate: claimDetails.defaultGstRate || 18, requestedDate: claimDetails.activityDate || claimDetails.activity_date || apiRequest.createdAt, // Use activityDate as requestedDate, fallback to createdAt location, period: (periodStartDate && periodEndDate) ? { diff --git a/src/utils/validationUtils.ts b/src/utils/validationUtils.ts new file mode 100644 index 0000000..38b6a80 --- /dev/null +++ b/src/utils/validationUtils.ts @@ -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: '' }; +};