multi iteration flow added for re-quotation and multiple io block feature added

This commit is contained in:
laxmanhalaki 2026-03-03 18:14:10 +05:30
parent e11f13d248
commit 6d6b2a3f9c
6 changed files with 256 additions and 56 deletions

View File

@ -5,7 +5,7 @@
* Located in: src/dealer-claim/components/request-detail/ * 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@ -30,7 +30,7 @@ interface IOBlockedDetails {
blockedDate: string; blockedDate: string;
blockedBy: string; // User who blocked blockedBy: string; // User who blocked
sapDocumentNumber: string; sapDocumentNumber: string;
status: 'blocked' | 'released' | 'failed'; status: 'blocked' | 'released' | 'failed' | 'pending';
} }
export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) { export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
@ -38,18 +38,39 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
const requestId = apiRequest?.requestId || request?.requestId; const requestId = apiRequest?.requestId || request?.requestId;
// Get organizer user object from association (organizer) or fallback to organizedBy UUID // 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 proposalDetails = apiRequest?.proposalDetails || {};
const claimDetails = apiRequest?.claimDetails || apiRequest || {}; const claimDetails = apiRequest?.claimDetails || apiRequest || {};
// Use totalProposedTaxableAmount if available (for Scenario 2), fallback to estimated budget // Calculate total base amount (needed for budget verification as requested)
const estimatedBudget = Number(claimDetails?.totalProposedTaxableAmount || proposalDetails?.totalEstimatedBudget || proposalDetails?.total_estimated_budget || 0); // 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) // 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 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 [ioNumber, setIoNumber] = useState('');
const [fetchingAmount, setFetchingAmount] = useState(false); const [fetchingAmount, setFetchingAmount] = useState(false);
@ -60,9 +81,8 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
// Load existing IO blocks // Load existing IO blocks
useEffect(() => { useEffect(() => {
const ios = apiRequest?.internalOrders || apiRequest?.internal_orders || []; if (internalOrdersList.length > 0) {
if (ios.length > 0) { const formattedIOs = internalOrdersList.map((io: any) => {
const formattedIOs = ios.map((io: any) => {
const org = io.organizer || null; const org = io.organizer || null;
const blockedByName = org?.displayName || const blockedByName = org?.displayName ||
org?.display_name || org?.display_name ||
@ -79,7 +99,8 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
blockedBy: blockedByName, blockedBy: blockedByName,
sapDocumentNumber: io.sapDocumentNumber || io.sap_document_number || '', sapDocumentNumber: io.sapDocumentNumber || io.sap_document_number || '',
status: (io.status === 'BLOCKED' ? 'blocked' : 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); setBlockedIOs(formattedIOs);
@ -89,7 +110,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
setIoNumber(formattedIOs[formattedIOs.length - 1].ioNumber); setIoNumber(formattedIOs[formattedIOs.length - 1].ioNumber);
} }
} }
}, [apiRequest, isAdditionalBlockingNeeded]); }, [apiRequest, request, isAdditionalBlockingNeeded, internalOrdersList]);
/** /**
* Fetch available budget from SAP * Fetch available budget from SAP
@ -114,12 +135,22 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
if (ioData.isValid && ioData.availableBalance > 0) { if (ioData.isValid && ioData.availableBalance > 0) {
setFetchedAmount(ioData.availableBalance); setFetchedAmount(ioData.availableBalance);
// Pre-fill amount to block with estimated budget (if available), otherwise use available balance
if (estimatedBudget > 0) { // Calculate total already blocked amount
setAmountToBlock(String(estimatedBudget)); 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 { } 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 })}`); toast.success(`IO fetched from SAP. Available balance: ₹${ioData.availableBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
} else { } else {
toast.error('Invalid IO number or no available balance found'); toast.error('Invalid IO number or no available balance found');
@ -417,8 +448,13 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
<div key={idx} className="border rounded-lg overflow-hidden"> <div key={idx} className="border rounded-lg overflow-hidden">
<div className={`p-3 flex justify-between items-center ${idx === 0 ? 'bg-green-50' : 'bg-gray-50'}`}> <div className={`p-3 flex justify-between items-center ${idx === 0 ? 'bg-green-50' : 'bg-gray-50'}`}>
<span className="font-semibold text-sm">IO: {io.ioNumber}</span> <span className="font-semibold text-sm">IO: {io.ioNumber}</span>
<Badge className={io.status === 'blocked' ? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800'}> <Badge className={
{io.status === 'blocked' ? 'Blocked' : 'Released'} io.status === 'blocked' ? 'bg-green-100 text-green-800' :
io.status === 'pending' ? 'bg-amber-100 text-amber-800' :
'bg-blue-100 text-blue-800'
}>
{io.status === 'blocked' ? 'Blocked' :
io.status === 'pending' ? 'Provisioned' : 'Released'}
</Badge> </Badge>
</div> </div>
<div className="grid grid-cols-2 divide-x divide-y"> <div className="grid grid-cols-2 divide-x divide-y">

View File

@ -49,7 +49,7 @@ interface WorkflowStep {
approver: string; approver: string;
description: string; description: string;
tatHours: number; tatHours: number;
status: 'pending' | 'approved' | 'waiting' | 'rejected' | 'in_progress'; status: 'pending' | 'approved' | 'waiting' | 'rejected' | 'in_progress' | 'skipped';
comment?: string; comment?: string;
approvedAt?: string; approvedAt?: string;
elapsedHours?: number; elapsedHours?: number;
@ -109,6 +109,8 @@ const getStepIcon = (status: string) => {
switch (status) { switch (status) {
case 'approved': case 'approved':
return <CircleCheckBig className="w-5 h-5 text-green-600" />; return <CircleCheckBig className="w-5 h-5 text-green-600" />;
case 'skipped':
return <CheckCircle className="w-5 h-5 text-green-600" />;
case 'pending': case 'pending':
return <Clock className="w-5 h-5 text-blue-600" />; return <Clock className="w-5 h-5 text-blue-600" />;
case 'rejected': case 'rejected':
@ -125,6 +127,8 @@ const getStepBadgeVariant = (status: string) => {
switch (status) { switch (status) {
case 'approved': case 'approved':
return 'bg-green-100 text-green-800 border-green-200'; return 'bg-green-100 text-green-800 border-green-200';
case 'skipped':
return 'bg-green-50 text-green-700 border-green-200';
case 'pending': case 'pending':
return 'bg-purple-100 text-purple-800 border-purple-200'; return 'bg-purple-100 text-purple-800 border-purple-200';
case 'rejected': case 'rejected':
@ -141,7 +145,7 @@ const getStepCardStyle = (status: string, isActive: boolean) => {
if (isActive && (status === 'pending' || status === 'in_progress')) { if (isActive && (status === 'pending' || status === 'in_progress')) {
return 'border-purple-500 bg-purple-50 shadow-md'; return 'border-purple-500 bg-purple-50 shadow-md';
} }
if (status === 'approved') { if (status === 'approved' || status === 'skipped') {
return 'border-green-500 bg-green-50'; return 'border-green-500 bg-green-50';
} }
if (status === 'rejected') { if (status === 'rejected') {
@ -157,6 +161,8 @@ const getStepIconBg = (status: string) => {
switch (status) { switch (status) {
case 'approved': case 'approved':
return 'bg-green-100'; return 'bg-green-100';
case 'skipped':
return 'bg-green-100';
case 'pending': case 'pending':
return 'bg-purple-100'; return 'bg-purple-100';
case 'rejected': case 'rejected':
@ -670,6 +676,8 @@ export function DealerClaimWorkflowTab({
const approvalStatus = approval.status.toLowerCase(); const approvalStatus = approval.status.toLowerCase();
if (approvalStatus === 'approved') { if (approvalStatus === 'approved') {
normalizedStatus = 'approved'; normalizedStatus = 'approved';
} else if (approvalStatus === 'skipped') {
normalizedStatus = 'skipped';
} else if (approvalStatus === 'rejected') { } else if (approvalStatus === 'rejected') {
normalizedStatus = 'rejected'; normalizedStatus = 'rejected';
} else { } else {
@ -1675,7 +1683,7 @@ export function DealerClaimWorkflowTab({
// - AND it matches the current step // - AND it matches the current step
// - AND is pending/in_progress // - AND is pending/in_progress
const isActive = isRequestActive && isPendingOrInProgress && matchesCurrentStep; 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 // Find approval data for this step to get SLA information
// First find the corresponding level in approvalFlow to get levelId // 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} previousProposalData={versionHistory?.find(v => v.snapshotType === 'PROPOSAL')?.snapshotData}
documentPolicy={documentPolicy} documentPolicy={documentPolicy}
taxationType={request?.claimDetails?.taxationType} 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 */} {/* Initiator Proposal Approval Modal */}

View File

@ -50,6 +50,7 @@ interface CostItem {
cessAmt: number; cessAmt: number;
totalAmt: number; totalAmt: number;
isService: boolean; isService: boolean;
isOriginal?: boolean; // Flag to identify items from previous iterations
} }
interface DealerProposalSubmissionModalProps { interface DealerProposalSubmissionModalProps {
@ -58,7 +59,10 @@ interface DealerProposalSubmissionModalProps {
onSubmit: (data: { onSubmit: (data: {
proposalDocument: File | null; proposalDocument: File | null;
costBreakup: CostItem[]; costBreakup: CostItem[];
totalEstimatedBudget: number;
expectedCompletionDate: string; expectedCompletionDate: string;
timelineMode: 'date' | 'days';
expectedCompletionDays: number;
otherDocuments: File[]; otherDocuments: File[];
dealerComments: string; dealerComments: string;
}) => Promise<void>; }) => Promise<void>;
@ -73,6 +77,7 @@ interface DealerProposalSubmissionModalProps {
allowedFileTypes: string[]; allowedFileTypes: string[];
}; };
taxationType?: string | null; taxationType?: string | null;
totalBlockedAmount?: number;
} }
export function DealerProposalSubmissionModal({ export function DealerProposalSubmissionModal({
@ -87,6 +92,7 @@ export function DealerProposalSubmissionModal({
defaultGstRate = 18, defaultGstRate = 18,
documentPolicy, documentPolicy,
taxationType, taxationType,
totalBlockedAmount = 0,
}: DealerProposalSubmissionModalProps) { }: DealerProposalSubmissionModalProps) {
const [proposalDocument, setProposalDocument] = useState<File | null>(null); const [proposalDocument, setProposalDocument] = useState<File | null>(null);
@ -237,7 +243,6 @@ export function DealerProposalSubmissionModal({
}); });
}; };
// Handle download file (for non-previewable files)
const handleDownloadFile = (file: File) => { const handleDownloadFile = (file: File) => {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
const a = document.createElement('a'); const a = document.createElement('a');
@ -249,6 +254,71 @@ export function DealerProposalSubmissionModal({
URL.revokeObjectURL(url); 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) // Calculate total estimated budget (inclusive of GST)
const totalBudget = useMemo(() => { const totalBudget = useMemo(() => {
return costItems.reduce((sum, item) => sum + (item.totalAmt || item.amount || 0), 0); return costItems.reduce((sum, item) => sum + (item.totalAmt || item.amount || 0), 0);
@ -368,9 +438,9 @@ export function DealerProposalSubmissionModal({
let updatedItem = { ...item, [field]: value }; let updatedItem = { ...item, [field]: value };
// Re-calculate GST if relevant fields change // 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 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 cgstRate = item.cgstRate;
let sgstRate = item.sgstRate; 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({ await onSubmit({
proposalDocument, proposalDocument,
costBreakup: costItems.filter(item => item.description.trim() !== '' && item.amount > 0), costBreakup,
totalEstimatedBudget: totalBudget,
expectedCompletionDate: finalCompletionDate, expectedCompletionDate: finalCompletionDate,
timelineMode,
expectedCompletionDays: timelineMode === 'days' ? parseInt(numberOfDays) : 0,
otherDocuments, otherDocuments,
dealerComments, dealerComments,
}); });
@ -691,7 +774,6 @@ export function DealerProposalSubmissionModal({
<thead className="bg-gray-50 text-gray-600"> <thead className="bg-gray-50 text-gray-600">
<tr> <tr>
<th className="p-2 font-medium">Description</th> <th className="p-2 font-medium">Description</th>
<th className="p-2 font-medium text-right">Qty</th>
<th className="p-2 font-medium text-right">Amount</th> <th className="p-2 font-medium text-right">Amount</th>
</tr> </tr>
</thead> </thead>
@ -699,16 +781,13 @@ export function DealerProposalSubmissionModal({
{(previousProposalData.costItems || previousProposalData.costBreakup).map((item: any, idx: number) => ( {(previousProposalData.costItems || previousProposalData.costBreakup).map((item: any, idx: number) => (
<tr key={idx} className="bg-white"> <tr key={idx} className="bg-white">
<td className="p-2 text-gray-800">{item.description}</td> <td className="p-2 text-gray-800">{item.description}</td>
<td className="p-2 text-right text-gray-800 font-medium">
{item.quantity || 1}
</td>
<td className="p-2 text-right text-gray-800 font-medium"> <td className="p-2 text-right text-gray-800 font-medium">
{Number(item.amount).toLocaleString('en-IN')} {Number(item.amount).toLocaleString('en-IN')}
</td> </td>
</tr> </tr>
))} ))}
<tr className="bg-gray-50 font-bold"> <tr className="bg-gray-50 font-bold">
<td colSpan={2} className="p-2 text-gray-900">Total</td> <td colSpan={1} className="p-2 text-gray-900">Total</td>
<td className="p-2 text-right text-gray-900"> <td className="p-2 text-right text-gray-900">
{Number(previousProposalData.totalEstimatedBudget || previousProposalData.totalBudget || 0).toLocaleString('en-IN')} {Number(previousProposalData.totalEstimatedBudget || previousProposalData.totalBudget || 0).toLocaleString('en-IN')}
</td> </td>
@ -853,12 +932,20 @@ export function DealerProposalSubmissionModal({
{/* Row 1: Description and Close */} {/* Row 1: Description and Close */}
<div className="flex gap-3 items-start"> <div className="flex gap-3 items-start">
<div className={`${isNonGst ? 'flex-[3]' : 'flex-1'}`}> <div className={`${isNonGst ? 'flex-[3]' : 'flex-1'}`}>
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Item Description</Label> <div className="flex items-center justify-between mb-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold">Item Description</Label>
{item.isOriginal ? (
<Badge className="text-[9px] h-4 bg-gray-200 text-gray-700 hover:bg-gray-200 border-none">ORIGINAL</Badge>
) : (previousProposalData && totalBlockedAmount > 0) ? (
<Badge className="text-[9px] h-4 bg-amber-100 text-amber-700 hover:bg-amber-100 border-none">ADDITIONAL</Badge>
) : null}
</div>
<Input <Input
placeholder="e.g., Venue branding, Logistics, etc." placeholder="e.g., Venue branding, Logistics, etc."
value={item.description} value={item.description}
onChange={(e) => handleCostItemChange(item.id, 'description', e.target.value)} onChange={(e) => 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}
/> />
</div> </div>
{isNonGst && ( {isNonGst && (
@ -870,7 +957,8 @@ export function DealerProposalSubmissionModal({
type="number" type="number"
value={item.amount || ''} value={item.amount || ''}
onChange={(e) => handleCostItemChange(item.id, 'amount', e.target.value)} 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}
/> />
</div> </div>
</div> </div>
@ -881,7 +969,7 @@ export function DealerProposalSubmissionModal({
size="sm" size="sm"
className="mt-6 hover:bg-red-50 hover:text-red-600 h-9 w-9 p-0 rounded-full" className="mt-6 hover:bg-red-50 hover:text-red-600 h-9 w-9 p-0 rounded-full"
onClick={() => handleRemoveCostItem(item.id)} onClick={() => handleRemoveCostItem(item.id)}
disabled={costItems.length === 1} disabled={costItems.length === 1 || item.isOriginal}
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</Button> </Button>
@ -897,7 +985,8 @@ export function DealerProposalSubmissionModal({
type="number" type="number"
value={item.amount || ''} value={item.amount || ''}
onChange={(e) => handleCostItemChange(item.id, 'amount', e.target.value)} 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}
/> />
</div> </div>
</div> </div>
@ -906,7 +995,8 @@ export function DealerProposalSubmissionModal({
<Input <Input
value={item.hsnCode || ''} value={item.hsnCode || ''}
onChange={(e) => handleCostItemChange(item.id, 'hsnCode', e.target.value)} onChange={(e) => 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}
/> />
</div> </div>
<div className="w-28 sm:w-32"> <div className="w-28 sm:w-32">
@ -914,7 +1004,8 @@ export function DealerProposalSubmissionModal({
<select <select
value={item.isService ? 'SAC' : 'HSN'} value={item.isService ? 'SAC' : 'HSN'}
onChange={(e) => handleCostItemChange(item.id, 'isService', e.target.value === 'SAC')} onChange={(e) => handleCostItemChange(item.id, 'isService', e.target.value === 'SAC')}
className="flex h-10 w-full rounded-md border border-input bg-white px-3 py-2 text-sm shadow-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-[#2d4a3e]" className={`flex h-10 w-full rounded-md border border-input bg-white px-3 py-2 text-sm shadow-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-[#2d4a3e] ${item.isOriginal ? 'bg-gray-100 cursor-not-allowed' : ''}`}
disabled={item.isOriginal}
> >
<option value="HSN">HSN (Goods)</option> <option value="HSN">HSN (Goods)</option>
<option value="SAC">SAC (Service)</option> <option value="SAC">SAC (Service)</option>

View File

@ -97,19 +97,7 @@ export function InitiatorProposalApprovalModal({
const [actionType, setActionType] = useState<'approve' | 'reject' | 'revision' | null>(null); const [actionType, setActionType] = useState<'approve' | 'reject' | 'revision' | null>(null);
const [showPreviousProposal, setShowPreviousProposal] = useState(false); const [showPreviousProposal, setShowPreviousProposal] = useState(false);
// Check if IO is blocked (IO blocking moved to Requestor Evaluation level) // Calculate total budget (needed for display)
const internalOrder = request?.internalOrder || request?.internal_order;
const ioBlockedAmount = internalOrder?.ioBlockedAmount || internalOrder?.io_blocked_amount || 0;
const isIOBlocked = ioBlockedAmount > 0;
const [previewDoc, setPreviewDoc] = useState<{
name: string;
url: string;
type?: string;
size?: number;
id?: string;
} | null>(null);
// Calculate total budget
const totalBudget = useMemo(() => { const totalBudget = useMemo(() => {
if (!proposalData?.costBreakup) return 0; if (!proposalData?.costBreakup) return 0;
@ -132,6 +120,57 @@ export function InitiatorProposalApprovalModal({
}, 0); }, 0);
}, [proposalData]); }, [proposalData]);
// Calculate total base amount (needed for budget verification as requested)
// This is the taxable amount excluding GST
const totalBaseAmount = useMemo(() => {
if (!proposalData?.costBreakup) return 0;
const costBreakup = Array.isArray(proposalData.costBreakup)
? proposalData.costBreakup
: (typeof proposalData.costBreakup === 'string'
? JSON.parse(proposalData.costBreakup)
: []);
if (!Array.isArray(costBreakup)) return 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);
}, [proposalData]);
// Check if IO is blocked (IO blocking moved to Requestor Evaluation level)
// Sum up all successful blocks from internalOrders array
const totalBlockedAmount = useMemo(() => {
const internalOrders = request?.internalOrders || request?.internal_orders || [];
// If we have an array, sum the blocked amounts
if (Array.isArray(internalOrders) && internalOrders.length > 0) {
return internalOrders.reduce((sum: number, io: any) => {
const amt = Number(io.ioBlockedAmount || io.io_blocked_amount || 0);
return sum + amt;
}, 0);
}
// Fallback to single internalOrder object for backward compatibility
const singleIO = request?.internalOrder || request?.internal_order;
return Number(singleIO?.ioBlockedAmount || singleIO?.io_blocked_amount || 0);
}, [request?.internalOrders, request?.internal_orders, request?.internalOrder, request?.internal_order]);
// Budget is considered blocked only if the total blocked amount matches or exceeds the proposed base amount
// Allow a small margin for floating point comparison if needed, but here simple >= should suffice
const isIOBlocked = totalBlockedAmount >= (totalBaseAmount - 0.01);
const remainingBaseToBlock = Math.max(0, totalBaseAmount - totalBlockedAmount);
const [previewDoc, setPreviewDoc] = useState<{
name: string;
url: string;
type?: string;
size?: number;
id?: string;
} | null>(null);
// Format date // Format date
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
if (!dateString) return '—'; if (!dateString) return '—';
@ -632,7 +671,20 @@ export function InitiatorProposalApprovalModal({
{costBreakup.map((item: any, index: number) => ( {costBreakup.map((item: any, index: number) => (
<div key={item?.id || item?.description || index} className={`px-3 lg:px-4 py-2 lg:py-3 grid ${isNonGst ? 'grid-cols-3' : 'grid-cols-4'} gap-4`}> <div key={item?.id || item?.description || index} className={`px-3 lg:px-4 py-2 lg:py-3 grid ${isNonGst ? 'grid-cols-3' : 'grid-cols-4'} gap-4`}>
<div className="col-span-1 text-xs lg:text-sm text-gray-700"> <div className="col-span-1 text-xs lg:text-sm text-gray-700">
{item?.description || 'N/A'} <div className="flex items-center gap-1.5 mb-0.5">
<span className="font-medium">
{item?.description?.startsWith('[ADDITIONAL]')
? item.description.replace('[ADDITIONAL]', '').trim()
: (item?.description || 'N/A')}
</span>
{costBreakup.some((i: any) => i?.description?.startsWith('[ADDITIONAL]')) && (
item?.description?.startsWith('[ADDITIONAL]') ? (
<Badge className="text-[9px] h-3.5 px-1 bg-amber-100 text-amber-700 hover:bg-amber-100 border-none leading-none">ADDITIONAL</Badge>
) : (
<Badge className="text-[9px] h-3.5 px-1 bg-gray-100 text-gray-600 hover:bg-gray-100 border-none leading-none">ORIGINAL</Badge>
)
)}
</div>
{!isNonGst && item?.gstRate ? <span className="block text-[10px] text-gray-400">{item.gstRate}% GST</span> : null} {!isNonGst && item?.gstRate ? <span className="block text-[10px] text-gray-400">{item.gstRate}% GST</span> : null}
</div> </div>
<div className="text-xs lg:text-sm text-gray-900 text-right"> <div className="text-xs lg:text-sm text-gray-900 text-right">
@ -787,8 +839,10 @@ export function InitiatorProposalApprovalModal({
</div> </div>
{/* Warning for IO not blocked - shown below Approve button */} {/* Warning for IO not blocked - shown below Approve button */}
{!isIOBlocked && ( {!isIOBlocked && (
<p className="text-xs text-red-600 text-center sm:text-left"> <p className="text-xs text-red-600 text-center sm:text-left font-medium">
Please block IO budget in the IO Tab before approving {totalBlockedAmount > 0
? `Pending block: ₹${remainingBaseToBlock.toLocaleString('en-IN', { minimumFractionDigits: 2 })} more needs to be blocked in the IO Tab.`
: "Please block IO budget in the IO Tab before approving."}
</p> </p>
)} )}
</div> </div>

View File

@ -155,8 +155,12 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
const currentUserId = (user as any)?.userId || ''; const currentUserId = (user as any)?.userId || '';
// IO tab visibility for dealer claims // IO tab visibility for dealer claims
// Show IO tab for initiator (Requestor Evaluation level) - initiator can now fetch and block IO // Restricted: Hide from Dealers, show for internal roles (Initiator, Dept Lead, Finance, Admin)
const showIOTab = isInitiator; const isDealer = (user as any)?.jobTitle === 'Dealer' || (user as any)?.designation === 'Dealer';
const isClaimManagement = request?.workflowType === 'CLAIM_MANAGEMENT' ||
apiRequest?.workflowType === 'CLAIM_MANAGEMENT' ||
request?.templateType === 'claim-management';
const showIOTab = isClaimManagement && !isDealer;
const { const {
mergedMessages, mergedMessages,

View File

@ -238,6 +238,7 @@ export function useRequestDetails(
let proposalDetails = null; let proposalDetails = null;
let completionDetails = null; let completionDetails = null;
let internalOrder = null; let internalOrder = null;
let internalOrders = [];
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') { if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
try { try {
@ -250,6 +251,7 @@ export function useRequestDetails(
proposalDetails = claimData.proposalDetails || claimData.proposal_details; proposalDetails = claimData.proposalDetails || claimData.proposal_details;
completionDetails = claimData.completionDetails || claimData.completion_details; completionDetails = claimData.completionDetails || claimData.completion_details;
internalOrder = claimData.internalOrder || claimData.internal_order || null; internalOrder = claimData.internalOrder || claimData.internal_order || null;
internalOrders = claimData.internalOrders || claimData.internal_orders || [];
// New normalized tables // New normalized tables
const budgetTracking = claimData.budgetTracking || claimData.budget_tracking || null; const budgetTracking = claimData.budgetTracking || claimData.budget_tracking || null;
const invoice = claimData.invoice || null; const invoice = claimData.invoice || null;
@ -326,6 +328,7 @@ export function useRequestDetails(
proposalDetails: proposalDetails || null, proposalDetails: proposalDetails || null,
completionDetails: completionDetails || null, completionDetails: completionDetails || null,
internalOrder: internalOrder || null, internalOrder: internalOrder || null,
internalOrders: internalOrders || [],
// New normalized tables (also available via claimDetails for backward compatibility) // New normalized tables (also available via claimDetails for backward compatibility)
budgetTracking: (claimDetails as any)?.budgetTracking || null, budgetTracking: (claimDetails as any)?.budgetTracking || null,
invoice: (claimDetails as any)?.invoice || null, invoice: (claimDetails as any)?.invoice || null,
@ -517,6 +520,7 @@ export function useRequestDetails(
let proposalDetails = null; let proposalDetails = null;
let completionDetails = null; let completionDetails = null;
let internalOrder = null; let internalOrder = null;
let internalOrders = [];
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') { if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
try { try {
@ -528,6 +532,7 @@ export function useRequestDetails(
proposalDetails = claimData.proposalDetails || claimData.proposal_details; proposalDetails = claimData.proposalDetails || claimData.proposal_details;
completionDetails = claimData.completionDetails || claimData.completion_details; completionDetails = claimData.completionDetails || claimData.completion_details;
internalOrder = claimData.internalOrder || claimData.internal_order || null; internalOrder = claimData.internalOrder || claimData.internal_order || null;
internalOrders = claimData.internalOrders || claimData.internal_orders || [];
// New normalized tables // New normalized tables
const budgetTracking = claimData.budgetTracking || claimData.budget_tracking || null; const budgetTracking = claimData.budgetTracking || claimData.budget_tracking || null;
const invoice = claimData.invoice || null; const invoice = claimData.invoice || null;
@ -591,6 +596,7 @@ export function useRequestDetails(
proposalDetails: proposalDetails || null, proposalDetails: proposalDetails || null,
completionDetails: completionDetails || null, completionDetails: completionDetails || null,
internalOrder: internalOrder || null, internalOrder: internalOrder || null,
internalOrders: internalOrders || [],
// New normalized tables (also available via claimDetails for backward compatibility) // New normalized tables (also available via claimDetails for backward compatibility)
budgetTracking: (claimDetails as any)?.budgetTracking || null, budgetTracking: (claimDetails as any)?.budgetTracking || null,
invoice: (claimDetails as any)?.invoice || null, invoice: (claimDetails as any)?.invoice || null,