500 lines
22 KiB
TypeScript
500 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, 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';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { DollarSign, Download, CircleCheckBig, Target, CircleAlert } 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' | 'pending';
|
|
}
|
|
|
|
export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|
const { user } = useAuth();
|
|
const requestId = apiRequest?.requestId || request?.requestId;
|
|
|
|
// Get organizer user object from association (organizer) or fallback to organizedBy UUID
|
|
const proposalDetails = apiRequest?.proposalDetails || {};
|
|
const claimDetails = apiRequest?.claimDetails || apiRequest || {};
|
|
|
|
// 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)
|
|
// 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 internalOrdersList = apiRequest?.internalOrders || apiRequest?.internal_orders || request?.internalOrders || [];
|
|
const isAdditionalBlockingNeeded = budgetStatus === 'PROPOSED' && internalOrdersList.length > 0;
|
|
|
|
const [ioNumber, setIoNumber] = useState('');
|
|
const [fetchingAmount, setFetchingAmount] = useState(false);
|
|
const [fetchedAmount, setFetchedAmount] = useState<number | null>(null);
|
|
const [amountToBlock, setAmountToBlock] = useState<string>('');
|
|
const [blockedIOs, setBlockedIOs] = useState<IOBlockedDetails[]>([]);
|
|
const [blockingBudget, setBlockingBudget] = useState(false);
|
|
|
|
// Load existing IO blocks
|
|
useEffect(() => {
|
|
if (internalOrdersList.length > 0) {
|
|
const formattedIOs = internalOrdersList.map((io: any) => {
|
|
const org = io.organizer || null;
|
|
const blockedByName = org?.displayName ||
|
|
org?.display_name ||
|
|
org?.name ||
|
|
(org?.firstName && org?.lastName ? `${org.firstName} ${org.lastName}`.trim() : null) ||
|
|
org?.email ||
|
|
'Unknown User';
|
|
return {
|
|
ioNumber: io.ioNumber || io.io_number,
|
|
blockedAmount: Number(io.ioBlockedAmount || io.io_blocked_amount || 0),
|
|
availableBalance: Number(io.ioAvailableBalance || io.io_available_balance || 0),
|
|
remainingBalance: Number(io.ioRemainingBalance || io.io_remaining_balance || 0),
|
|
blockedDate: io.organizedAt || io.organized_at || new Date().toISOString(),
|
|
blockedBy: blockedByName,
|
|
sapDocumentNumber: io.sapDocumentNumber || io.sap_document_number || '',
|
|
status: (io.status === 'BLOCKED' ? 'blocked' :
|
|
io.status === 'RELEASED' ? 'released' :
|
|
io.status === 'PENDING' ? 'pending' : 'blocked') as any,
|
|
};
|
|
});
|
|
setBlockedIOs(formattedIOs);
|
|
|
|
// If we are not in Scenario 2 (additional blocking), set the IO number from the last block for convenience
|
|
if (!isAdditionalBlockingNeeded && formattedIOs.length > 0) {
|
|
setIoNumber(formattedIOs[formattedIOs.length - 1].ioNumber);
|
|
}
|
|
}
|
|
}, [apiRequest, request, isAdditionalBlockingNeeded, internalOrdersList]);
|
|
|
|
/**
|
|
* 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);
|
|
|
|
// 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.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');
|
|
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;
|
|
}
|
|
|
|
|
|
// Calculate total already blocked
|
|
const totalAlreadyBlocked = blockedIOs.reduce((sum, io) => sum + io.blockedAmount, 0);
|
|
const totalPlanned = totalAlreadyBlocked + blockAmount;
|
|
|
|
// Validate that total planned must exactly match estimated budget
|
|
if (estimatedBudget > 0) {
|
|
const roundedEstimatedBudget = parseFloat(estimatedBudget.toFixed(2));
|
|
const roundedTotalPlanned = parseFloat(totalPlanned.toFixed(2));
|
|
|
|
if (Math.abs(roundedTotalPlanned - roundedEstimatedBudget) > 0.01) {
|
|
toast.error(`Total blocked amount (₹${roundedTotalPlanned.toLocaleString('en-IN')}) must be exactly equal to the estimated budget (₹${roundedEstimatedBudget.toLocaleString('en-IN')})`);
|
|
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',
|
|
};
|
|
|
|
setBlockedIOs(prev => [...prev, blocked]);
|
|
setAmountToBlock(''); // Clear the input
|
|
setFetchedAmount(null); // Reset fetched state
|
|
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 || (blockedIOs.length > 0 && !isAdditionalBlockingNeeded)}
|
|
className="flex-1"
|
|
/>
|
|
<Button
|
|
onClick={handleFetchAmount}
|
|
disabled={!ioNumber.trim() || fetchingAmount || (blockedIOs.length > 0 && !isAdditionalBlockingNeeded)}
|
|
className="bg-[#2d4a3e] hover:bg-[#1f3329]"
|
|
>
|
|
<Download className="w-4 h-4 mr-2" />
|
|
{fetchingAmount ? 'Fetching...' : 'Fetch Amount'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Instructions when IO number is entered but not fetched */}
|
|
{!fetchedAmount && blockedIOs.length === 0 && ioNumber.trim() && (
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<p className="text-sm text-blue-800">
|
|
<strong>Next Step:</strong> Click "Fetch Amount" to validate the IO number and get available balance from SAP.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Fetched Amount Display */}
|
|
{fetchedAmount !== null && (blockedIOs.length === 0 || isAdditionalBlockingNeeded) && (
|
|
<>
|
|
<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>
|
|
{estimatedBudget > 0 && (
|
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
|
<p className="text-xs text-amber-800">
|
|
<strong>Required:</strong> Amount must be exactly equal to the estimated budget: <strong>₹{estimatedBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</strong>
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Block Button */}
|
|
<Button
|
|
onClick={handleBlockBudget}
|
|
disabled={
|
|
blockingBudget ||
|
|
!amountToBlock ||
|
|
parseFloat(amountToBlock) <= 0 ||
|
|
parseFloat(amountToBlock) > fetchedAmount ||
|
|
(estimatedBudget > 0 && Math.abs((blockedIOs.reduce((s, i) => s + i.blockedAmount, 0) + parseFloat(amountToBlock)) - estimatedBudget) > 0.01)
|
|
}
|
|
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>
|
|
{blockedIOs.length > 0 ? (
|
|
<div className="space-y-6">
|
|
{isAdditionalBlockingNeeded && (
|
|
<div className="bg-amber-50 border-2 border-amber-500 rounded-lg p-4 animate-pulse">
|
|
<div className="flex items-start gap-3">
|
|
<CircleAlert className="w-6 h-6 text-amber-600 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p className="font-semibold text-amber-900">Additional Budget Blocking Required</p>
|
|
<p className="text-sm text-amber-700 mt-1">Actual expenses exceed the previously blocked amount. Please block an additional ₹{(estimatedBudget - blockedIOs.reduce((s, i) => s + i.blockedAmount, 0)).toLocaleString('en-IN', { minimumFractionDigits: 2 })}.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{blockedIOs.slice().reverse().map((io, idx) => (
|
|
<div key={idx} className="border rounded-lg overflow-hidden">
|
|
<div className={`p-3 flex justify-between items-center ${idx === 0 ? 'bg-green-50' : 'bg-gray-50'}`}>
|
|
<span className="font-semibold text-sm">IO: {io.ioNumber}</span>
|
|
<Badge className={
|
|
io.status === 'blocked' ? 'bg-green-100 text-green-800' :
|
|
io.status === 'pending' ? 'bg-amber-100 text-amber-800' :
|
|
'bg-blue-100 text-blue-800'
|
|
}>
|
|
{io.status === 'blocked' ? 'Blocked' :
|
|
io.status === 'pending' ? 'Provisioned' : 'Released'}
|
|
</Badge>
|
|
</div>
|
|
<div className="grid grid-cols-2 divide-x divide-y">
|
|
<div className="p-3">
|
|
<p className="text-[10px] text-gray-500 uppercase">Amount</p>
|
|
<p className="text-sm font-bold text-green-700">₹{io.blockedAmount.toLocaleString('en-IN')}</p>
|
|
</div>
|
|
<div className="p-3">
|
|
<p className="text-[10px] text-gray-500 uppercase">SAP Doc</p>
|
|
<p className="text-sm font-medium">{io.sapDocumentNumber || 'N/A'}</p>
|
|
</div>
|
|
<div className="p-3">
|
|
<p className="text-[10px] text-gray-500 uppercase">Blocked By</p>
|
|
<p className="text-xs">{io.blockedBy}</p>
|
|
</div>
|
|
<div className="p-3">
|
|
<p className="text-[10px] text-gray-500 uppercase">Date</p>
|
|
<p className="text-[10px]">{new Date(io.blockedDate).toLocaleString()}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<div className="mt-4 p-4 bg-[#2d4a3e] text-white rounded-lg flex justify-between items-center">
|
|
<span className="font-bold">Total Blocked:</span>
|
|
<span className="text-xl font-bold">₹{blockedIOs.reduce((s, i) => s + i.blockedAmount, 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}</span>
|
|
</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>
|
|
);
|
|
}
|