Compare commits

..

No commits in common. "80ed407cd8e6f660c98b0b50fc9bc45aaf57db32" and "08cda349f3ee4f3cfc4c12cd5e0adf8f8a3081f3" have entirely different histories.

16 changed files with 2451 additions and 2381 deletions

View File

@ -412,6 +412,201 @@ function AppRoutes({ onLogout }: AppProps) {
}); });
} }
// Keep the old code below for backward compatibility (local storage fallback)
// This can be removed once API integration is fully tested
/*
// Generate unique ID for the new claim request
const requestId = `RE-REQ-2024-CM-${String(dynamicRequests.length + 2).padStart(3, '0')}`;
// Create full request object
const newRequest = {
id: requestId,
title: `${claimData.activityName} - Claim Request`,
description: claimData.requestDescription,
category: 'Dealer Operations',
subcategory: 'Claim Management',
status: 'pending',
priority: 'standard',
amount: 'TBD',
slaProgress: 0,
slaRemaining: '7 days',
slaEndDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
currentStep: 1,
totalSteps: 8,
templateType: 'claim-management',
templateName: 'Claim Management',
initiator: {
name: 'Current User',
role: 'Regional Marketing Coordinator',
department: 'Marketing',
email: 'current.user@royalenfield.com',
phone: '+91 98765 43290',
avatar: 'CU'
},
department: 'Marketing',
createdAt: new Date().toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true
}),
updatedAt: new Date().toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true
}),
dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
conclusionRemark: '',
claimDetails: {
activityName: claimData.activityName,
activityType: claimData.activityType,
activityDate: claimData.activityDate ? new Date(claimData.activityDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '',
location: claimData.location,
dealerCode: claimData.dealerCode,
dealerName: claimData.dealerName,
dealerEmail: claimData.dealerEmail || 'N/A',
dealerPhone: claimData.dealerPhone || 'N/A',
dealerAddress: claimData.dealerAddress || 'N/A',
requestDescription: claimData.requestDescription,
estimatedBudget: claimData.estimatedBudget || 'TBD',
periodStart: claimData.periodStartDate ? new Date(claimData.periodStartDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '',
periodEnd: claimData.periodEndDate ? new Date(claimData.periodEndDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : ''
},
approvalFlow: claimData.workflowSteps || [
{
step: 1,
approver: `${claimData.dealerName} (Dealer)`,
role: 'Dealer - Document Upload',
status: 'pending',
tatHours: 72,
elapsedHours: 0,
assignedAt: new Date().toISOString(),
comment: null,
timestamp: null,
description: 'Dealer uploads proposal document, cost breakup, timeline for closure, and other supporting documents'
},
{
step: 2,
approver: 'Current User (Initiator)',
role: 'Initiator Evaluation',
status: 'waiting',
tatHours: 48,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Initiator reviews dealer documents and approves or requests modifications'
},
{
step: 3,
approver: 'System Auto-Process',
role: 'IO Confirmation',
status: 'waiting',
tatHours: 1,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Automatic IO (Internal Order) confirmation generated upon initiator approval'
},
{
step: 4,
approver: 'Rajesh Kumar',
role: 'Department Lead Approval',
status: 'waiting',
tatHours: 72,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Department head approves and blocks budget in IO for this activity'
},
{
step: 5,
approver: `${claimData.dealerName} (Dealer)`,
role: 'Dealer - Completion Documents',
status: 'waiting',
tatHours: 120,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Dealer submits activity completion documents and description'
},
{
step: 6,
approver: 'Current User (Initiator)',
role: 'Initiator Verification',
status: 'waiting',
tatHours: 48,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Initiator verifies completion documents and can modify approved amount'
},
{
step: 7,
approver: 'System Auto-Process',
role: 'E-Invoice Generation',
status: 'waiting',
tatHours: 1,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Auto-generate e-invoice based on final approved amount'
},
{
step: 8,
approver: 'Finance Team',
role: 'Credit Note Issuance',
status: 'waiting',
tatHours: 48,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Finance team issues credit note to dealer'
}
],
documents: [],
spectators: [],
auditTrail: [
{
type: 'created',
action: 'Request Created',
details: `Claim request for ${claimData.activityName} created`,
user: 'Current User',
timestamp: new Date().toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true
})
}
],
tags: ['claim-management', 'new-request', claimData.activityType?.toLowerCase().replace(/\s+/g, '-')]
};
// Add to dynamic requests
setDynamicRequests(prev => [...prev, newRequest]);
// Also add to REQUEST_DATABASE for immediate viewing
(REQUEST_DATABASE as any)[requestId] = newRequest;
toast.success('Claim Request Submitted', {
description: 'Your claim management request has been created successfully.',
});
navigate('/my-requests');
*/
}; };
return ( return (

View File

@ -31,7 +31,7 @@ export function AnalyticsConfig() {
}); });
const handleSave = () => { const handleSave = () => {
// TODO: Implement API call to save configuration
toast.success('Analytics configuration saved successfully'); toast.success('Analytics configuration saved successfully');
}; };

View File

@ -59,7 +59,7 @@ export function DashboardConfig() {
}); });
const handleSave = () => { const handleSave = () => {
// TODO: Implement API call to save dashboard configuration
toast.success('Dashboard layout saved successfully'); toast.success('Dashboard layout saved successfully');
}; };

View File

@ -28,7 +28,7 @@ export function NotificationConfig() {
}); });
const handleSave = () => { const handleSave = () => {
// TODO: Implement API call to save notification configuration
toast.success('Notification configuration saved successfully'); toast.success('Notification configuration saved successfully');
}; };

View File

@ -23,7 +23,7 @@ export function SharingConfig() {
}); });
const handleSave = () => { const handleSave = () => {
// TODO: Implement API call to save sharing configuration
toast.success('Sharing policy saved successfully'); toast.success('Sharing policy saved successfully');
}; };

View File

@ -318,7 +318,7 @@ export function UserManagement() {
const user = users.find(u => u.userId === userId); const user = users.find(u => u.userId === userId);
if (!user) return; if (!user) return;
// TODO: Implement backend API for toggling user status
toast.info('User status toggle functionality coming soon'); toast.info('User status toggle functionality coming soon');
}; };
@ -332,6 +332,7 @@ export function UserManagement() {
return; return;
} }
// TODO: Implement backend API for deleting users
toast.info('User deletion functionality coming soon'); toast.info('User deletion functionality coming soon');
}; };
@ -514,10 +515,11 @@ export function UserManagement() {
{/* Message */} {/* Message */}
{message && ( {message && (
<div className={`border-2 rounded-lg p-4 ${message.type === 'success' <div className={`border-2 rounded-lg p-4 ${
? 'border-green-200 bg-green-50' message.type === 'success'
: 'border-red-200 bg-red-50' ? 'border-green-200 bg-green-50'
}`}> : 'border-red-200 bg-red-50'
}`}>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
{message.type === 'success' ? ( {message.type === 'success' ? (
<CheckCircle className="w-5 h-5 text-green-600 shrink-0 mt-0.5" /> <CheckCircle className="w-5 h-5 text-green-600 shrink-0 mt-0.5" />
@ -662,10 +664,11 @@ export function UserManagement() {
variant={currentPage === pageNum ? "default" : "outline"} variant={currentPage === pageNum ? "default" : "outline"}
size="sm" size="sm"
onClick={() => handlePageChange(pageNum)} onClick={() => handlePageChange(pageNum)}
className={`w-9 h-9 p-0 ${currentPage === pageNum className={`w-9 h-9 p-0 ${
? 'bg-re-green hover:bg-re-green/90' currentPage === pageNum
: '' ? 'bg-re-green hover:bg-re-green/90'
}`} : ''
}`}
> >
{pageNum} {pageNum}
</Button> </Button>

View File

@ -193,7 +193,7 @@ export function ClaimApproverSelectionStep({
// Only update if there are actual changes (to avoid infinite loops) // Only update if there are actual changes (to avoid infinite loops)
const hasChanges = JSON.stringify(currentApprovers.map(a => ({ level: a.level, originalStepLevel: a.originalStepLevel }))) !== const hasChanges = JSON.stringify(currentApprovers.map(a => ({ level: a.level, originalStepLevel: a.originalStepLevel }))) !==
JSON.stringify(newApprovers.map(a => ({ level: a.level, originalStepLevel: a.originalStepLevel }))); JSON.stringify(newApprovers.map(a => ({ level: a.level, originalStepLevel: a.originalStepLevel })));
if (hasChanges) { if (hasChanges) {
updateFormData('approvers', newApprovers); updateFormData('approvers', newApprovers);
@ -876,258 +876,237 @@ export function ClaimApproverSelectionStep({
<div className="w-px h-3 bg-gray-300"></div> <div className="w-px h-3 bg-gray-300"></div>
</div> </div>
{/* Render additional approvers before this step if any */} {/* Render additional approvers before this step if any */}
{index === 0 && additionalBeforeFirst.map((addApprover: ClaimApprover, addIndex: number) => { {index === 0 && additionalBeforeFirst.map((addApprover: ClaimApprover, addIndex: number) => {
const addDisplayNumber = addIndex + 1; // Number from 1 for first additional approvers const addDisplayNumber = addIndex + 1; // Number from 1 for first additional approvers
return ( return (
<div key={`additional-${addApprover.level}`} className="space-y-1"> <div key={`additional-${addApprover.level}`} className="space-y-1">
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-px h-3 bg-gray-300"></div> <div className="w-px h-3 bg-gray-300"></div>
</div> </div>
<div className="p-3 rounded-lg border-2 border-purple-200 bg-purple-50"> <div className="p-3 rounded-lg border-2 border-purple-200 bg-purple-50">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-purple-600"> <div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-purple-600">
<span className="text-white font-semibold text-sm">{addDisplayNumber}</span> <span className="text-white font-semibold text-sm">{addDisplayNumber}</span>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap"> <div className="flex items-center gap-2 mb-1 flex-wrap">
<span className="font-semibold text-gray-900 text-sm"> <span className="font-semibold text-gray-900 text-sm">
Additional Approver Additional Approver
</span> </span>
<Badge variant="outline" className="text-xs bg-purple-50 text-purple-700 border-purple-300"> <Badge variant="outline" className="text-xs bg-purple-50 text-purple-700 border-purple-300">
ADDITIONAL ADDITIONAL
</Badge> </Badge>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleRemoveAdditionalApprover(addApprover.level)} onClick={() => handleRemoveAdditionalApprover(addApprover.level)}
className="h-6 w-6 p-0 text-red-600 hover:text-red-700 hover:bg-red-50" className="h-6 w-6 p-0 text-red-600 hover:text-red-700 hover:bg-red-50"
> >
<X className="w-3 h-3" /> <X className="w-3 h-3" />
</Button> </Button>
</div> </div>
<p className="text-xs text-gray-600 mb-2"> <p className="text-xs text-gray-600 mb-2">
{addApprover.name || addApprover.email} {addApprover.name || addApprover.email}
</p> </p>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
<div>Email: {addApprover.email}</div> <div>Email: {addApprover.email}</div>
<div>TAT: {addApprover.tat} {addApprover.tatType}</div> <div>TAT: {addApprover.tat} {addApprover.tatType}</div>
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>
); </div>
})} );
})}
<div className={`p-3 rounded-lg border-2 transition-all ${approver.email && approver.userId <div className={`p-3 rounded-lg border-2 transition-all ${
approver.email && approver.userId
? 'border-green-200 bg-green-50' ? 'border-green-200 bg-green-50'
: isPreFilled : isPreFilled
? 'border-blue-200 bg-blue-50' ? 'border-blue-200 bg-blue-50'
: 'border-gray-200 bg-gray-50' : 'border-gray-200 bg-gray-50'
}`}> }`}>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${approver.email && approver.userId <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
approver.email && approver.userId
? 'bg-green-600' ? 'bg-green-600'
: isPreFilled : isPreFilled
? 'bg-blue-600' ? 'bg-blue-600'
: 'bg-gray-400' : 'bg-gray-400'
}`}> }`}>
<span className="text-white font-semibold text-sm">{currentStepDisplayNumber}</span> <span className="text-white font-semibold text-sm">{currentStepDisplayNumber}</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className="font-semibold text-gray-900 text-sm">
{step.name}
</span>
{isLast && (
<Badge variant="destructive" className="text-xs">FINAL</Badge>
)}
{isPreFilled && (
<Badge variant="outline" className="text-xs">PRE-FILLED</Badge>
)}
</div>
<p className="text-xs text-gray-600 mb-2">{step.description}</p>
{isEditable && (() => {
const isVerified = !!(approver.email && approver.userId);
const isEmpty = !approver.email && !isPreFilled;
return (
<div className="space-y-2">
<div>
<div className="flex items-center justify-between mb-1">
<Label htmlFor={`approver-${step.level}`} className={`text-xs font-bold ${isEmpty ? 'text-blue-900' : isVerified ? 'text-green-900' : 'text-gray-900'
}`}>
Approver Email {!isPreFilled && '*'}
{isEmpty && <span className="ml-2 text-[10px] font-semibold italic text-blue-600">(Required)</span>}
</Label>
{isVerified && (
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
<CheckCircle className="w-3 h-3 mr-1" />
Verified
</Badge>
)}
</div>
<div className="relative">
<Input
id={`approver-${step.level}`}
type="text"
placeholder={isPreFilled ? approver.email : "@username or email..."}
value={approver.email || ''}
onChange={(e) => {
const newValue = e.target.value;
if (!isPreFilled) {
handleApproverEmailChange(step.level, newValue);
}
}}
disabled={isPreFilled || step.isAuto}
className={`h-9 border-2 transition-all mt-1 w-full text-sm ${isPreFilled
? 'bg-gray-100/80 border-gray-300 text-gray-700 cursor-not-allowed font-medium'
: isVerified
? 'bg-green-50/50 border-green-600 focus:border-green-700 ring-offset-green-50 focus:ring-1 focus:ring-green-100 font-semibold text-gray-900'
: 'bg-white border-blue-300 shadow-sm shadow-blue-100/50 focus:border-blue-500 focus:ring-1 focus:ring-blue-100 text-gray-900'
}`}
/>
{/* Search suggestions dropdown */}
{!isPreFilled && !step.isAuto && (userSearchLoading[step.level - 1] || (userSearchResults[step.level - 1]?.length || 0) > 0) && (
<div className="absolute left-0 right-0 top-full mt-1 z-50 border rounded-md bg-white shadow-lg">
{userSearchLoading[step.level - 1] ? (
<div className="p-2 text-xs text-gray-500">Searching...</div>
) : (
<ul className="max-h-56 overflow-auto divide-y">
{userSearchResults[step.level - 1]?.map((u) => (
<li
key={u.userId}
className="p-2 text-sm cursor-pointer hover:bg-gray-50"
onClick={() => handleUserSelect(step.level, u)}
>
<div className="font-medium text-gray-900">{u.displayName || u.email}</div>
<div className="text-xs text-gray-600">{u.email}</div>
{u.department && (
<div className="text-xs text-gray-500">{u.department}</div>
)}
</li>
))}
</ul>
)}
</div>
)}
</div>
{approver.name && (
<p className="text-xs text-green-600 mt-1">
Selected: <span className="font-semibold">{approver.name}</span>
</p>
)}
</div>
<div>
<Label htmlFor={`tat-${step.level}`} className={`text-xs font-bold ${isEmpty ? 'text-blue-900' : isVerified ? 'text-green-900' : 'text-gray-900'
}`}>
TAT (Turn Around Time) *
</Label>
<div className="flex items-center gap-2 mt-1">
<Input
id={`tat-${step.level}`}
type="number"
placeholder={approver.tatType === 'days' ? '7' : '24'}
min="1"
max={approver.tatType === 'days' ? '30' : '720'}
value={approver.tat || ''}
onChange={(e) => handleTatChange(step.level, parseInt(e.target.value) || '')}
disabled={step.isAuto}
className={`h-9 border-2 transition-all flex-1 text-sm ${isPreFilled
? 'bg-gray-100/80 border-gray-300 text-gray-700 cursor-not-allowed font-medium'
: isVerified
? 'bg-green-50/50 border-green-600 focus:border-green-700 focus:ring-1 focus:ring-green-100 font-semibold text-gray-900'
: 'bg-white border-blue-300 shadow-sm shadow-blue-100/50 focus:border-blue-500 focus:ring-1 focus:ring-blue-100 text-gray-900'
}`}
/>
<Select
value={approver.tatType || 'hours'}
onValueChange={(value) => handleTatTypeChange(step.level, value as 'hours' | 'days')}
disabled={step.isAuto}
>
<SelectTrigger className={`w-20 h-9 border-2 transition-all text-sm ${isPreFilled
? 'bg-gray-100/80 border-gray-300 text-gray-700 cursor-not-allowed'
: isVerified
? 'bg-green-50/50 border-green-600 focus:border-green-700 focus:ring-1 focus:ring-green-100 text-gray-900 font-medium'
: 'bg-white border-blue-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-100 text-gray-900'
}`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hours">Hours</SelectItem>
<SelectItem value="days">Days</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
);
})()}
</div>
</div> </div>
</div> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className="font-semibold text-gray-900 text-sm">
{step.name}
</span>
{isLast && (
<Badge variant="destructive" className="text-xs">FINAL</Badge>
)}
{isPreFilled && (
<Badge variant="outline" className="text-xs">PRE-FILLED</Badge>
)}
</div>
<p className="text-xs text-gray-600 mb-2">{step.description}</p>
{/* Render additional approvers after this step */} {isEditable && (
{additionalApproversAfter.map((addApprover: ClaimApprover, addIndex: number) => { <div className="space-y-2">
// Additional approvers come after the current step, so they should be numbered after it <div>
const addDisplayNumber = currentStepDisplayNumber + addIndex + 1; <div className="flex items-center justify-between mb-1">
return ( <Label htmlFor={`approver-${step.level}`} className="text-xs font-medium">
<div key={`additional-${addApprover.level}`} className="space-y-1"> Email Address {!isPreFilled && '*'}
<div className="flex justify-center"> </Label>
<div className="w-px h-3 bg-gray-300"></div> {approver.email && approver.userId && (
</div> <Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
<div className="p-3 rounded-lg border-2 border-purple-200 bg-purple-50"> <CheckCircle className="w-3 h-3 mr-1" />
<div className="flex items-start gap-3"> Verified
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-purple-600">
<span className="text-white font-semibold text-sm">{addDisplayNumber}</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className="font-semibold text-gray-900 text-sm">
{addApprover.stepName || 'Additional Approver'}
</span>
<Badge variant="outline" className="text-xs bg-purple-50 text-purple-700 border-purple-300">
ADDITIONAL
</Badge> </Badge>
{addApprover.email && addApprover.userId && ( )}
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300"> </div>
<CheckCircle className="w-3 h-3 mr-1" /> <div className="relative">
Verified <Input
</Badge> id={`approver-${step.level}`}
)} type="text"
<Button placeholder={isPreFilled ? approver.email : "@approver@royalenfield.com"}
type="button" value={approver.email || ''}
variant="ghost" onChange={(e) => {
size="sm" const newValue = e.target.value;
onClick={() => handleRemoveAdditionalApprover(addApprover.level)} if (!isPreFilled) {
className="h-6 w-6 p-0 text-red-600 hover:text-red-700 hover:bg-red-50" handleApproverEmailChange(step.level, newValue);
> }
<X className="w-3 h-3" /> }}
</Button> disabled={isPreFilled || step.isAuto}
</div> className="h-9 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full text-sm"
<p className="text-xs text-gray-600 mb-2"> />
{addApprover.name || addApprover.email || 'No approver assigned'} {/* Search suggestions dropdown */}
</p> {!isPreFilled && !step.isAuto && (userSearchLoading[step.level - 1] || (userSearchResults[step.level - 1]?.length || 0) > 0) && (
{addApprover.email && ( <div className="absolute left-0 right-0 top-full mt-1 z-50 border rounded-md bg-white shadow-lg">
<div className="text-xs text-gray-500 space-y-1"> {userSearchLoading[step.level - 1] ? (
<div>Email: {addApprover.email}</div> <div className="p-2 text-xs text-gray-500">Searching...</div>
{addApprover.tat && ( ) : (
<div>TAT: {addApprover.tat} {addApprover.tatType}</div> <ul className="max-h-56 overflow-auto divide-y">
{userSearchResults[step.level - 1]?.map((u) => (
<li
key={u.userId}
className="p-2 text-sm cursor-pointer hover:bg-gray-50"
onClick={() => handleUserSelect(step.level, u)}
>
<div className="font-medium text-gray-900">{u.displayName || u.email}</div>
<div className="text-xs text-gray-600">{u.email}</div>
{u.department && (
<div className="text-xs text-gray-500">{u.department}</div>
)}
</li>
))}
</ul>
)} )}
</div> </div>
)} )}
</div> </div>
{approver.name && (
<p className="text-xs text-green-600 mt-1">
Selected: <span className="font-semibold">{approver.name}</span>
</p>
)}
</div>
<div>
<Label htmlFor={`tat-${step.level}`} className="text-xs font-medium">
TAT (Turn Around Time) *
</Label>
<div className="flex items-center gap-2 mt-1">
<Input
id={`tat-${step.level}`}
type="number"
placeholder={approver.tatType === 'days' ? '7' : '24'}
min="1"
max={approver.tatType === 'days' ? '30' : '720'}
value={approver.tat || ''}
onChange={(e) => handleTatChange(step.level, parseInt(e.target.value) || '')}
disabled={step.isAuto}
className="h-9 border-2 border-gray-300 focus:border-blue-500 flex-1 text-sm"
/>
<Select
value={approver.tatType || 'hours'}
onValueChange={(value) => handleTatTypeChange(step.level, value as 'hours' | 'days')}
disabled={step.isAuto}
>
<SelectTrigger className="w-20 h-9 border-2 border-gray-300 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hours">Hours</SelectItem>
<SelectItem value="days">Days</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
)}
</div>
</div>
</div>
{/* Render additional approvers after this step */}
{additionalApproversAfter.map((addApprover: ClaimApprover, addIndex: number) => {
// Additional approvers come after the current step, so they should be numbered after it
const addDisplayNumber = currentStepDisplayNumber + addIndex + 1;
return (
<div key={`additional-${addApprover.level}`} className="space-y-1">
<div className="flex justify-center">
<div className="w-px h-3 bg-gray-300"></div>
</div>
<div className="p-3 rounded-lg border-2 border-purple-200 bg-purple-50">
<div className="flex items-start gap-3">
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-purple-600">
<span className="text-white font-semibold text-sm">{addDisplayNumber}</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className="font-semibold text-gray-900 text-sm">
{addApprover.stepName || 'Additional Approver'}
</span>
<Badge variant="outline" className="text-xs bg-purple-50 text-purple-700 border-purple-300">
ADDITIONAL
</Badge>
{addApprover.email && addApprover.userId && (
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
<CheckCircle className="w-3 h-3 mr-1" />
Verified
</Badge>
)}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleRemoveAdditionalApprover(addApprover.level)}
className="h-6 w-6 p-0 text-red-600 hover:text-red-700 hover:bg-red-50"
>
<X className="w-3 h-3" />
</Button>
</div>
<p className="text-xs text-gray-600 mb-2">
{addApprover.name || addApprover.email || 'No approver assigned'}
</p>
{addApprover.email && (
<div className="text-xs text-gray-500 space-y-1">
<div>Email: {addApprover.email}</div>
{addApprover.tat && (
<div>TAT: {addApprover.tat} {addApprover.tatType}</div>
)}
</div>
)}
</div> </div>
</div> </div>
</div> </div>
); </div>
})} );
</div> })}
); </div>
);
}); });
})()} })()}
</CardContent> </CardContent>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
/** /**
* ProcessDetailsCard Component * ProcessDetailsCard Component
* Displays process-related details: IO Number, E-Invoice, Claim Amount, and Budget Breakdowns * Displays process-related details: IO Number, DMS Number, Claim Amount, and Budget Breakdowns
* Visibility controlled by user role * Visibility controlled by user role
*/ */
@ -172,18 +172,21 @@ export function ProcessDetailsCard({
</div> </div>
)} )}
{/* E-Invoice Details */} {/* DMS Details */}
{visibility.showDMSDetails && dmsDetails && ( {visibility.showDMSDetails && dmsDetails && (
<div className="bg-white rounded-lg p-3 border border-purple-200"> <div className="bg-white rounded-lg p-3 border border-purple-200">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Activity className="w-4 h-4 text-purple-600" /> <Activity className="w-4 h-4 text-purple-600" />
<Label className="text-xs font-semibold text-purple-900 uppercase tracking-wide"> <Label className="text-xs font-semibold text-purple-900 uppercase tracking-wide">
E-Invoice Details DMS & E-Invoice Details
</Label> </Label>
</div> </div>
<div className="grid grid-cols-2 gap-3 mb-2"> <div className="grid grid-cols-2 gap-3 mb-2">
<div>
<p className="text-[10px] text-gray-500 uppercase">DMS Number</p>
<p className="font-bold text-sm text-gray-900">{dmsDetails.dmsNumber || 'N/A'}</p>
</div>
{dmsDetails.ackNo && ( {dmsDetails.ackNo && (
<div> <div>
<p className="text-[10px] text-gray-500 uppercase">Ack No</p> <p className="text-[10px] text-gray-500 uppercase">Ack No</p>

View File

@ -22,7 +22,6 @@ interface ProposalCostItem {
interface ProposalDetails { interface ProposalDetails {
costBreakup: ProposalCostItem[]; costBreakup: ProposalCostItem[];
estimatedBudgetTotal?: number | null; estimatedBudgetTotal?: number | null;
totalEstimatedBudget?: number | null;
timelineForClosure?: string | null; timelineForClosure?: string | null;
dealerComments?: string | null; dealerComments?: string | null;
submittedOn?: string | null; submittedOn?: string | null;
@ -36,9 +35,8 @@ interface ProposalDetailsCardProps {
export function ProposalDetailsCard({ proposalDetails, className }: ProposalDetailsCardProps) { export function ProposalDetailsCard({ proposalDetails, className }: ProposalDetailsCardProps) {
// Calculate estimated total from costBreakup if not provided // Calculate estimated total from costBreakup if not provided
const calculateEstimatedTotal = () => { const calculateEstimatedTotal = () => {
const total = proposalDetails.totalEstimatedBudget ?? proposalDetails.estimatedBudgetTotal; if (proposalDetails.estimatedBudgetTotal !== undefined && proposalDetails.estimatedBudgetTotal !== null) {
if (total !== undefined && total !== null) { return proposalDetails.estimatedBudgetTotal;
return total;
} }
// Calculate sum from costBreakup items // Calculate sum from costBreakup items

View File

@ -1,16 +1,12 @@
.settlement-push-modal { .dms-push-modal {
width: 90vw !important; width: 90vw !important;
max-width: 1000px !important; max-width: 90vw !important;
min-width: 320px !important;
max-height: 95vh !important; max-height: 95vh !important;
overflow: hidden;
display: flex;
flex-direction: column;
} }
/* Mobile responsive */ /* Mobile responsive */
@media (max-width: 640px) { @media (max-width: 640px) {
.settlement-push-modal { .dms-push-modal {
width: 95vw !important; width: 95vw !important;
max-width: 95vw !important; max-width: 95vw !important;
max-height: 95vh !important; max-height: 95vh !important;
@ -19,48 +15,25 @@
/* Tablet and small desktop */ /* Tablet and small desktop */
@media (min-width: 641px) and (max-width: 1023px) { @media (min-width: 641px) and (max-width: 1023px) {
.settlement-push-modal { .dms-push-modal {
width: 90vw !important; width: 90vw !important;
max-width: 900px !important; max-width: 90vw !important;
} }
} }
/* Scrollable content area */ /* Large screens - fixed max-width for better readability */
.settlement-push-modal .flex-1 { @media (min-width: 1024px) {
overflow-y: auto; .dms-push-modal {
padding-right: 4px; width: 90vw !important;
max-width: 1000px !important;
}
} }
/* Custom scrollbar for the modal content */ /* Extra large screens */
.settlement-push-modal .flex-1::-webkit-scrollbar { @media (min-width: 1536px) {
width: 6px; .dms-push-modal {
width: 90vw !important;
max-width: 1000px !important;
}
} }
.settlement-push-modal .flex-1::-webkit-scrollbar-track {
background: transparent;
}
.settlement-push-modal .flex-1::-webkit-scrollbar-thumb {
background: #e2e8f0;
border-radius: 10px;
}
.settlement-push-modal .flex-1::-webkit-scrollbar-thumb:hover {
background: #cbd5e1;
}
.file-preview-dialog {
width: 95vw !important;
max-width: 1200px !important;
max-height: 95vh !important;
padding: 0 !important;
display: flex;
flex-direction: column;
}
.file-preview-content {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}

View File

@ -149,11 +149,11 @@ export function DMSPushModal({
if (!doc.name) return false; if (!doc.name) return false;
const name = doc.name.toLowerCase(); const name = doc.name.toLowerCase();
return name.endsWith('.pdf') || return name.endsWith('.pdf') ||
name.endsWith('.jpg') || name.endsWith('.jpg') ||
name.endsWith('.jpeg') || name.endsWith('.jpeg') ||
name.endsWith('.png') || name.endsWith('.png') ||
name.endsWith('.gif') || name.endsWith('.gif') ||
name.endsWith('.webp'); name.endsWith('.webp');
}; };
// Handle document preview - fetch as blob to avoid CSP issues // Handle document preview - fetch as blob to avoid CSP issues
@ -228,7 +228,7 @@ export function DMSPushModal({
const handleSubmit = async () => { const handleSubmit = async () => {
if (!comments.trim()) { if (!comments.trim()) {
toast.error('Please provide comments before proceeding'); toast.error('Please provide comments before pushing to DMS');
return; return;
} }
@ -238,8 +238,8 @@ export function DMSPushModal({
handleReset(); handleReset();
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('Failed to generate e-invoice:', error); console.error('Failed to push to DMS:', error);
toast.error('Failed to generate e-invoice. Please try again.'); toast.error('Failed to push to DMS. Please try again.');
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
@ -257,408 +257,211 @@ export function DMSPushModal({
}; };
return ( return (
<> <Dialog open={isOpen} onOpenChange={handleClose}>
<Dialog open={isOpen} onOpenChange={handleClose}> <DialogContent className="dms-push-modal overflow-hidden flex flex-col">
<DialogContent className="settlement-push-modal overflow-hidden flex flex-col w-full max-w-none"> <DialogHeader className="px-6 pt-6 pb-3 flex-shrink-0">
<DialogHeader className="px-6 pt-6 pb-3 flex-shrink-0"> <div className="flex items-center gap-2 sm:gap-3 mb-2">
<div className="flex items-center gap-2 sm:gap-3 mb-2"> <div className="p-1.5 sm:p-2 rounded-lg bg-indigo-100">
<div className="p-1.5 sm:p-2 rounded-lg bg-indigo-100"> <Activity className="w-4 h-4 sm:w-5 sm:h-5 sm:w-6 sm:h-6 text-indigo-600" />
<Activity className="w-4 h-4 sm:w-5 sm:h-5 sm:w-6 sm:h-6 text-indigo-600" />
</div>
<div className="flex-1">
<DialogTitle className="font-semibold text-lg sm:text-xl">
E-Invoice Generation & Sync
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm mt-1">
Review completion details and expenses before generating e-invoice and initiating SAP settlement
</DialogDescription>
</div>
</div> </div>
<div className="flex-1">
<DialogTitle className="font-semibold text-lg sm:text-xl">
Push to DMS - Verification
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm mt-1">
Review completion details and expenses before pushing to DMS for e-invoice generation
</DialogDescription>
</div>
</div>
{/* Request Info Card - Grid layout */} {/* Request Info Card - Grid layout */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg border"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg border">
<div className="flex items-center justify-between sm:flex-col sm:items-start sm:gap-1"> <div className="flex items-center justify-between sm:flex-col sm:items-start sm:gap-1">
<span className="font-medium text-xs sm:text-sm text-gray-600">Workflow Step:</span> <span className="font-medium text-xs sm:text-sm text-gray-600">Workflow Step:</span>
<Badge variant="outline" className="font-mono text-xs">Requestor Claim Approval</Badge> <Badge variant="outline" className="font-mono text-xs">Requestor Claim Approval</Badge>
</div> </div>
{requestNumber && ( {requestNumber && (
<div className="flex flex-col gap-1">
<span className="font-medium text-xs sm:text-sm text-gray-600">Request Number:</span>
<p className="text-gray-700 font-mono text-xs sm:text-sm">{requestNumber}</p>
</div>
)}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="font-medium text-xs sm:text-sm text-gray-600">Title:</span> <span className="font-medium text-xs sm:text-sm text-gray-600">Request Number:</span>
<p className="text-gray-700 text-xs sm:text-sm line-clamp-2">{requestTitle || '—'}</p> <p className="text-gray-700 font-mono text-xs sm:text-sm">{requestNumber}</p>
</div>
<div className="flex items-center gap-2 sm:flex-col sm:items-start sm:gap-1">
<span className="font-medium text-xs sm:text-sm text-gray-600">Action:</span>
<Badge className="bg-indigo-100 text-indigo-800 border-indigo-200 text-xs">
<Activity className="w-3 h-3 mr-1" />
SYNC TO SAP
</Badge>
</div> </div>
)}
<div className="flex flex-col gap-1">
<span className="font-medium text-xs sm:text-sm text-gray-600">Title:</span>
<p className="text-gray-700 text-xs sm:text-sm line-clamp-2">{requestTitle || '—'}</p>
</div> </div>
</DialogHeader> <div className="flex items-center gap-2 sm:flex-col sm:items-start sm:gap-1">
<span className="font-medium text-xs sm:text-sm text-gray-600">Action:</span>
<Badge className="bg-indigo-100 text-indigo-800 border-indigo-200 text-xs">
<Activity className="w-3 h-3 mr-1" />
PUSH TO DMS
</Badge>
</div>
</div>
</DialogHeader>
<div className="flex-1 overflow-y-auto overflow-x-hidden px-6 py-3"> <div className="flex-1 overflow-y-auto overflow-x-hidden px-6 py-3">
<div className="space-y-3 sm:space-y-4 max-w-7xl mx-auto"> <div className="space-y-3 sm:space-y-4 max-w-7xl mx-auto">
{/* Grid layout for all three cards on larger screens */} {/* Grid layout for all three cards on larger screens */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
{/* Completion Details Card */} {/* Completion Details Card */}
{completionDetails && ( {completionDetails && (
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base sm:text-lg"> <CardTitle className="flex items-center gap-2 text-base sm:text-lg">
<CheckCircle2 className="w-4 h-4 sm:w-5 sm:h-5 text-green-600" /> <CheckCircle2 className="w-4 h-4 sm:w-5 sm:h-5 text-green-600" />
Completion Details Completion Details
</CardTitle> </CardTitle>
<CardDescription className="text-xs sm:text-sm"> <CardDescription className="text-xs sm:text-sm">
Review activity completion information Review activity completion information
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-2 sm:space-y-3"> <CardContent className="space-y-2 sm:space-y-3">
{completionDetails.activityCompletionDate && ( {completionDetails.activityCompletionDate && (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 py-1.5 sm:py-2 border-b"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 py-1.5 sm:py-2 border-b">
<span className="text-xs sm:text-sm text-gray-600">Activity Completion Date:</span> <span className="text-xs sm:text-sm text-gray-600">Activity Completion Date:</span>
<span className="text-xs sm:text-sm font-semibold text-gray-900"> <span className="text-xs sm:text-sm font-semibold text-gray-900">
{formatDate(completionDetails.activityCompletionDate)} {formatDate(completionDetails.activityCompletionDate)}
</span> </span>
</div> </div>
)} )}
{completionDetails.numberOfParticipants !== undefined && ( {completionDetails.numberOfParticipants !== undefined && (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 py-1.5 sm:py-2 border-b"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 py-1.5 sm:py-2 border-b">
<span className="text-xs sm:text-sm text-gray-600">Number of Participants:</span> <span className="text-xs sm:text-sm text-gray-600">Number of Participants:</span>
<span className="text-xs sm:text-sm font-semibold text-gray-900"> <span className="text-xs sm:text-sm font-semibold text-gray-900">
{completionDetails.numberOfParticipants} {completionDetails.numberOfParticipants}
</span> </span>
</div> </div>
)} )}
{completionDetails.completionDescription && ( {completionDetails.completionDescription && (
<div className="pt-2"> <div className="pt-2">
<p className="text-xs text-gray-600 mb-1">Completion Description:</p> <p className="text-xs text-gray-600 mb-1">Completion Description:</p>
<p className="text-xs sm:text-sm text-gray-900 line-clamp-3"> <p className="text-xs sm:text-sm text-gray-900 line-clamp-3">
{completionDetails.completionDescription} {completionDetails.completionDescription}
</p>
</div>
)}
</CardContent>
</Card>
)}
{/* IO Details Card */}
{ioDetails && ioDetails.ioNumber && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base sm:text-lg">
<Receipt className="w-4 h-4 sm:w-5 sm:h-5 text-purple-600" />
IO Details
</CardTitle>
<CardDescription className="text-xs sm:text-sm">
Internal Order information for budget reference
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 sm:space-y-3">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 py-1.5 sm:py-2 border-b">
<span className="text-xs sm:text-sm text-gray-600">IO Number:</span>
<span className="text-xs sm:text-sm font-semibold text-gray-900 font-mono">
{ioDetails.ioNumber}
</span>
</div>
{ioDetails.blockedAmount !== undefined && ioDetails.blockedAmount > 0 && (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 py-1.5 sm:py-2 border-b">
<span className="text-xs sm:text-sm text-gray-600">Blocked Amount:</span>
<span className="text-xs sm:text-sm font-bold text-green-700">
{formatCurrency(ioDetails.blockedAmount)}
</span>
</div>
)}
{ioDetails.remainingBalance !== undefined && ioDetails.remainingBalance !== null && (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 py-1.5 sm:py-2">
<span className="text-xs sm:text-sm text-gray-600">Remaining Balance:</span>
<span className="text-xs sm:text-sm font-semibold text-gray-900">
{formatCurrency(ioDetails.remainingBalance)}
</span>
</div>
)}
</CardContent>
</Card>
)}
{/* Expense Breakdown Card */}
{completionDetails?.closedExpenses && completionDetails.closedExpenses.length > 0 && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base sm:text-lg">
<DollarSign className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" />
Expense Breakdown
</CardTitle>
<CardDescription className="text-xs sm:text-sm">
Review closed expenses before pushing to DMS
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-1.5 sm:space-y-2 max-h-[200px] overflow-y-auto">
{completionDetails.closedExpenses.map((expense, index) => (
<div
key={index}
className="flex items-center justify-between py-1.5 sm:py-2 px-2 sm:px-3 bg-gray-50 rounded border"
>
<div className="flex-1 min-w-0 pr-2">
<p className="text-xs sm:text-sm font-medium text-gray-900 truncate">
{expense.description || `Expense ${index + 1}`}
</p> </p>
</div> </div>
)} <div className="ml-2 flex-shrink-0">
</CardContent> <p className="text-xs sm:text-sm font-semibold text-gray-900">
</Card> {formatCurrency(typeof expense === 'object' ? (expense.amount || 0) : 0)}
)} </p>
{/* IO Details Card */}
{ioDetails && ioDetails.ioNumber && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base sm:text-lg">
<Receipt className="w-4 h-4 sm:w-5 sm:h-5 text-purple-600" />
IO Details
</CardTitle>
<CardDescription className="text-xs sm:text-sm">
Internal Order information for budget reference
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 sm:space-y-3">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 py-1.5 sm:py-2 border-b">
<span className="text-xs sm:text-sm text-gray-600">IO Number:</span>
<span className="text-xs sm:text-sm font-semibold text-gray-900 font-mono">
{ioDetails.ioNumber}
</span>
</div>
{ioDetails.blockedAmount !== undefined && ioDetails.blockedAmount > 0 && (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 py-1.5 sm:py-2 border-b">
<span className="text-xs sm:text-sm text-gray-600">Blocked Amount:</span>
<span className="text-xs sm:text-sm font-bold text-green-700">
{formatCurrency(ioDetails.blockedAmount)}
</span>
</div> </div>
)} </div>
{ioDetails.remainingBalance !== undefined && ioDetails.remainingBalance !== null && ( ))}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 py-1.5 sm:py-2"> </div>
<span className="text-xs sm:text-sm text-gray-600">Remaining Balance:</span> <div className="flex items-center justify-between py-2 sm:py-3 px-2 sm:px-3 bg-blue-50 rounded border-2 border-blue-200 mt-2 sm:mt-3">
<span className="text-xs sm:text-sm font-semibold text-gray-900"> <span className="text-xs sm:text-sm font-semibold text-gray-900">Total:</span>
{formatCurrency(ioDetails.remainingBalance)} <span className="text-sm sm:text-base font-bold text-blue-700">
</span> {formatCurrency(totalClosedExpenses)}
</div> </span>
)} </div>
</CardContent> </CardContent>
</Card> </Card>
)} )}
</div>
{/* Expense Breakdown Card */} {/* Completion Documents Section */}
{completionDetails?.closedExpenses && completionDetails.closedExpenses.length > 0 && ( {completionDocuments && (
<Card> <div className="space-y-4">
<CardHeader className="pb-3"> {/* Completion Documents */}
<CardTitle className="flex items-center gap-2 text-base sm:text-lg"> {completionDocuments.completionDocuments && completionDocuments.completionDocuments.length > 0 && (
<DollarSign className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" /> <div className="space-y-2">
Expense Breakdown <div className="flex items-center gap-2">
</CardTitle> <h3 className="font-semibold text-sm sm:text-base flex items-center gap-2">
<CardDescription className="text-xs sm:text-sm"> <CheckCircle2 className="w-4 h-4 sm:w-5 sm:h-5 text-green-600" />
Review closed expenses before generation Completion Documents
</CardDescription> </h3>
</CardHeader> <Badge variant="secondary" className="text-xs">
<CardContent> {completionDocuments.completionDocuments.length} file(s)
<div className="space-y-1.5 sm:space-y-2 max-h-[200px] overflow-y-auto"> </Badge>
{completionDetails.closedExpenses.map((expense, index) => ( </div>
<div <div className="space-y-2 max-h-[200px] overflow-y-auto">
key={index} {completionDocuments.completionDocuments.map((doc, index) => (
className="flex items-center justify-between py-1.5 sm:py-2 px-2 sm:px-3 bg-gray-50 rounded border" <div
> key={index}
<div className="flex-1 min-w-0 pr-2"> className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2"
<p className="text-xs sm:text-sm font-medium text-gray-900 truncate"> >
{expense.description || `Expense ${index + 1}`}
</p>
</div>
<div className="ml-2 flex-shrink-0">
<p className="text-xs sm:text-sm font-semibold text-gray-900">
{formatCurrency(typeof expense === 'object' ? (expense.amount || 0) : 0)}
</p>
</div>
</div>
))}
</div>
<div className="flex items-center justify-between py-2 sm:py-3 px-2 sm:px-3 bg-blue-50 rounded border-2 border-blue-200 mt-2 sm:mt-3">
<span className="text-xs sm:text-sm font-semibold text-gray-900">Total:</span>
<span className="text-sm sm:text-base font-bold text-blue-700">
{formatCurrency(totalClosedExpenses)}
</span>
</div>
</CardContent>
</Card>
)}
</div>
{/* Completion Documents Section */}
{completionDocuments && (
<div className="space-y-4">
{/* Completion Documents */}
{completionDocuments.completionDocuments && completionDocuments.completionDocuments.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-sm sm:text-base flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 sm:w-5 sm:h-5 text-green-600" />
Completion Documents
</h3>
<Badge variant="secondary" className="text-xs">
{completionDocuments.completionDocuments.length} file(s)
</Badge>
</div>
<div className="space-y-2 max-h-[200px] overflow-y-auto">
{completionDocuments.completionDocuments.map((doc, index) => (
<div
key={index}
className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2"
>
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
<CheckCircle2 className="w-4 h-4 lg:w-5 lg:h-5 text-green-600 flex-shrink-0" />
<div className="min-w-0 flex-1">
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={doc.name}>
{doc.name}
</p>
</div>
</div>
{doc.id && (
<div className="flex items-center gap-1 flex-shrink-0">
{canPreviewDocument(doc) && (
<button
type="button"
onClick={() => handlePreviewDocument(doc)}
disabled={previewLoading}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Preview document"
>
{previewLoading ? (
<Loader2 className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 animate-spin" />
) : (
<Eye className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600" />
)}
</button>
)}
<button
type="button"
onClick={async () => {
try {
if (doc.id) {
await downloadDocument(doc.id);
}
} catch (error) {
console.error('Failed to download document:', error);
toast.error('Failed to download document');
}
}}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
title="Download document"
>
<Download className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600" />
</button>
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Activity Photos */}
{completionDocuments.activityPhotos && completionDocuments.activityPhotos.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-sm sm:text-base flex items-center gap-2">
<Activity className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" />
Activity Photos
</h3>
<Badge variant="secondary" className="text-xs">
{completionDocuments.activityPhotos.length} file(s)
</Badge>
</div>
<div className="space-y-2 max-h-[200px] overflow-y-auto">
{completionDocuments.activityPhotos.map((doc, index) => (
<div
key={index}
className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2"
>
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
<Activity className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 flex-shrink-0" />
<div className="min-w-0 flex-1">
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={doc.name}>
{doc.name}
</p>
</div>
</div>
{doc.id && (
<div className="flex items-center gap-1 flex-shrink-0">
{canPreviewDocument(doc) && (
<button
type="button"
onClick={() => handlePreviewDocument(doc)}
disabled={previewLoading}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Preview photo"
>
{previewLoading ? (
<Loader2 className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 animate-spin" />
) : (
<Eye className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600" />
)}
</button>
)}
<button
type="button"
onClick={async () => {
try {
if (doc.id) {
await downloadDocument(doc.id);
}
} catch (error) {
console.error('Failed to download document:', error);
toast.error('Failed to download document');
}
}}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
title="Download photo"
>
<Download className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600" />
</button>
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Invoices / Receipts */}
{completionDocuments.invoicesReceipts && completionDocuments.invoicesReceipts.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-sm sm:text-base flex items-center gap-2">
<Receipt className="w-4 h-4 sm:w-5 sm:h-5 text-purple-600" />
Invoices / Receipts
</h3>
<Badge variant="secondary" className="text-xs">
{completionDocuments.invoicesReceipts.length} file(s)
</Badge>
</div>
<div className="space-y-2 max-h-[200px] overflow-y-auto">
{completionDocuments.invoicesReceipts.map((doc, index) => (
<div
key={index}
className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2"
>
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
<Receipt className="w-4 h-4 lg:w-5 lg:h-5 text-purple-600 flex-shrink-0" />
<div className="min-w-0 flex-1">
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={doc.name}>
{doc.name}
</p>
</div>
</div>
{doc.id && (
<div className="flex items-center gap-1 flex-shrink-0">
{canPreviewDocument(doc) && (
<button
type="button"
onClick={() => handlePreviewDocument(doc)}
disabled={previewLoading}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Preview document"
>
{previewLoading ? (
<Loader2 className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 animate-spin" />
) : (
<Eye className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600" />
)}
</button>
)}
<button
type="button"
onClick={async () => {
try {
if (doc.id) {
await downloadDocument(doc.id);
}
} catch (error) {
console.error('Failed to download document:', error);
toast.error('Failed to download document');
}
}}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
title="Download document"
>
<Download className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600" />
</button>
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Attendance Sheet */}
{completionDocuments.attendanceSheet && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-sm sm:text-base flex items-center gap-2">
<Activity className="w-4 h-4 sm:w-5 sm:h-5 text-indigo-600" />
Attendance Sheet
</h3>
</div>
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2">
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1"> <div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
<Activity className="w-4 h-4 lg:w-5 lg:h-5 text-indigo-600 flex-shrink-0" /> <CheckCircle2 className="w-4 h-4 lg:w-5 lg:h-5 text-green-600 flex-shrink-0" />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={completionDocuments.attendanceSheet.name}> <p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={doc.name}>
{completionDocuments.attendanceSheet.name} {doc.name}
</p> </p>
</div> </div>
</div> </div>
{completionDocuments.attendanceSheet.id && ( {doc.id && (
<div className="flex items-center gap-1 flex-shrink-0"> <div className="flex items-center gap-1 flex-shrink-0">
{canPreviewDocument(completionDocuments.attendanceSheet) && ( {canPreviewDocument(doc) && (
<button <button
type="button" type="button"
onClick={() => handlePreviewDocument(completionDocuments.attendanceSheet!)} onClick={() => handlePreviewDocument(doc)}
disabled={previewLoading} disabled={previewLoading}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed" className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Preview document" title="Preview document"
@ -674,8 +477,8 @@ export function DMSPushModal({
type="button" type="button"
onClick={async () => { onClick={async () => {
try { try {
if (completionDocuments.attendanceSheet?.id) { if (doc.id) {
await downloadDocument(completionDocuments.attendanceSheet.id); await downloadDocument(doc.id);
} }
} catch (error) { } catch (error) {
console.error('Failed to download document:', error); console.error('Failed to download document:', error);
@ -690,81 +493,275 @@ export function DMSPushModal({
</div> </div>
)} )}
</div> </div>
</div> ))}
)} </div>
</div> </div>
)} )}
{/* Verification Warning */} {/* Activity Photos */}
<div className="p-2.5 sm:p-3 bg-yellow-50 border border-yellow-200 rounded-lg"> {completionDocuments.activityPhotos && completionDocuments.activityPhotos.length > 0 && (
<div className="flex items-start gap-2"> <div className="space-y-2">
<TriangleAlert className="w-4 h-4 sm:w-5 sm:h-5 text-yellow-600 flex-shrink-0 mt-0.5" /> <div className="flex items-center gap-2">
<div> <h3 className="font-semibold text-sm sm:text-base flex items-center gap-2">
<p className="text-xs sm:text-sm font-semibold text-yellow-900"> <Activity className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" />
Please verify all details before generation Activity Photos
</p> </h3>
<p className="text-xs text-yellow-700 mt-1"> <Badge variant="secondary" className="text-xs">
Once submitted, the system will generate an e-invoice and initiate the SAP settlement process. {completionDocuments.activityPhotos.length} file(s)
</p> </Badge>
</div>
<div className="space-y-2 max-h-[200px] overflow-y-auto">
{completionDocuments.activityPhotos.map((doc, index) => (
<div
key={index}
className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2"
>
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
<Activity className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 flex-shrink-0" />
<div className="min-w-0 flex-1">
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={doc.name}>
{doc.name}
</p>
</div>
</div>
{doc.id && (
<div className="flex items-center gap-1 flex-shrink-0">
{canPreviewDocument(doc) && (
<button
type="button"
onClick={() => handlePreviewDocument(doc)}
disabled={previewLoading}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Preview photo"
>
{previewLoading ? (
<Loader2 className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 animate-spin" />
) : (
<Eye className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600" />
)}
</button>
)}
<button
type="button"
onClick={async () => {
try {
if (doc.id) {
await downloadDocument(doc.id);
}
} catch (error) {
console.error('Failed to download document:', error);
toast.error('Failed to download document');
}
}}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
title="Download photo"
>
<Download className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600" />
</button>
</div>
)}
</div>
))}
</div> </div>
</div> </div>
</div> )}
{/* Comments & Remarks */} {/* Invoices / Receipts */}
<div className="space-y-1.5 max-w-2xl"> {completionDocuments.invoicesReceipts && completionDocuments.invoicesReceipts.length > 0 && (
<Label htmlFor="comment" className="text-xs sm:text-sm font-semibold text-gray-900 flex items-center gap-2"> <div className="space-y-2">
Comments & Remarks <span className="text-red-500">*</span> <div className="flex items-center gap-2">
</Label> <h3 className="font-semibold text-sm sm:text-base flex items-center gap-2">
<Textarea <Receipt className="w-4 h-4 sm:w-5 sm:h-5 text-purple-600" />
id="comment" Invoices / Receipts
placeholder="Enter your comments about e-invoice generation (e.g., verified expenses, ready for settlement)..." </h3>
value={comments} <Badge variant="secondary" className="text-xs">
onChange={(e) => { {completionDocuments.invoicesReceipts.length} file(s)
const value = e.target.value; </Badge>
if (value.length <= maxCommentsChars) { </div>
setComments(value); <div className="space-y-2 max-h-[200px] overflow-y-auto">
} {completionDocuments.invoicesReceipts.map((doc, index) => (
}} <div
rows={4} key={index}
className="text-sm min-h-[80px] resize-none" className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2"
/> >
<div className="flex items-center justify-between text-xs text-gray-500"> <div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
<div className="flex items-center gap-1"> <Receipt className="w-4 h-4 lg:w-5 lg:h-5 text-purple-600 flex-shrink-0" />
<TriangleAlert className="w-3 h-3" /> <div className="min-w-0 flex-1">
Required and visible to all <p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={doc.name}>
{doc.name}
</p>
</div>
</div>
{doc.id && (
<div className="flex items-center gap-1 flex-shrink-0">
{canPreviewDocument(doc) && (
<button
type="button"
onClick={() => handlePreviewDocument(doc)}
disabled={previewLoading}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Preview document"
>
{previewLoading ? (
<Loader2 className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 animate-spin" />
) : (
<Eye className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600" />
)}
</button>
)}
<button
type="button"
onClick={async () => {
try {
if (doc.id) {
await downloadDocument(doc.id);
}
} catch (error) {
console.error('Failed to download document:', error);
toast.error('Failed to download document');
}
}}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
title="Download document"
>
<Download className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600" />
</button>
</div>
)}
</div>
))}
</div> </div>
<span>{commentsChars}/{maxCommentsChars}</span>
</div> </div>
)}
{/* Attendance Sheet */}
{completionDocuments.attendanceSheet && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-sm sm:text-base flex items-center gap-2">
<Activity className="w-4 h-4 sm:w-5 sm:h-5 text-indigo-600" />
Attendance Sheet
</h3>
</div>
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2">
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
<Activity className="w-4 h-4 lg:w-5 lg:h-5 text-indigo-600 flex-shrink-0" />
<div className="min-w-0 flex-1">
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={completionDocuments.attendanceSheet.name}>
{completionDocuments.attendanceSheet.name}
</p>
</div>
</div>
{completionDocuments.attendanceSheet.id && (
<div className="flex items-center gap-1 flex-shrink-0">
{canPreviewDocument(completionDocuments.attendanceSheet) && (
<button
type="button"
onClick={() => handlePreviewDocument(completionDocuments.attendanceSheet!)}
disabled={previewLoading}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Preview document"
>
{previewLoading ? (
<Loader2 className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 animate-spin" />
) : (
<Eye className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600" />
)}
</button>
)}
<button
type="button"
onClick={async () => {
try {
if (completionDocuments.attendanceSheet?.id) {
await downloadDocument(completionDocuments.attendanceSheet.id);
}
} catch (error) {
console.error('Failed to download document:', error);
toast.error('Failed to download document');
}
}}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
title="Download document"
>
<Download className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600" />
</button>
</div>
)}
</div>
</div>
)}
</div>
)}
{/* Verification Warning */}
<div className="p-2.5 sm:p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-start gap-2">
<TriangleAlert className="w-4 h-4 sm:w-5 sm:h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-xs sm:text-sm font-semibold text-yellow-900">
Please verify all details before pushing to DMS
</p>
<p className="text-xs text-yellow-700 mt-1">
Once pushed, the system will automatically generate an e-invoice and log it as an activity.
</p>
</div> </div>
</div> </div>
</div> </div>
<DialogFooter className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2 px-6 pt-3 pb-6 border-t flex-shrink-0"> {/* Comments & Remarks */}
<Button <div className="space-y-1.5 max-w-2xl">
variant="outline" <Label htmlFor="comment" className="text-xs sm:text-sm font-semibold text-gray-900 flex items-center gap-2">
onClick={handleClose} Comments & Remarks <span className="text-red-500">*</span>
disabled={submitting} </Label>
> <Textarea
Cancel id="comment"
</Button> placeholder="Enter your comments about pushing to DMS (e.g., verified expenses, ready for invoice generation)..."
<Button value={comments}
onClick={handleSubmit} onChange={(e) => {
disabled={!comments.trim() || submitting} const value = e.target.value;
className="bg-indigo-600 hover:bg-indigo-700 text-white" if (value.length <= maxCommentsChars) {
> setComments(value);
{submitting ? ( }
'Processing...' }}
) : ( rows={4}
<> className="text-sm min-h-[80px] resize-none"
<Activity className="w-4 h-4 mr-2" /> />
Generate & Sync <div className="flex items-center justify-between text-xs text-gray-500">
</> <div className="flex items-center gap-1">
)} <TriangleAlert className="w-3 h-3" />
</Button> Required and visible to all
</DialogFooter> </div>
</DialogContent> <span>{commentsChars}/{maxCommentsChars}</span>
</div>
</div>
</div>
</div>
</Dialog> <DialogFooter className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2 px-6 pt-3 pb-6 border-t flex-shrink-0">
<Button
variant="outline"
onClick={handleClose}
disabled={submitting}
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={!comments.trim() || submitting}
className="bg-indigo-600 hover:bg-indigo-700 text-white"
>
{submitting ? (
'Pushing to DMS...'
) : (
<>
<Activity className="w-4 h-4 mr-2" />
Push to DMS
</>
)}
</Button>
</DialogFooter>
</DialogContent>
{/* File Preview Modal - Matching DocumentsTab style */} {/* File Preview Modal - Matching DocumentsTab style */}
{previewDocument && ( {previewDocument && (
@ -869,7 +866,7 @@ export function DMSPushModal({
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}
</> </Dialog>
); );
} }

View File

@ -41,7 +41,7 @@ import { downloadDocument } from '@/services/workflowApi';
import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi'; import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal'; import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal';
import { getSocket, joinUserRoom } from '@/utils/socket'; import { getSocket, joinUserRoom } from '@/utils/socket';
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
// Dealer Claim Components (import from index to get properly aliased exports) // Dealer Claim Components (import from index to get properly aliased exports)
import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index'; import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index';
@ -377,8 +377,8 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
const notifRequestId = notif.requestId || notif.request_id; const notifRequestId = notif.requestId || notif.request_id;
const notifRequestNumber = notif.metadata?.requestNumber || notif.metadata?.request_number; const notifRequestNumber = notif.metadata?.requestNumber || notif.metadata?.request_number;
if (notifRequestId !== apiRequest.requestId && if (notifRequestId !== apiRequest.requestId &&
notifRequestNumber !== requestIdentifier && notifRequestNumber !== requestIdentifier &&
notifRequestNumber !== apiRequest.requestNumber) return; notifRequestNumber !== apiRequest.requestNumber) return;
// Check for credit note metadata // Check for credit note metadata
if (notif.metadata?.creditNoteNumber || notif.metadata?.credit_note_number) { if (notif.metadata?.creditNoteNumber || notif.metadata?.credit_note_number) {
@ -673,7 +673,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
request={request} request={request}
isInitiator={isInitiator} isInitiator={isInitiator}
isSpectator={isSpectator} isSpectator={isSpectator}
currentApprovalLevel={currentApprovalLevel} currentApprovalLevel={isClaimManagementRequest(apiRequest) ? null : currentApprovalLevel}
onAddApprover={() => setShowAddApproverModal(true)} onAddApprover={() => setShowAddApproverModal(true)}
onAddSpectator={() => setShowAddSpectatorModal(true)} onAddSpectator={() => setShowAddSpectatorModal(true)}
onApprove={() => setShowApproveModal(true)} onApprove={() => setShowApproveModal(true)}

View File

@ -175,14 +175,14 @@ export function mapToClaimManagementRequest(
// Get closed expenses breakdown from new completionExpenses table // Get closed expenses breakdown from new completionExpenses table
const closedExpensesBreakdown = Array.isArray(completionExpenses) && completionExpenses.length > 0 const closedExpensesBreakdown = Array.isArray(completionExpenses) && completionExpenses.length > 0
? completionExpenses.map((exp: any) => ({ ? completionExpenses.map((exp: any) => ({
description: exp.description || exp.itemDescription || exp.item_description || '', description: exp.description || exp.itemDescription || '',
amount: Number(exp.amount) || 0, amount: Number(exp.amount) || 0,
gstRate: exp.gstRate ?? exp.gst_rate, gstRate: exp.gstRate,
gstAmt: exp.gstAmt ?? exp.gst_amt, gstAmt: exp.gstAmt,
cgstAmt: exp.cgstAmt ?? exp.cgst_amt, cgstAmt: exp.cgstAmt,
sgstAmt: exp.sgstAmt ?? exp.sgst_amt, sgstAmt: exp.sgstAmt,
igstAmt: exp.igstAmt ?? exp.igst_amt, igstAmt: exp.igstAmt,
totalAmt: exp.totalAmt ?? exp.total_amt totalAmt: exp.totalAmt
})) }))
: (completionDetails?.closedExpenses || : (completionDetails?.closedExpenses ||
completionDetails?.closed_expenses || completionDetails?.closed_expenses ||
@ -232,14 +232,14 @@ export function mapToClaimManagementRequest(
proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url, proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url,
costBreakup: Array.isArray(proposalDetails.costBreakup || proposalDetails.cost_breakup) costBreakup: Array.isArray(proposalDetails.costBreakup || proposalDetails.cost_breakup)
? (proposalDetails.costBreakup || proposalDetails.cost_breakup).map((item: any) => ({ ? (proposalDetails.costBreakup || proposalDetails.cost_breakup).map((item: any) => ({
description: item.description || item.itemDescription || item.item_description || '', description: item.description || '',
amount: Number(item.amount) || 0, amount: Number(item.amount) || 0,
gstRate: item.gstRate ?? item.gst_rate, gstRate: item.gstRate,
gstAmt: item.gstAmt ?? item.gst_amt, gstAmt: item.gstAmt,
cgstAmt: item.cgstAmt ?? item.cgst_amt, cgstAmt: item.cgstAmt,
sgstAmt: item.sgstAmt ?? item.sgst_amt, sgstAmt: item.sgstAmt,
igstAmt: item.igstAmt ?? item.igst_amt, igstAmt: item.igstAmt,
totalAmt: item.totalAmt ?? item.total_amt totalAmt: item.totalAmt
})) }))
: [], : [],
totalEstimatedBudget: proposalDetails.totalEstimatedBudget || proposalDetails.total_estimated_budget || 0, totalEstimatedBudget: proposalDetails.totalEstimatedBudget || proposalDetails.total_estimated_budget || 0,

View File

@ -30,7 +30,7 @@ async function ensureConfigLoaded() {
} }
// Initialize config on first import (non-blocking) // Initialize config on first import (non-blocking)
ensureConfigLoaded().catch(() => { }); ensureConfigLoaded().catch(() => {});
/** /**
* Check if current time is within working hours * Check if current time is within working hours
@ -54,6 +54,7 @@ export function isWorkingTime(date: Date = new Date(), priority: string = 'stand
return false; return false;
} }
// TODO: Add holiday check if holiday API is available
return true; return true;
} }

View File

@ -75,13 +75,26 @@ export const cookieUtils = {
}, },
}; };
/**
* Token Manager - Handles token storage and retrieval
*
* SECURITY MODES:
* - Production: Tokens stored in httpOnly cookies by backend only
* Frontend does NOT store access/refresh tokens anywhere
* All API requests rely on cookies being sent automatically
*
* - Development: Tokens stored in localStorage for debugging
* Needed because frontend/backend run on different ports
*/
export class TokenManager { export class TokenManager {
/** /**
* Store access token * Store access token
* In production: No-op (backend handles via httpOnly cookies)
* In development: Store in localStorage for Authorization header
*/ */
static setAccessToken(token: string): void { static setAccessToken(token: string): void {
// SECURITY: In production, don't store tokens client-side
// Backend sets httpOnly cookies that are sent automatically
if (isProduction()) { if (isProduction()) {
return; // No-op - rely on httpOnly cookies return; // No-op - rely on httpOnly cookies
} }
@ -92,12 +105,13 @@ export class TokenManager {
/** /**
* Get access token * Get access token
* * In production: Returns null (cookies are sent automatically)
* In development: Returns from localStorage
*/ */
static getAccessToken(): string | null { static getAccessToken(): string | null {
// SECURITY: In production, return null - cookies are used instead
if (isProduction()) { if (isProduction()) {
return null; return null; // API calls use cookies via withCredentials: true
} }
// Development: Return from localStorage // Development: Return from localStorage
@ -106,6 +120,8 @@ export class TokenManager {
/** /**
* Store refresh token * Store refresh token
* In production: No-op (backend handles via httpOnly cookies)
* In development: Store in localStorage
*/ */
static setRefreshToken(token: string): void { static setRefreshToken(token: string): void {
// SECURITY: In production, don't store tokens client-side // SECURITY: In production, don't store tokens client-side
@ -119,6 +135,8 @@ export class TokenManager {
/** /**
* Get refresh token * Get refresh token
* In production: Returns null (cookies are used)
* In development: Returns from localStorage
*/ */
static getRefreshToken(): string | null { static getRefreshToken(): string | null {
// SECURITY: In production, return null - backend reads from cookie // SECURITY: In production, return null - backend reads from cookie
@ -129,6 +147,10 @@ export class TokenManager {
return localStorage.getItem(REFRESH_TOKEN_KEY); return localStorage.getItem(REFRESH_TOKEN_KEY);
} }
/**
* Store ID token (from Okta) - needed for logout
* Stored in sessionStorage (cleared when tab closes)
*/
static setIdToken(token: string): void { static setIdToken(token: string): void {
// ID token is needed for Okta logout, use sessionStorage (more secure than localStorage) // ID token is needed for Okta logout, use sessionStorage (more secure than localStorage)
sessionStorage.setItem(ID_TOKEN_KEY, token); sessionStorage.setItem(ID_TOKEN_KEY, token);
@ -161,7 +183,18 @@ export class TokenManager {
} }
} }
/**
* Clear all tokens and user data
*
* PRODUCTION MODE:
* - Clears user data from localStorage
* - Clears ID token from sessionStorage
* - Backend logout endpoint clears httpOnly cookies
*
* DEVELOPMENT MODE:
* - Clears all localStorage and sessionStorage
* - Clears client-side cookies
*/
static clearAll(): void { static clearAll(): void {
// CRITICAL: Set logout flag in sessionStorage FIRST (before clearing) // CRITICAL: Set logout flag in sessionStorage FIRST (before clearing)
// This flag survives the redirect and prevents auto-authentication // This flag survives the redirect and prevents auto-authentication
@ -263,7 +296,11 @@ export class TokenManager {
return !!this.getAccessToken(); return !!this.getAccessToken();
} }
/**
* Check if refresh token exists
* In production: Always returns true if user data exists
* In development: Checks localStorage
*/
static hasRefreshToken(): boolean { static hasRefreshToken(): boolean {
if (isProduction()) { if (isProduction()) {
return !!this.getUserData(); return !!this.getUserData();