Re_Figma_Code/src/dealer-claim/components/request-detail/IOTab.tsx

534 lines
22 KiB
TypeScript

/**
* 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<number | null>(null);
const [amountToBlock, setAmountToBlock] = useState<string>('');
const [blockedDetails, setBlockedDetails] = useState<IOBlockedDetails | null>(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 (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* IO Budget Management Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="w-5 h-5 text-[#2d4a3e]" />
IO Budget Management
</CardTitle>
<CardDescription>
Enter IO number to fetch available budget from SAP
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* IO Number Input */}
<div className="space-y-3">
<Label htmlFor="ioNumber">IO Number *</Label>
<div className="flex gap-2">
<Input
id="ioNumber"
placeholder="Enter IO number (e.g., IO-2024-12345)"
value={ioNumber}
onChange={(e) => setIoNumber(e.target.value)}
disabled={fetchingAmount || !!blockedDetails}
className="flex-1"
/>
<Button
onClick={handleFetchAmount}
disabled={!ioNumber.trim() || fetchingAmount || !!blockedDetails}
className="bg-[#2d4a3e] hover:bg-[#1f3329]"
>
<Download className="w-4 h-4 mr-2" />
{fetchingAmount ? 'Fetching...' : 'Fetch Amount'}
</Button>
</div>
</div>
{/* IO Remark Input */}
<div className="space-y-2">
<Label htmlFor="ioRemark" className="text-sm font-medium text-gray-900 flex items-center gap-2">
IO Remark <span className="text-red-500">*</span>
</Label>
<Textarea
id="ioRemark"
placeholder="Enter remarks about IO organization"
value={ioRemark}
onChange={(e) => {
const value = e.target.value;
if (value.length <= maxIoRemarkChars) {
setIoRemark(value);
}
}}
rows={3}
disabled={!!blockedDetails}
className="bg-white text-sm min-h-[80px] resize-none"
required
/>
<div className="flex justify-end text-xs text-gray-600">
<span>{ioRemarkChars}/{maxIoRemarkChars}</span>
</div>
</div>
{/* Save IO Details Button (shown when IO number is entered but amount not fetched) */}
{!fetchedAmount && !blockedDetails && ioNumber.trim() && (
<Button
onClick={handleSaveIODetails}
disabled={blockingBudget || !ioNumber.trim() || !ioRemark.trim()}
variant="outline"
className="w-full border-[#2d4a3e] text-[#2d4a3e] hover:bg-[#2d4a3e] hover:text-white"
>
{blockingBudget ? 'Saving...' : 'Save IO Details'}
</Button>
)}
{/* Fetched Amount Display */}
{fetchedAmount !== null && !blockedDetails && (
<>
<div className="bg-green-50 border-2 border-green-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600 uppercase tracking-wide mb-1">Available Amount</p>
<p className="text-2xl font-bold text-green-700">
{fetchedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</p>
</div>
<CircleCheckBig className="w-8 h-8 text-green-600" />
</div>
<div className="mt-3 pt-3 border-t border-green-200">
<p className="text-xs text-gray-600"><strong>IO Number:</strong> {ioNumber}</p>
<p className="text-xs text-gray-600 mt-1"><strong>Fetched from:</strong> SAP System</p>
</div>
</div>
{/* Amount to Block Input */}
<div className="space-y-3">
<Label htmlFor="blockAmount">Amount to Block *</Label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></span>
<Input
type="number"
id="blockAmount"
placeholder="Enter amount to block"
min="0"
step="0.01"
value={amountToBlock}
onChange={(e) => setAmountToBlock(e.target.value)}
className="pl-8"
/>
</div>
</div>
{/* Block Button */}
<Button
onClick={handleBlockBudget}
disabled={blockingBudget || !amountToBlock || parseFloat(amountToBlock) <= 0 || parseFloat(amountToBlock) > fetchedAmount}
className="w-full bg-[#2d4a3e] hover:bg-[#1f3329]"
>
<Target className="w-4 h-4 mr-2" />
{blockingBudget ? 'Blocking in SAP...' : 'Block IO in SAP'}
</Button>
</>
)}
</CardContent>
</Card>
{/* IO Blocked Details Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CircleCheckBig className="w-5 h-5 text-green-600" />
IO Blocked Details
</CardTitle>
<CardDescription>
Details of IO blocked in SAP system
</CardDescription>
</CardHeader>
<CardContent>
{blockedDetails ? (
<div className="space-y-4">
{/* Success Banner */}
<div className="bg-green-50 border-2 border-green-500 rounded-lg p-4">
<div className="flex items-start gap-3">
<CircleCheckBig className="w-6 h-6 text-green-600 flex-shrink-0 mt-0.5" />
<div>
<p className="font-semibold text-green-900">IO Blocked Successfully</p>
<p className="text-sm text-green-700 mt-1">Budget has been reserved in SAP system</p>
</div>
</div>
</div>
{/* Blocked Details */}
<div className="border rounded-lg divide-y">
<div className="p-4">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">IO Number</p>
<p className="text-sm font-semibold text-gray-900">{blockedDetails.ioNumber}</p>
</div>
<div className="p-4">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">SAP Document Number</p>
<p className="text-sm font-semibold text-gray-900">{blockedDetails.sapDocumentNumber || 'N/A'}</p>
</div>
{blockedDetails.ioRemark && (
<div className="p-4">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">IO Remark</p>
<p className="text-sm font-medium text-gray-900 whitespace-pre-wrap">{blockedDetails.ioRemark}</p>
</div>
)}
<div className="p-4 bg-green-50">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Blocked Amount</p>
<p className="text-xl font-bold text-green-700">
{blockedDetails.blockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</p>
</div>
<div className="p-4">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Available Amount (Before Block)</p>
<p className="text-sm font-medium text-gray-900">
{blockedDetails.availableBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</p>
</div>
<div className="p-4 bg-blue-50">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Remaining Amount (After Block)</p>
<p className="text-sm font-bold text-blue-700">
{blockedDetails.remainingBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</p>
</div>
<div className="p-4">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Blocked By</p>
<p className="text-sm font-medium text-gray-900">{blockedDetails.blockedBy}</p>
</div>
<div className="p-4">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Blocked At</p>
<p className="text-sm font-medium text-gray-900">
{new Date(blockedDetails.blockedDate).toLocaleString('en-IN', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true
})}
</p>
</div>
<div className="p-4 bg-gray-50">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Status</p>
<Badge className="bg-green-100 text-green-800 border-green-200">
<CircleCheckBig className="w-3 h-3 mr-1" />
Blocked
</Badge>
</div>
</div>
</div>
) : (
<div className="text-center py-12">
<DollarSign className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-sm text-gray-500 mb-2">No IO blocked yet</p>
<p className="text-xs text-gray-400">
Enter IO number and fetch amount to block budget
</p>
</div>
)}
</CardContent>
</Card>
</div>
);
}