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: '' };
+};