dealer from external source implemented and re-iteration and multiple io block implemented need to test end to end

This commit is contained in:
laxmanhalaki 2026-03-02 21:31:40 +05:30
parent b04776a5f8
commit e11f13d248
7 changed files with 299 additions and 232 deletions

View File

@ -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'
: '' : ''
}`} }`}

View File

@ -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>
) : ( ) : (

View File

@ -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) */}

View File

@ -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}

View File

@ -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)

View File

@ -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)}

View File

@ -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;
}
}