Compare commits

...

2 Commits

16 changed files with 2433 additions and 2503 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -318,7 +318,7 @@ export function UserManagement() {
const user = users.find(u => u.userId === userId); const user = users.find(u => u.userId === userId);
if (!user) return; if (!user) return;
// TODO: Implement backend API for toggling user status
toast.info('User status toggle functionality coming soon'); toast.info('User status toggle functionality coming soon');
}; };
@ -332,7 +332,6 @@ 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');
}; };
@ -515,8 +514,7 @@ export function UserManagement() {
{/* Message */} {/* Message */}
{message && ( {message && (
<div className={`border-2 rounded-lg p-4 ${ <div className={`border-2 rounded-lg p-4 ${message.type === 'success'
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'
}`}> }`}>
@ -664,8 +662,7 @@ 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 ${ className={`w-9 h-9 p-0 ${currentPage === pageNum
currentPage === pageNum
? 'bg-re-green hover:bg-re-green/90' ? 'bg-re-green hover:bg-re-green/90'
: '' : ''
}`} }`}

View File

@ -921,16 +921,14 @@ export function ClaimApproverSelectionStep({
); );
})} })}
<div className={`p-3 rounded-lg border-2 transition-all ${ <div className={`p-3 rounded-lg border-2 transition-all ${approver.email && approver.userId
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 ${ <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${approver.email && approver.userId
approver.email && approver.userId
? 'bg-green-600' ? 'bg-green-600'
: isPreFilled : isPreFilled
? 'bg-blue-600' ? 'bg-blue-600'
@ -952,14 +950,20 @@ 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-medium"> <Label htmlFor={`approver-${step.level}`} className={`text-xs font-bold ${isEmpty ? 'text-blue-900' : isVerified ? 'text-green-900' : 'text-gray-900'
Email Address {!isPreFilled && '*'} }`}>
Approver Email {!isPreFilled && '*'}
{isEmpty && <span className="ml-2 text-[10px] font-semibold italic text-blue-600">(Required)</span>}
</Label> </Label>
{approver.email && approver.userId && ( {isVerified && (
<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
@ -970,7 +974,7 @@ export function ClaimApproverSelectionStep({
<Input <Input
id={`approver-${step.level}`} id={`approver-${step.level}`}
type="text" type="text"
placeholder={isPreFilled ? approver.email : "@approver@royalenfield.com"} placeholder={isPreFilled ? approver.email : "@username or email..."}
value={approver.email || ''} value={approver.email || ''}
onChange={(e) => { onChange={(e) => {
const newValue = e.target.value; const newValue = e.target.value;
@ -979,7 +983,12 @@ export function ClaimApproverSelectionStep({
} }
}} }}
disabled={isPreFilled || step.isAuto} disabled={isPreFilled || step.isAuto}
className="h-9 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full text-sm" 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 */} {/* 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) && (
@ -1014,7 +1023,8 @@ export function ClaimApproverSelectionStep({
</div> </div>
<div> <div>
<Label htmlFor={`tat-${step.level}`} className="text-xs font-medium"> <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) * TAT (Turn Around Time) *
</Label> </Label>
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
@ -1027,14 +1037,24 @@ 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 border-gray-300 focus:border-blue-500 flex-1 text-sm" 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 <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 border-gray-300 text-sm"> <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 /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -1045,7 +1065,8 @@ export function ClaimApproverSelectionStep({
</div> </div>
</div> </div>
</div> </div>
)} );
})()}
</div> </div>
</div> </div>
</div> </div>

View File

@ -29,6 +29,7 @@ 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;
@ -69,6 +70,7 @@ interface WorkflowStep {
}; };
einvoiceUrl?: string; einvoiceUrl?: string;
emailTemplateUrl?: string; emailTemplateUrl?: string;
levelName?: string;
versionHistory?: { versionHistory?: {
current: any; current: any;
previous: any; previous: any;
@ -329,19 +331,31 @@ 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 (levelName.toLowerCase().includes('additional approver')) { if (levelNameLower.includes('additional approver')) {
return levelName;
}
// Otherwise use the levelName from backend (preserved from original step)
return levelName; return levelName;
} }
// Fallback to mapping based on step number // If levelName is NOT generic "Step X", return it
const stepTitleMap: Record<number, string> = { // This fixes the issue where backend sends "Step 1" instead of "Dealer Proposal Submission"
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',
@ -350,6 +364,16 @@ 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
@ -377,6 +401,9 @@ 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()) {
@ -392,6 +419,7 @@ 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.';
} }
@ -401,24 +429,36 @@ 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')) { if (levelNameLower.includes('e-invoice') || levelNameLower.includes('invoice generation') || levelNameLower.includes('dms')) {
return 'E-invoice will be generated through DMS.'; return 'E-Invoice will be generated upon settlement initiation.';
} }
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 (for backwards compatibility) // Fallback to step number mapping depending on flow version
const stepDescriptionMap: Record<number, string> = { const stepDescriptionMap: Record<number, string> = isLegacyFlow
? {
// 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 through DMS.', 7: 'E-Invoice will be generated upon settlement initiation.',
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]) {
@ -845,13 +885,26 @@ export function DealerClaimWorkflowTab({
await uploadDocument(file, requestId, 'SUPPORTING'); await uploadDocument(file, requestId, 'SUPPORTING');
} }
// Submit proposal using dealer claim API // Submit proposal using dealer claim API (calculate total from inclusive item totals)
const totalBudget = data.costBreakup.reduce((sum, item) => sum + item.amount, 0); const totalBudget = data.costBreakup.reduce((sum, item: any) => sum + (item.totalAmt || item.amount || 0), 0);
await submitProposal(requestId, { await submitProposal(requestId, {
proposalDocument: data.proposalDocument || undefined, proposalDocument: data.proposalDocument || undefined,
costBreakup: data.costBreakup.map(item => ({ costBreakup: data.costBreakup.map((item: any) => ({
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,
@ -1106,10 +1159,23 @@ export function DealerClaimWorkflowTab({
const requestId = request.id || request.requestId; const requestId = request.id || request.requestId;
// Transform expense items to match API format // Transform expense items to match API format (include GST fields)
const closedExpenses = data.closedExpenses.map(item => ({ const closedExpenses = data.closedExpenses.map((item: any) => ({
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
@ -1145,7 +1211,7 @@ export function DealerClaimWorkflowTab({
} }
}; };
// Handle DMS push (Step 6) // Handle E-Invoice generation (Step 6)
const handleDMSPush = async (_comments: string) => { const handleDMSPush = async (_comments: string) => {
try { try {
if (!request?.id && !request?.requestId) { if (!request?.id && !request?.requestId) {
@ -1162,11 +1228,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('Pushed to DMS successfully. E-invoice will be generated automatically.'); toast.success('E-Invoice generation initiated successfully.');
handleRefresh(); handleRefresh();
} catch (error: any) { } catch (error: any) {
console.error('[DealerClaimWorkflowTab] Error pushing to DMS:', error); console.error('[DealerClaimWorkflowTab] Error generating e-invoice:', error);
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to push to DMS. Please try again.'; const errorMessage = error?.response?.data?.message || error?.message || 'Failed to generate e-invoice. Please try again.';
toast.error(errorMessage); toast.error(errorMessage);
throw error; throw error;
} }
@ -1381,6 +1447,31 @@ 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 ||
@ -1513,6 +1604,23 @@ 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>
@ -1720,8 +1828,7 @@ export function DealerClaimWorkflowTab({
</div> </div>
{/* Current Approver - Time Tracking */} {/* Current Approver - Time Tracking */}
<div className={`border rounded-lg p-3 ${ <div className={`border rounded-lg p-3 ${isPaused ? 'bg-gray-100 border-gray-300' :
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' :
@ -1857,25 +1964,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">
DMS Processing Details E-Invoice & Settlement 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">DMS Number:</span> <span className="text-xs text-gray-600">Settlement ID:</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">DMS Remarks:</p> <p className="text-xs text-gray-600 mb-1">Settlement 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">
Pushed by {step.dmsDetails.pushedBy} on{' '} Initiated by {step.dmsDetails.pushedBy} on{' '}
{formatDateSafe(step.dmsDetails.pushedAt)} {formatDateSafe(step.dmsDetails.pushedAt)}
</div> </div>
)} )}
@ -1903,7 +2010,16 @@ 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={() => {
@ -1919,7 +2035,16 @@ 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.step === initiatorStepNumber && (isInitiator || isStep2Approver) && ( {/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */}
{(() => {
// 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={() => {
@ -2081,20 +2206,26 @@ export function DealerClaimWorkflowTab({
}} }}
> >
<Activity className="w-4 h-4 mr-2" /> <Activity className="w-4 h-4 mr-2" />
Push to DMS Generate E-Invoice & Sync
</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 step8Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 8); const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
const step8ApproverEmail = (step8Level?.approverEmail || '').toLowerCase(); // Check for "Credit Note" or "SAP" in level name, or fallback to step 8 if it's the last step
const isStep8Approver = step8ApproverEmail && userEmail === step8ApproverEmail; const isCreditNoteStep = levelName.includes('credit note') ||
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"
@ -2114,35 +2245,21 @@ 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 =
step.step === 1 || // Proposal
step.step === initiatorStepNumber || (step.step === 1 || levelName.includes('proposal') || levelName.includes('submission')) ||
(() => { // Evaluation
const deptLeadStepLevel = approvalFlow.find((l: any) => { (levelName.includes('requestor evaluation') || levelName.includes('confirmation')) ||
const ln = (l.levelName || '').toLowerCase(); // Dept Lead
return ln.includes('department lead'); levelName.includes('department lead') ||
}); // Dealer Completion
return deptLeadStepLevel && (levelName.includes('dealer completion') || levelName.includes('completion documents')) ||
(step.step === (deptLeadStepLevel.step || deptLeadStepLevel.levelNumber || deptLeadStepLevel.level_number)); // Requestor Claim
})() || (levelName.includes('requestor claim') || levelName.includes('requestor - claim')) ||
(() => { // Credit Note
const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step); (levelName.includes('credit note') || levelName.includes('sap'));
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
@ -2332,7 +2449,6 @@ 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 () => {

View File

@ -1,6 +1,6 @@
/** /**
* ProcessDetailsCard Component * 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 * Visibility controlled by user role
*/ */
@ -172,21 +172,18 @@ export function ProcessDetailsCard({
</div> </div>
)} )}
{/* DMS Details */} {/* E-Invoice 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">
DMS & E-Invoice Details E-Invoice Details
</Label> </Label>
</div> </div>
<div className="grid grid-cols-2 gap-3 mb-2"> <div className="grid grid-cols-2 gap-3 mb-2">
<div>
<p className="text-[10px] text-gray-500 uppercase">DMS Number</p>
<p className="font-bold text-sm text-gray-900">{dmsDetails.dmsNumber || 'N/A'}</p>
</div>
{dmsDetails.ackNo && ( {dmsDetails.ackNo && (
<div> <div>
<p className="text-[10px] text-gray-500 uppercase">Ack No</p> <p className="text-[10px] text-gray-500 uppercase">Ack No</p>

View File

@ -22,6 +22,7 @@ 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;
@ -35,8 +36,9 @@ 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 = () => {
if (proposalDetails.estimatedBudgetTotal !== undefined && proposalDetails.estimatedBudgetTotal !== null) { const total = proposalDetails.totalEstimatedBudget ?? proposalDetails.estimatedBudgetTotal;
return proposalDetails.estimatedBudgetTotal; if (total !== undefined && total !== null) {
return total;
} }
// Calculate sum from costBreakup items // Calculate sum from costBreakup items

View File

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

View File

@ -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 pushing to DMS'); toast.error('Please provide comments before proceeding');
return; return;
} }
@ -238,8 +238,8 @@ export function DMSPushModal({
handleReset(); handleReset();
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('Failed to push to DMS:', error); console.error('Failed to generate e-invoice:', error);
toast.error('Failed to push to DMS. Please try again.'); toast.error('Failed to generate e-invoice. Please try again.');
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
@ -257,8 +257,9 @@ export function DMSPushModal({
}; };
return ( return (
<>
<Dialog open={isOpen} onOpenChange={handleClose}> <Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="dms-push-modal overflow-hidden flex flex-col"> <DialogContent className="settlement-push-modal overflow-hidden flex flex-col w-full max-w-none">
<DialogHeader className="px-6 pt-6 pb-3 flex-shrink-0"> <DialogHeader className="px-6 pt-6 pb-3 flex-shrink-0">
<div className="flex items-center gap-2 sm:gap-3 mb-2"> <div className="flex items-center gap-2 sm:gap-3 mb-2">
<div className="p-1.5 sm:p-2 rounded-lg bg-indigo-100"> <div className="p-1.5 sm:p-2 rounded-lg bg-indigo-100">
@ -266,10 +267,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">
Push to DMS - Verification E-Invoice Generation & Sync
</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 pushing to DMS for e-invoice generation Review completion details and expenses before generating e-invoice and initiating SAP settlement
</DialogDescription> </DialogDescription>
</div> </div>
</div> </div>
@ -294,7 +295,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" />
PUSH TO DMS SYNC TO SAP
</Badge> </Badge>
</div> </div>
</div> </div>
@ -393,7 +394,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 pushing to DMS Review closed expenses before generation
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -700,10 +701,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 pushing to DMS Please verify all details before generation
</p> </p>
<p className="text-xs text-yellow-700 mt-1"> <p className="text-xs text-yellow-700 mt-1">
Once pushed, the system will automatically generate an e-invoice and log it as an activity. Once submitted, the system will generate an e-invoice and initiate the SAP settlement process.
</p> </p>
</div> </div>
</div> </div>
@ -716,7 +717,7 @@ export function DMSPushModal({
</Label> </Label>
<Textarea <Textarea
id="comment" id="comment"
placeholder="Enter your comments about pushing to DMS (e.g., verified expenses, ready for invoice generation)..." placeholder="Enter your comments about e-invoice generation (e.g., verified expenses, ready for settlement)..."
value={comments} value={comments}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
@ -752,17 +753,19 @@ 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 ? (
'Pushing to DMS...' 'Processing...'
) : ( ) : (
<> <>
<Activity className="w-4 h-4 mr-2" /> <Activity className="w-4 h-4 mr-2" />
Push to DMS Generate & Sync
</> </>
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog>
{/* File Preview Modal - Matching DocumentsTab style */} {/* File Preview Modal - Matching DocumentsTab style */}
{previewDocument && ( {previewDocument && (
<Dialog <Dialog
@ -866,7 +869,7 @@ export function DMSPushModal({
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}
</Dialog> </>
); );
} }

View File

@ -41,7 +41,7 @@ import { downloadDocument } from '@/services/workflowApi';
import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi'; import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal'; import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal';
import { getSocket, joinUserRoom } from '@/utils/socket'; import { getSocket, joinUserRoom } from '@/utils/socket';
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
// Dealer Claim Components (import from index to get properly aliased exports) // Dealer Claim Components (import from index to get properly aliased exports)
import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index'; import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index';
@ -673,7 +673,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
request={request} request={request}
isInitiator={isInitiator} isInitiator={isInitiator}
isSpectator={isSpectator} isSpectator={isSpectator}
currentApprovalLevel={isClaimManagementRequest(apiRequest) ? null : currentApprovalLevel} currentApprovalLevel={currentApprovalLevel}
onAddApprover={() => setShowAddApproverModal(true)} onAddApprover={() => setShowAddApproverModal(true)}
onAddSpectator={() => setShowAddSpectatorModal(true)} onAddSpectator={() => setShowAddSpectatorModal(true)}
onApprove={() => setShowApproveModal(true)} onApprove={() => setShowApproveModal(true)}

View File

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

View File

@ -54,7 +54,6 @@ export function isWorkingTime(date: Date = new Date(), priority: string = 'stand
return false; return false;
} }
// TODO: Add holiday check if holiday API is available
return true; return true;
} }

View File

@ -75,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 { 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
} }
@ -105,13 +92,12 @@ 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; // API calls use cookies via withCredentials: true return null;
} }
// Development: Return from localStorage // Development: Return from localStorage
@ -120,8 +106,6 @@ 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
@ -135,8 +119,6 @@ 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
@ -147,10 +129,6 @@ 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);
@ -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 { 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
@ -296,11 +263,7 @@ 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();