diff --git a/src/dealer-claim/components/request-detail/IOTab.tsx b/src/dealer-claim/components/request-detail/IOTab.tsx
index f5b5562..92e734b 100644
--- a/src/dealer-claim/components/request-detail/IOTab.tsx
+++ b/src/dealer-claim/components/request-detail/IOTab.tsx
@@ -5,7 +5,7 @@
* Located in: src/dealer-claim/components/request-detail/
*/
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useMemo } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -30,7 +30,7 @@ interface IOBlockedDetails {
blockedDate: string;
blockedBy: string; // User who blocked
sapDocumentNumber: string;
- status: 'blocked' | 'released' | 'failed';
+ status: 'blocked' | 'released' | 'failed' | 'pending';
}
export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
@@ -38,18 +38,39 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
const requestId = apiRequest?.requestId || request?.requestId;
// Get organizer user object from association (organizer) or fallback to organizedBy UUID
-
- // Get estimated budget from proposal details or DealerClaimDetails
const proposalDetails = apiRequest?.proposalDetails || {};
const claimDetails = apiRequest?.claimDetails || apiRequest || {};
- // Use totalProposedTaxableAmount if available (for Scenario 2), fallback to estimated budget
- const estimatedBudget = Number(claimDetails?.totalProposedTaxableAmount || proposalDetails?.totalEstimatedBudget || proposalDetails?.total_estimated_budget || 0);
+ // Calculate total base amount (needed for budget verification as requested)
+ // This is the taxable amount excluding GST
+ const totalBaseAmount = useMemo(() => {
+ const costBreakupRaw = proposalDetails?.costBreakup || claimDetails?.costBreakup || [];
+ const costBreakup = Array.isArray(costBreakupRaw)
+ ? costBreakupRaw
+ : (typeof costBreakupRaw === 'string'
+ ? JSON.parse(costBreakupRaw)
+ : []);
+
+ if (!Array.isArray(costBreakup) || costBreakup.length === 0) {
+ return Number(claimDetails?.totalProposedTaxableAmount || proposalDetails?.totalEstimatedBudget || proposalDetails?.total_estimated_budget || 0);
+ }
+
+ return costBreakup.reduce((sum: number, item: any) => {
+ const amount = typeof item === 'object' ? (item.amount || 0) : 0;
+ const quantity = typeof item === 'object' ? (item.quantity || 1) : 1;
+ return sum + (Number(amount) * Number(quantity));
+ }, 0);
+ }, [proposalDetails?.costBreakup, claimDetails?.costBreakup, claimDetails?.totalProposedTaxableAmount, proposalDetails?.totalEstimatedBudget]);
+
+ // Use base amount as the target budget for blocking
+ const estimatedBudget = totalBaseAmount;
// Budget status for signaling (Scenario 2)
- const budgetTracking = apiRequest?.budgetTracking || apiRequest?.budget_tracking || {};
+ // Use apiRequest as the primary source of truth, fall back to request
+ const budgetTracking = apiRequest?.budgetTracking || request?.budgetTracking || {};
const budgetStatus = budgetTracking?.budgetStatus || budgetTracking?.budget_status || '';
- const isAdditionalBlockingNeeded = budgetStatus === 'PROPOSED' && (apiRequest?.internalOrders?.length > 0 || apiRequest?.internal_orders?.length > 0);
+ const internalOrdersList = apiRequest?.internalOrders || apiRequest?.internal_orders || request?.internalOrders || [];
+ const isAdditionalBlockingNeeded = budgetStatus === 'PROPOSED' && internalOrdersList.length > 0;
const [ioNumber, setIoNumber] = useState('');
const [fetchingAmount, setFetchingAmount] = useState(false);
@@ -60,9 +81,8 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
// Load existing IO blocks
useEffect(() => {
- const ios = apiRequest?.internalOrders || apiRequest?.internal_orders || [];
- if (ios.length > 0) {
- const formattedIOs = ios.map((io: any) => {
+ if (internalOrdersList.length > 0) {
+ const formattedIOs = internalOrdersList.map((io: any) => {
const org = io.organizer || null;
const blockedByName = org?.displayName ||
org?.display_name ||
@@ -79,7 +99,8 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
blockedBy: blockedByName,
sapDocumentNumber: io.sapDocumentNumber || io.sap_document_number || '',
status: (io.status === 'BLOCKED' ? 'blocked' :
- io.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed',
+ io.status === 'RELEASED' ? 'released' :
+ io.status === 'PENDING' ? 'pending' : 'blocked') as any,
};
});
setBlockedIOs(formattedIOs);
@@ -89,7 +110,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
setIoNumber(formattedIOs[formattedIOs.length - 1].ioNumber);
}
}
- }, [apiRequest, isAdditionalBlockingNeeded]);
+ }, [apiRequest, request, isAdditionalBlockingNeeded, internalOrdersList]);
/**
* Fetch available budget from SAP
@@ -114,12 +135,22 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
if (ioData.isValid && ioData.availableBalance > 0) {
setFetchedAmount(ioData.availableBalance);
- // Pre-fill amount to block with estimated budget (if available), otherwise use available balance
- if (estimatedBudget > 0) {
- setAmountToBlock(String(estimatedBudget));
+
+ // Calculate total already blocked amount
+ const totalAlreadyBlocked = blockedIOs.reduce((sum, io) => sum + io.blockedAmount, 0);
+
+ // Calculate remaining budget to block
+ const remainingToBlock = Math.max(0, estimatedBudget - totalAlreadyBlocked);
+
+ // Pre-fill amount to block with remaining budget, otherwise use available balance
+ if (remainingToBlock > 0) {
+ setAmountToBlock(String(remainingToBlock.toFixed(2)));
+ } else if (estimatedBudget > 0 && totalAlreadyBlocked === 0) {
+ setAmountToBlock(String(estimatedBudget.toFixed(2)));
} else {
- setAmountToBlock(String(ioData.availableBalance));
+ setAmountToBlock(String(ioData.availableBalance.toFixed(2)));
}
+
toast.success(`IO fetched from SAP. Available balance: ₹${ioData.availableBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
} else {
toast.error('Invalid IO number or no available balance found');
@@ -417,8 +448,13 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
IO: {io.ioNumber}
-
- {io.status === 'blocked' ? 'Blocked' : 'Released'}
+
+ {io.status === 'blocked' ? 'Blocked' :
+ io.status === 'pending' ? 'Provisioned' : 'Released'}
diff --git a/src/dealer-claim/components/request-detail/WorkflowTab.tsx b/src/dealer-claim/components/request-detail/WorkflowTab.tsx
index a1beecb..ad71461 100644
--- a/src/dealer-claim/components/request-detail/WorkflowTab.tsx
+++ b/src/dealer-claim/components/request-detail/WorkflowTab.tsx
@@ -49,7 +49,7 @@ interface WorkflowStep {
approver: string;
description: string;
tatHours: number;
- status: 'pending' | 'approved' | 'waiting' | 'rejected' | 'in_progress';
+ status: 'pending' | 'approved' | 'waiting' | 'rejected' | 'in_progress' | 'skipped';
comment?: string;
approvedAt?: string;
elapsedHours?: number;
@@ -109,6 +109,8 @@ const getStepIcon = (status: string) => {
switch (status) {
case 'approved':
return
;
+ case 'skipped':
+ return
;
case 'pending':
return
;
case 'rejected':
@@ -125,6 +127,8 @@ const getStepBadgeVariant = (status: string) => {
switch (status) {
case 'approved':
return 'bg-green-100 text-green-800 border-green-200';
+ case 'skipped':
+ return 'bg-green-50 text-green-700 border-green-200';
case 'pending':
return 'bg-purple-100 text-purple-800 border-purple-200';
case 'rejected':
@@ -141,7 +145,7 @@ const getStepCardStyle = (status: string, isActive: boolean) => {
if (isActive && (status === 'pending' || status === 'in_progress')) {
return 'border-purple-500 bg-purple-50 shadow-md';
}
- if (status === 'approved') {
+ if (status === 'approved' || status === 'skipped') {
return 'border-green-500 bg-green-50';
}
if (status === 'rejected') {
@@ -157,6 +161,8 @@ const getStepIconBg = (status: string) => {
switch (status) {
case 'approved':
return 'bg-green-100';
+ case 'skipped':
+ return 'bg-green-100';
case 'pending':
return 'bg-purple-100';
case 'rejected':
@@ -670,6 +676,8 @@ export function DealerClaimWorkflowTab({
const approvalStatus = approval.status.toLowerCase();
if (approvalStatus === 'approved') {
normalizedStatus = 'approved';
+ } else if (approvalStatus === 'skipped') {
+ normalizedStatus = 'skipped';
} else if (approvalStatus === 'rejected') {
normalizedStatus = 'rejected';
} else {
@@ -1675,7 +1683,7 @@ export function DealerClaimWorkflowTab({
// - AND it matches the current step
// - AND is pending/in_progress
const isActive = isRequestActive && isPendingOrInProgress && matchesCurrentStep;
- const isCompleted = step.status === 'approved';
+ const isCompleted = step.status === 'approved' || step.status === 'skipped';
// Find approval data for this step to get SLA information
// First find the corresponding level in approvalFlow to get levelId
@@ -2527,6 +2535,7 @@ export function DealerClaimWorkflowTab({
previousProposalData={versionHistory?.find(v => v.snapshotType === 'PROPOSAL')?.snapshotData}
documentPolicy={documentPolicy}
taxationType={request?.claimDetails?.taxationType}
+ totalBlockedAmount={(request?.internalOrders || []).reduce((sum: number, io: any) => sum + (Number(io.ioBlockedAmount || io.io_blocked_amount || io.blockedAmount || 0)), 0)}
/>
{/* Initiator Proposal Approval Modal */}
diff --git a/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx b/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx
index bf26ed0..8530d9d 100644
--- a/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx
+++ b/src/dealer-claim/components/request-detail/modals/DealerProposalSubmissionModal.tsx
@@ -50,6 +50,7 @@ interface CostItem {
cessAmt: number;
totalAmt: number;
isService: boolean;
+ isOriginal?: boolean; // Flag to identify items from previous iterations
}
interface DealerProposalSubmissionModalProps {
@@ -58,7 +59,10 @@ interface DealerProposalSubmissionModalProps {
onSubmit: (data: {
proposalDocument: File | null;
costBreakup: CostItem[];
+ totalEstimatedBudget: number;
expectedCompletionDate: string;
+ timelineMode: 'date' | 'days';
+ expectedCompletionDays: number;
otherDocuments: File[];
dealerComments: string;
}) => Promise
;
@@ -73,6 +77,7 @@ interface DealerProposalSubmissionModalProps {
allowedFileTypes: string[];
};
taxationType?: string | null;
+ totalBlockedAmount?: number;
}
export function DealerProposalSubmissionModal({
@@ -87,6 +92,7 @@ export function DealerProposalSubmissionModal({
defaultGstRate = 18,
documentPolicy,
taxationType,
+ totalBlockedAmount = 0,
}: DealerProposalSubmissionModalProps) {
const [proposalDocument, setProposalDocument] = useState(null);
@@ -237,7 +243,6 @@ export function DealerProposalSubmissionModal({
});
};
- // Handle download file (for non-previewable files)
const handleDownloadFile = (file: File) => {
const url = URL.createObjectURL(file);
const a = document.createElement('a');
@@ -249,6 +254,71 @@ export function DealerProposalSubmissionModal({
URL.revokeObjectURL(url);
};
+ // Pre-populate with previous proposal data if available
+ useEffect(() => {
+ if (isOpen && previousProposalData) {
+ const isDeltaFlow = totalBlockedAmount > 0;
+ if (previousProposalData.costItems && previousProposalData.costItems.length > 0) {
+ const mappedItems: CostItem[] = previousProposalData.costItems.map((item: any, index: number) => ({
+ id: `original-${index}`,
+ description: item.itemDescription || item.description || '',
+ amount: Number(item.amount) || 0,
+ quantity: Number(item.quantity) || 1,
+ hsnCode: item.hsnCode || '',
+ isService: !!item.isService,
+ gstRate: Number(item.gstRate) || defaultGstRate,
+ cgstRate: Number(item.cgstRate) || 0,
+ sgstRate: Number(item.sgstRate) || 0,
+ utgstRate: Number(item.utgstRate) || 0,
+ igstRate: Number(item.igstRate) || 0,
+ gstAmt: Number(item.gstAmt) || 0,
+ cgstAmt: Number(item.cgstAmt) || 0,
+ sgstAmt: Number(item.sgstAmt) || 0,
+ utgstAmt: Number(item.utgstAmt) || 0,
+ igstAmt: Number(item.igstAmt) || 0,
+ cessRate: Number(item.cessRate) || 0,
+ cessAmt: Number(item.cessAmt) || 0,
+ totalAmt: Number(item.totalAmt) || 0,
+ isOriginal: isDeltaFlow // Only lock as original if budget is already being blocked
+ }));
+ setCostItems(mappedItems);
+ }
+
+ if (previousProposalData.expectedCompletionDate) {
+ setExpectedCompletionDate(previousProposalData.expectedCompletionDate.split('T')[0]);
+ setTimelineMode('date');
+ }
+
+ // We DON'T pre-populate comments to force dealer to explain the re-quotation
+ // setDealerComments(previousProposalData.comments || '');
+ } else if (isOpen && !previousProposalData) {
+ // Default state for new proposals
+ setCostItems([{
+ 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
+ }]);
+ setExpectedCompletionDate('');
+ setDealerComments('');
+ }
+ }, [isOpen, previousProposalData, defaultGstRate, totalBlockedAmount]);
+
// Calculate total estimated budget (inclusive of GST)
const totalBudget = useMemo(() => {
return costItems.reduce((sum, item) => sum + (item.totalAmt || item.amount || 0), 0);
@@ -368,9 +438,9 @@ export function DealerProposalSubmissionModal({
let updatedItem = { ...item, [field]: value };
// Re-calculate GST if relevant fields change
- if (['amount', 'gstRate', 'cgstRate', 'sgstRate', 'utgstRate', 'igstRate', 'quantity'].includes(field)) {
+ if (['amount', 'gstRate', 'cgstRate', 'sgstRate', 'utgstRate', 'igstRate'].includes(field)) {
const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount;
- const quantity = field === 'quantity' ? parseInt(value) || 1 : item.quantity;
+ const quantity = 1; // Quantity is now fixed to 1
let cgstRate = item.cgstRate;
let sgstRate = item.sgstRate;
@@ -481,10 +551,23 @@ export function DealerProposalSubmissionModal({
}
}
+ const isDeltaFlow = totalBlockedAmount > 0;
+ const costBreakup = costItems
+ .filter(item => item.description.trim() !== '' && item.amount > 0)
+ .map(item => ({
+ ...item,
+ description: (previousProposalData && isDeltaFlow && !item.isOriginal)
+ ? `[ADDITIONAL] ${item.description}`
+ : item.description
+ }));
+
await onSubmit({
proposalDocument,
- costBreakup: costItems.filter(item => item.description.trim() !== '' && item.amount > 0),
+ costBreakup,
+ totalEstimatedBudget: totalBudget,
expectedCompletionDate: finalCompletionDate,
+ timelineMode,
+ expectedCompletionDays: timelineMode === 'days' ? parseInt(numberOfDays) : 0,
otherDocuments,
dealerComments,
});
@@ -691,7 +774,6 @@ export function DealerProposalSubmissionModal({
| Description |
- Qty |
Amount |
@@ -699,16 +781,13 @@ export function DealerProposalSubmissionModal({
{(previousProposalData.costItems || previousProposalData.costBreakup).map((item: any, idx: number) => (
| {item.description} |
-
- {item.quantity || 1}
- |
₹{Number(item.amount).toLocaleString('en-IN')}
|
))}
- | Total |
+ Total |
₹{Number(previousProposalData.totalEstimatedBudget || previousProposalData.totalBudget || 0).toLocaleString('en-IN')}
|
@@ -853,12 +932,20 @@ export function DealerProposalSubmissionModal({
{/* Row 1: Description and Close */}
-
+
+
+ {item.isOriginal ? (
+ ORIGINAL
+ ) : (previousProposalData && totalBlockedAmount > 0) ? (
+ ADDITIONAL
+ ) : null}
+
handleCostItemChange(item.id, 'description', e.target.value)}
- className="w-full bg-white shadow-sm"
+ className={`w-full bg-white shadow-sm ${item.isOriginal ? 'bg-gray-100 cursor-not-allowed' : ''}`}
+ disabled={item.isOriginal}
/>
{isNonGst && (
@@ -870,7 +957,8 @@ export function DealerProposalSubmissionModal({
type="number"
value={item.amount || ''}
onChange={(e) => handleCostItemChange(item.id, 'amount', e.target.value)}
- className="pl-8 bg-white shadow-sm"
+ className={`pl-8 bg-white shadow-sm ${item.isOriginal ? 'bg-gray-100 cursor-not-allowed' : ''}`}
+ disabled={item.isOriginal}
/>
@@ -881,7 +969,7 @@ export function DealerProposalSubmissionModal({
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}
+ disabled={costItems.length === 1 || item.isOriginal}
>
@@ -897,7 +985,8 @@ export function DealerProposalSubmissionModal({
type="number"
value={item.amount || ''}
onChange={(e) => handleCostItemChange(item.id, 'amount', e.target.value)}
- className="pl-8 bg-white shadow-sm"
+ className={`pl-8 bg-white shadow-sm ${item.isOriginal ? 'bg-gray-100 cursor-not-allowed' : ''}`}
+ disabled={item.isOriginal}
/>
@@ -906,7 +995,8 @@ export function DealerProposalSubmissionModal({
handleCostItemChange(item.id, 'hsnCode', e.target.value)}
- className={`bg-white shadow-sm ${!validateHSNSAC(item.hsnCode, item.isService).isValid ? 'border-red-500' : ''}`}
+ className={`bg-white shadow-sm ${!validateHSNSAC(item.hsnCode, item.isService).isValid ? 'border-red-500' : ''} ${item.isOriginal ? 'bg-gray-100 cursor-not-allowed' : ''}`}
+ disabled={item.isOriginal}
/>
@@ -914,7 +1004,8 @@ export function DealerProposalSubmissionModal({