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/
*/
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) {
<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'}`}>
<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'}>
{io.status === 'blocked' ? 'Blocked' : 'Released'}
<Badge className={
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>
</div>
<div className="grid grid-cols-2 divide-x divide-y">

View File

@ -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 <CircleCheckBig className="w-5 h-5 text-green-600" />;
case 'skipped':
return <CheckCircle className="w-5 h-5 text-green-600" />;
case 'pending':
return <Clock className="w-5 h-5 text-blue-600" />;
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 */}

View File

@ -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<void>;
@ -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<File | null>(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({
<thead className="bg-gray-50 text-gray-600">
<tr>
<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>
</tr>
</thead>
@ -699,16 +781,13 @@ export function DealerProposalSubmissionModal({
{(previousProposalData.costItems || previousProposalData.costBreakup).map((item: any, idx: number) => (
<tr key={idx} className="bg-white">
<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">
{Number(item.amount).toLocaleString('en-IN')}
</td>
</tr>
))}
<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">
{Number(previousProposalData.totalEstimatedBudget || previousProposalData.totalBudget || 0).toLocaleString('en-IN')}
</td>
@ -853,12 +932,20 @@ export function DealerProposalSubmissionModal({
{/* Row 1: Description and Close */}
<div className="flex gap-3 items-start">
<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
placeholder="e.g., Venue branding, Logistics, etc."
value={item.description}
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>
{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}
/>
</div>
</div>
@ -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}
>
<X className="w-4 h-4" />
</Button>
@ -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}
/>
</div>
</div>
@ -906,7 +995,8 @@ export function DealerProposalSubmissionModal({
<Input
value={item.hsnCode || ''}
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 className="w-28 sm:w-32">
@ -914,7 +1004,8 @@ export function DealerProposalSubmissionModal({
<select
value={item.isService ? 'SAC' : 'HSN'}
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="SAC">SAC (Service)</option>

View File

@ -97,19 +97,7 @@ export function InitiatorProposalApprovalModal({
const [actionType, setActionType] = useState<'approve' | 'reject' | 'revision' | null>(null);
const [showPreviousProposal, setShowPreviousProposal] = useState(false);
// Check if IO is blocked (IO blocking moved to Requestor Evaluation level)
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
// Calculate total budget (needed for display)
const totalBudget = useMemo(() => {
if (!proposalData?.costBreakup) return 0;
@ -132,6 +120,57 @@ export function InitiatorProposalApprovalModal({
}, 0);
}, [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
const formatDate = (dateString: string) => {
if (!dateString) return '—';
@ -632,7 +671,20 @@ export function InitiatorProposalApprovalModal({
{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 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}
</div>
<div className="text-xs lg:text-sm text-gray-900 text-right">
@ -787,8 +839,10 @@ export function InitiatorProposalApprovalModal({
</div>
{/* Warning for IO not blocked - shown below Approve button */}
{!isIOBlocked && (
<p className="text-xs text-red-600 text-center sm:text-left">
Please block IO budget in the IO Tab before approving
<p className="text-xs text-red-600 text-center sm:text-left font-medium">
{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>
)}
</div>

View File

@ -155,8 +155,12 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
const currentUserId = (user as any)?.userId || '';
// IO tab visibility for dealer claims
// Show IO tab for initiator (Requestor Evaluation level) - initiator can now fetch and block IO
const showIOTab = isInitiator;
// Restricted: Hide from Dealers, show for internal roles (Initiator, Dept Lead, Finance, Admin)
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 {
mergedMessages,

View File

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