added hsn validation and removed quatity part from the cost related items

This commit is contained in:
laxmanhalaki 2026-02-17 20:37:45 +05:30
parent 5e91b85854
commit 5dce660f05
7 changed files with 237 additions and 65 deletions

View File

@ -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}
/>

View File

@ -109,7 +109,7 @@ export function ActivityInformationCard({
</label>
<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" />
{activityInfo.estimatedBudget
{activityInfo.estimatedBudget !== undefined && activityInfo.estimatedBudget !== null
? formatCurrency(activityInfo.estimatedBudget)
: 'TBD'}
</p>

View File

@ -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 && (
<span className="text-[9px] text-red-500 mt-1 block leading-tight">
{validateHSNSAC(item.hsnCode, item.isService).message}
</span>
)}
</div>
<div className="w-16 sm:w-20 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Qty</Label>
<Input
type="number"
min="1"
value={item.quantity || 1}
<div className="w-24 sm:w-28 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Item Type</Label>
<select
value={item.isService ? 'SAC' : 'HSN'}
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 className="w-20 sm:w-24 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">GST %</Label>

View File

@ -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<File | null>(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<HTMLInputElement>) => {
@ -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 && (
<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">Qty</Label>
<Input
type="number"
min="1"
value={item.quantity || 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, '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 className="flex-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">GST %</Label>

View File

@ -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;
@ -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[];

View File

@ -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) ? {

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