dealer from external source implemented and re-iteration and multiple io block implemented need to test end to end
This commit is contained in:
parent
b04776a5f8
commit
e11f13d248
@ -29,7 +29,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getAllDealers as fetchDealersFromAPI, verifyDealerLogin, type DealerInfo } from '@/services/dealerApi';
|
import { verifyDealerLogin, searchExternalDealerByCode, type DealerInfo } from '@/services/dealerApi';
|
||||||
import { ClaimApproverSelectionStep } from './ClaimApproverSelectionStep';
|
import { ClaimApproverSelectionStep } from './ClaimApproverSelectionStep';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { getPublicConfigurations, AdminConfiguration, getActivityTypes, type ActivityType } from '@/services/adminApi';
|
import { getPublicConfigurations, AdminConfiguration, getActivityTypes, type ActivityType } from '@/services/adminApi';
|
||||||
@ -194,10 +194,26 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
// Debounce search
|
// Debounce search
|
||||||
dealerSearchTimer.current = setTimeout(async () => {
|
dealerSearchTimer.current = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const results = await fetchDealersFromAPI(value, 10); // Limit to 10 results
|
const result = await searchExternalDealerByCode(value);
|
||||||
setDealerSearchResults(results);
|
if (result) {
|
||||||
|
// Map external API response to DealerInfo structure
|
||||||
|
const mappedDealer: DealerInfo = {
|
||||||
|
dealerId: result.dealer || result.dealer_code || value,
|
||||||
|
dealerCode: result.dealer || result.dealer_code || value,
|
||||||
|
dealerName: result['dealer name'] || result.dealer_name || 'Unknown Dealer',
|
||||||
|
displayName: result['dealer name'] || result.dealer_name || 'Unknown Dealer',
|
||||||
|
email: result['dealer email'] || '',
|
||||||
|
phone: result['dealer phone'] || '',
|
||||||
|
city: result['re city'] || result.city || '',
|
||||||
|
state: result['re state code'] || result.state || '',
|
||||||
|
isLoggedIn: true, // We'll verify this in the next step
|
||||||
|
};
|
||||||
|
setDealerSearchResults([mappedDealer]);
|
||||||
|
} else {
|
||||||
|
setDealerSearchResults([]);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error searching dealers:', error);
|
console.error('Error searching external dealer:', error);
|
||||||
setDealerSearchResults([]);
|
setDealerSearchResults([]);
|
||||||
} finally {
|
} finally {
|
||||||
setDealerSearchLoading(false);
|
setDealerSearchLoading(false);
|
||||||
@ -882,8 +898,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`${approver.level}-${approver.email}`} className={`p-3 rounded-lg border ${
|
<div key={`${approver.level}-${approver.email}`} className={`p-3 rounded-lg border ${approver.isAdditional ? 'bg-purple-50 border-purple-200' : 'bg-gray-50 border-gray-200'
|
||||||
approver.isAdditional ? 'bg-purple-50 border-purple-200' : 'bg-gray-50 border-gray-200'
|
|
||||||
}`}>
|
}`}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -1050,8 +1065,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
{STEP_NAMES.map((_name, index) => (
|
{STEP_NAMES.map((_name, index) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
className={`text-xs sm:text-sm ${
|
className={`text-xs sm:text-sm ${index + 1 <= currentStep ? 'text-blue-600 font-medium' : 'text-gray-400'
|
||||||
index + 1 <= currentStep ? 'text-blue-600 font-medium' : 'text-gray-400'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
@ -1085,8 +1099,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
{currentStep < totalSteps ? (
|
{currentStep < totalSteps ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={nextStep}
|
onClick={nextStep}
|
||||||
className={`gap-2 w-full sm:w-auto order-1 sm:order-2 ${
|
className={`gap-2 w-full sm:w-auto order-1 sm:order-2 ${!isStepValid()
|
||||||
!isStepValid()
|
|
||||||
? 'opacity-50 cursor-pointer hover:opacity-60'
|
? 'opacity-50 cursor-pointer hover:opacity-60'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { DollarSign, Download, CircleCheckBig, Target } from 'lucide-react';
|
import { DollarSign, Download, CircleCheckBig, Target, CircleAlert } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { validateIO, updateIODetails, getClaimDetails } from '@/services/dealerClaimApi';
|
import { validateIO, updateIODetails, getClaimDetails } from '@/services/dealerClaimApi';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
@ -37,88 +37,59 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const requestId = apiRequest?.requestId || request?.requestId;
|
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
|
// Get organizer user object from association (organizer) or fallback to organizedBy UUID
|
||||||
const organizer = internalOrder?.organizer || null;
|
|
||||||
|
|
||||||
// Get estimated budget from proposal details
|
// Get estimated budget from proposal details or DealerClaimDetails
|
||||||
const proposalDetails = apiRequest?.proposalDetails || {};
|
const proposalDetails = apiRequest?.proposalDetails || {};
|
||||||
const estimatedBudget = Number(proposalDetails?.totalEstimatedBudget || proposalDetails?.total_estimated_budget || 0);
|
const claimDetails = apiRequest?.claimDetails || apiRequest || {};
|
||||||
|
|
||||||
const [ioNumber, setIoNumber] = useState(existingIONumber);
|
// Use totalProposedTaxableAmount if available (for Scenario 2), fallback to estimated budget
|
||||||
|
const estimatedBudget = Number(claimDetails?.totalProposedTaxableAmount || proposalDetails?.totalEstimatedBudget || proposalDetails?.total_estimated_budget || 0);
|
||||||
|
|
||||||
|
// Budget status for signaling (Scenario 2)
|
||||||
|
const budgetTracking = apiRequest?.budgetTracking || apiRequest?.budget_tracking || {};
|
||||||
|
const budgetStatus = budgetTracking?.budgetStatus || budgetTracking?.budget_status || '';
|
||||||
|
const isAdditionalBlockingNeeded = budgetStatus === 'PROPOSED' && (apiRequest?.internalOrders?.length > 0 || apiRequest?.internal_orders?.length > 0);
|
||||||
|
|
||||||
|
const [ioNumber, setIoNumber] = useState('');
|
||||||
const [fetchingAmount, setFetchingAmount] = useState(false);
|
const [fetchingAmount, setFetchingAmount] = useState(false);
|
||||||
const [fetchedAmount, setFetchedAmount] = useState<number | null>(null);
|
const [fetchedAmount, setFetchedAmount] = useState<number | null>(null);
|
||||||
const [amountToBlock, setAmountToBlock] = useState<string>('');
|
const [amountToBlock, setAmountToBlock] = useState<string>('');
|
||||||
const [blockedDetails, setBlockedDetails] = useState<IOBlockedDetails | null>(null);
|
const [blockedIOs, setBlockedIOs] = useState<IOBlockedDetails[]>([]);
|
||||||
const [blockingBudget, setBlockingBudget] = useState(false);
|
const [blockingBudget, setBlockingBudget] = useState(false);
|
||||||
|
|
||||||
// Load existing IO block details from apiRequest
|
// Load existing IO blocks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (internalOrder && existingIONumber) {
|
const ios = apiRequest?.internalOrders || apiRequest?.internal_orders || [];
|
||||||
// IMPORTANT: ioAvailableBalance is already the available balance BEFORE blocking
|
if (ios.length > 0) {
|
||||||
// We should NOT add blockedAmount to it - that would cause double deduction
|
const formattedIOs = ios.map((io: any) => {
|
||||||
// Backend stores: availableBalance (before block), blockedAmount, remainingBalance (after block)
|
const org = io.organizer || null;
|
||||||
const availableBeforeBlock = Number(existingAvailableBalance) || 0;
|
const blockedByName = org?.displayName ||
|
||||||
|
org?.display_name ||
|
||||||
// Get blocked by user name from organizer association (who blocked the amount)
|
org?.name ||
|
||||||
// When amount is blocked, organizedBy stores the user who blocked it
|
(org?.firstName && org?.lastName ? `${org.firstName} ${org.lastName}`.trim() : null) ||
|
||||||
const blockedByName = organizer?.displayName ||
|
org?.email ||
|
||||||
organizer?.display_name ||
|
|
||||||
organizer?.name ||
|
|
||||||
(organizer?.firstName && organizer?.lastName ? `${organizer.firstName} ${organizer.lastName}`.trim() : null) ||
|
|
||||||
organizer?.email ||
|
|
||||||
'Unknown User';
|
'Unknown User';
|
||||||
|
return {
|
||||||
// Set IO number from existing data
|
ioNumber: io.ioNumber || io.io_number,
|
||||||
setIoNumber(existingIONumber);
|
blockedAmount: Number(io.ioBlockedAmount || io.io_blocked_amount || 0),
|
||||||
|
availableBalance: Number(io.ioAvailableBalance || io.io_available_balance || 0),
|
||||||
// Only set blocked details if amount is blocked
|
remainingBalance: Number(io.ioRemainingBalance || io.io_remaining_balance || 0),
|
||||||
if (existingBlockedAmount > 0) {
|
blockedDate: io.organizedAt || io.organized_at || new Date().toISOString(),
|
||||||
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,
|
blockedBy: blockedByName,
|
||||||
sapDocumentNumber: sapDocNumber,
|
sapDocumentNumber: io.sapDocumentNumber || io.sap_document_number || '',
|
||||||
status: (internalOrder.status === 'BLOCKED' ? 'blocked' :
|
status: (io.status === 'BLOCKED' ? 'blocked' :
|
||||||
internalOrder.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed',
|
io.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
setBlockedIOs(formattedIOs);
|
||||||
|
|
||||||
// Set fetched amount if available balance exists
|
// If we are not in Scenario 2 (additional blocking), set the IO number from the last block for convenience
|
||||||
if (availableBeforeBlock > 0) {
|
if (!isAdditionalBlockingNeeded && formattedIOs.length > 0) {
|
||||||
setFetchedAmount(availableBeforeBlock);
|
setIoNumber(formattedIOs[formattedIOs.length - 1].ioNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [apiRequest, isAdditionalBlockingNeeded]);
|
||||||
}, [internalOrder, existingIONumber, existingBlockedAmount, existingAvailableBalance, existingRemainingBalance, sapDocNumber, organizer]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch available budget from SAP
|
* Fetch available budget from SAP
|
||||||
@ -199,11 +170,18 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that amount to block must exactly match estimated budget
|
|
||||||
|
// 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) {
|
if (estimatedBudget > 0) {
|
||||||
const roundedEstimatedBudget = parseFloat(estimatedBudget.toFixed(2));
|
const roundedEstimatedBudget = parseFloat(estimatedBudget.toFixed(2));
|
||||||
if (Math.abs(blockAmount - roundedEstimatedBudget) > 0.01) {
|
const roundedTotalPlanned = parseFloat(totalPlanned.toFixed(2));
|
||||||
toast.error(`Amount to block must be exactly equal to the estimated budget (₹${roundedEstimatedBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -279,8 +257,9 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
status: 'blocked',
|
status: 'blocked',
|
||||||
};
|
};
|
||||||
|
|
||||||
setBlockedDetails(blocked);
|
setBlockedIOs(prev => [...prev, blocked]);
|
||||||
setAmountToBlock(''); // Clear the input
|
setAmountToBlock(''); // Clear the input
|
||||||
|
setFetchedAmount(null); // Reset fetched state
|
||||||
toast.success('IO budget blocked successfully in SAP');
|
toast.success('IO budget blocked successfully in SAP');
|
||||||
|
|
||||||
// Refresh request details
|
// Refresh request details
|
||||||
@ -321,12 +300,12 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
placeholder="Enter IO number (e.g., IO-2024-12345)"
|
placeholder="Enter IO number (e.g., IO-2024-12345)"
|
||||||
value={ioNumber}
|
value={ioNumber}
|
||||||
onChange={(e) => setIoNumber(e.target.value)}
|
onChange={(e) => setIoNumber(e.target.value)}
|
||||||
disabled={fetchingAmount || !!blockedDetails}
|
disabled={fetchingAmount || (blockedIOs.length > 0 && !isAdditionalBlockingNeeded)}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleFetchAmount}
|
onClick={handleFetchAmount}
|
||||||
disabled={!ioNumber.trim() || fetchingAmount || !!blockedDetails}
|
disabled={!ioNumber.trim() || fetchingAmount || (blockedIOs.length > 0 && !isAdditionalBlockingNeeded)}
|
||||||
className="bg-[#2d4a3e] hover:bg-[#1f3329]"
|
className="bg-[#2d4a3e] hover:bg-[#1f3329]"
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4 mr-2" />
|
<Download className="w-4 h-4 mr-2" />
|
||||||
@ -336,7 +315,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Instructions when IO number is entered but not fetched */}
|
{/* Instructions when IO number is entered but not fetched */}
|
||||||
{!fetchedAmount && !blockedDetails && ioNumber.trim() && (
|
{!fetchedAmount && blockedIOs.length === 0 && ioNumber.trim() && (
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
<p className="text-sm text-blue-800">
|
<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.
|
<strong>Next Step:</strong> Click "Fetch Amount" to validate the IO number and get available balance from SAP.
|
||||||
@ -345,7 +324,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Fetched Amount Display */}
|
{/* Fetched Amount Display */}
|
||||||
{fetchedAmount !== null && !blockedDetails && (
|
{fetchedAmount !== null && (blockedIOs.length === 0 || isAdditionalBlockingNeeded) && (
|
||||||
<>
|
<>
|
||||||
<div className="bg-green-50 border-2 border-green-200 rounded-lg p-4">
|
<div className="bg-green-50 border-2 border-green-200 rounded-lg p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -396,7 +375,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
!amountToBlock ||
|
!amountToBlock ||
|
||||||
parseFloat(amountToBlock) <= 0 ||
|
parseFloat(amountToBlock) <= 0 ||
|
||||||
parseFloat(amountToBlock) > fetchedAmount ||
|
parseFloat(amountToBlock) > fetchedAmount ||
|
||||||
(estimatedBudget > 0 && Math.abs(parseFloat(amountToBlock) - estimatedBudget) > 0.01)
|
(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]"
|
className="w-full bg-[#2d4a3e] hover:bg-[#1f3329]"
|
||||||
>
|
>
|
||||||
@ -420,71 +399,52 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{blockedDetails ? (
|
{blockedIOs.length > 0 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
{/* Success Banner */}
|
{isAdditionalBlockingNeeded && (
|
||||||
<div className="bg-green-50 border-2 border-green-500 rounded-lg p-4">
|
<div className="bg-amber-50 border-2 border-amber-500 rounded-lg p-4 animate-pulse">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<CircleCheckBig className="w-6 h-6 text-green-600 flex-shrink-0 mt-0.5" />
|
<CircleAlert className="w-6 h-6 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-green-900">IO Blocked Successfully</p>
|
<p className="font-semibold text-amber-900">Additional Budget Blocking Required</p>
|
||||||
<p className="text-sm text-green-700 mt-1">Budget has been reserved in SAP system</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Blocked Details */}
|
{blockedIOs.slice().reverse().map((io, idx) => (
|
||||||
<div className="border rounded-lg divide-y">
|
<div key={idx} className="border rounded-lg overflow-hidden">
|
||||||
<div className="p-4">
|
<div className={`p-3 flex justify-between items-center ${idx === 0 ? 'bg-green-50' : 'bg-gray-50'}`}>
|
||||||
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">IO Number</p>
|
<span className="font-semibold text-sm">IO: {io.ioNumber}</span>
|
||||||
<p className="text-sm font-semibold text-gray-900">{blockedDetails.ioNumber}</p>
|
<Badge className={io.status === 'blocked' ? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800'}>
|
||||||
</div>
|
{io.status === 'blocked' ? 'Blocked' : 'Released'}
|
||||||
<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>
|
</Badge>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1094,6 +1094,45 @@ export function DealerClaimWorkflowTab({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle re-quotation request from Claim Approval step (Step 6)
|
||||||
|
const handleClaimReQuotation = async (comments: string) => {
|
||||||
|
try {
|
||||||
|
if (!request?.id && !request?.requestId) {
|
||||||
|
throw new Error('Request ID not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = request.id || request.requestId;
|
||||||
|
|
||||||
|
// Get workflow details to find the Requestor Claim Approval levelId dynamically
|
||||||
|
const details = await getWorkflowDetails(requestId);
|
||||||
|
const approvals = details?.approvalLevels || details?.approvals || [];
|
||||||
|
|
||||||
|
// Find the Requestor Claim Approval step
|
||||||
|
const claimApprovalLevel = approvals.find((level: any) => {
|
||||||
|
const levelName = (level.levelName || level.level_name || '').toLowerCase();
|
||||||
|
return levelName.includes('requestor claim') || levelName.includes('requestor - claim');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!claimApprovalLevel?.levelId && !claimApprovalLevel?.level_id) {
|
||||||
|
throw new Error('Claim approval level not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelId = claimApprovalLevel.levelId || claimApprovalLevel.level_id;
|
||||||
|
|
||||||
|
// Reject the claim approval step with 'Revised Quotation Requested'
|
||||||
|
// This will trigger the backend to return the workflow to the Dealer Proposal step
|
||||||
|
await rejectLevel(requestId, levelId, 'Revised Quotation Requested', comments);
|
||||||
|
|
||||||
|
toast.success('Re-quotation requested. Request returned to dealer.');
|
||||||
|
handleRefresh();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to request re-quotation:', error);
|
||||||
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to request re-quotation. Please try again.';
|
||||||
|
toast.error(errorMessage);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle IO approval (Department Lead step - found dynamically)
|
// Handle IO approval (Department Lead step - found dynamically)
|
||||||
const handleIOApproval = async (data: {
|
const handleIOApproval = async (data: {
|
||||||
ioNumber: string;
|
ioNumber: string;
|
||||||
@ -2564,6 +2603,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
requestTitle={request?.title}
|
requestTitle={request?.title}
|
||||||
requestNumber={request?.requestNumber || request?.request_number || request?.id}
|
requestNumber={request?.requestNumber || request?.request_number || request?.id}
|
||||||
taxationType={request?.claimDetails?.taxationType}
|
taxationType={request?.claimDetails?.taxationType}
|
||||||
|
onReQuotation={handleClaimReQuotation}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Credit Note from SAP Modal (Step 8) */}
|
{/* Credit Note from SAP Modal (Step 8) */}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Eye,
|
Eye,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
RefreshCw,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi';
|
import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi';
|
||||||
@ -80,6 +81,7 @@ interface DMSPushModalProps {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onPush: (comments: string) => Promise<void>;
|
onPush: (comments: string) => Promise<void>;
|
||||||
|
onReQuotation?: (comments: string) => Promise<void>;
|
||||||
completionDetails?: CompletionDetails | null;
|
completionDetails?: CompletionDetails | null;
|
||||||
ioDetails?: IODetails | null;
|
ioDetails?: IODetails | null;
|
||||||
completionDocuments?: CompletionDocuments | null;
|
completionDocuments?: CompletionDocuments | null;
|
||||||
@ -92,6 +94,7 @@ export function DMSPushModal({
|
|||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onPush,
|
onPush,
|
||||||
|
onReQuotation,
|
||||||
completionDetails,
|
completionDetails,
|
||||||
ioDetails,
|
ioDetails,
|
||||||
completionDocuments,
|
completionDocuments,
|
||||||
@ -257,6 +260,30 @@ export function DMSPushModal({
|
|||||||
setComments('');
|
setComments('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReQuotation = async () => {
|
||||||
|
if (!comments.trim()) {
|
||||||
|
toast.error('Please provide comments (reason) for re-quotation request');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!onReQuotation) {
|
||||||
|
toast.error('Re-quotation handler not provided');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
await onReQuotation(comments.trim());
|
||||||
|
handleReset();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to request re-quotation:', error);
|
||||||
|
// Error is handled in the parent handler
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (!submitting) {
|
if (!submitting) {
|
||||||
handleReset();
|
handleReset();
|
||||||
@ -805,6 +832,21 @@ export function DMSPushModal({
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
{onReQuotation && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-orange-500 text-orange-600 hover:bg-orange-50"
|
||||||
|
onClick={handleReQuotation}
|
||||||
|
disabled={!comments.trim() || submitting}
|
||||||
|
>
|
||||||
|
{submitting ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
Request Re-quotation
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={!comments.trim() || submitting}
|
disabled={!comments.trim() || submitting}
|
||||||
|
|||||||
@ -670,7 +670,6 @@ export function DealerCompletionDocumentsModal({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-semibold text-base sm:text-lg">Closed Expenses</h3>
|
<h3 className="font-semibold text-base sm:text-lg">Closed Expenses</h3>
|
||||||
<Badge className="bg-secondary text-secondary-foreground text-xs">Optional</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
{!isNonGst && (
|
{!isNonGst && (
|
||||||
<div className="text-[10px] text-gray-500 italic mt-0.5">
|
<div className="text-[10px] text-gray-500 italic mt-0.5">
|
||||||
@ -743,9 +742,9 @@ export function DealerCompletionDocumentsModal({
|
|||||||
{!isNonGst && (
|
{!isNonGst && (
|
||||||
<>
|
<>
|
||||||
<div className="w-20 sm:w-24 flex-shrink-0">
|
<div className="w-20 sm:w-24 flex-shrink-0">
|
||||||
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">HSN Code</Label>
|
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">HSN/SAC Code</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="HSN"
|
placeholder="HSN/SAC Code"
|
||||||
value={item.hsnCode || ''}
|
value={item.hsnCode || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleExpenseChange(item.id, 'hsnCode', e.target.value)
|
handleExpenseChange(item.id, 'hsnCode', e.target.value)
|
||||||
|
|||||||
@ -902,7 +902,7 @@ export function DealerProposalSubmissionModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-28 sm:w-32">
|
<div className="w-28 sm:w-32">
|
||||||
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">HSN/SAC</Label>
|
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">HSN/SAC Code</Label>
|
||||||
<Input
|
<Input
|
||||||
value={item.hsnCode || ''}
|
value={item.hsnCode || ''}
|
||||||
onChange={(e) => handleCostItemChange(item.id, 'hsnCode', e.target.value)}
|
onChange={(e) => handleCostItemChange(item.id, 'hsnCode', e.target.value)}
|
||||||
|
|||||||
@ -105,3 +105,16 @@ export async function verifyDealerLogin(dealerCode: string): Promise<DealerInfo>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search dealer by code from external Royal Enfield API
|
||||||
|
* @param dealerCode - The code to search for
|
||||||
|
*/
|
||||||
|
export async function searchExternalDealerByCode(dealerCode: string): Promise<any | null> {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(`/dealers-external/search/${dealerCode}`);
|
||||||
|
return res.data?.data || res.data || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DealerAPI] Error searching external dealer:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user