/** * Dealer Claim IO Tab * * This component handles IO (Internal Order) management for dealer claims. * Located in: src/dealer-claim/components/request-detail/ */ import { useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { DollarSign, Download, CircleCheckBig, Target } from 'lucide-react'; import { toast } from 'sonner'; import { validateIO, updateIODetails, getClaimDetails } from '@/services/dealerClaimApi'; import { useAuth } from '@/contexts/AuthContext'; interface IOTabProps { request: any; apiRequest?: any; onRefresh?: () => void; } interface IOBlockedDetails { ioNumber: string; blockedAmount: number; availableBalance: number; // Available amount before block remainingBalance: number; // Remaining amount after block blockedDate: string; blockedBy: string; // User who blocked sapDocumentNumber: string; status: 'blocked' | 'released' | 'failed'; } export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) { const { user } = useAuth(); const requestId = apiRequest?.requestId || request?.requestId; // Load existing IO data from apiRequest or request const internalOrder = apiRequest?.internalOrder || apiRequest?.internal_order || null; const existingIONumber = internalOrder?.ioNumber || internalOrder?.io_number || request?.ioNumber || ''; const existingBlockedAmount = internalOrder?.ioBlockedAmount || internalOrder?.io_blocked_amount || 0; const existingAvailableBalance = internalOrder?.ioAvailableBalance || internalOrder?.io_available_balance || 0; const existingRemainingBalance = internalOrder?.ioRemainingBalance || internalOrder?.io_remaining_balance || 0; const sapDocNumber = internalOrder?.sapDocumentNumber || internalOrder?.sap_document_number || ''; // Get organizer user object from association (organizer) or fallback to organizedBy UUID const organizer = internalOrder?.organizer || null; // Get estimated budget from proposal details const proposalDetails = apiRequest?.proposalDetails || {}; const estimatedBudget = Number(proposalDetails?.totalEstimatedBudget || proposalDetails?.total_estimated_budget || 0); const [ioNumber, setIoNumber] = useState(existingIONumber); const [fetchingAmount, setFetchingAmount] = useState(false); const [fetchedAmount, setFetchedAmount] = useState(null); const [amountToBlock, setAmountToBlock] = useState(''); const [blockedDetails, setBlockedDetails] = useState(null); const [blockingBudget, setBlockingBudget] = useState(false); // Load existing IO block details from apiRequest useEffect(() => { if (internalOrder && existingIONumber) { // IMPORTANT: ioAvailableBalance is already the available balance BEFORE blocking // We should NOT add blockedAmount to it - that would cause double deduction // Backend stores: availableBalance (before block), blockedAmount, remainingBalance (after block) const availableBeforeBlock = Number(existingAvailableBalance) || 0; // Get blocked by user name from organizer association (who blocked the amount) // When amount is blocked, organizedBy stores the user who blocked it const blockedByName = organizer?.displayName || organizer?.display_name || organizer?.name || (organizer?.firstName && organizer?.lastName ? `${organizer.firstName} ${organizer.lastName}`.trim() : null) || organizer?.email || 'Unknown User'; // Set IO number from existing data setIoNumber(existingIONumber); // Only set blocked details if amount is blocked if (existingBlockedAmount > 0) { const blockedAmt = Number(existingBlockedAmount) || 0; const backendRemaining = Number(existingRemainingBalance) || 0; // Calculate expected remaining balance for validation/debugging // Formula: remaining = availableBeforeBlock - blockedAmount const expectedRemaining = availableBeforeBlock - blockedAmt; // Loading existing IO block // Warn if remaining balance calculation seems incorrect (for backend debugging) if (Math.abs(backendRemaining - expectedRemaining) > 0.01) { console.warn('[IOTab] ⚠️ Remaining balance calculation issue detected!', { availableBalance: availableBeforeBlock, blockedAmount: blockedAmt, expectedRemaining, backendRemaining, difference: backendRemaining - expectedRemaining, }); } setBlockedDetails({ ioNumber: existingIONumber, blockedAmount: blockedAmt, availableBalance: availableBeforeBlock, // Available amount before block remainingBalance: backendRemaining, // Use backend calculated value blockedDate: internalOrder.organizedAt || internalOrder.organized_at || new Date().toISOString(), blockedBy: blockedByName, sapDocumentNumber: sapDocNumber, status: (internalOrder.status === 'BLOCKED' ? 'blocked' : internalOrder.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed', }); // Set fetched amount if available balance exists if (availableBeforeBlock > 0) { setFetchedAmount(availableBeforeBlock); } } } }, [internalOrder, existingIONumber, existingBlockedAmount, existingAvailableBalance, existingRemainingBalance, sapDocNumber, organizer]); /** * Fetch available budget from SAP * Validates IO number and gets available balance (returns dummy data for now) * Does not store anything in database - only validates */ const handleFetchAmount = async () => { if (!ioNumber.trim()) { toast.error('Please enter an IO number'); return; } if (!requestId) { toast.error('Request ID not found'); return; } setFetchingAmount(true); try { // Call validate IO endpoint - returns dummy data for now, will integrate with SAP later const ioData = await validateIO(requestId, ioNumber.trim()); 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)); } else { setAmountToBlock(String(ioData.availableBalance)); } 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'); setFetchedAmount(null); setAmountToBlock(''); } } catch (error: any) { console.error('Failed to fetch IO budget:', error); const errorMessage = error?.response?.data?.message || error?.message || 'Failed to validate IO number or fetch budget from SAP'; toast.error(errorMessage); setFetchedAmount(null); } finally { setFetchingAmount(false); } }; /** * Block budget in SAP system * This function: * 1. Validates the IO number and amount * 2. Calls SAP to block the budget * 3. Saves IO number, blocked amount, and balance details to database */ const handleBlockBudget = async () => { if (!ioNumber.trim() || fetchedAmount === null) { toast.error('Please fetch IO amount first'); return; } if (!requestId) { toast.error('Request ID not found'); return; } const blockAmountRaw = parseFloat(amountToBlock); if (!amountToBlock || isNaN(blockAmountRaw) || blockAmountRaw <= 0) { toast.error('Please enter a valid amount to block'); return; } // Round to exactly 2 decimal places to avoid floating point precision issues // Use parseFloat with toFixed to ensure exact 2 decimal precision const blockAmount = parseFloat(blockAmountRaw.toFixed(2)); if (blockAmount > fetchedAmount) { toast.error('Amount to block exceeds available IO budget'); return; } // Validate that amount to block must exactly match estimated budget if (estimatedBudget > 0) { const roundedEstimatedBudget = parseFloat(estimatedBudget.toFixed(2)); if (Math.abs(blockAmount - roundedEstimatedBudget) > 0.01) { toast.error(`Amount to block must be exactly equal to the estimated budget (₹${roundedEstimatedBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })})`); return; } } // Blocking budget setBlockingBudget(true); try { // Call updateIODetails with blockedAmount to block budget in SAP and store in database // This will store in internal_orders and claim_budget_tracking tables // Note: Backend will recalculate remainingBalance from SAP's response, so we pass it for reference only // Ensure all amounts are rounded to 2 decimal places for consistency const roundedFetchedAmount = parseFloat(fetchedAmount.toFixed(2)); const calculatedRemaining = parseFloat((roundedFetchedAmount - blockAmount).toFixed(2)); const payload = { ioNumber: ioNumber.trim(), ioAvailableBalance: roundedFetchedAmount, ioBlockedAmount: blockAmount, ioRemainingBalance: calculatedRemaining, // Calculated value (backend will use SAP's actual value) }; // Sending to backend await updateIODetails(requestId, payload); // Fetch updated claim details to get the blocked IO data const claimData = await getClaimDetails(requestId); const updatedInternalOrder = claimData?.internalOrder || claimData?.internal_order; if (updatedInternalOrder) { const savedBlockedAmount = Number(updatedInternalOrder.ioBlockedAmount || updatedInternalOrder.io_blocked_amount || blockAmount); const savedRemainingBalance = Number(updatedInternalOrder.ioRemainingBalance || updatedInternalOrder.io_remaining_balance || 0); // Calculate expected remaining balance for validation/debugging const expectedRemainingBalance = fetchedAmount - savedBlockedAmount; // Blocking result processed // Warn if the saved amount differs from what we sent if (Math.abs(savedBlockedAmount - blockAmount) > 0.01) { console.warn('[IOTab] ⚠️ Amount mismatch! Sent:', blockAmount, 'Saved:', savedBlockedAmount); } // Warn if remaining balance calculation seems incorrect (for backend debugging) if (Math.abs(savedRemainingBalance - expectedRemainingBalance) > 0.01) { console.warn('[IOTab] ⚠️ Remaining balance calculation issue detected!', { availableBalance: fetchedAmount, blockedAmount: savedBlockedAmount, expectedRemaining: expectedRemainingBalance, backendRemaining: savedRemainingBalance, difference: savedRemainingBalance - expectedRemainingBalance, }); } const currentUser = user as any; // When blocking, always use the current user who is performing the block action // The organizer association may be from initial IO organization, but we want who blocked the amount const blockedByName = currentUser?.displayName || currentUser?.display_name || currentUser?.name || (currentUser?.firstName && currentUser?.lastName ? `${currentUser.firstName} ${currentUser.lastName}`.trim() : null) || currentUser?.email || 'Current User'; const blocked: IOBlockedDetails = { ioNumber: updatedInternalOrder.ioNumber || updatedInternalOrder.io_number || ioNumber, blockedAmount: savedBlockedAmount, availableBalance: fetchedAmount, // Available amount before block remainingBalance: savedRemainingBalance, blockedDate: updatedInternalOrder.organizedAt || updatedInternalOrder.organized_at || new Date().toISOString(), blockedBy: blockedByName, sapDocumentNumber: updatedInternalOrder.sapDocumentNumber || updatedInternalOrder.sap_document_number || '', status: 'blocked', }; setBlockedDetails(blocked); setAmountToBlock(''); // Clear the input toast.success('IO budget blocked successfully in SAP'); // Refresh request details onRefresh?.(); } else { toast.error('IO blocked but failed to fetch updated details'); onRefresh?.(); } } catch (error: any) { console.error('Failed to block IO budget:', error); const errorMessage = error?.response?.data?.message || error?.message || 'Failed to block IO budget in SAP'; toast.error(errorMessage); } finally { setBlockingBudget(false); } }; return (
{/* IO Budget Management Card */} IO Budget Management Enter IO number to fetch available budget from SAP {/* IO Number Input */}
setIoNumber(e.target.value)} disabled={fetchingAmount || !!blockedDetails} className="flex-1" />
{/* Instructions when IO number is entered but not fetched */} {!fetchedAmount && !blockedDetails && ioNumber.trim() && (

Next Step: Click "Fetch Amount" to validate the IO number and get available balance from SAP.

)} {/* Fetched Amount Display */} {fetchedAmount !== null && !blockedDetails && ( <>

Available Amount

₹{fetchedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

IO Number: {ioNumber}

Fetched from: SAP System

{/* Amount to Block Input */}
setAmountToBlock(e.target.value)} className="pl-8" />
{estimatedBudget > 0 && (

Required: Amount must be exactly equal to the estimated budget: ₹{estimatedBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

)}
{/* Block Button */} )}
{/* IO Blocked Details Card */} IO Blocked Details Details of IO blocked in SAP system {blockedDetails ? (
{/* Success Banner */}

IO Blocked Successfully

Budget has been reserved in SAP system

{/* Blocked Details */}

IO Number

{blockedDetails.ioNumber}

SAP Document Number

{blockedDetails.sapDocumentNumber || 'N/A'}

Blocked Amount

₹{blockedDetails.blockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

Available Amount (Before Block)

₹{blockedDetails.availableBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

Remaining Amount (After Block)

₹{blockedDetails.remainingBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

Blocked By

{blockedDetails.blockedBy}

Blocked At

{new Date(blockedDetails.blockedDate).toLocaleString('en-IN', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true })}

Status

Blocked
) : (

No IO blocked yet

Enter IO number and fetch amount to block budget

)}
); }