ui enhnce on validation points
This commit is contained in:
parent
058ab97600
commit
aedba86ae3
@ -1,5 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useState, useRef } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@ -24,6 +23,7 @@ import {
|
||||
Info,
|
||||
FileText,
|
||||
Users,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { toast } from 'sonner';
|
||||
@ -72,11 +72,7 @@ const STEP_NAMES = [
|
||||
|
||||
export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizardProps) {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [dealers, setDealers] = useState<DealerInfo[]>([]);
|
||||
const [loadingDealers, setLoadingDealers] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [verifyingDealer, setVerifyingDealer] = useState(false);
|
||||
const [dealerSearchResults, setDealerSearchResults] = useState<DealerInfo[]>([]);
|
||||
const [dealerSearchLoading, setDealerSearchLoading] = useState(false);
|
||||
@ -114,23 +110,6 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
|
||||
const totalSteps = STEP_NAMES.length;
|
||||
|
||||
// Fetch dealers from API on component mount
|
||||
useEffect(() => {
|
||||
const fetchDealers = async () => {
|
||||
setLoadingDealers(true);
|
||||
try {
|
||||
const fetchedDealers = await fetchDealersFromAPI(undefined, 10); // Limit to 10 records
|
||||
setDealers(fetchedDealers);
|
||||
} catch (error) {
|
||||
toast.error('Failed to load dealer list.');
|
||||
console.error('Error fetching dealers:', error);
|
||||
} finally {
|
||||
setLoadingDealers(false);
|
||||
}
|
||||
};
|
||||
fetchDealers();
|
||||
}, []);
|
||||
|
||||
// Handle dealer search input with debouncing
|
||||
const handleDealerSearchInputChange = (value: string) => {
|
||||
setDealerSearchInput(value);
|
||||
@ -489,14 +468,11 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<div className="ml-2 flex-shrink-0">
|
||||
{dealer.isLoggedIn ? (
|
||||
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
Logged In
|
||||
</Badge>
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
) : (
|
||||
<Badge variant="destructive" className="text-xs">Not Logged In</Badge>
|
||||
<XCircle className="w-4 h-4 text-red-500" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -508,6 +484,18 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2 text-xs text-gray-500">
|
||||
<span>Status:</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle className="w-3 h-3 text-green-600" />
|
||||
<span>Logged in</span>
|
||||
</div>
|
||||
<span className="mx-1">•</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<XCircle className="w-3 h-3 text-red-500" />
|
||||
<span>Not logged in</span>
|
||||
</div>
|
||||
</div>
|
||||
{formData.dealerCode && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<p className="text-sm text-gray-600">
|
||||
@ -528,10 +516,10 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left mt-2 h-12"
|
||||
className="w-full justify-start text-left mt-2 h-12 pl-3"
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{formData.activityDate ? format(formData.activityDate, 'PPP') : 'Select date'}
|
||||
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||
<span className="flex-1 text-left">{formData.activityDate ? format(formData.activityDate, 'PPP') : 'Select date'}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
@ -585,10 +573,10 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left mt-2 h-12"
|
||||
className="w-full justify-start text-left mt-2 h-12 pl-3"
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{formData.periodStartDate ? format(formData.periodStartDate, 'PPP') : 'Start date'}
|
||||
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||
<span className="flex-1 text-left">{formData.periodStartDate ? format(formData.periodStartDate, 'PPP') : 'Start date'}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
@ -610,11 +598,11 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left mt-2 h-12"
|
||||
className="w-full justify-start text-left mt-2 h-12 pl-3"
|
||||
disabled={!formData.periodStartDate}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{formData.periodEndDate ? format(formData.periodEndDate, 'PPP') : 'End date'}
|
||||
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||
<span className="flex-1 text-left">{formData.periodEndDate ? format(formData.periodEndDate, 'PPP') : 'End date'}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
|
||||
@ -80,11 +80,36 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
|
||||
// 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
|
||||
const expectedRemaining = availableBeforeBlock - blockedAmt;
|
||||
|
||||
// Log for debugging backend calculation
|
||||
console.log('[IOTab] Loading existing IO block:', {
|
||||
availableBeforeBlock,
|
||||
blockedAmount: blockedAmt,
|
||||
expectedRemaining,
|
||||
backendRemaining,
|
||||
});
|
||||
|
||||
// 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: Number(existingBlockedAmount) || 0,
|
||||
blockedAmount: blockedAmt,
|
||||
availableBalance: availableBeforeBlock, // Available amount before block
|
||||
remainingBalance: Number(existingRemainingBalance) || Number(existingAvailableBalance),
|
||||
remainingBalance: backendRemaining, // Use backend calculated value
|
||||
blockedDate: internalOrder.organizedAt || internalOrder.organized_at || new Date().toISOString(),
|
||||
blockedBy: blockedByName,
|
||||
sapDocumentNumber: sapDocNumber,
|
||||
@ -252,16 +277,20 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
|
||||
if (updatedInternalOrder) {
|
||||
const savedBlockedAmount = Number(updatedInternalOrder.ioBlockedAmount || updatedInternalOrder.io_blocked_amount || blockAmount);
|
||||
const savedRemainingBalance = Number(updatedInternalOrder.ioRemainingBalance || updatedInternalOrder.io_remaining_balance || (fetchedAmount - blockAmount));
|
||||
const savedRemainingBalance = Number(updatedInternalOrder.ioRemainingBalance || updatedInternalOrder.io_remaining_balance || 0);
|
||||
|
||||
// Calculate expected remaining balance for validation/debugging
|
||||
const expectedRemainingBalance = fetchedAmount - savedBlockedAmount;
|
||||
|
||||
// Log what was saved vs what we sent
|
||||
console.log('[IOTab] Blocking result:', {
|
||||
sentAmount: blockAmount,
|
||||
savedBlockedAmount,
|
||||
sentRemaining: fetchedAmount - blockAmount,
|
||||
savedRemainingBalance,
|
||||
availableBalance: fetchedAmount,
|
||||
expectedRemaining: expectedRemainingBalance,
|
||||
backendRemaining: savedRemainingBalance,
|
||||
difference: savedBlockedAmount - blockAmount,
|
||||
remainingDifference: fetchedAmount - savedRemainingBalance,
|
||||
});
|
||||
|
||||
// Warn if the saved amount differs from what we sent
|
||||
@ -269,6 +298,17 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
console.warn('[IOTab] ⚠️ Amount mismatch! Sent:', blockAmount, 'Saved:', savedBlockedAmount);
|
||||
}
|
||||
|
||||
// Warn if remaining balance calculation seems incorrect (for backend debugging)
|
||||
if (Math.abs(savedRemainingBalance - expectedRemainingBalance) > 0.01) {
|
||||
console.warn('[IOTab] ⚠️ Remaining balance calculation issue detected!', {
|
||||
availableBalance: fetchedAmount,
|
||||
blockedAmount: savedBlockedAmount,
|
||||
expectedRemaining: expectedRemainingBalance,
|
||||
backendRemaining: savedRemainingBalance,
|
||||
difference: savedRemainingBalance - expectedRemainingBalance,
|
||||
});
|
||||
}
|
||||
|
||||
const currentUser = user as any;
|
||||
// When blocking, always use the current user who is performing the block action
|
||||
// The organizer association may be from initial IO organization, but we want who blocked the amount
|
||||
@ -351,11 +391,8 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
|
||||
{/* IO Remark Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ioRemark" className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
||||
<Label htmlFor="ioRemark" className="text-sm font-medium text-gray-900">
|
||||
IO Remark <span className="text-red-500">*</span>
|
||||
{fetchedAmount !== null && !blockedDetails && (
|
||||
<Badge variant="destructive" className="text-xs ml-2">Required before blocking</Badge>
|
||||
)}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="ioRemark"
|
||||
|
||||
@ -37,3 +37,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Date input calendar icon positioning */
|
||||
.dealer-completion-documents-modal input[type="date"] {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dealer-completion-documents-modal input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.dealer-completion-documents-modal input[type="date"]::-webkit-inner-spin-button,
|
||||
.dealer-completion-documents-modal input[type="date"]::-webkit-clear-button {
|
||||
display: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/* Firefox date input */
|
||||
.dealer-completion-documents-modal input[type="date"]::-moz-calendar-picker-indicator {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@ -332,7 +332,13 @@ export function DealerCompletionDocumentsModal({
|
||||
max={maxDate}
|
||||
value={activityCompletionDate}
|
||||
onChange={(e) => setActivityCompletionDate(e.target.value)}
|
||||
className="max-w-xs"
|
||||
onClick={(e) => {
|
||||
// Open calendar picker when clicking anywhere on the input
|
||||
if (e.currentTarget.showPicker) {
|
||||
e.currentTarget.showPicker();
|
||||
}
|
||||
}}
|
||||
className="w-full max-w-[280px] text-left pr-10 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -412,7 +418,7 @@ export function DealerCompletionDocumentsModal({
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-base sm:text-lg">Completion Evidence</h3>
|
||||
<Badge className="bg-destructive text-white text-xs">Required</Badge>
|
||||
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
||||
</div>
|
||||
|
||||
{/* Grid layout for Completion Documents and Activity Photos */}
|
||||
|
||||
@ -37,3 +37,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Date input calendar icon positioning */
|
||||
.dealer-proposal-modal input[type="date"] {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dealer-proposal-modal input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.dealer-proposal-modal input[type="date"]::-webkit-inner-spin-button,
|
||||
.dealer-proposal-modal input[type="date"]::-webkit-clear-button {
|
||||
display: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/* Firefox date input */
|
||||
.dealer-proposal-modal input[type="date"]::-moz-calendar-picker-indicator {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@ -277,7 +277,7 @@ export function DealerProposalSubmissionModal({
|
||||
<div className="space-y-3 lg:space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-base lg:text-lg">Proposal Document</h3>
|
||||
<Badge className="bg-red-500 text-white text-xs">Required</Badge>
|
||||
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm lg:text-base font-semibold flex items-center gap-2">
|
||||
@ -359,7 +359,7 @@ export function DealerProposalSubmissionModal({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-base lg:text-lg">Cost Breakup</h3>
|
||||
<Badge className="bg-red-500 text-white text-xs">Required</Badge>
|
||||
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@ -426,7 +426,7 @@ export function DealerProposalSubmissionModal({
|
||||
<div className="space-y-3 lg:space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-base lg:text-lg">Timeline for Closure</h3>
|
||||
<Badge className="bg-red-500 text-white text-xs">Required</Badge>
|
||||
<Badge variant="outline" className="text-xs border-red-500 text-red-700 bg-red-50 font-medium">Required</Badge>
|
||||
</div>
|
||||
<div className="space-y-2 lg:space-y-2">
|
||||
<div className="flex gap-2">
|
||||
@ -457,7 +457,7 @@ export function DealerProposalSubmissionModal({
|
||||
</Button>
|
||||
</div>
|
||||
{timelineMode === 'date' ? (
|
||||
<div>
|
||||
<div className="w-full">
|
||||
<Label className="text-xs lg:text-sm font-medium mb-1.5 lg:mb-2 block">
|
||||
Expected Completion Date
|
||||
</Label>
|
||||
@ -466,11 +466,17 @@ export function DealerProposalSubmissionModal({
|
||||
min={minDate}
|
||||
value={expectedCompletionDate}
|
||||
onChange={(e) => setExpectedCompletionDate(e.target.value)}
|
||||
className="h-9 lg:h-10 w-full max-w-xs"
|
||||
onClick={(e) => {
|
||||
// Open calendar picker when clicking anywhere on the input
|
||||
if (e.currentTarget.showPicker) {
|
||||
e.currentTarget.showPicker();
|
||||
}
|
||||
}}
|
||||
className="h-9 lg:h-10 w-full max-w-[280px] text-left pr-10 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="w-full">
|
||||
<Label className="text-xs lg:text-sm font-medium mb-1.5 lg:mb-2 block">
|
||||
Number of Days
|
||||
</Label>
|
||||
@ -480,7 +486,7 @@ export function DealerProposalSubmissionModal({
|
||||
min="1"
|
||||
value={numberOfDays}
|
||||
onChange={(e) => setNumberOfDays(e.target.value)}
|
||||
className="h-9 lg:h-10 w-full max-w-xs"
|
||||
className="h-9 lg:h-10 w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -491,7 +497,7 @@ export function DealerProposalSubmissionModal({
|
||||
<div className="space-y-3 lg:space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-base lg:text-lg">Other Supporting Documents</h3>
|
||||
<Badge variant="secondary" className="text-xs">Optional</Badge>
|
||||
<Badge variant="outline" className="text-xs border-gray-300 text-gray-600 bg-gray-50 font-medium">Optional</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="flex items-center gap-2 text-sm lg:text-base font-semibold">
|
||||
|
||||
@ -38,6 +38,7 @@ import { useDocumentUpload } from '@/hooks/useDocumentUpload';
|
||||
import { useModalManager } from '@/hooks/useModalManager';
|
||||
import { useConclusionRemark } from '@/hooks/useConclusionRemark';
|
||||
import { downloadDocument } from '@/services/workflowApi';
|
||||
import { getSocket, joinUserRoom } from '@/utils/socket';
|
||||
|
||||
// Dealer Claim Components (import from index to get properly aliased exports)
|
||||
import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index';
|
||||
@ -324,6 +325,37 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
||||
fetchSummaryDetails();
|
||||
}, [isClosed, apiRequest?.requestId]);
|
||||
|
||||
// Listen for credit note notifications and trigger silent refresh
|
||||
useEffect(() => {
|
||||
if (!currentUserId || !apiRequest?.requestId) return;
|
||||
|
||||
const socket = getSocket();
|
||||
if (!socket) return;
|
||||
|
||||
joinUserRoom(socket, currentUserId);
|
||||
|
||||
const handleNewNotification = (data: { notification: any }) => {
|
||||
const notif = data?.notification;
|
||||
if (!notif) return;
|
||||
|
||||
const notifRequestId = notif.requestId || notif.request_id;
|
||||
const notifRequestNumber = notif.metadata?.requestNumber || notif.metadata?.request_number;
|
||||
if (notifRequestId !== apiRequest.requestId &&
|
||||
notifRequestNumber !== requestIdentifier &&
|
||||
notifRequestNumber !== apiRequest.requestNumber) return;
|
||||
|
||||
// Check for credit note metadata
|
||||
if (notif.metadata?.creditNoteNumber || notif.metadata?.credit_note_number) {
|
||||
refreshDetails();
|
||||
}
|
||||
};
|
||||
|
||||
socket.on('notification:new', handleNewNotification);
|
||||
return () => {
|
||||
socket.off('notification:new', handleNewNotification);
|
||||
};
|
||||
}, [currentUserId, apiRequest?.requestId, requestIdentifier, refreshDetails]);
|
||||
|
||||
// Get current levels for WorkNotesTab
|
||||
const currentLevels = (request?.approvalFlow || [])
|
||||
.filter((flow: any) => flow && typeof flow.step === 'number')
|
||||
|
||||
@ -89,7 +89,7 @@ export function Auth() {
|
||||
) : (
|
||||
<>
|
||||
<LogIn className="mr-2 h-5 w-5" />
|
||||
SSO with OKTA
|
||||
Login with OKTA
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@ -119,7 +119,7 @@ export function Auth() {
|
||||
) : (
|
||||
<>
|
||||
<Shield className="mr-2 h-5 w-5" />
|
||||
SSO with Tanflow
|
||||
Login with Tanflow
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user