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

394 lines
17 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 { 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;
const [ioNumber, setIoNumber] = useState(existingIONumber);
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);
// Load existing IO block details from apiRequest
useEffect(() => {
if (internalOrder && existingIONumber && existingBlockedAmount > 0) {
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';
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,
status: (internalOrder.status === 'BLOCKED' ? 'blocked' :
internalOrder.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed',
});
setIoNumber(existingIONumber);
// 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 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);
}
};
/**
* 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 blockAmount = parseFloat(amountToBlock);
if (!amountToBlock || isNaN(blockAmount) || blockAmount <= 0) {
toast.error('Please enter a valid amount to block');
return;
}
if (blockAmount > fetchedAmount) {
toast.error('Amount to block exceeds available IO budget');
return;
}
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
await updateIODetails(requestId, {
ioNumber: ioNumber.trim(),
ioAvailableBalance: fetchedAmount,
ioBlockedAmount: blockAmount,
ioRemainingBalance: fetchedAmount - blockAmount,
});
// 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 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: Number(updatedInternalOrder.ioBlockedAmount || updatedInternalOrder.io_blocked_amount || blockAmount),
availableBalance: fetchedAmount, // Available amount before block
remainingBalance: Number(updatedInternalOrder.ioRemainingBalance || updatedInternalOrder.io_remaining_balance || (fetchedAmount - blockAmount)),
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 (
<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>
{/* 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>
<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>
);
}