Compare commits
No commits in common. "80ed407cd8e6f660c98b0b50fc9bc45aaf57db32" and "08cda349f3ee4f3cfc4c12cd5e0adf8f8a3081f3" have entirely different histories.
80ed407cd8
...
08cda349f3
195
src/App.tsx
195
src/App.tsx
@ -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 (
|
||||||
|
|||||||
@ -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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,7 +515,8 @@ 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 ${
|
||||||
|
message.type === 'success'
|
||||||
? 'border-green-200 bg-green-50'
|
? 'border-green-200 bg-green-50'
|
||||||
: 'border-red-200 bg-red-50'
|
: 'border-red-200 bg-red-50'
|
||||||
}`}>
|
}`}>
|
||||||
@ -662,7 +664,8 @@ 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 ${
|
||||||
|
currentPage === pageNum
|
||||||
? 'bg-re-green hover:bg-re-green/90'
|
? 'bg-re-green hover:bg-re-green/90'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@ -921,14 +921,16 @@ export function ClaimApproverSelectionStep({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<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'
|
||||||
@ -950,20 +952,14 @@ export function ClaimApproverSelectionStep({
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-600 mb-2">{step.description}</p>
|
<p className="text-xs text-gray-600 mb-2">{step.description}</p>
|
||||||
|
|
||||||
{isEditable && (() => {
|
{isEditable && (
|
||||||
const isVerified = !!(approver.email && approver.userId);
|
|
||||||
const isEmpty = !approver.email && !isPreFilled;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<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'
|
<Label htmlFor={`approver-${step.level}`} className="text-xs font-medium">
|
||||||
}`}>
|
Email Address {!isPreFilled && '*'}
|
||||||
Approver Email {!isPreFilled && '*'}
|
|
||||||
{isEmpty && <span className="ml-2 text-[10px] font-semibold italic text-blue-600">(Required)</span>}
|
|
||||||
</Label>
|
</Label>
|
||||||
{isVerified && (
|
{approver.email && approver.userId && (
|
||||||
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
|
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
|
||||||
<CheckCircle className="w-3 h-3 mr-1" />
|
<CheckCircle className="w-3 h-3 mr-1" />
|
||||||
Verified
|
Verified
|
||||||
@ -974,7 +970,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
<Input
|
<Input
|
||||||
id={`approver-${step.level}`}
|
id={`approver-${step.level}`}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={isPreFilled ? approver.email : "@username or email..."}
|
placeholder={isPreFilled ? approver.email : "@approver@royalenfield.com"}
|
||||||
value={approver.email || ''}
|
value={approver.email || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
@ -983,12 +979,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={isPreFilled || step.isAuto}
|
disabled={isPreFilled || step.isAuto}
|
||||||
className={`h-9 border-2 transition-all mt-1 w-full text-sm ${isPreFilled
|
className="h-9 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full text-sm"
|
||||||
? '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 */}
|
{/* Search suggestions dropdown */}
|
||||||
{!isPreFilled && !step.isAuto && (userSearchLoading[step.level - 1] || (userSearchResults[step.level - 1]?.length || 0) > 0) && (
|
{!isPreFilled && !step.isAuto && (userSearchLoading[step.level - 1] || (userSearchResults[step.level - 1]?.length || 0) > 0) && (
|
||||||
@ -1023,8 +1014,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor={`tat-${step.level}`} className={`text-xs font-bold ${isEmpty ? 'text-blue-900' : isVerified ? 'text-green-900' : 'text-gray-900'
|
<Label htmlFor={`tat-${step.level}`} className="text-xs font-medium">
|
||||||
}`}>
|
|
||||||
TAT (Turn Around Time) *
|
TAT (Turn Around Time) *
|
||||||
</Label>
|
</Label>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
@ -1037,24 +1027,14 @@ export function ClaimApproverSelectionStep({
|
|||||||
value={approver.tat || ''}
|
value={approver.tat || ''}
|
||||||
onChange={(e) => handleTatChange(step.level, parseInt(e.target.value) || '')}
|
onChange={(e) => handleTatChange(step.level, parseInt(e.target.value) || '')}
|
||||||
disabled={step.isAuto}
|
disabled={step.isAuto}
|
||||||
className={`h-9 border-2 transition-all flex-1 text-sm ${isPreFilled
|
className="h-9 border-2 border-gray-300 focus:border-blue-500 flex-1 text-sm"
|
||||||
? '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
|
<Select
|
||||||
value={approver.tatType || 'hours'}
|
value={approver.tatType || 'hours'}
|
||||||
onValueChange={(value) => handleTatTypeChange(step.level, value as 'hours' | 'days')}
|
onValueChange={(value) => handleTatTypeChange(step.level, value as 'hours' | 'days')}
|
||||||
disabled={step.isAuto}
|
disabled={step.isAuto}
|
||||||
>
|
>
|
||||||
<SelectTrigger className={`w-20 h-9 border-2 transition-all text-sm ${isPreFilled
|
<SelectTrigger className="w-20 h-9 border-2 border-gray-300 text-sm">
|
||||||
? '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 />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -1065,8 +1045,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -29,7 +29,6 @@ import { toast } from 'sonner';
|
|||||||
import { submitProposal, updateIODetails, submitCompletion, updateEInvoice, sendCreditNoteToDealer } from '@/services/dealerClaimApi';
|
import { submitProposal, updateIODetails, submitCompletion, updateEInvoice, sendCreditNoteToDealer } from '@/services/dealerClaimApi';
|
||||||
import { getWorkflowDetails, approveLevel, rejectLevel, handleInitiatorAction, getWorkflowHistory } from '@/services/workflowApi';
|
import { getWorkflowDetails, approveLevel, rejectLevel, handleInitiatorAction, getWorkflowHistory } from '@/services/workflowApi';
|
||||||
import { uploadDocument } from '@/services/documentApi';
|
import { uploadDocument } from '@/services/documentApi';
|
||||||
import { TokenManager } from '@/utils/tokenManager';
|
|
||||||
|
|
||||||
interface DealerClaimWorkflowTabProps {
|
interface DealerClaimWorkflowTabProps {
|
||||||
request: any;
|
request: any;
|
||||||
@ -70,7 +69,6 @@ interface WorkflowStep {
|
|||||||
};
|
};
|
||||||
einvoiceUrl?: string;
|
einvoiceUrl?: string;
|
||||||
emailTemplateUrl?: string;
|
emailTemplateUrl?: string;
|
||||||
levelName?: string;
|
|
||||||
versionHistory?: {
|
versionHistory?: {
|
||||||
current: any;
|
current: any;
|
||||||
previous: any;
|
previous: any;
|
||||||
@ -188,7 +186,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
const [versionHistory, setVersionHistory] = useState<any[]>([]);
|
const [versionHistory, setVersionHistory] = useState<any[]>([]);
|
||||||
const [showHistory, setShowHistory] = useState(false);
|
const [showHistory, setShowHistory] = useState(false);
|
||||||
const [expandedVersionSteps, setExpandedVersionSteps] = useState<Set<number>>(new Set());
|
const [expandedVersionSteps, setExpandedVersionSteps] = useState<Set<number>>(new Set());
|
||||||
const [viewSnapshot, setViewSnapshot] = useState<{ data: any, type: 'PROPOSAL' | 'COMPLETION', title?: string } | null>(null);
|
const [viewSnapshot, setViewSnapshot] = useState<{data: any, type: 'PROPOSAL' | 'COMPLETION', title?: string} | null>(null);
|
||||||
|
|
||||||
// Load approval flows from real API
|
// Load approval flows from real API
|
||||||
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
||||||
@ -331,31 +329,19 @@ export function DealerClaimWorkflowTab({
|
|||||||
// Step title and description mapping based on actual step number (not array index)
|
// Step title and description mapping based on actual step number (not array index)
|
||||||
// This handles cases where approvers are added between steps
|
// This handles cases where approvers are added between steps
|
||||||
const getStepTitle = (stepNumber: number, levelName?: string, approverName?: string): string => {
|
const getStepTitle = (stepNumber: number, levelName?: string, approverName?: string): string => {
|
||||||
// Check if this is a legacy workflow (8 steps) or new workflow (5 steps)
|
|
||||||
// Legacy flows have system steps (Activity, E-Invoice, Credit Note) as approval levels
|
|
||||||
const isLegacyFlow = (request?.totalLevels || 0) > 5 || (request?.approvalLevels?.length || 0) > 5;
|
|
||||||
|
|
||||||
// Use levelName from backend if available (most accurate)
|
// Use levelName from backend if available (most accurate)
|
||||||
// Check if it's an "Additional Approver" - this indicates a dynamically added approver
|
// Check if it's an "Additional Approver" - this indicates a dynamically added approver
|
||||||
if (levelName && levelName.trim()) {
|
if (levelName && levelName.trim()) {
|
||||||
const levelNameLower = levelName.toLowerCase();
|
|
||||||
|
|
||||||
// If it starts with "Additional Approver", use it as-is (it's already formatted)
|
// If it starts with "Additional Approver", use it as-is (it's already formatted)
|
||||||
if (levelNameLower.includes('additional approver')) {
|
if (levelName.toLowerCase().includes('additional approver')) {
|
||||||
|
return levelName;
|
||||||
|
}
|
||||||
|
// Otherwise use the levelName from backend (preserved from original step)
|
||||||
return levelName;
|
return levelName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If levelName is NOT generic "Step X", return it
|
// Fallback to mapping based on step number
|
||||||
// This fixes the issue where backend sends "Step 1" instead of "Dealer Proposal Submission"
|
const stepTitleMap: Record<number, string> = {
|
||||||
if (!/^step\s+\d+$/i.test(levelName)) {
|
|
||||||
return levelName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to mapping based on step number and flow version
|
|
||||||
const stepTitleMap: Record<number, string> = isLegacyFlow
|
|
||||||
? {
|
|
||||||
// Legacy 8-step flow
|
|
||||||
1: 'Dealer - Proposal Submission',
|
1: 'Dealer - Proposal Submission',
|
||||||
2: 'Requestor Evaluation & Confirmation',
|
2: 'Requestor Evaluation & Confirmation',
|
||||||
3: 'Department Lead Approval',
|
3: 'Department Lead Approval',
|
||||||
@ -364,16 +350,6 @@ export function DealerClaimWorkflowTab({
|
|||||||
6: 'Requestor - Claim Approval',
|
6: 'Requestor - Claim Approval',
|
||||||
7: 'E-Invoice Generation',
|
7: 'E-Invoice Generation',
|
||||||
8: 'Credit Note from SAP',
|
8: 'Credit Note from SAP',
|
||||||
}
|
|
||||||
: {
|
|
||||||
// New 5-step flow
|
|
||||||
1: 'Dealer - Proposal Submission',
|
|
||||||
2: 'Requestor Evaluation & Confirmation',
|
|
||||||
3: 'Department Lead Approval',
|
|
||||||
4: 'Dealer - Completion Documents',
|
|
||||||
5: 'Requestor - Claim Approval',
|
|
||||||
6: 'E-Invoice Generation',
|
|
||||||
7: 'Credit Note from SAP',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// If step number exists in map, use it
|
// If step number exists in map, use it
|
||||||
@ -401,9 +377,6 @@ export function DealerClaimWorkflowTab({
|
|||||||
return `Additional approver will review and approve this request.`;
|
return `Additional approver will review and approve this request.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a legacy workflow (8 steps) or new workflow (5 steps)
|
|
||||||
const isLegacyFlow = (request?.totalLevels || 0) > 5 || (request?.approvalLevels?.length || 0) > 5;
|
|
||||||
|
|
||||||
// Use levelName to determine description (handles shifted steps correctly)
|
// Use levelName to determine description (handles shifted steps correctly)
|
||||||
// This ensures descriptions shift with their steps when approvers are added
|
// This ensures descriptions shift with their steps when approvers are added
|
||||||
if (levelName && levelName.trim()) {
|
if (levelName && levelName.trim()) {
|
||||||
@ -419,7 +392,6 @@ export function DealerClaimWorkflowTab({
|
|||||||
if (levelNameLower.includes('department lead')) {
|
if (levelNameLower.includes('department lead')) {
|
||||||
return 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)';
|
return 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)';
|
||||||
}
|
}
|
||||||
// Re-added for legacy support
|
|
||||||
if (levelNameLower.includes('activity creation')) {
|
if (levelNameLower.includes('activity creation')) {
|
||||||
return 'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.';
|
return 'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.';
|
||||||
}
|
}
|
||||||
@ -429,36 +401,24 @@ export function DealerClaimWorkflowTab({
|
|||||||
if (levelNameLower.includes('requestor') && (levelNameLower.includes('claim') || levelNameLower.includes('approval'))) {
|
if (levelNameLower.includes('requestor') && (levelNameLower.includes('claim') || levelNameLower.includes('approval'))) {
|
||||||
return 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.';
|
return 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.';
|
||||||
}
|
}
|
||||||
if (levelNameLower.includes('e-invoice') || levelNameLower.includes('invoice generation') || levelNameLower.includes('dms')) {
|
if (levelNameLower.includes('e-invoice') || levelNameLower.includes('invoice generation')) {
|
||||||
return 'E-Invoice will be generated upon settlement initiation.';
|
return 'E-invoice will be generated through DMS.';
|
||||||
}
|
}
|
||||||
if (levelNameLower.includes('credit note') || levelNameLower.includes('sap')) {
|
if (levelNameLower.includes('credit note') || levelNameLower.includes('sap')) {
|
||||||
return 'Got credit note from SAP. Review and send to dealer to complete the claim management process.';
|
return 'Got credit note from SAP. Review and send to dealer to complete the claim management process.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to step number mapping depending on flow version
|
// Fallback to step number mapping (for backwards compatibility)
|
||||||
const stepDescriptionMap: Record<number, string> = isLegacyFlow
|
const stepDescriptionMap: Record<number, string> = {
|
||||||
? {
|
|
||||||
// Legacy 8-step flow
|
|
||||||
1: 'Dealer submits the proposal for the activity with comments including proposal document with requested details, cost break-up, timeline for closure, and other requests',
|
1: 'Dealer submits the proposal for the activity with comments including proposal document with requested details, cost break-up, timeline for closure, and other requests',
|
||||||
2: 'Requestor evaluates the request and confirms with comments. Decision point: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)',
|
2: 'Requestor evaluates the request and confirms with comments. Decision point: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)',
|
||||||
3: 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)',
|
3: 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)',
|
||||||
4: 'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.',
|
4: 'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.',
|
||||||
5: 'Dealer submits the necessary documents upon completion of the activity including document attachments (Zip Folder) and brief description',
|
5: 'Dealer submits the necessary documents upon completion of the activity including document attachments (Zip Folder) and brief description',
|
||||||
6: 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.',
|
6: 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.',
|
||||||
7: 'E-Invoice will be generated upon settlement initiation.',
|
7: 'E-invoice will be generated through DMS.',
|
||||||
8: 'Got credit note from SAP. Review and send to dealer to complete the claim management process.',
|
8: 'Got credit note from SAP. Review and send to dealer to complete the claim management process.',
|
||||||
}
|
|
||||||
: {
|
|
||||||
// New 5-step flow
|
|
||||||
1: 'Dealer submits the proposal for the activity with comments including proposal document with requested details, cost break-up, timeline for closure, and other requests',
|
|
||||||
2: 'Requestor evaluates the request and confirms with comments. Decision point: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)',
|
|
||||||
3: 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)',
|
|
||||||
4: 'Dealer submits the necessary documents upon completion of the activity including document attachments (Zip Folder) and brief description',
|
|
||||||
5: 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.',
|
|
||||||
6: 'E-Invoice will be generated upon settlement initiation.',
|
|
||||||
7: 'Got credit note from SAP. Review and send to dealer to complete the claim management process.',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (stepDescriptionMap[stepNumber]) {
|
if (stepDescriptionMap[stepNumber]) {
|
||||||
@ -885,26 +845,13 @@ export function DealerClaimWorkflowTab({
|
|||||||
await uploadDocument(file, requestId, 'SUPPORTING');
|
await uploadDocument(file, requestId, 'SUPPORTING');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit proposal using dealer claim API (calculate total from inclusive item totals)
|
// Submit proposal using dealer claim API
|
||||||
const totalBudget = data.costBreakup.reduce((sum, item: any) => sum + (item.totalAmt || item.amount || 0), 0);
|
const totalBudget = data.costBreakup.reduce((sum, item) => sum + item.amount, 0);
|
||||||
await submitProposal(requestId, {
|
await submitProposal(requestId, {
|
||||||
proposalDocument: data.proposalDocument || undefined,
|
proposalDocument: data.proposalDocument || undefined,
|
||||||
costBreakup: data.costBreakup.map((item: any) => ({
|
costBreakup: data.costBreakup.map(item => ({
|
||||||
description: item.description,
|
description: item.description,
|
||||||
amount: item.amount,
|
amount: item.amount,
|
||||||
gstRate: item.gstRate,
|
|
||||||
gstAmt: item.gstAmt,
|
|
||||||
cgstRate: item.cgstRate,
|
|
||||||
cgstAmt: item.cgstAmt,
|
|
||||||
sgstRate: item.sgstRate,
|
|
||||||
sgstAmt: item.sgstAmt,
|
|
||||||
igstRate: item.igstRate,
|
|
||||||
igstAmt: item.igstAmt,
|
|
||||||
utgstRate: item.utgstRate,
|
|
||||||
utgstAmt: item.utgstAmt,
|
|
||||||
cessRate: item.cessRate,
|
|
||||||
cessAmt: item.cessAmt,
|
|
||||||
totalAmt: item.totalAmt
|
|
||||||
})),
|
})),
|
||||||
totalEstimatedBudget: totalBudget,
|
totalEstimatedBudget: totalBudget,
|
||||||
expectedCompletionDate: data.expectedCompletionDate,
|
expectedCompletionDate: data.expectedCompletionDate,
|
||||||
@ -1159,23 +1106,10 @@ export function DealerClaimWorkflowTab({
|
|||||||
|
|
||||||
const requestId = request.id || request.requestId;
|
const requestId = request.id || request.requestId;
|
||||||
|
|
||||||
// Transform expense items to match API format (include GST fields)
|
// Transform expense items to match API format
|
||||||
const closedExpenses = data.closedExpenses.map((item: any) => ({
|
const closedExpenses = data.closedExpenses.map(item => ({
|
||||||
description: item.description,
|
description: item.description,
|
||||||
amount: item.amount,
|
amount: item.amount,
|
||||||
gstRate: item.gstRate,
|
|
||||||
gstAmt: item.gstAmt,
|
|
||||||
cgstRate: item.cgstRate,
|
|
||||||
cgstAmt: item.cgstAmt,
|
|
||||||
sgstRate: item.sgstRate,
|
|
||||||
sgstAmt: item.sgstAmt,
|
|
||||||
igstRate: item.igstRate,
|
|
||||||
igstAmt: item.igstAmt,
|
|
||||||
utgstRate: item.utgstRate,
|
|
||||||
utgstAmt: item.utgstAmt,
|
|
||||||
cessRate: item.cessRate,
|
|
||||||
cessAmt: item.cessAmt,
|
|
||||||
totalAmt: item.totalAmt
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Submit completion documents using dealer claim API
|
// Submit completion documents using dealer claim API
|
||||||
@ -1211,7 +1145,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle E-Invoice generation (Step 6)
|
// Handle DMS push (Step 6)
|
||||||
const handleDMSPush = async (_comments: string) => {
|
const handleDMSPush = async (_comments: string) => {
|
||||||
try {
|
try {
|
||||||
if (!request?.id && !request?.requestId) {
|
if (!request?.id && !request?.requestId) {
|
||||||
@ -1228,11 +1162,11 @@ export function DealerClaimWorkflowTab({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Activity is logged by backend service - no need to create work note
|
// Activity is logged by backend service - no need to create work note
|
||||||
toast.success('E-Invoice generation initiated successfully.');
|
toast.success('Pushed to DMS successfully. E-invoice will be generated automatically.');
|
||||||
handleRefresh();
|
handleRefresh();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[DealerClaimWorkflowTab] Error generating e-invoice:', error);
|
console.error('[DealerClaimWorkflowTab] Error pushing to DMS:', error);
|
||||||
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to generate e-invoice. Please try again.';
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to push to DMS. Please try again.';
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -1447,31 +1381,6 @@ export function DealerClaimWorkflowTab({
|
|||||||
loadCompletionDocuments();
|
loadCompletionDocuments();
|
||||||
}, [request]);
|
}, [request]);
|
||||||
|
|
||||||
const handlePreviewInvoice = async () => {
|
|
||||||
try {
|
|
||||||
const requestId = request.id || request.requestId;
|
|
||||||
if (!requestId) {
|
|
||||||
toast.error('Request ID not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if invoice exists
|
|
||||||
if (!request.invoice && !request.irn) {
|
|
||||||
toast.error('Invoice not generated yet');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = TokenManager.getAccessToken();
|
|
||||||
// Construct API URL for PDF preview
|
|
||||||
const previewUrl = `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1'}/dealer-claims/${requestId}/e-invoice/pdf?token=${token}`;
|
|
||||||
|
|
||||||
window.open(previewUrl, '_blank');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to preview invoice:', error);
|
|
||||||
toast.error('Failed to open invoice preview');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get dealer and activity info
|
// Get dealer and activity info
|
||||||
const dealerName = request?.claimDetails?.dealerName ||
|
const dealerName = request?.claimDetails?.dealerName ||
|
||||||
request?.dealerInfo?.name ||
|
request?.dealerInfo?.name ||
|
||||||
@ -1604,23 +1513,6 @@ export function DealerClaimWorkflowTab({
|
|||||||
<Download className="w-3.5 h-3.5 text-green-600" />
|
<Download className="w-3.5 h-3.5 text-green-600" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* Invoice Preview Button (Requestor Claim Approval) */}
|
|
||||||
{(() => {
|
|
||||||
const isRequestorClaimStep = (step.levelName || step.title || '').toLowerCase().includes('requestor claim') ||
|
|
||||||
(step.levelName || step.title || '').toLowerCase().includes('requestor - claim');
|
|
||||||
const hasInvoice = request?.invoice || (request?.irn && step.status === 'approved');
|
|
||||||
return isRequestorClaimStep && hasInvoice && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 hover:bg-amber-100"
|
|
||||||
title="Preview Invoice"
|
|
||||||
onClick={handlePreviewInvoice}
|
|
||||||
>
|
|
||||||
<Receipt className="w-3.5 h-3.5 text-amber-600" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600">{step.approver}</p>
|
<p className="text-sm text-gray-600">{step.approver}</p>
|
||||||
<p className="text-sm text-gray-500 mt-2 italic">{step.description}</p>
|
<p className="text-sm text-gray-500 mt-2 italic">{step.description}</p>
|
||||||
@ -1828,7 +1720,8 @@ export function DealerClaimWorkflowTab({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Current Approver - Time Tracking */}
|
{/* Current Approver - Time Tracking */}
|
||||||
<div className={`border rounded-lg p-3 ${isPaused ? 'bg-gray-100 border-gray-300' :
|
<div className={`border rounded-lg p-3 ${
|
||||||
|
isPaused ? 'bg-gray-100 border-gray-300' :
|
||||||
(approval.sla.percentageUsed || 0) >= 100 ? 'bg-red-50 border-red-200' :
|
(approval.sla.percentageUsed || 0) >= 100 ? 'bg-red-50 border-red-200' :
|
||||||
(approval.sla.percentageUsed || 0) >= 75 ? 'bg-orange-50 border-orange-200' :
|
(approval.sla.percentageUsed || 0) >= 75 ? 'bg-orange-50 border-orange-200' :
|
||||||
(approval.sla.percentageUsed || 0) >= 50 ? 'bg-amber-50 border-amber-200' :
|
(approval.sla.percentageUsed || 0) >= 50 ? 'bg-amber-50 border-amber-200' :
|
||||||
@ -1964,25 +1857,25 @@ export function DealerClaimWorkflowTab({
|
|||||||
<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" />
|
||||||
<p className="text-xs font-semibold text-purple-900 uppercase tracking-wide">
|
<p className="text-xs font-semibold text-purple-900 uppercase tracking-wide">
|
||||||
E-Invoice & Settlement Details
|
DMS Processing Details
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-gray-600">Settlement ID:</span>
|
<span className="text-xs text-gray-600">DMS Number:</span>
|
||||||
<span className="text-sm font-semibold text-gray-900">
|
<span className="text-sm font-semibold text-gray-900">
|
||||||
{step.dmsDetails.dmsNumber}
|
{step.dmsDetails.dmsNumber}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{step.dmsDetails.dmsRemarks && (
|
{step.dmsDetails.dmsRemarks && (
|
||||||
<div className="pt-1.5 border-t border-purple-100">
|
<div className="pt-1.5 border-t border-purple-100">
|
||||||
<p className="text-xs text-gray-600 mb-1">Settlement Remarks:</p>
|
<p className="text-xs text-gray-600 mb-1">DMS Remarks:</p>
|
||||||
<p className="text-sm text-gray-900">{step.dmsDetails.dmsRemarks}</p>
|
<p className="text-sm text-gray-900">{step.dmsDetails.dmsRemarks}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{step.dmsDetails.pushedAt && (
|
{step.dmsDetails.pushedAt && (
|
||||||
<div className="pt-1.5 border-t border-purple-100 text-xs text-gray-500">
|
<div className="pt-1.5 border-t border-purple-100 text-xs text-gray-500">
|
||||||
Initiated by {step.dmsDetails.pushedBy} on{' '}
|
Pushed by {step.dmsDetails.pushedBy} on{' '}
|
||||||
{formatDateSafe(step.dmsDetails.pushedAt)}
|
{formatDateSafe(step.dmsDetails.pushedAt)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -2010,16 +1903,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
})() && (
|
})() && (
|
||||||
<div className="mt-4 flex gap-2">
|
<div className="mt-4 flex gap-2">
|
||||||
{/* Step 1: Submit Proposal Button - Only for dealer or step 1 approver */}
|
{/* Step 1: Submit Proposal Button - Only for dealer or step 1 approver */}
|
||||||
{(() => {
|
{step.step === 1 && (isDealer || isStep1Approver) && (
|
||||||
// Check if this is Step 1 (Dealer Proposal Submission)
|
|
||||||
// Use levelName match or fallback to step 1
|
|
||||||
const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
|
|
||||||
const isProposalStep = step.step === 1 ||
|
|
||||||
levelName.includes('proposal') ||
|
|
||||||
levelName.includes('submission');
|
|
||||||
|
|
||||||
return isProposalStep && (isDealer || isStep1Approver);
|
|
||||||
})() && (
|
|
||||||
<Button
|
<Button
|
||||||
className="bg-purple-600 hover:bg-purple-700"
|
className="bg-purple-600 hover:bg-purple-700"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -2035,16 +1919,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
{/* Use initiatorStepNumber to handle cases where approvers are added between steps */}
|
{/* Use initiatorStepNumber to handle cases where approvers are added between steps */}
|
||||||
{/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */}
|
{/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */}
|
||||||
{/* Use initiatorStepNumber to handle cases where approvers are added between steps */}
|
{/* Use initiatorStepNumber to handle cases where approvers are added between steps */}
|
||||||
{/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */}
|
{step.step === initiatorStepNumber && (isInitiator || isStep2Approver) && (
|
||||||
{(() => {
|
|
||||||
// Check if this is the Requestor Evaluation step
|
|
||||||
const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
|
|
||||||
const isEvaluationStep = levelName.includes('requestor evaluation') ||
|
|
||||||
levelName.includes('confirmation') ||
|
|
||||||
step.step === initiatorStepNumber; // Fallback
|
|
||||||
|
|
||||||
return isEvaluationStep && (isInitiator || isStep2Approver);
|
|
||||||
})() && (
|
|
||||||
<Button
|
<Button
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -2206,26 +2081,20 @@ export function DealerClaimWorkflowTab({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Activity className="w-4 h-4 mr-2" />
|
<Activity className="w-4 h-4 mr-2" />
|
||||||
Generate E-Invoice & Sync
|
Push to DMS
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Step 8: View & Send Credit Note - Only for finance approver or step 8 approver */}
|
{/* Step 8: View & Send Credit Note - Only for finance approver or step 8 approver */}
|
||||||
{(() => {
|
{step.step === 8 && (() => {
|
||||||
const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
|
const step8Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 8);
|
||||||
// Check for "Credit Note" or "SAP" in level name, or fallback to step 8 if it's the last step
|
const step8ApproverEmail = (step8Level?.approverEmail || '').toLowerCase();
|
||||||
const isCreditNoteStep = levelName.includes('credit note') ||
|
const isStep8Approver = step8ApproverEmail && userEmail === step8ApproverEmail;
|
||||||
levelName.includes('sap') ||
|
|
||||||
(step.step === 8 && !levelName.includes('additional'));
|
|
||||||
|
|
||||||
const stepApproverEmail = (stepLevel?.approverEmail || '').toLowerCase();
|
|
||||||
const isStepApprover = stepApproverEmail && userEmail === stepApproverEmail;
|
|
||||||
// Also check if user has finance role
|
// Also check if user has finance role
|
||||||
const userRole = (user as any)?.role?.toUpperCase() || '';
|
const userRole = (user as any)?.role?.toUpperCase() || '';
|
||||||
const isFinanceUser = userRole === 'FINANCE' || userRole === 'ADMIN';
|
const isFinanceUser = userRole === 'FINANCE' || userRole === 'ADMIN';
|
||||||
|
return isStep8Approver || isFinanceUser;
|
||||||
return isCreditNoteStep && (isStepApprover || isFinanceUser);
|
|
||||||
})() && (
|
})() && (
|
||||||
<Button
|
<Button
|
||||||
className="bg-green-600 hover:bg-green-700"
|
className="bg-green-600 hover:bg-green-700"
|
||||||
@ -2245,21 +2114,35 @@ export function DealerClaimWorkflowTab({
|
|||||||
const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
|
const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
|
||||||
const isAdditionalApprover = levelName.includes('additional approver');
|
const isAdditionalApprover = levelName.includes('additional approver');
|
||||||
|
|
||||||
// Check if this step doesn't have any of the specific workflow action buttons above
|
|
||||||
// Check if this step doesn't have any of the specific workflow action buttons above
|
// Check if this step doesn't have any of the specific workflow action buttons above
|
||||||
const hasSpecificWorkflowAction =
|
const hasSpecificWorkflowAction =
|
||||||
// Proposal
|
step.step === 1 ||
|
||||||
(step.step === 1 || levelName.includes('proposal') || levelName.includes('submission')) ||
|
step.step === initiatorStepNumber ||
|
||||||
// Evaluation
|
(() => {
|
||||||
(levelName.includes('requestor evaluation') || levelName.includes('confirmation')) ||
|
const deptLeadStepLevel = approvalFlow.find((l: any) => {
|
||||||
// Dept Lead
|
const ln = (l.levelName || '').toLowerCase();
|
||||||
levelName.includes('department lead') ||
|
return ln.includes('department lead');
|
||||||
// Dealer Completion
|
});
|
||||||
(levelName.includes('dealer completion') || levelName.includes('completion documents')) ||
|
return deptLeadStepLevel &&
|
||||||
// Requestor Claim
|
(step.step === (deptLeadStepLevel.step || deptLeadStepLevel.levelNumber || deptLeadStepLevel.level_number));
|
||||||
(levelName.includes('requestor claim') || levelName.includes('requestor - claim')) ||
|
})() ||
|
||||||
// Credit Note
|
(() => {
|
||||||
(levelName.includes('credit note') || levelName.includes('sap'));
|
const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step);
|
||||||
|
const stepApproverEmail = (stepLevel?.approverEmail || '').toLowerCase();
|
||||||
|
const isDealerForThisStep = isDealer && stepApproverEmail === dealerEmail;
|
||||||
|
const ln = (stepLevel?.levelName || step.title || '').toLowerCase();
|
||||||
|
const isDealerCompletionStep = ln.includes('dealer completion') || ln.includes('completion documents');
|
||||||
|
return isDealerForThisStep && isDealerCompletionStep;
|
||||||
|
})() ||
|
||||||
|
(() => {
|
||||||
|
const requestorClaimStepLevel = approvalFlow.find((l: any) => {
|
||||||
|
const ln = (l.levelName || '').toLowerCase();
|
||||||
|
return ln.includes('requestor claim') || ln.includes('requestor - claim');
|
||||||
|
});
|
||||||
|
return requestorClaimStepLevel &&
|
||||||
|
(step.step === (requestorClaimStepLevel.step || requestorClaimStepLevel.levelNumber || requestorClaimStepLevel.level_number));
|
||||||
|
})() ||
|
||||||
|
step.step === 8;
|
||||||
|
|
||||||
// Show "Review Request" button for additional approvers or steps without specific workflow actions
|
// Show "Review Request" button for additional approvers or steps without specific workflow actions
|
||||||
// Similar to the requestor approval step
|
// Similar to the requestor approval step
|
||||||
@ -2449,6 +2332,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
isOpen={showCreditNoteModal}
|
isOpen={showCreditNoteModal}
|
||||||
onClose={() => setShowCreditNoteModal(false)}
|
onClose={() => setShowCreditNoteModal(false)}
|
||||||
onDownload={async () => {
|
onDownload={async () => {
|
||||||
|
// TODO: Implement download functionality
|
||||||
toast.info('Download functionality will be implemented');
|
toast.info('Download functionality will be implemented');
|
||||||
}}
|
}}
|
||||||
onSendToDealer={async () => {
|
onSendToDealer={async () => {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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%;
|
|
||||||
}
|
|
||||||
@ -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,9 +257,8 @@ export function DMSPushModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||||
<DialogContent className="settlement-push-modal overflow-hidden flex flex-col w-full max-w-none">
|
<DialogContent className="dms-push-modal overflow-hidden flex flex-col">
|
||||||
<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">
|
||||||
@ -267,10 +266,10 @@ export function DMSPushModal({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<DialogTitle className="font-semibold text-lg sm:text-xl">
|
<DialogTitle className="font-semibold text-lg sm:text-xl">
|
||||||
E-Invoice Generation & Sync
|
Push to DMS - Verification
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-xs sm:text-sm mt-1">
|
<DialogDescription className="text-xs sm:text-sm mt-1">
|
||||||
Review completion details and expenses before generating e-invoice and initiating SAP settlement
|
Review completion details and expenses before pushing to DMS for e-invoice generation
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -295,7 +294,7 @@ export function DMSPushModal({
|
|||||||
<span className="font-medium text-xs sm:text-sm text-gray-600">Action:</span>
|
<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">
|
<Badge className="bg-indigo-100 text-indigo-800 border-indigo-200 text-xs">
|
||||||
<Activity className="w-3 h-3 mr-1" />
|
<Activity className="w-3 h-3 mr-1" />
|
||||||
SYNC TO SAP
|
PUSH TO DMS
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -394,7 +393,7 @@ export function DMSPushModal({
|
|||||||
Expense Breakdown
|
Expense Breakdown
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs sm:text-sm">
|
<CardDescription className="text-xs sm:text-sm">
|
||||||
Review closed expenses before generation
|
Review closed expenses before pushing to DMS
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@ -701,10 +700,10 @@ export function DMSPushModal({
|
|||||||
<TriangleAlert className="w-4 h-4 sm:w-5 sm:h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
|
<TriangleAlert className="w-4 h-4 sm:w-5 sm:h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs sm:text-sm font-semibold text-yellow-900">
|
<p className="text-xs sm:text-sm font-semibold text-yellow-900">
|
||||||
Please verify all details before generation
|
Please verify all details before pushing to DMS
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-yellow-700 mt-1">
|
<p className="text-xs text-yellow-700 mt-1">
|
||||||
Once submitted, the system will generate an e-invoice and initiate the SAP settlement process.
|
Once pushed, the system will automatically generate an e-invoice and log it as an activity.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -717,7 +716,7 @@ export function DMSPushModal({
|
|||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="comment"
|
id="comment"
|
||||||
placeholder="Enter your comments about e-invoice generation (e.g., verified expenses, ready for settlement)..."
|
placeholder="Enter your comments about pushing to DMS (e.g., verified expenses, ready for invoice generation)..."
|
||||||
value={comments}
|
value={comments}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
@ -753,19 +752,17 @@ export function DMSPushModal({
|
|||||||
className="bg-indigo-600 hover:bg-indigo-700 text-white"
|
className="bg-indigo-600 hover:bg-indigo-700 text-white"
|
||||||
>
|
>
|
||||||
{submitting ? (
|
{submitting ? (
|
||||||
'Processing...'
|
'Pushing to DMS...'
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Activity className="w-4 h-4 mr-2" />
|
<Activity className="w-4 h-4 mr-2" />
|
||||||
Generate & Sync
|
Push to DMS
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* File Preview Modal - Matching DocumentsTab style */}
|
{/* File Preview Modal - Matching DocumentsTab style */}
|
||||||
{previewDocument && (
|
{previewDocument && (
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -869,7 +866,7 @@ export function DMSPushModal({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
</>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
@ -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)}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user