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';
|
||||
import { format } from 'date-fns';
|
||||
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 { useAuth } from '@/contexts/AuthContext';
|
||||
import { getPublicConfigurations, AdminConfiguration, getActivityTypes, type ActivityType } from '@/services/adminApi';
|
||||
@ -194,10 +194,26 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
// Debounce search
|
||||
dealerSearchTimer.current = setTimeout(async () => {
|
||||
try {
|
||||
const results = await fetchDealersFromAPI(value, 10); // Limit to 10 results
|
||||
setDealerSearchResults(results);
|
||||
const result = await searchExternalDealerByCode(value);
|
||||
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) {
|
||||
console.error('Error searching dealers:', error);
|
||||
console.error('Error searching external dealer:', error);
|
||||
setDealerSearchResults([]);
|
||||
} finally {
|
||||
setDealerSearchLoading(false);
|
||||
@ -234,12 +250,12 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return formData.activityName &&
|
||||
formData.activityType &&
|
||||
formData.dealerCode &&
|
||||
formData.dealerName &&
|
||||
formData.activityDate &&
|
||||
formData.location &&
|
||||
formData.requestDescription;
|
||||
formData.activityType &&
|
||||
formData.dealerCode &&
|
||||
formData.dealerName &&
|
||||
formData.activityDate &&
|
||||
formData.location &&
|
||||
formData.requestDescription;
|
||||
case 2:
|
||||
// Validate that all required approvers are assigned (Step 3 only, Step 8 is now system/Finance)
|
||||
const approvers = formData.approvers || [];
|
||||
@ -882,9 +898,8 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
}
|
||||
|
||||
return (
|
||||
<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'
|
||||
}`}>
|
||||
<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'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
@ -1050,9 +1065,8 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
{STEP_NAMES.map((_name, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={`text-xs sm:text-sm ${
|
||||
index + 1 <= currentStep ? 'text-blue-600 font-medium' : 'text-gray-400'
|
||||
}`}
|
||||
className={`text-xs sm:text-sm ${index + 1 <= currentStep ? 'text-blue-600 font-medium' : 'text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
@ -1085,11 +1099,10 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
{currentStep < totalSteps ? (
|
||||
<Button
|
||||
onClick={nextStep}
|
||||
className={`gap-2 w-full sm:w-auto order-1 sm:order-2 ${
|
||||
!isStepValid()
|
||||
? 'opacity-50 cursor-pointer hover:opacity-60'
|
||||
: ''
|
||||
}`}
|
||||
className={`gap-2 w-full sm:w-auto order-1 sm:order-2 ${!isStepValid()
|
||||
? 'opacity-50 cursor-pointer hover:opacity-60'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
Next
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
|
||||
@ -11,7 +11,7 @@ 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 { DollarSign, Download, CircleCheckBig, Target, CircleAlert } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { validateIO, updateIODetails, getClaimDetails } from '@/services/dealerClaimApi';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
@ -37,88 +37,59 @@ 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;
|
||||
|
||||
// Get estimated budget from proposal details
|
||||
// Get estimated budget from proposal details or DealerClaimDetails
|
||||
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 [fetchedAmount, setFetchedAmount] = useState<number | null>(null);
|
||||
const [amountToBlock, setAmountToBlock] = useState<string>('');
|
||||
const [blockedDetails, setBlockedDetails] = useState<IOBlockedDetails | null>(null);
|
||||
const [blockedIOs, setBlockedIOs] = useState<IOBlockedDetails[]>([]);
|
||||
const [blockingBudget, setBlockingBudget] = useState(false);
|
||||
|
||||
// Load existing IO block details from apiRequest
|
||||
// Load existing IO blocks
|
||||
useEffect(() => {
|
||||
if (internalOrder && existingIONumber) {
|
||||
// IMPORTANT: ioAvailableBalance is already the available balance BEFORE blocking
|
||||
// We should NOT add blockedAmount to it - that would cause double deduction
|
||||
// Backend stores: availableBalance (before block), blockedAmount, remainingBalance (after block)
|
||||
const availableBeforeBlock = Number(existingAvailableBalance) || 0;
|
||||
|
||||
// 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 from existing data
|
||||
setIoNumber(existingIONumber);
|
||||
|
||||
// Only set blocked details if amount is blocked
|
||||
if (existingBlockedAmount > 0) {
|
||||
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(),
|
||||
const ios = apiRequest?.internalOrders || apiRequest?.internal_orders || [];
|
||||
if (ios.length > 0) {
|
||||
const formattedIOs = ios.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: sapDocNumber,
|
||||
status: (internalOrder.status === 'BLOCKED' ? 'blocked' :
|
||||
internalOrder.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed',
|
||||
});
|
||||
sapDocumentNumber: io.sapDocumentNumber || io.sap_document_number || '',
|
||||
status: (io.status === 'BLOCKED' ? 'blocked' :
|
||||
io.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed',
|
||||
};
|
||||
});
|
||||
setBlockedIOs(formattedIOs);
|
||||
|
||||
// Set fetched amount if available balance exists
|
||||
if (availableBeforeBlock > 0) {
|
||||
setFetchedAmount(availableBeforeBlock);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}, [internalOrder, existingIONumber, existingBlockedAmount, existingAvailableBalance, existingRemainingBalance, sapDocNumber, organizer]);
|
||||
}, [apiRequest, isAdditionalBlockingNeeded]);
|
||||
|
||||
/**
|
||||
* Fetch available budget from SAP
|
||||
@ -199,11 +170,18 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
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) {
|
||||
const roundedEstimatedBudget = parseFloat(estimatedBudget.toFixed(2));
|
||||
if (Math.abs(blockAmount - roundedEstimatedBudget) > 0.01) {
|
||||
toast.error(`Amount to block must be exactly equal to the estimated budget (₹${roundedEstimatedBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 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;
|
||||
}
|
||||
}
|
||||
@ -262,11 +240,11 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
// 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';
|
||||
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,
|
||||
@ -279,8 +257,9 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
status: 'blocked',
|
||||
};
|
||||
|
||||
setBlockedDetails(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
|
||||
@ -321,12 +300,12 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
placeholder="Enter IO number (e.g., IO-2024-12345)"
|
||||
value={ioNumber}
|
||||
onChange={(e) => setIoNumber(e.target.value)}
|
||||
disabled={fetchingAmount || !!blockedDetails}
|
||||
disabled={fetchingAmount || (blockedIOs.length > 0 && !isAdditionalBlockingNeeded)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleFetchAmount}
|
||||
disabled={!ioNumber.trim() || fetchingAmount || !!blockedDetails}
|
||||
disabled={!ioNumber.trim() || fetchingAmount || (blockedIOs.length > 0 && !isAdditionalBlockingNeeded)}
|
||||
className="bg-[#2d4a3e] hover:bg-[#1f3329]"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
@ -336,7 +315,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<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.
|
||||
@ -345,7 +324,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
)}
|
||||
|
||||
{/* 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="flex items-center justify-between">
|
||||
@ -396,7 +375,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
!amountToBlock ||
|
||||
parseFloat(amountToBlock) <= 0 ||
|
||||
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]"
|
||||
>
|
||||
@ -420,71 +399,52 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
</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>
|
||||
{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>
|
||||
</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>
|
||||
{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' : 'bg-blue-100 text-blue-800'}>
|
||||
{io.status === 'blocked' ? 'Blocked' : '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>
|
||||
) : (
|
||||
|
||||
@ -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)
|
||||
const handleIOApproval = async (data: {
|
||||
ioNumber: string;
|
||||
@ -2564,6 +2603,7 @@ export function DealerClaimWorkflowTab({
|
||||
requestTitle={request?.title}
|
||||
requestNumber={request?.requestNumber || request?.request_number || request?.id}
|
||||
taxationType={request?.claimDetails?.taxationType}
|
||||
onReQuotation={handleClaimReQuotation}
|
||||
/>
|
||||
|
||||
{/* Credit Note from SAP Modal (Step 8) */}
|
||||
|
||||
@ -27,6 +27,7 @@ import {
|
||||
Download,
|
||||
Eye,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi';
|
||||
@ -80,6 +81,7 @@ interface DMSPushModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onPush: (comments: string) => Promise<void>;
|
||||
onReQuotation?: (comments: string) => Promise<void>;
|
||||
completionDetails?: CompletionDetails | null;
|
||||
ioDetails?: IODetails | null;
|
||||
completionDocuments?: CompletionDocuments | null;
|
||||
@ -92,6 +94,7 @@ export function DMSPushModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onPush,
|
||||
onReQuotation,
|
||||
completionDetails,
|
||||
ioDetails,
|
||||
completionDocuments,
|
||||
@ -257,6 +260,30 @@ export function DMSPushModal({
|
||||
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 = () => {
|
||||
if (!submitting) {
|
||||
handleReset();
|
||||
@ -805,6 +832,21 @@ export function DMSPushModal({
|
||||
>
|
||||
Cancel
|
||||
</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
|
||||
onClick={handleSubmit}
|
||||
disabled={!comments.trim() || submitting}
|
||||
|
||||
@ -670,7 +670,6 @@ export function DealerCompletionDocumentsModal({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-base sm:text-lg">Closed Expenses</h3>
|
||||
<Badge className="bg-secondary text-secondary-foreground text-xs">Optional</Badge>
|
||||
</div>
|
||||
{!isNonGst && (
|
||||
<div className="text-[10px] text-gray-500 italic mt-0.5">
|
||||
@ -743,9 +742,9 @@ export function DealerCompletionDocumentsModal({
|
||||
{!isNonGst && (
|
||||
<>
|
||||
<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
|
||||
placeholder="HSN"
|
||||
placeholder="HSN/SAC Code"
|
||||
value={item.hsnCode || ''}
|
||||
onChange={(e) =>
|
||||
handleExpenseChange(item.id, 'hsnCode', e.target.value)
|
||||
|
||||
@ -902,7 +902,7 @@ export function DealerProposalSubmissionModal({
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
value={item.hsnCode || ''}
|
||||
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