ui enhnce on validation points

This commit is contained in:
laxmanhalaki 2025-12-26 15:03:24 +05:30
parent 058ab97600
commit aedba86ae3
8 changed files with 186 additions and 59 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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