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