multi iteration flow added for re-quotation and multiple io block feature added
This commit is contained in:
parent
e11f13d248
commit
6d6b2a3f9c
@ -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">
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user