/** * 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 { Textarea } from '@/components/ui/textarea'; 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; ioRemark?: string; // IO remark 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 existingIORemark = internalOrder?.ioRemark || internalOrder?.io_remark || ''; 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; const [ioNumber, setIoNumber] = useState(existingIONumber); const [ioRemark, setIoRemark] = useState(existingIORemark); const [fetchingAmount, setFetchingAmount] = useState(false); const [fetchedAmount, setFetchedAmount] = useState(null); const [amountToBlock, setAmountToBlock] = useState(''); const [blockedDetails, setBlockedDetails] = useState(null); const [blockingBudget, setBlockingBudget] = useState(false); const maxIoRemarkChars = 300; const ioRemarkChars = ioRemark.length; // Load existing IO block details from apiRequest useEffect(() => { if (internalOrder && existingIONumber) { const availableBeforeBlock = Number(existingAvailableBalance) + Number(existingBlockedAmount) || Number(existingAvailableBalance); // 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 and remark from existing data setIoNumber(existingIONumber); setIoRemark(existingIORemark); // Only set blocked details if amount is blocked if (existingBlockedAmount > 0) { setBlockedDetails({ ioNumber: existingIONumber, blockedAmount: Number(existingBlockedAmount) || 0, availableBalance: availableBeforeBlock, // Available amount before block remainingBalance: Number(existingRemainingBalance) || Number(existingAvailableBalance), blockedDate: internalOrder.organizedAt || internalOrder.organized_at || new Date().toISOString(), blockedBy: blockedByName, sapDocumentNumber: sapDocNumber, ioRemark: existingIORemark, 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, existingIORemark, 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 available balance 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); } }; /** * Save IO details (IO number and remark) without blocking budget */ const handleSaveIODetails = async () => { if (!ioNumber.trim()) { toast.error('Please enter an IO number'); return; } if (!ioRemark.trim()) { toast.error('Please enter an IO remark'); return; } if (!requestId) { toast.error('Request ID not found'); return; } setBlockingBudget(true); try { // Save only IO number and remark (no balance fields) const payload = { ioNumber: ioNumber.trim(), ioRemark: ioRemark.trim(), }; await updateIODetails(requestId, payload); toast.success('IO details saved successfully'); // Refresh request details onRefresh?.(); } catch (error: any) { console.error('Failed to save IO details:', error); const errorMessage = error?.response?.data?.message || error?.message || 'Failed to save IO details'; toast.error(errorMessage); } finally { setBlockingBudget(false); } }; /** * Block budget in SAP system */ 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 2 decimal places to avoid floating point precision issues // This ensures we send clean values like 240.00 instead of 239.9999999 const blockAmount = Math.round(blockAmountRaw * 100) / 100; if (blockAmount > fetchedAmount) { toast.error('Amount to block exceeds available IO budget'); return; } // Log the amount being sent to backend for debugging console.log('[IOTab] Blocking budget:', { ioNumber: ioNumber.trim(), originalInput: amountToBlock, parsedAmount: blockAmountRaw, roundedAmount: blockAmount, fetchedAmount, calculatedRemaining: fetchedAmount - blockAmount, }); 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 const payload = { ioNumber: ioNumber.trim(), ioRemark: ioRemark.trim(), ioAvailableBalance: fetchedAmount, ioBlockedAmount: blockAmount, ioRemainingBalance: fetchedAmount - blockAmount, // Calculated value (backend will use SAP's actual value) }; console.log('[IOTab] Sending to backend:', payload); 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 || (fetchedAmount - blockAmount)); // Log what was saved vs what we sent console.log('[IOTab] Blocking result:', { sentAmount: blockAmount, savedBlockedAmount, sentRemaining: fetchedAmount - blockAmount, savedRemainingBalance, availableBalance: fetchedAmount, difference: savedBlockedAmount - blockAmount, }); // 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); } 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 savedIoRemark = updatedInternalOrder.ioRemark || updatedInternalOrder.io_remark || ioRemark.trim(); 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 || '', ioRemark: savedIoRemark, 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" />
{/* IO Remark Input */}