Compare commits
2 Commits
08cda349f3
...
80ed407cd8
| Author | SHA1 | Date | |
|---|---|---|---|
| 80ed407cd8 | |||
| 7ae9133b98 |
195
src/App.tsx
195
src/App.tsx
@ -412,201 +412,6 @@ 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 (
|
||||
|
||||
@ -31,7 +31,7 @@ export function AnalyticsConfig() {
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
// TODO: Implement API call to save configuration
|
||||
|
||||
toast.success('Analytics configuration saved successfully');
|
||||
};
|
||||
|
||||
|
||||
@ -59,7 +59,7 @@ export function DashboardConfig() {
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
// TODO: Implement API call to save dashboard configuration
|
||||
|
||||
toast.success('Dashboard layout saved successfully');
|
||||
};
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ export function NotificationConfig() {
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
// TODO: Implement API call to save notification configuration
|
||||
|
||||
toast.success('Notification configuration saved successfully');
|
||||
};
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ export function SharingConfig() {
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
// TODO: Implement API call to save sharing configuration
|
||||
|
||||
toast.success('Sharing policy saved successfully');
|
||||
};
|
||||
|
||||
|
||||
@ -318,7 +318,7 @@ export function UserManagement() {
|
||||
const user = users.find(u => u.userId === userId);
|
||||
if (!user) return;
|
||||
|
||||
// TODO: Implement backend API for toggling user status
|
||||
|
||||
toast.info('User status toggle functionality coming soon');
|
||||
};
|
||||
|
||||
@ -332,7 +332,6 @@ export function UserManagement() {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement backend API for deleting users
|
||||
toast.info('User deletion functionality coming soon');
|
||||
};
|
||||
|
||||
@ -515,11 +514,10 @@ export function UserManagement() {
|
||||
|
||||
{/* Message */}
|
||||
{message && (
|
||||
<div className={`border-2 rounded-lg p-4 ${
|
||||
message.type === 'success'
|
||||
? 'border-green-200 bg-green-50'
|
||||
: 'border-red-200 bg-red-50'
|
||||
}`}>
|
||||
<div className={`border-2 rounded-lg p-4 ${message.type === 'success'
|
||||
? 'border-green-200 bg-green-50'
|
||||
: 'border-red-200 bg-red-50'
|
||||
}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
{message.type === 'success' ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-600 shrink-0 mt-0.5" />
|
||||
@ -664,11 +662,10 @@ export function UserManagement() {
|
||||
variant={currentPage === pageNum ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
className={`w-9 h-9 p-0 ${
|
||||
currentPage === pageNum
|
||||
? 'bg-re-green hover:bg-re-green/90'
|
||||
: ''
|
||||
}`}
|
||||
className={`w-9 h-9 p-0 ${currentPage === pageNum
|
||||
? 'bg-re-green hover:bg-re-green/90'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{pageNum}
|
||||
</Button>
|
||||
|
||||
@ -193,7 +193,7 @@ export function ClaimApproverSelectionStep({
|
||||
|
||||
// Only update if there are actual changes (to avoid infinite loops)
|
||||
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) {
|
||||
updateFormData('approvers', newApprovers);
|
||||
@ -876,237 +876,258 @@ export function ClaimApproverSelectionStep({
|
||||
<div className="w-px h-3 bg-gray-300"></div>
|
||||
</div>
|
||||
|
||||
{/* Render additional approvers before this step if any */}
|
||||
{index === 0 && additionalBeforeFirst.map((addApprover: ClaimApprover, addIndex: number) => {
|
||||
const addDisplayNumber = addIndex + 1; // Number from 1 for first additional approvers
|
||||
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">
|
||||
Additional Approver
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs bg-purple-50 text-purple-700 border-purple-300">
|
||||
ADDITIONAL
|
||||
</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}
|
||||
</p>
|
||||
<div className="text-xs text-gray-500">
|
||||
<div>Email: {addApprover.email}</div>
|
||||
<div>TAT: {addApprover.tat} {addApprover.tatType}</div>
|
||||
</div>
|
||||
{/* Render additional approvers before this step if any */}
|
||||
{index === 0 && additionalBeforeFirst.map((addApprover: ClaimApprover, addIndex: number) => {
|
||||
const addDisplayNumber = addIndex + 1; // Number from 1 for first additional approvers
|
||||
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">
|
||||
Additional Approver
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs bg-purple-50 text-purple-700 border-purple-300">
|
||||
ADDITIONAL
|
||||
</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}
|
||||
</p>
|
||||
<div className="text-xs text-gray-500">
|
||||
<div>Email: {addApprover.email}</div>
|
||||
<div>TAT: {addApprover.tat} {addApprover.tatType}</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'
|
||||
: isPreFilled
|
||||
? 'border-blue-200 bg-blue-50'
|
||||
: 'border-gray-200 bg-gray-50'
|
||||
}`}>
|
||||
<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
|
||||
? 'border-blue-200 bg-blue-50'
|
||||
: 'border-gray-200 bg-gray-50'
|
||||
}`}>
|
||||
<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
|
||||
? 'bg-green-600'
|
||||
: isPreFilled
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-400'
|
||||
}`}>
|
||||
<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>
|
||||
)}
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-400'
|
||||
}`}>
|
||||
<span className="text-white font-semibold text-sm">{currentStepDisplayNumber}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mb-2">{step.description}</p>
|
||||
<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 && (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label htmlFor={`approver-${step.level}`} className="text-xs font-medium">
|
||||
Email Address {!isPreFilled && '*'}
|
||||
</Label>
|
||||
{approver.email && approver.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>
|
||||
)}
|
||||
{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 className="relative">
|
||||
<Input
|
||||
id={`approver-${step.level}`}
|
||||
type="text"
|
||||
placeholder={isPreFilled ? approver.email : "@approver@royalenfield.com"}
|
||||
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 border-gray-300 focus:border-blue-500 mt-1 w-full text-sm"
|
||||
/>
|
||||
{/* 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>
|
||||
</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>
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
);
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</CardContent>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* ProcessDetailsCard Component
|
||||
* Displays process-related details: IO Number, DMS Number, Claim Amount, and Budget Breakdowns
|
||||
* Displays process-related details: IO Number, E-Invoice, Claim Amount, and Budget Breakdowns
|
||||
* Visibility controlled by user role
|
||||
*/
|
||||
|
||||
@ -172,21 +172,18 @@ export function ProcessDetailsCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* DMS Details */}
|
||||
{/* E-Invoice Details */}
|
||||
{visibility.showDMSDetails && dmsDetails && (
|
||||
<div className="bg-white rounded-lg p-3 border border-purple-200">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Activity className="w-4 h-4 text-purple-600" />
|
||||
<Label className="text-xs font-semibold text-purple-900 uppercase tracking-wide">
|
||||
DMS & E-Invoice Details
|
||||
E-Invoice Details
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<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 && (
|
||||
<div>
|
||||
<p className="text-[10px] text-gray-500 uppercase">Ack No</p>
|
||||
|
||||
@ -22,6 +22,7 @@ interface ProposalCostItem {
|
||||
interface ProposalDetails {
|
||||
costBreakup: ProposalCostItem[];
|
||||
estimatedBudgetTotal?: number | null;
|
||||
totalEstimatedBudget?: number | null;
|
||||
timelineForClosure?: string | null;
|
||||
dealerComments?: string | null;
|
||||
submittedOn?: string | null;
|
||||
@ -35,8 +36,9 @@ interface ProposalDetailsCardProps {
|
||||
export function ProposalDetailsCard({ proposalDetails, className }: ProposalDetailsCardProps) {
|
||||
// Calculate estimated total from costBreakup if not provided
|
||||
const calculateEstimatedTotal = () => {
|
||||
if (proposalDetails.estimatedBudgetTotal !== undefined && proposalDetails.estimatedBudgetTotal !== null) {
|
||||
return proposalDetails.estimatedBudgetTotal;
|
||||
const total = proposalDetails.totalEstimatedBudget ?? proposalDetails.estimatedBudgetTotal;
|
||||
if (total !== undefined && total !== null) {
|
||||
return total;
|
||||
}
|
||||
|
||||
// Calculate sum from costBreakup items
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
.dms-push-modal {
|
||||
.settlement-push-modal {
|
||||
width: 90vw !important;
|
||||
max-width: 90vw !important;
|
||||
max-width: 1000px !important;
|
||||
min-width: 320px !important;
|
||||
max-height: 95vh !important;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 640px) {
|
||||
.dms-push-modal {
|
||||
.settlement-push-modal {
|
||||
width: 95vw !important;
|
||||
max-width: 95vw !important;
|
||||
max-height: 95vh !important;
|
||||
@ -15,25 +19,48 @@
|
||||
|
||||
/* Tablet and small desktop */
|
||||
@media (min-width: 641px) and (max-width: 1023px) {
|
||||
.dms-push-modal {
|
||||
.settlement-push-modal {
|
||||
width: 90vw !important;
|
||||
max-width: 90vw !important;
|
||||
max-width: 900px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Large screens - fixed max-width for better readability */
|
||||
@media (min-width: 1024px) {
|
||||
.dms-push-modal {
|
||||
width: 90vw !important;
|
||||
max-width: 1000px !important;
|
||||
}
|
||||
/* Scrollable content area */
|
||||
.settlement-push-modal .flex-1 {
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
/* Extra large screens */
|
||||
@media (min-width: 1536px) {
|
||||
.dms-push-modal {
|
||||
width: 90vw !important;
|
||||
max-width: 1000px !important;
|
||||
}
|
||||
/* Custom scrollbar for the modal content */
|
||||
.settlement-push-modal .flex-1::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -41,7 +41,7 @@ import { downloadDocument } from '@/services/workflowApi';
|
||||
import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
|
||||
import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal';
|
||||
import { getSocket, joinUserRoom } from '@/utils/socket';
|
||||
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
||||
|
||||
|
||||
// Dealer Claim Components (import from index to get properly aliased exports)
|
||||
import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index';
|
||||
@ -377,8 +377,8 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
||||
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;
|
||||
notifRequestNumber !== requestIdentifier &&
|
||||
notifRequestNumber !== apiRequest.requestNumber) return;
|
||||
|
||||
// Check for credit note metadata
|
||||
if (notif.metadata?.creditNoteNumber || notif.metadata?.credit_note_number) {
|
||||
@ -673,7 +673,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
||||
request={request}
|
||||
isInitiator={isInitiator}
|
||||
isSpectator={isSpectator}
|
||||
currentApprovalLevel={isClaimManagementRequest(apiRequest) ? null : currentApprovalLevel}
|
||||
currentApprovalLevel={currentApprovalLevel}
|
||||
onAddApprover={() => setShowAddApproverModal(true)}
|
||||
onAddSpectator={() => setShowAddSpectatorModal(true)}
|
||||
onApprove={() => setShowApproveModal(true)}
|
||||
|
||||
@ -175,14 +175,14 @@ export function mapToClaimManagementRequest(
|
||||
// Get closed expenses breakdown from new completionExpenses table
|
||||
const closedExpensesBreakdown = Array.isArray(completionExpenses) && completionExpenses.length > 0
|
||||
? completionExpenses.map((exp: any) => ({
|
||||
description: exp.description || exp.itemDescription || '',
|
||||
description: exp.description || exp.itemDescription || exp.item_description || '',
|
||||
amount: Number(exp.amount) || 0,
|
||||
gstRate: exp.gstRate,
|
||||
gstAmt: exp.gstAmt,
|
||||
cgstAmt: exp.cgstAmt,
|
||||
sgstAmt: exp.sgstAmt,
|
||||
igstAmt: exp.igstAmt,
|
||||
totalAmt: exp.totalAmt
|
||||
gstRate: exp.gstRate ?? exp.gst_rate,
|
||||
gstAmt: exp.gstAmt ?? exp.gst_amt,
|
||||
cgstAmt: exp.cgstAmt ?? exp.cgst_amt,
|
||||
sgstAmt: exp.sgstAmt ?? exp.sgst_amt,
|
||||
igstAmt: exp.igstAmt ?? exp.igst_amt,
|
||||
totalAmt: exp.totalAmt ?? exp.total_amt
|
||||
}))
|
||||
: (completionDetails?.closedExpenses ||
|
||||
completionDetails?.closed_expenses ||
|
||||
@ -232,14 +232,14 @@ export function mapToClaimManagementRequest(
|
||||
proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url,
|
||||
costBreakup: Array.isArray(proposalDetails.costBreakup || proposalDetails.cost_breakup)
|
||||
? (proposalDetails.costBreakup || proposalDetails.cost_breakup).map((item: any) => ({
|
||||
description: item.description || '',
|
||||
description: item.description || item.itemDescription || item.item_description || '',
|
||||
amount: Number(item.amount) || 0,
|
||||
gstRate: item.gstRate,
|
||||
gstAmt: item.gstAmt,
|
||||
cgstAmt: item.cgstAmt,
|
||||
sgstAmt: item.sgstAmt,
|
||||
igstAmt: item.igstAmt,
|
||||
totalAmt: item.totalAmt
|
||||
gstRate: item.gstRate ?? item.gst_rate,
|
||||
gstAmt: item.gstAmt ?? item.gst_amt,
|
||||
cgstAmt: item.cgstAmt ?? item.cgst_amt,
|
||||
sgstAmt: item.sgstAmt ?? item.sgst_amt,
|
||||
igstAmt: item.igstAmt ?? item.igst_amt,
|
||||
totalAmt: item.totalAmt ?? item.total_amt
|
||||
}))
|
||||
: [],
|
||||
totalEstimatedBudget: proposalDetails.totalEstimatedBudget || proposalDetails.total_estimated_budget || 0,
|
||||
|
||||
@ -30,7 +30,7 @@ async function ensureConfigLoaded() {
|
||||
}
|
||||
|
||||
// Initialize config on first import (non-blocking)
|
||||
ensureConfigLoaded().catch(() => {});
|
||||
ensureConfigLoaded().catch(() => { });
|
||||
|
||||
/**
|
||||
* Check if current time is within working hours
|
||||
@ -54,7 +54,6 @@ export function isWorkingTime(date: Date = new Date(), priority: string = 'stand
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Add holiday check if holiday API is available
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -75,26 +75,13 @@ 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 {
|
||||
/**
|
||||
* 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 {
|
||||
// SECURITY: In production, don't store tokens client-side
|
||||
// Backend sets httpOnly cookies that are sent automatically
|
||||
|
||||
if (isProduction()) {
|
||||
return; // No-op - rely on httpOnly cookies
|
||||
}
|
||||
@ -105,13 +92,12 @@ export class TokenManager {
|
||||
|
||||
/**
|
||||
* Get access token
|
||||
* In production: Returns null (cookies are sent automatically)
|
||||
* In development: Returns from localStorage
|
||||
*
|
||||
*/
|
||||
static getAccessToken(): string | null {
|
||||
// SECURITY: In production, return null - cookies are used instead
|
||||
|
||||
if (isProduction()) {
|
||||
return null; // API calls use cookies via withCredentials: true
|
||||
return null;
|
||||
}
|
||||
|
||||
// Development: Return from localStorage
|
||||
@ -120,8 +106,6 @@ export class TokenManager {
|
||||
|
||||
/**
|
||||
* Store refresh token
|
||||
* In production: No-op (backend handles via httpOnly cookies)
|
||||
* In development: Store in localStorage
|
||||
*/
|
||||
static setRefreshToken(token: string): void {
|
||||
// SECURITY: In production, don't store tokens client-side
|
||||
@ -135,8 +119,6 @@ export class TokenManager {
|
||||
|
||||
/**
|
||||
* Get refresh token
|
||||
* In production: Returns null (cookies are used)
|
||||
* In development: Returns from localStorage
|
||||
*/
|
||||
static getRefreshToken(): string | null {
|
||||
// SECURITY: In production, return null - backend reads from cookie
|
||||
@ -147,10 +129,6 @@ export class TokenManager {
|
||||
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 {
|
||||
// ID token is needed for Okta logout, use sessionStorage (more secure than localStorage)
|
||||
sessionStorage.setItem(ID_TOKEN_KEY, token);
|
||||
@ -183,18 +161,7 @@ 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 {
|
||||
// CRITICAL: Set logout flag in sessionStorage FIRST (before clearing)
|
||||
// This flag survives the redirect and prevents auto-authentication
|
||||
@ -296,11 +263,7 @@ export class TokenManager {
|
||||
return !!this.getAccessToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if refresh token exists
|
||||
* In production: Always returns true if user data exists
|
||||
* In development: Checks localStorage
|
||||
*/
|
||||
|
||||
static hasRefreshToken(): boolean {
|
||||
if (isProduction()) {
|
||||
return !!this.getUserData();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user