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';
|
||||
@ -68,7 +68,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
const dealerSearchTimer = useRef<any>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const submitTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
|
||||
// System policy state
|
||||
const [systemPolicy, setSystemPolicy] = useState({
|
||||
maxApprovalLevels: 10,
|
||||
@ -76,7 +76,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
allowSpectators: true,
|
||||
maxSpectators: 20
|
||||
});
|
||||
|
||||
|
||||
const [policyViolationModal, setPolicyViolationModal] = useState<{
|
||||
open: boolean;
|
||||
violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>;
|
||||
@ -140,7 +140,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
activityName: '',
|
||||
activityType: '',
|
||||
@ -175,7 +175,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
// Handle dealer search input with debouncing
|
||||
const handleDealerSearchInputChange = (value: string) => {
|
||||
setDealerSearchInput(value);
|
||||
|
||||
|
||||
// Clear previous timer
|
||||
if (dealerSearchTimer.current) {
|
||||
clearTimeout(dealerSearchTimer.current);
|
||||
@ -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);
|
||||
@ -208,7 +224,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
const updateFormData = (field: string, value: any) => {
|
||||
setFormData(prev => {
|
||||
const updated = { ...prev, [field]: value };
|
||||
|
||||
|
||||
// Validate period dates
|
||||
if (field === 'periodStartDate') {
|
||||
// If start date is selected and end date exists, validate end date
|
||||
@ -225,7 +241,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
@ -233,18 +249,18 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
const isStepValid = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return formData.activityName &&
|
||||
formData.activityType &&
|
||||
formData.dealerCode &&
|
||||
formData.dealerName &&
|
||||
formData.activityDate &&
|
||||
formData.location &&
|
||||
formData.requestDescription;
|
||||
return formData.activityName &&
|
||||
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 || [];
|
||||
// Find step 3 approver by originalStepLevel first, then fallback to level
|
||||
const step3Approver = approvers.find((a: any) =>
|
||||
const step3Approver = approvers.find((a: any) =>
|
||||
a.originalStepLevel === 3 || (!a.originalStepLevel && a.level === 3 && !a.isAdditional)
|
||||
);
|
||||
// Step 8 is now a system step, no validation needed
|
||||
@ -263,15 +279,15 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
if (currentStep === 2) {
|
||||
const approvers = formData.approvers || [];
|
||||
// Find step 3 approver by originalStepLevel first, then fallback to level
|
||||
const step3Approver = approvers.find((a: any) =>
|
||||
const step3Approver = approvers.find((a: any) =>
|
||||
a.originalStepLevel === 3 || (!a.originalStepLevel && a.level === 3 && !a.isAdditional)
|
||||
);
|
||||
const missingSteps: string[] = [];
|
||||
|
||||
|
||||
if (!step3Approver?.email || !step3Approver?.userId || !step3Approver?.tat) {
|
||||
missingSteps.push('Department Lead Approval');
|
||||
}
|
||||
|
||||
|
||||
if (missingSteps.length > 0) {
|
||||
toast.error(`Please add missing approvers: ${missingSteps.join(', ')}`);
|
||||
} else {
|
||||
@ -297,7 +313,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
setVerifyingDealer(true);
|
||||
try {
|
||||
const verifiedDealer = await verifyDealerLogin(selectedDealer.dealerCode);
|
||||
|
||||
|
||||
if (!verifiedDealer.isLoggedIn) {
|
||||
toast.error(
|
||||
`Dealer "${verifiedDealer.dealerName || verifiedDealer.displayName}" (${verifiedDealer.dealerCode}) is not mapped to the system.`,
|
||||
@ -321,14 +337,14 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
updateFormData('dealerEmail', verifiedDealer.email || '');
|
||||
updateFormData('dealerPhone', verifiedDealer.phone || '');
|
||||
updateFormData('dealerAddress', ''); // Address not available in API response
|
||||
|
||||
|
||||
// Clear search input and results
|
||||
setDealerSearchInput(verifiedDealer.dealerName || verifiedDealer.displayName);
|
||||
setDealerSearchResults([]);
|
||||
|
||||
|
||||
toast.success(`Dealer "${verifiedDealer.dealerName || verifiedDealer.displayName}" verified and mapped to the System`);
|
||||
} catch (error: any) {
|
||||
const errorMessage = 'Dealer is not mapped to the system'
|
||||
const errorMessage = 'Dealer is not mapped to the system'
|
||||
toast.error(errorMessage, { duration: 5000 });
|
||||
// Clear the selection
|
||||
setDealerSearchInput('');
|
||||
@ -353,11 +369,11 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
// Just sort them and prepare for submission
|
||||
const approvers = formData.approvers || [];
|
||||
const sortedApprovers = [...approvers].sort((a, b) => a.level - b.level);
|
||||
|
||||
|
||||
// Check for duplicate levels (should not happen, but safeguard)
|
||||
const levelMap = new Map<number, typeof sortedApprovers[0]>();
|
||||
const duplicates: number[] = [];
|
||||
|
||||
|
||||
sortedApprovers.forEach((approver) => {
|
||||
if (levelMap.has(approver.level)) {
|
||||
duplicates.push(approver.level);
|
||||
@ -365,13 +381,13 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
levelMap.set(approver.level, approver);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
toast.error(`Duplicate approver levels detected: ${duplicates.join(', ')}. Please refresh and try again.`);
|
||||
console.error('Duplicate levels found:', duplicates, sortedApprovers);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Prepare final approvers array - preserve stepName for additional approvers
|
||||
// The backend will use stepName to set the levelName for approval levels
|
||||
// Also preserve originalStepLevel so backend can identify which step each approver belongs to
|
||||
@ -384,18 +400,18 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
tat: approver.tat,
|
||||
tatType: approver.tatType,
|
||||
};
|
||||
|
||||
|
||||
// Preserve stepName for additional approvers
|
||||
if (approver.isAdditional && approver.stepName) {
|
||||
result.stepName = approver.stepName;
|
||||
result.isAdditional = true;
|
||||
}
|
||||
|
||||
|
||||
// Preserve originalStepLevel for fixed steps (so backend can identify which step this is)
|
||||
if (approver.originalStepLevel) {
|
||||
result.originalStepLevel = approver.originalStepLevel;
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
@ -486,8 +502,8 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
|
||||
<div>
|
||||
<Label htmlFor="activityType" className="text-base font-semibold">Activity Type *</Label>
|
||||
<Select
|
||||
value={formData.activityType}
|
||||
<Select
|
||||
value={formData.activityType}
|
||||
onValueChange={(value) => updateFormData('activityType', value)}
|
||||
disabled={loadingActivityTypes}
|
||||
>
|
||||
@ -734,7 +750,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500">
|
||||
{formData.periodStartDate
|
||||
{formData.periodStartDate
|
||||
? 'Please select end date for the period'
|
||||
: 'Please select start date first'}
|
||||
</p>
|
||||
@ -754,9 +770,9 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
currentUserEmail={(user as any)?.email || ''}
|
||||
currentUserId={(user as any)?.userId || ''}
|
||||
currentUserName={
|
||||
(user as any)?.displayName ||
|
||||
(user as any)?.name ||
|
||||
((user as any)?.firstName && (user as any)?.lastName
|
||||
(user as any)?.displayName ||
|
||||
(user as any)?.name ||
|
||||
((user as any)?.firstName && (user as any)?.lastName
|
||||
? `${(user as any).firstName} ${(user as any).lastName}`.trim()
|
||||
: (user as any)?.email || 'User')
|
||||
}
|
||||
@ -857,16 +873,16 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
const sortedApprovers = [...(formData.approvers || [])]
|
||||
.filter((a: any) => !a.email?.includes('system@') && !a.email?.includes('finance@'))
|
||||
.sort((a: any, b: any) => a.level - b.level);
|
||||
|
||||
|
||||
return sortedApprovers.map((approver: any) => {
|
||||
const tat = Number(approver.tat || 0);
|
||||
const tatType = approver.tatType || 'hours';
|
||||
const hours = tatType === 'days' ? tat * 24 : tat;
|
||||
|
||||
|
||||
// Find step name - handle additional approvers and shifted levels
|
||||
let stepName = 'Unknown';
|
||||
let stepLabel = '';
|
||||
|
||||
|
||||
if (approver.isAdditional) {
|
||||
// Additional approver - use stepName if available
|
||||
stepName = approver.stepName || 'Additional Approver';
|
||||
@ -874,17 +890,16 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
stepLabel = approver.stepName || `Additional Approver (after "${afterStep?.name || 'Unknown'}")`;
|
||||
} else {
|
||||
// Fixed step - find by originalStepLevel first, then fallback to level
|
||||
const step = approver.originalStepLevel
|
||||
const step = approver.originalStepLevel
|
||||
? CLAIM_STEPS.find(s => s.level === approver.originalStepLevel)
|
||||
: CLAIM_STEPS.find(s => s.level === approver.level && !s.isAuto);
|
||||
stepName = step?.name || 'Unknown';
|
||||
stepLabel = stepName;
|
||||
}
|
||||
|
||||
|
||||
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">
|
||||
@ -960,8 +975,8 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
<div>
|
||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Brief Requirement</Label>
|
||||
<div className="mt-2 p-4 bg-gray-50 rounded-lg border">
|
||||
<FormattedDescription
|
||||
content={formData.requestDescription || ''}
|
||||
<FormattedDescription
|
||||
content={formData.requestDescription || ''}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
@ -1032,7 +1047,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
<span className="hidden sm:inline">Back to Templates</span>
|
||||
<span className="sm:hidden">Back</span>
|
||||
</Button>
|
||||
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-0">
|
||||
<div>
|
||||
<Badge variant="secondary" className="mb-2 text-xs">Claim Management Template</Badge>
|
||||
@ -1048,11 +1063,10 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
<Progress value={(currentStep / totalSteps) * 100} className="h-2" />
|
||||
<div className="flex justify-between mt-2 px-1">
|
||||
{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'
|
||||
}`}
|
||||
<span
|
||||
key={index}
|
||||
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';
|
||||
@ -36,89 +36,60 @@ interface IOBlockedDetails {
|
||||
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 [ioNumber, setIoNumber] = useState(existingIONumber);
|
||||
const claimDetails = apiRequest?.claimDetails || apiRequest || {};
|
||||
|
||||
// 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',
|
||||
});
|
||||
|
||||
// Set fetched amount if available balance exists
|
||||
if (availableBeforeBlock > 0) {
|
||||
setFetchedAmount(availableBeforeBlock);
|
||||
}
|
||||
sapDocumentNumber: io.sapDocumentNumber || io.sap_document_number || '',
|
||||
status: (io.status === 'BLOCKED' ? 'blocked' :
|
||||
io.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed',
|
||||
};
|
||||
});
|
||||
setBlockedIOs(formattedIOs);
|
||||
|
||||
// If we are not in Scenario 2 (additional blocking), set the IO number from the last block for convenience
|
||||
if (!isAdditionalBlockingNeeded && formattedIOs.length > 0) {
|
||||
setIoNumber(formattedIOs[formattedIOs.length - 1].ioNumber);
|
||||
}
|
||||
}
|
||||
}, [internalOrder, existingIONumber, existingBlockedAmount, existingAvailableBalance, existingRemainingBalance, sapDocNumber, organizer]);
|
||||
}, [apiRequest, isAdditionalBlockingNeeded]);
|
||||
|
||||
/**
|
||||
* Fetch available budget from SAP
|
||||
@ -140,7 +111,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
try {
|
||||
// Call validate IO endpoint - returns dummy data for now, will integrate with SAP later
|
||||
const ioData = await validateIO(requestId, ioNumber.trim());
|
||||
|
||||
|
||||
if (ioData.isValid && ioData.availableBalance > 0) {
|
||||
setFetchedAmount(ioData.availableBalance);
|
||||
// Pre-fill amount to block with estimated budget (if available), otherwise use available balance
|
||||
@ -184,26 +155,33 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
}
|
||||
|
||||
const blockAmountRaw = parseFloat(amountToBlock);
|
||||
|
||||
|
||||
if (!amountToBlock || isNaN(blockAmountRaw) || blockAmountRaw <= 0) {
|
||||
toast.error('Please enter a valid amount to block');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Round to exactly 2 decimal places to avoid floating point precision issues
|
||||
// Use parseFloat with toFixed to ensure exact 2 decimal precision
|
||||
const blockAmount = parseFloat(blockAmountRaw.toFixed(2));
|
||||
|
||||
|
||||
if (blockAmount > fetchedAmount) {
|
||||
toast.error('Amount to block exceeds available IO budget');
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@ -224,29 +202,29 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
ioBlockedAmount: blockAmount,
|
||||
ioRemainingBalance: calculatedRemaining, // Calculated value (backend will use SAP's actual value)
|
||||
};
|
||||
|
||||
|
||||
// Sending to backend
|
||||
|
||||
|
||||
await updateIODetails(requestId, payload);
|
||||
|
||||
|
||||
// Fetch updated claim details to get the blocked IO data
|
||||
const claimData = await getClaimDetails(requestId);
|
||||
const updatedInternalOrder = claimData?.internalOrder || claimData?.internal_order;
|
||||
|
||||
|
||||
if (updatedInternalOrder) {
|
||||
const savedBlockedAmount = Number(updatedInternalOrder.ioBlockedAmount || updatedInternalOrder.io_blocked_amount || blockAmount);
|
||||
const savedRemainingBalance = Number(updatedInternalOrder.ioRemainingBalance || updatedInternalOrder.io_remaining_balance || 0);
|
||||
|
||||
|
||||
// Calculate expected remaining balance for validation/debugging
|
||||
const expectedRemainingBalance = fetchedAmount - savedBlockedAmount;
|
||||
|
||||
|
||||
// Blocking result processed
|
||||
|
||||
|
||||
// Warn if the saved amount differs from what we sent
|
||||
if (Math.abs(savedBlockedAmount - blockAmount) > 0.01) {
|
||||
console.warn('[IOTab] ⚠️ Amount mismatch! Sent:', blockAmount, 'Saved:', savedBlockedAmount);
|
||||
}
|
||||
|
||||
|
||||
// Warn if remaining balance calculation seems incorrect (for backend debugging)
|
||||
if (Math.abs(savedRemainingBalance - expectedRemainingBalance) > 0.01) {
|
||||
console.warn('[IOTab] ⚠️ Remaining balance calculation issue detected!', {
|
||||
@ -257,17 +235,17 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
difference: savedRemainingBalance - expectedRemainingBalance,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const currentUser = user as any;
|
||||
// When blocking, always use the current user who is performing the block action
|
||||
// The organizer association may be from initial IO organization, but we want who blocked the amount
|
||||
const blockedByName = currentUser?.displayName ||
|
||||
currentUser?.display_name ||
|
||||
currentUser?.name ||
|
||||
(currentUser?.firstName && currentUser?.lastName ? `${currentUser.firstName} ${currentUser.lastName}`.trim() : null) ||
|
||||
currentUser?.email ||
|
||||
'Current User';
|
||||
|
||||
const blockedByName = currentUser?.displayName ||
|
||||
currentUser?.display_name ||
|
||||
currentUser?.name ||
|
||||
(currentUser?.firstName && currentUser?.lastName ? `${currentUser.firstName} ${currentUser.lastName}`.trim() : null) ||
|
||||
currentUser?.email ||
|
||||
'Current User';
|
||||
|
||||
const blocked: IOBlockedDetails = {
|
||||
ioNumber: updatedInternalOrder.ioNumber || updatedInternalOrder.io_number || ioNumber,
|
||||
blockedAmount: savedBlockedAmount,
|
||||
@ -278,11 +256,12 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
sapDocumentNumber: updatedInternalOrder.sapDocumentNumber || updatedInternalOrder.sap_document_number || '',
|
||||
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
|
||||
onRefresh?.();
|
||||
} else {
|
||||
@ -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">
|
||||
@ -392,11 +371,11 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
<Button
|
||||
onClick={handleBlockBudget}
|
||||
disabled={
|
||||
blockingBudget ||
|
||||
!amountToBlock ||
|
||||
parseFloat(amountToBlock) <= 0 ||
|
||||
blockingBudget ||
|
||||
!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