From 7ae9133b9858829f7328c67940098651d56a2854 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Tue, 10 Feb 2026 09:54:54 +0530 Subject: [PATCH] removed suspicious comments --- src/App.tsx | 195 -- .../admin/AnalyticsConfig/AnalyticsConfig.tsx | 2 +- .../admin/DashboardConfig/DashboardConfig.tsx | 4 +- .../NotificationConfig/NotificationConfig.tsx | 2 +- .../admin/SharingConfig/SharingConfig.tsx | 2 +- .../admin/UserManagement/UserManagement.tsx | 65 +- .../components/request-detail/WorkflowTab.tsx | 2568 ++++++++--------- src/utils/slaTracker.ts | 71 +- src/utils/tokenManager.ts | 81 +- 9 files changed, 1376 insertions(+), 1614 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 17216cd..21442ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -412,201 +412,6 @@ function AppRoutes({ onLogout }: AppProps) { }); } - // Keep the old code below for backward compatibility (local storage fallback) - // This can be removed once API integration is fully tested - /* - // Generate unique ID for the new claim request - const requestId = `RE-REQ-2024-CM-${String(dynamicRequests.length + 2).padStart(3, '0')}`; - - // Create full request object - const newRequest = { - id: requestId, - title: `${claimData.activityName} - Claim Request`, - description: claimData.requestDescription, - category: 'Dealer Operations', - subcategory: 'Claim Management', - status: 'pending', - priority: 'standard', - amount: 'TBD', - slaProgress: 0, - slaRemaining: '7 days', - slaEndDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), - currentStep: 1, - totalSteps: 8, - templateType: 'claim-management', - templateName: 'Claim Management', - initiator: { - name: 'Current User', - role: 'Regional Marketing Coordinator', - department: 'Marketing', - email: 'current.user@royalenfield.com', - phone: '+91 98765 43290', - avatar: 'CU' - }, - department: 'Marketing', - createdAt: new Date().toLocaleString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: 'numeric', - hour12: true - }), - updatedAt: new Date().toLocaleString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: 'numeric', - hour12: true - }), - dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), - conclusionRemark: '', - claimDetails: { - activityName: claimData.activityName, - activityType: claimData.activityType, - activityDate: claimData.activityDate ? new Date(claimData.activityDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '', - location: claimData.location, - dealerCode: claimData.dealerCode, - dealerName: claimData.dealerName, - dealerEmail: claimData.dealerEmail || 'N/A', - dealerPhone: claimData.dealerPhone || 'N/A', - dealerAddress: claimData.dealerAddress || 'N/A', - requestDescription: claimData.requestDescription, - estimatedBudget: claimData.estimatedBudget || 'TBD', - periodStart: claimData.periodStartDate ? new Date(claimData.periodStartDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '', - periodEnd: claimData.periodEndDate ? new Date(claimData.periodEndDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '' - }, - approvalFlow: claimData.workflowSteps || [ - { - step: 1, - approver: `${claimData.dealerName} (Dealer)`, - role: 'Dealer - Document Upload', - status: 'pending', - tatHours: 72, - elapsedHours: 0, - assignedAt: new Date().toISOString(), - comment: null, - timestamp: null, - description: 'Dealer uploads proposal document, cost breakup, timeline for closure, and other supporting documents' - }, - { - step: 2, - approver: 'Current User (Initiator)', - role: 'Initiator Evaluation', - status: 'waiting', - tatHours: 48, - elapsedHours: 0, - assignedAt: null, - comment: null, - timestamp: null, - description: 'Initiator reviews dealer documents and approves or requests modifications' - }, - { - step: 3, - approver: 'System Auto-Process', - role: 'IO Confirmation', - status: 'waiting', - tatHours: 1, - elapsedHours: 0, - assignedAt: null, - comment: null, - timestamp: null, - description: 'Automatic IO (Internal Order) confirmation generated upon initiator approval' - }, - { - step: 4, - approver: 'Rajesh Kumar', - role: 'Department Lead Approval', - status: 'waiting', - tatHours: 72, - elapsedHours: 0, - assignedAt: null, - comment: null, - timestamp: null, - description: 'Department head approves and blocks budget in IO for this activity' - }, - { - step: 5, - approver: `${claimData.dealerName} (Dealer)`, - role: 'Dealer - Completion Documents', - status: 'waiting', - tatHours: 120, - elapsedHours: 0, - assignedAt: null, - comment: null, - timestamp: null, - description: 'Dealer submits activity completion documents and description' - }, - { - step: 6, - approver: 'Current User (Initiator)', - role: 'Initiator Verification', - status: 'waiting', - tatHours: 48, - elapsedHours: 0, - assignedAt: null, - comment: null, - timestamp: null, - description: 'Initiator verifies completion documents and can modify approved amount' - }, - { - step: 7, - approver: 'System Auto-Process', - role: 'E-Invoice Generation', - status: 'waiting', - tatHours: 1, - elapsedHours: 0, - assignedAt: null, - comment: null, - timestamp: null, - description: 'Auto-generate e-invoice based on final approved amount' - }, - { - step: 8, - approver: 'Finance Team', - role: 'Credit Note Issuance', - status: 'waiting', - tatHours: 48, - elapsedHours: 0, - assignedAt: null, - comment: null, - timestamp: null, - description: 'Finance team issues credit note to dealer' - } - ], - documents: [], - spectators: [], - auditTrail: [ - { - type: 'created', - action: 'Request Created', - details: `Claim request for ${claimData.activityName} created`, - user: 'Current User', - timestamp: new Date().toLocaleString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: 'numeric', - hour12: true - }) - } - ], - tags: ['claim-management', 'new-request', claimData.activityType?.toLowerCase().replace(/\s+/g, '-')] - }; - - // Add to dynamic requests - setDynamicRequests(prev => [...prev, newRequest]); - - // Also add to REQUEST_DATABASE for immediate viewing - (REQUEST_DATABASE as any)[requestId] = newRequest; - - toast.success('Claim Request Submitted', { - description: 'Your claim management request has been created successfully.', - }); - navigate('/my-requests'); - */ }; return ( diff --git a/src/components/admin/AnalyticsConfig/AnalyticsConfig.tsx b/src/components/admin/AnalyticsConfig/AnalyticsConfig.tsx index 7de8d2b..8699728 100644 --- a/src/components/admin/AnalyticsConfig/AnalyticsConfig.tsx +++ b/src/components/admin/AnalyticsConfig/AnalyticsConfig.tsx @@ -31,7 +31,7 @@ export function AnalyticsConfig() { }); const handleSave = () => { - // TODO: Implement API call to save configuration + toast.success('Analytics configuration saved successfully'); }; diff --git a/src/components/admin/DashboardConfig/DashboardConfig.tsx b/src/components/admin/DashboardConfig/DashboardConfig.tsx index 86547da..b8fbdcc 100644 --- a/src/components/admin/DashboardConfig/DashboardConfig.tsx +++ b/src/components/admin/DashboardConfig/DashboardConfig.tsx @@ -8,7 +8,7 @@ import { toast } from 'sonner'; export type Role = 'Initiator' | 'Approver' | 'Spectator'; -export type KPICard = +export type KPICard = | 'Total Requests' | 'Open Requests' | 'Approved Requests' @@ -59,7 +59,7 @@ export function DashboardConfig() { }); const handleSave = () => { - // TODO: Implement API call to save dashboard configuration + toast.success('Dashboard layout saved successfully'); }; diff --git a/src/components/admin/NotificationConfig/NotificationConfig.tsx b/src/components/admin/NotificationConfig/NotificationConfig.tsx index b28f077..7e6933f 100644 --- a/src/components/admin/NotificationConfig/NotificationConfig.tsx +++ b/src/components/admin/NotificationConfig/NotificationConfig.tsx @@ -28,7 +28,7 @@ export function NotificationConfig() { }); const handleSave = () => { - // TODO: Implement API call to save notification configuration + toast.success('Notification configuration saved successfully'); }; diff --git a/src/components/admin/SharingConfig/SharingConfig.tsx b/src/components/admin/SharingConfig/SharingConfig.tsx index e9383fc..fb97683 100644 --- a/src/components/admin/SharingConfig/SharingConfig.tsx +++ b/src/components/admin/SharingConfig/SharingConfig.tsx @@ -23,7 +23,7 @@ export function SharingConfig() { }); const handleSave = () => { - // TODO: Implement API call to save sharing configuration + toast.success('Sharing policy saved successfully'); }; diff --git a/src/components/admin/UserManagement/UserManagement.tsx b/src/components/admin/UserManagement/UserManagement.tsx index 6c116c3..e5f883b 100644 --- a/src/components/admin/UserManagement/UserManagement.tsx +++ b/src/components/admin/UserManagement/UserManagement.tsx @@ -2,18 +2,18 @@ import { useState, useCallback, useEffect, useRef } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { - Plus, - Search, - Users, - Shield, +import { + Plus, + Search, + Users, + Shield, Loader2, CheckCircle, AlertCircle, @@ -75,7 +75,7 @@ export function UserManagement() { const [users, setUsers] = useState([]); const [loadingUsers, setLoadingUsers] = useState(false); const [roleStats, setRoleStats] = useState({ admins: 0, management: 0, users: 0, total: 0, active: 0, inactive: 0 }); - + // Pagination and filtering const [roleFilter, setRoleFilter] = useState<'ELEVATED' | 'ALL' | 'ADMIN' | 'MANAGEMENT' | 'USER'>('ELEVATED'); const [currentPage, setCurrentPage] = useState(1); @@ -135,14 +135,14 @@ export function UserManagement() { // We'll search with a broader filter to find the user const response = await userApi.getUsersByRole('ALL', 1, 1000); const allUsers = response.data?.data?.users || []; - const foundUser = allUsers.find((u: any) => + const foundUser = allUsers.find((u: any) => u.email?.toLowerCase() === email.toLowerCase() ); - + if (foundUser && foundUser.role) { return foundUser.role as 'USER' | 'MANAGEMENT' | 'ADMIN'; } - + return null; // User not found in system, no role assigned } catch (error) { console.error('Failed to fetch user role:', error); @@ -156,7 +156,7 @@ export function UserManagement() { setSearchQuery(user.email); setSearchResults([]); setFetchingRole(true); - + try { // Fetch and set the user's current role if they have one const currentRole = await fetchUserRole(user.email); @@ -186,7 +186,7 @@ export function UserManagement() { try { await userApi.assignRole(selectedUser.email, selectedRole); - + setMessage({ type: 'success', text: `Successfully assigned ${selectedRole} role to ${selectedUser.displayName || selectedUser.email}` @@ -200,7 +200,7 @@ export function UserManagement() { // Refresh the users list await fetchUsers(); await fetchRoleStatistics(); - + toast.success(`Role assigned successfully`); } catch (error: any) { console.error('Role assignment failed:', error); @@ -220,7 +220,7 @@ export function UserManagement() { setLoadingUsers(true); try { const response = await userApi.getUsersByRole(roleFilter, page, limit); - + const usersData = response.data?.data?.users || []; const paginationData = response.data?.data?.pagination; const summaryData = response.data?.data?.summary; @@ -234,13 +234,13 @@ export function UserManagement() { designation: u.designation, isActive: u.isActive !== false // Default to true if not specified }))); - + if (paginationData) { setCurrentPage(paginationData.currentPage); setTotalPages(paginationData.totalPages); setTotalUsers(paginationData.totalUsers); } - + // Update summary stats if available if (summaryData) { setRoleStats(prev => ({ @@ -264,13 +264,13 @@ export function UserManagement() { try { const response = await userApi.getRoleStatistics(); const statsData = response.data?.data?.statistics || response.data?.statistics || []; - + const stats = { admins: parseInt(statsData.find((s: any) => s.role === 'ADMIN')?.count || '0'), management: parseInt(statsData.find((s: any) => s.role === 'MANAGEMENT')?.count || '0'), users: parseInt(statsData.find((s: any) => s.role === 'USER')?.count || '0') }; - + setRoleStats(prev => ({ ...prev, ...stats, @@ -317,8 +317,8 @@ export function UserManagement() { const handleToggleUserStatus = async (userId: string) => { const user = users.find(u => u.userId === userId); if (!user) return; - - // TODO: Implement backend API for toggling user status + + toast.info('User status toggle functionality coming soon'); }; @@ -326,13 +326,12 @@ export function UserManagement() { const handleDeleteUser = async (userId: string) => { const user = users.find(u => u.userId === userId); if (!user) return; - + if (user.role === 'ADMIN') { toast.error('Cannot delete admin user'); return; } - - // TODO: Implement backend API for deleting users + toast.info('User deletion functionality coming soon'); }; @@ -515,11 +514,10 @@ export function UserManagement() { {/* Message */} {message && ( -
+
{message.type === 'success' ? ( @@ -602,7 +600,7 @@ export function UserManagement() {

No users found

- {roleFilter === 'ELEVATED' + {roleFilter === 'ELEVATED' ? 'Assign ADMIN or MANAGEMENT roles to see users here' : 'No users match the selected filter' } @@ -664,11 +662,10 @@ export function UserManagement() { variant={currentPage === pageNum ? "default" : "outline"} size="sm" onClick={() => handlePageChange(pageNum)} - className={`w-9 h-9 p-0 ${ - currentPage === pageNum - ? 'bg-re-green hover:bg-re-green/90' - : '' - }`} + className={`w-9 h-9 p-0 ${currentPage === pageNum + ? 'bg-re-green hover:bg-re-green/90' + : '' + }`} > {pageNum} diff --git a/src/dealer-claim/components/request-detail/WorkflowTab.tsx b/src/dealer-claim/components/request-detail/WorkflowTab.tsx index 4f16a3d..a714a9f 100644 --- a/src/dealer-claim/components/request-detail/WorkflowTab.tsx +++ b/src/dealer-claim/components/request-detail/WorkflowTab.tsx @@ -13,7 +13,7 @@ import { Progress } from '@/components/ui/progress'; import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon, XCircle, History, ChevronDown, ChevronUp, RefreshCw, RotateCw, Eye } from 'lucide-react'; import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter'; import { formatHoursMinutes } from '@/utils/slaTracker'; -import { +import { AdditionalApproverReviewModal, DealerProposalSubmissionModal, InitiatorProposalApprovalModal, @@ -164,11 +164,11 @@ const getStepIconBg = (status: string) => { } }; -export function DealerClaimWorkflowTab({ - request, - user, - isInitiator, - onSkipApprover: _onSkipApprover, +export function DealerClaimWorkflowTab({ + request, + user, + isInitiator, + onSkipApprover: _onSkipApprover, onRefresh, documentPolicy }: DealerClaimWorkflowTabProps) { @@ -186,12 +186,12 @@ export function DealerClaimWorkflowTab({ const [versionHistory, setVersionHistory] = useState([]); const [showHistory, setShowHistory] = useState(false); const [expandedVersionSteps, setExpandedVersionSteps] = useState>(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 const [approvalFlow, setApprovalFlow] = useState([]); const [refreshTrigger, setRefreshTrigger] = useState(0); - + // Reload approval flows whenever request changes or after refresh // Always fetch from API to ensure fresh data (don't rely on cached request.approvalFlow) // Also watch for changes in totalLevels to detect when approvers are added @@ -222,7 +222,7 @@ export function DealerClaimWorkflowTab({ })) // Sort by levelNumber to ensure correct order (critical for proper display) .sort((a: any, b: any) => (a.levelNumber || 0) - (b.levelNumber || 0)); - + // Only update if the data actually changed (avoid unnecessary re-renders) setApprovalFlow(prevFlows => { // Check if flows are different @@ -233,11 +233,11 @@ export function DealerClaimWorkflowTab({ // Status change is critical - when an approval happens, status changes from 'in_progress' to 'approved' const hasChanges = prevFlows.some((prev: any, idx: number) => { const curr = flows[idx]; - return !curr || - prev.levelNumber !== curr.levelNumber || - prev.levelName !== curr.levelName || - prev.approverEmail !== curr.approverEmail || - prev.status !== curr.status; // Check status change + return !curr || + prev.levelNumber !== curr.levelNumber || + prev.levelName !== curr.levelName || + prev.approverEmail !== curr.approverEmail || + prev.status !== curr.status; // Check status change }); return hasChanges ? flows : prevFlows; }); @@ -260,7 +260,7 @@ export function DealerClaimWorkflowTab({ loadApprovalFlows(); }, [request?.id, request?.requestId, request?.totalLevels, refreshTrigger]); - + // Also reload when request.currentStep or totalLevels changes (to catch step transitions and new approvers) useEffect(() => { if (request?.id || request?.requestId) { @@ -286,7 +286,7 @@ export function DealerClaimWorkflowTab({ })) // Sort by levelNumber to ensure correct order .sort((a: any, b: any) => (a.levelNumber || 0) - (b.levelNumber || 0)); - + // Update state with new flows setApprovalFlow(flows); } @@ -297,7 +297,7 @@ export function DealerClaimWorkflowTab({ loadApprovalFlows(); } }, [request?.currentStep, request?.totalLevels]); - + // Enhanced refresh handler that also reloads approval flows const handleRefresh = async () => { setRefreshTrigger(prev => prev + 1); @@ -325,7 +325,7 @@ export function DealerClaimWorkflowTab({ useEffect(() => { loadVersionHistory(); }, [request?.id, request?.requestId, refreshTrigger]); - + // Step title and description mapping based on actual step number (not array index) // This handles cases where approvers are added between steps const getStepTitle = (stepNumber: number, levelName?: string, approverName?: string): string => { @@ -339,7 +339,7 @@ export function DealerClaimWorkflowTab({ // Otherwise use the levelName from backend (preserved from original step) return levelName; } - + // Fallback to mapping based on step number const stepTitleMap: Record = { 1: 'Dealer - Proposal Submission', @@ -351,24 +351,24 @@ export function DealerClaimWorkflowTab({ 7: 'E-Invoice Generation', 8: 'Credit Note from SAP', }; - + // If step number exists in map, use it if (stepTitleMap[stepNumber]) { return stepTitleMap[stepNumber]; } - + // For dynamically added steps, create a title from approver name or generic if (approverName && approverName !== 'Unknown' && approverName !== 'System') { return `Additional Approver - ${approverName}`; } - + return `Additional Approver - Step ${stepNumber}`; }; const getStepDescription = (stepNumber: number, levelName?: string, approverName?: string): string => { // Check if this is an "Additional Approver" (dynamically added) const isAdditionalApprover = levelName && levelName.toLowerCase().includes('additional approver'); - + // If this is an additional approver, use generic description if (isAdditionalApprover) { if (approverName && approverName !== 'Unknown' && approverName !== 'System') { @@ -376,12 +376,12 @@ export function DealerClaimWorkflowTab({ } return `Additional approver will review and approve this request.`; } - + // Use levelName to determine description (handles shifted steps correctly) // This ensures descriptions shift with their steps when approvers are added if (levelName && levelName.trim()) { const levelNameLower = levelName.toLowerCase(); - + // Map level names to descriptions (works even after shifting) if (levelNameLower.includes('dealer') && levelNameLower.includes('proposal')) { return 'Dealer submits the proposal for the activity with comments including proposal document with requested details, cost break-up, timeline for closure, and other requests'; @@ -408,7 +408,7 @@ export function DealerClaimWorkflowTab({ 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) const stepDescriptionMap: Record = { 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', @@ -420,16 +420,16 @@ export function DealerClaimWorkflowTab({ 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.', }; - + if (stepDescriptionMap[stepNumber]) { return stepDescriptionMap[stepNumber]; } - + // Final fallback if (approverName && approverName !== 'Unknown' && approverName !== 'System') { return `${approverName} will review and approve this request.`; } - + return `Step ${stepNumber} approval required.`; }; @@ -437,7 +437,7 @@ export function DealerClaimWorkflowTab({ // Uses levelName to match history (consistent and doesn't change with step shifts or additional approvers) const getStepVersionHistory = (levelName: string) => { if (!versionHistory || versionHistory.length === 0 || !levelName) return { current: null, previous: null }; - + // Filter history by levelName (most reliable and consistent) // Level names are consistent: "Dealer Proposal Submission", "Department Lead Approval", etc. // Additional approvers have unique names like "Additional Approver - John Doe" @@ -446,15 +446,15 @@ export function DealerClaimWorkflowTab({ if (history.levelName && history.levelName.trim() === levelName.trim()) { return true; // Include all snapshot types for this level } - + // Fallback: match by levelName from snapshotData (for approval snapshots) if (history.snapshotData?.levelName && history.snapshotData.levelName.trim() === levelName.trim()) { return true; } - + return false; }); - + // Sort by version descending to get most recent first // Prioritize APPROVE snapshots over WORKFLOW snapshots for the same version // This ensures approval comments are shown instead of just workflow movement @@ -468,14 +468,14 @@ export function DealerClaimWorkflowTab({ const bPriority = b.snapshotType === 'APPROVE' ? 1 : b.snapshotType === 'PROPOSAL' ? 2 : b.snapshotType === 'COMPLETION' ? 2 : 3; return aPriority - bPriority; }); - + // Filter out WORKFLOW snapshots if there's an APPROVE snapshot for the same level // This ensures we show APPROVE snapshots (with comments) instead of WORKFLOW snapshots const filteredVersions = sortedVersions.filter((version, _index, arr) => { // If this is a WORKFLOW snapshot, check if there's an APPROVE snapshot with same or higher version if (version.snapshotType === 'WORKFLOW') { - const hasApproveSnapshot = arr.some(v => - v.snapshotType === 'APPROVE' && + const hasApproveSnapshot = arr.some(v => + v.snapshotType === 'APPROVE' && v.levelName === version.levelName && v.version >= version.version ); @@ -485,44 +485,44 @@ export function DealerClaimWorkflowTab({ // Keep all non-WORKFLOW snapshots return true; }); - + const current = filteredVersions.length > 0 ? filteredVersions[0] : null; const previous = filteredVersions.length > 1 ? filteredVersions[1] : null; - + return { current, previous, all: filteredVersions }; }; - + // Get backend currentLevel to determine which steps are active vs waiting // This needs to be calculated before mapping steps so we can use it in status normalization // Convert to number to ensure proper comparison const backendCurrentLevel = request?.currentLevel || request?.current_level || request?.currentStep; - const currentLevelNumber = backendCurrentLevel !== undefined && backendCurrentLevel !== null - ? Number(backendCurrentLevel) + const currentLevelNumber = backendCurrentLevel !== undefined && backendCurrentLevel !== null + ? Number(backendCurrentLevel) : null; - + // Check request status - if rejected or closed, no steps should be active const requestStatus = (request?.status || '').toUpperCase(); const isRequestRejected = requestStatus === 'REJECTED'; const isRequestClosed = requestStatus === 'CLOSED'; - const isRequestActive = !isRequestRejected && !isRequestClosed && + const isRequestActive = !isRequestRejected && !isRequestClosed && (requestStatus === 'PENDING' || requestStatus === 'IN_PROGRESS' || requestStatus === 'IN-PROGRESS'); - + // Find the rejected step level (if any) - all steps after rejection should be inactive const rejectedStepLevel = approvalFlow.find((level: any) => { const levelId = level.levelId || level.level_id; const approval = request?.approvals?.find((a: any) => a.levelId === levelId); return approval?.status?.toLowerCase() === 'rejected'; }); - const rejectedStepNumber = rejectedStepLevel + const rejectedStepNumber = rejectedStepLevel ? Number(rejectedStepLevel.levelNumber || rejectedStepLevel.level_number || rejectedStepLevel.step || 0) : null; - + // Transform approval flow to dealer claim workflow steps const workflowSteps: WorkflowStep[] = approvalFlow.map((step: any, index: number) => { // Get actual step number from levelNumber or step field - ensure it's a number const actualStepNumber = Number(step.levelNumber || step.level_number || step.step || index + 1); - + // Get levelName from the approval level if available const levelName = step.levelName || step.level_name; @@ -535,20 +535,20 @@ export function DealerClaimWorkflowTab({ if (isDeptLeadStep || actualStepNumber === 3) { // Get IO details from dedicated internalOrder table const internalOrder = request?.internalOrder || request?.internal_order; - + if (internalOrder?.ioNumber || internalOrder?.io_number) { ioDetails = { ioNumber: internalOrder.ioNumber || internalOrder.io_number || '', blockedAmount: internalOrder.ioBlockedAmount || internalOrder.io_blocked_amount || 0, availableBalance: internalOrder.ioAvailableBalance || internalOrder.io_available_balance || 0, remainingBalance: internalOrder.ioRemainingBalance || internalOrder.io_remaining_balance || 0, - organizedBy: + organizedBy: internalOrder.organizer?.displayName || internalOrder.organizer?.name || internalOrder.organizedBy || step.approver || 'N/A', - organizedAt: + organizedAt: internalOrder.organizedAt || internalOrder.organized_at || step.approvedAt || @@ -582,13 +582,13 @@ export function DealerClaimWorkflowTab({ // Normalize status - CRITICAL: Check request status first, then step position // If request is rejected/closed, no steps should be active after rejection point let normalizedStatus: string; - + // FIRST: Check if request is rejected/closed - if so, handle accordingly if (isRequestRejected || isRequestClosed) { // If this step was rejected, mark it as rejected if (approval?.status?.toLowerCase() === 'rejected') { normalizedStatus = 'rejected'; - } + } // If there's a rejected step and this step comes after it, mark as waiting (inactive) else if (rejectedStepNumber !== null && actualStepNumber > rejectedStepNumber) { normalizedStatus = 'waiting'; @@ -658,7 +658,7 @@ export function DealerClaimWorkflowTab({ } } } - } + } // THIRD: No backend currentLevel or request status unclear - use approval status else { if (approval?.status) { @@ -687,20 +687,20 @@ export function DealerClaimWorkflowTab({ // Waiting steps (future steps) should have elapsedHours = 0 // This ensures that when in step 1, only step 1 shows elapsed time, others show 0 const isWaiting = normalizedStatus === 'waiting'; - + // Only calculate/show elapsed hours for active or completed steps // For waiting steps, elapsedHours should be 0 (they haven't started yet) const elapsedHours = isWaiting ? 0 : (step.elapsedHours || 0); const approverName = step.approver || step.approverName || 'Unknown'; - + // Get levelName for this step (consistent identifier, doesn't change with step shifts) // Level names are consistent: "Dealer Proposal Submission", "Department Lead Approval", etc. // Additional approvers have unique names like "Additional Approver - John Doe" - + // Get version history for this step using levelName (consistent and reliable) const stepVersionHistory = levelName ? getStepVersionHistory(levelName) : { current: null, previous: null }; - + return { step: actualStepNumber, title: getStepTitle(actualStepNumber, levelName, approverName), @@ -718,17 +718,17 @@ export function DealerClaimWorkflowTab({ versionHistory: stepVersionHistory, // Add version history to step }; }); - + // Calculate currentStep from approval flow - find the first pending or in_progress step // IMPORTANT: Use the workflow's currentLevel from backend (most accurate) // Fallback to finding first pending step if currentLevel not available // Note: Status normalization already handled in workflowSteps mapping above // backendCurrentLevel is already calculated above before the map function - + // CRITICAL: If request is rejected or closed, no step should be active let activeStep = null; let currentStep = 1; - + if (isRequestRejected || isRequestClosed) { // Request is rejected/closed - no active step // Find the rejected step to show as the "current" step for display purposes @@ -742,30 +742,30 @@ export function DealerClaimWorkflowTab({ } else if (isRequestActive) { // Request is active - find the active step // Find the step that matches backend's currentLevel - const activeStepFromBackend = currentLevelNumber !== null + const activeStepFromBackend = currentLevelNumber !== null ? workflowSteps.find(s => s.step === currentLevelNumber) : null; - + // If backend currentLevel exists and step is pending/in_progress, use it // Otherwise, find first pending/in_progress step - activeStep = activeStepFromBackend && - (activeStepFromBackend.status === 'pending' || activeStepFromBackend.status === 'in_progress') + activeStep = activeStepFromBackend && + (activeStepFromBackend.status === 'pending' || activeStepFromBackend.status === 'in_progress') ? activeStepFromBackend : workflowSteps.find(s => { - const status = s.status?.toLowerCase() || ''; - return status === 'pending' || status === 'in_progress' || status === 'in-review' || status === 'in_review'; - }); - + const status = s.status?.toLowerCase() || ''; + return status === 'pending' || status === 'in_progress' || status === 'in-review' || status === 'in_review'; + }); + currentStep = activeStep ? activeStep.step : (currentLevelNumber || request?.currentStep || 1); } else { // Request status unclear - use backend currentLevel or default currentStep = currentLevelNumber || request?.currentStep || 1; } - + // Check if current user is the dealer (for steps 1 and 5) const userEmail = (user as any)?.email?.toLowerCase() || ''; const dealerEmail = ( - (request as any)?.dealerEmail?.toLowerCase() || + (request as any)?.dealerEmail?.toLowerCase() || (request as any)?.dealer?.email?.toLowerCase() || (request as any)?.claimDetails?.dealerEmail?.toLowerCase() || (request as any)?.claimDetails?.dealer_email?.toLowerCase() || @@ -774,7 +774,7 @@ export function DealerClaimWorkflowTab({ const isDealer = dealerEmail && userEmail === dealerEmail; // Check if current user is the approver for the current step - const currentApprovalLevel = approvalFlow.find((level: any) => + const currentApprovalLevel = approvalFlow.find((level: any) => (level.step || level.levelNumber || level.level_number) === currentStep ); const approverEmail = (currentApprovalLevel?.approverEmail || '').toLowerCase(); @@ -787,21 +787,21 @@ export function DealerClaimWorkflowTab({ (request as any)?.initiatorEmail?.toLowerCase() || '' ); - + // Find the step where the initiator is the approver // Check by: 1) approverEmail matches initiatorEmail, OR 2) levelName contains "Requestor Evaluation" const initiatorStepLevel = approvalFlow.find((l: any) => { const levelApproverEmail = (l.approverEmail || '').toLowerCase(); const levelName = (l.levelName || '').toLowerCase(); return (initiatorEmail && levelApproverEmail === initiatorEmail) || - levelName.includes('requestor evaluation') || - levelName.includes('requestor') && levelName.includes('confirmation'); + levelName.includes('requestor evaluation') || + levelName.includes('requestor') && levelName.includes('confirmation'); }); - - const initiatorStepNumber = initiatorStepLevel + + const initiatorStepNumber = initiatorStepLevel ? (initiatorStepLevel.step || initiatorStepLevel.levelNumber || initiatorStepLevel.level_number || 2) : 2; // Fallback to 2 if not found - + // Check if user is approver for the initiator's step (requestor evaluation) const step2Level = initiatorStepLevel || approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 2); const step2ApproverEmail = (step2Level?.approverEmail || '').toLowerCase(); @@ -881,26 +881,26 @@ export function DealerClaimWorkflowTab({ // Get approval levels to find the initiator's step levelId dynamically const details = await getWorkflowDetails(requestId); const approvals = details?.approvalLevels || details?.approvals || []; - + // Find the initiator's step by checking approverEmail or levelName const initiatorEmail = ( (request as any)?.initiator?.email?.toLowerCase() || (request as any)?.initiatorEmail?.toLowerCase() || '' ); - + const step2Level = approvals.find((level: any) => { const levelApproverEmail = (level.approverEmail || level.approver_email || '').toLowerCase(); const levelName = (level.levelName || level.level_name || '').toLowerCase(); const levelNumber = level.levelNumber || level.level_number; - + // Check if this is the initiator's step return (initiatorEmail && levelApproverEmail === initiatorEmail) || - levelName.includes('requestor evaluation') || - (levelName.includes('requestor') && levelName.includes('confirmation')) || - // Fallback: if initiatorStepNumber was found earlier, use it - (levelNumber === initiatorStepNumber); - }) || approvals.find((level: any) => + levelName.includes('requestor evaluation') || + (levelName.includes('requestor') && levelName.includes('confirmation')) || + // Fallback: if initiatorStepNumber was found earlier, use it + (levelNumber === initiatorStepNumber); + }) || approvals.find((level: any) => (level.levelNumber || level.level_number) === 2 ); // Final fallback to level 2 @@ -936,26 +936,26 @@ export function DealerClaimWorkflowTab({ // Get approval levels to find the initiator's step levelId dynamically const details = await getWorkflowDetails(requestId); const approvals = details?.approvalLevels || details?.approvals || []; - + // Find the initiator's step by checking approverEmail or levelName const initiatorEmail = ( (request as any)?.initiator?.email?.toLowerCase() || (request as any)?.initiatorEmail?.toLowerCase() || '' ); - + const step2Level = approvals.find((level: any) => { const levelApproverEmail = (level.approverEmail || level.approver_email || '').toLowerCase(); const levelName = (level.levelName || level.level_name || '').toLowerCase(); const levelNumber = level.levelNumber || level.level_number; - + // Check if this is the initiator's step return (initiatorEmail && levelApproverEmail === initiatorEmail) || - levelName.includes('requestor evaluation') || - (levelName.includes('requestor') && levelName.includes('confirmation')) || - // Fallback: if initiatorStepNumber was found earlier, use it - (levelNumber === initiatorStepNumber); - }) || approvals.find((level: any) => + levelName.includes('requestor evaluation') || + (levelName.includes('requestor') && levelName.includes('confirmation')) || + // Fallback: if initiatorStepNumber was found earlier, use it + (levelNumber === initiatorStepNumber); + }) || approvals.find((level: any) => (level.levelNumber || level.level_number) === 2 ); // Final fallback to level 2 @@ -991,26 +991,26 @@ export function DealerClaimWorkflowTab({ // Get approval levels to find the initiator's step levelId dynamically const details = await getWorkflowDetails(requestId); const approvals = details?.approvalLevels || details?.approvals || []; - + // Find the initiator's step by checking approverEmail or levelName const initiatorEmail = ( (request as any)?.initiator?.email?.toLowerCase() || (request as any)?.initiatorEmail?.toLowerCase() || '' ); - + const step2Level = approvals.find((level: any) => { const levelApproverEmail = (level.approverEmail || level.approver_email || '').toLowerCase(); const levelName = (level.levelName || level.level_name || '').toLowerCase(); const levelNumber = level.levelNumber || level.level_number; - + // Check if this is the initiator's step return (initiatorEmail && levelApproverEmail === initiatorEmail) || - levelName.includes('requestor evaluation') || - (levelName.includes('requestor') && levelName.includes('confirmation')) || - // Fallback: if initiatorStepNumber was found earlier, use it - (levelNumber === initiatorStepNumber); - }) || approvals.find((level: any) => + levelName.includes('requestor evaluation') || + (levelName.includes('requestor') && levelName.includes('confirmation')) || + // Fallback: if initiatorStepNumber was found earlier, use it + (levelNumber === initiatorStepNumber); + }) || approvals.find((level: any) => (level.levelNumber || level.level_number) === 2 ); // Final fallback to level 2 @@ -1050,12 +1050,12 @@ export function DealerClaimWorkflowTab({ // Get approval levels to find Department Lead step levelId dynamically const details = await getWorkflowDetails(requestId); const approvals = details?.approvalLevels || details?.approvals || []; - + // Find Department Lead step by levelName (handles step shifts) const step3Level = approvals.find((level: any) => { const levelName = (level.levelName || level.level_name || '').toLowerCase(); return levelName.includes('department lead'); - }) || approvals.find((level: any) => + }) || approvals.find((level: any) => (level.levelNumber || level.level_number) === 3 ); // Fallback to level 3 @@ -1184,12 +1184,12 @@ export function DealerClaimWorkflowTab({ // Get approval levels to find Department Lead step levelId dynamically const details = await getWorkflowDetails(requestId); const approvals = details?.approvalLevels || details?.approvals || []; - + // Find Department Lead step by levelName (handles step shifts) const step3Level = approvals.find((level: any) => { const levelName = (level.levelName || level.level_name || '').toLowerCase(); return levelName.includes('department lead'); - }) || approvals.find((level: any) => + }) || approvals.find((level: any) => (level.levelNumber || level.level_number) === 3 ); // Fallback to level 3 @@ -1215,7 +1215,7 @@ export function DealerClaimWorkflowTab({ // Extract proposal data from request const [proposalData, setProposalData] = useState(null); - + useEffect(() => { if (!request) { setProposalData(null); @@ -1234,14 +1234,14 @@ export function DealerClaimWorkflowTab({ const details = await getWorkflowDetails(requestId); const documents = details?.documents || []; const proposalDetails = request.proposalDetails || details?.proposalDetails || {}; - + // Find proposal document (category APPROVAL or type proposal) - const proposalDoc = documents.find((d: any) => + const proposalDoc = documents.find((d: any) => d.category === 'APPROVAL' || d.type === 'proposal' || d.documentCategory === 'APPROVAL' ); - + // Find supporting documents - const otherDocs = documents.filter((d: any) => + const otherDocs = documents.filter((d: any) => d.category === 'SUPPORTING' || d.type === 'supporting' || d.documentCategory === 'SUPPORTING' ); @@ -1277,7 +1277,7 @@ export function DealerClaimWorkflowTab({ console.warn('Failed to load proposal data:', error); // Fallback to request data only const proposalDetails = request.proposalDetails || {}; - + // Ensure costBreakup is an array let costBreakup = proposalDetails.costBreakup || []; if (typeof costBreakup === 'string') { @@ -1326,7 +1326,7 @@ export function DealerClaimWorkflowTab({ // Get workflow details which includes all documents const details = await getWorkflowDetails(requestId); const documents = details?.documents || []; - + // Filter and categorize documents const completionDocs: any[] = []; const activityPhotos: any[] = []; @@ -1336,7 +1336,7 @@ export function DealerClaimWorkflowTab({ documents.forEach((doc: any) => { const category = (doc.category || doc.documentCategory || doc.type || '').toUpperCase(); const name = (doc.fileName || doc.file_name || doc.name || '').toLowerCase(); - + const docObj = { name: doc.fileName || doc.file_name || doc.name, id: doc.documentId || doc.document_id || doc.id, @@ -1361,8 +1361,8 @@ export function DealerClaimWorkflowTab({ // If documents came from the completionDetails directly (some backends might structure it this way) if (completionDocs.length === 0 && activityPhotos.length === 0 && request.completionDetails) { - // Try to extract from request.completionDetails if available/applicable - // This is a fallback in case documents aren't flattened in the main documents list yet + // Try to extract from request.completionDetails if available/applicable + // This is a fallback in case documents aren't flattened in the main documents list yet } setCompletionDocumentsData({ @@ -1382,1261 +1382,1259 @@ export function DealerClaimWorkflowTab({ }, [request]); // Get dealer and activity info - const dealerName = request?.claimDetails?.dealerName || - request?.dealerInfo?.name || - 'Dealer'; - const activityName = request?.claimDetails?.activityName || - request?.activityInfo?.activityName || - request?.title || - 'Activity'; + const dealerName = request?.claimDetails?.dealerName || + request?.dealerInfo?.name || + 'Dealer'; + const activityName = request?.claimDetails?.activityName || + request?.activityInfo?.activityName || + request?.title || + 'Activity'; return ( <> - -

-
- - - Claim Management Workflow - - - Approval process for dealer claim management - -
-
- - - {/* Returned to Initiator Banner */} - - {(() => { - const isReturnedToInitiator = request?.status === 'rejected' && - !(request?.closureDate || request?.closure_date) && isInitiator; - if (!isReturnedToInitiator) return null; - - return ( -
-
- -
-
-

Action Required: Request Returned

-

- This request has been returned to you by the department head. - {isInitiator ? - 'You can choose to resubmit, discuss with the dealer, request a revision, or cancel the request.' : - 'The initiator needs to take action to proceed.'} -

- {/* Actions are now in the step card itself - no need for separate button */} -
+ +
+
+ + + Claim Management Workflow + + + Approval process for dealer claim management +
- ); - })()} +
+
+ + {/* Returned to Initiator Banner */} -
- {workflowSteps.map((step, index) => { - // Step is active if: - // 1. Request is active (not rejected/closed) - // 2. It's pending or in_progress - // 3. AND it matches currentStep (from backend or calculated) - // 4. AND it's the actual current step (not a future step that happens to be pending) - const stepStatus = step.status?.toLowerCase() || ''; - const isPendingOrInProgress = stepStatus === 'in_progress'; - const matchesCurrentStep = step.step === currentStep; - - // Step is active only if: - // - Request is active (not rejected/closed) - // - AND it matches the current step - // - AND is pending/in_progress - const isActive = isRequestActive && isPendingOrInProgress && matchesCurrentStep; - const isCompleted = step.status === 'approved'; - - // Find approval data for this step to get SLA information - // First find the corresponding level in approvalFlow to get levelId - const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step); - const approval = stepLevel?.levelId - ? request?.approvals?.find((a: any) => a.levelId === stepLevel.levelId || a.level_id === stepLevel.levelId) - : null; - - // Check if step is paused - const isPaused = approval?.status === 'PAUSED' || - (request?.pauseInfo?.isPaused && - (request?.pauseInfo?.levelId === approval?.levelId || - request?.pauseInfo?.level_id === approval?.levelId)); + {(() => { + const isReturnedToInitiator = request?.status === 'rejected' && + !(request?.closureDate || request?.closure_date) && isInitiator; + if (!isReturnedToInitiator) return null; return ( -
-
- {/* Step Icon */} -
- {getStepIcon(step.status)} -
+
+
+ +
+
+

Action Required: Request Returned

+

+ This request has been returned to you by the department head. + {isInitiator ? + 'You can choose to resubmit, discuss with the dealer, request a revision, or cancel the request.' : + 'The initiator needs to take action to proceed.'} +

+ {/* Actions are now in the step card itself - no need for separate button */} +
+
+ ); + })()} - {/* Step Content */} -
-
-
-
-

- {step.title} -

- - {step.status.toLowerCase()} - - {/* Email Template Button - Show when step has emailTemplateUrl and is approved */} - {step.emailTemplateUrl && step.status === 'approved' && ( - - )} - {/* E-Invoice Download Button (Step 7) */} - {step.step === 7 && step.einvoiceUrl && isCompleted && ( - - )} -
-

{step.approver}

-

{step.description}

-
-
-

TAT: {formatHoursMinutes(step.tatHours)}

- {/* Only show elapsed time for active or completed steps, not for waiting steps */} - {step.elapsedHours && (isActive || isCompleted) && ( -

- Elapsed: {formatHoursMinutes(step.elapsedHours)} -

- )} -
+
+ {workflowSteps.map((step, index) => { + // Step is active if: + // 1. Request is active (not rejected/closed) + // 2. It's pending or in_progress + // 3. AND it matches currentStep (from backend or calculated) + // 4. AND it's the actual current step (not a future step that happens to be pending) + const stepStatus = step.status?.toLowerCase() || ''; + const isPendingOrInProgress = stepStatus === 'in_progress'; + const matchesCurrentStep = step.step === currentStep; + + // Step is active only if: + // - Request is active (not rejected/closed) + // - AND it matches the current step + // - AND is pending/in_progress + const isActive = isRequestActive && isPendingOrInProgress && matchesCurrentStep; + const isCompleted = step.status === 'approved'; + + // Find approval data for this step to get SLA information + // First find the corresponding level in approvalFlow to get levelId + const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step); + const approval = stepLevel?.levelId + ? request?.approvals?.find((a: any) => a.levelId === stepLevel.levelId || a.level_id === stepLevel.levelId) + : null; + + // Check if step is paused + const isPaused = approval?.status === 'PAUSED' || + (request?.pauseInfo?.isPaused && + (request?.pauseInfo?.levelId === approval?.levelId || + request?.pauseInfo?.level_id === approval?.levelId)); + + return ( +
+
+ {/* Step Icon */} +
+ {getStepIcon(step.status)}
- {/* Comment Section */} - {step.comment && ( -
-

{step.comment}

-
- )} - - {/* Version History Section */} - {step.versionHistory && step.versionHistory.all && step.versionHistory.all.length > 0 && ( -
- + )} + {/* E-Invoice Download Button (Step 7) */} + {step.step === 7 && step.einvoiceUrl && isCompleted && ( + )}
- {expandedVersionSteps.has(step.step) ? ( - - ) : ( - +

{step.approver}

+

{step.description}

+
+
+

TAT: {formatHoursMinutes(step.tatHours)}

+ {/* Only show elapsed time for active or completed steps, not for waiting steps */} + {step.elapsedHours && (isActive || isCompleted) && ( +

+ Elapsed: {formatHoursMinutes(step.elapsedHours)} +

)} - - - {expandedVersionSteps.has(step.step) && step.versionHistory.all && ( -
- {step.versionHistory.all.map((version: any, vIndex: number) => ( -
0 ? 'pt-2 border-t border-amber-200' : ''}`}> -
-
- - {vIndex === 0 ? 'Current' : 'Previous'}: v{version.version} - - - {formatDateSafe(version.createdAt)} - -
-
-

- {version.changeReason || 'Version Update'} -

-
-
- - {version.changer?.displayName?.charAt(0) || 'U'} - -
- - By {version.changer?.displayName || version.changer?.email || 'Unknown User'} - -
- - {/* Show snapshot data if available - JSONB structure */} - {version.snapshotType === 'PROPOSAL' && version.snapshotData && ( -
-
-
-

Proposal Snapshot

-

- Budget: ₹{Number(version.snapshotData.totalBudget || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -

-
- -
- - {version.snapshotData.comments && ( -

- Comments: {version.snapshotData.comments} -

- )} -
- )} - {version.snapshotType === 'INTERNAL_ORDER' && version.snapshotData && ( -
-

IO Block Snapshot:

-

- IO Number: {version.snapshotData.ioNumber || 'N/A'} -

-

- Blocked Amount: ₹{Number(version.snapshotData.blockedAmount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -

- {version.snapshotData.sapDocumentNumber && ( -

- SAP Doc: {version.snapshotData.sapDocumentNumber} -

- )} -
- )} - {version.snapshotType === 'COMPLETION' && version.snapshotData && ( -
-
-
-

Completion Snapshot

-

- Total: ₹{Number(version.snapshotData.totalExpenses || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -

-
- -
+
+
- {version.snapshotData.comments && ( -

- Comments: {version.snapshotData.comments} -

- )} + {/* Comment Section */} + {step.comment && ( +
+

{step.comment}

+
+ )} + + {/* Version History Section */} + {step.versionHistory && step.versionHistory.all && step.versionHistory.all.length > 0 && ( +
+ + + {expandedVersionSteps.has(step.step) && step.versionHistory.all && ( +
+ {step.versionHistory.all.map((version: any, vIndex: number) => ( +
0 ? 'pt-2 border-t border-amber-200' : ''}`}> +
+
+ + {vIndex === 0 ? 'Current' : 'Previous'}: v{version.version} + + + {formatDateSafe(version.createdAt)} + +
- )} - {version.snapshotType === 'APPROVE' && version.snapshotData && ( -
-

- {version.snapshotData.action === 'APPROVE' ? 'Approval' : 'Rejection'} Snapshot: -

-

- By: {version.snapshotData.approverName || version.snapshotData.approverEmail || 'Unknown'} -

- {version.snapshotData.comments && ( -

- Comments: {version.snapshotData.comments.substring(0, 100)} +

+ {version.changeReason || 'Version Update'} +

+
+
+ + {version.changer?.displayName?.charAt(0) || 'U'} + +
+ + By {version.changer?.displayName || version.changer?.email || 'Unknown User'} + +
+ + {/* Show snapshot data if available - JSONB structure */} + {version.snapshotType === 'PROPOSAL' && version.snapshotData && ( +
+
+
+

Proposal Snapshot

+

+ Budget: ₹{Number(version.snapshotData.totalBudget || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +

+
+ +
+ + {version.snapshotData.comments && ( +

+ Comments: {version.snapshotData.comments} +

+ )} +
+ )} + {version.snapshotType === 'INTERNAL_ORDER' && version.snapshotData && ( +
+

IO Block Snapshot:

+

+ IO Number: {version.snapshotData.ioNumber || 'N/A'} +

+

+ Blocked Amount: ₹{Number(version.snapshotData.blockedAmount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +

+ {version.snapshotData.sapDocumentNumber && ( +

+ SAP Doc: {version.snapshotData.sapDocumentNumber} +

+ )} +
+ )} + {version.snapshotType === 'COMPLETION' && version.snapshotData && ( +
+
+
+

Completion Snapshot

+

+ Total: ₹{Number(version.snapshotData.totalExpenses || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +

+
+ +
+ + {version.snapshotData.comments && ( +

+ Comments: {version.snapshotData.comments} +

+ )} +
+ )} + {version.snapshotType === 'APPROVE' && version.snapshotData && ( +
+

+ {version.snapshotData.action === 'APPROVE' ? 'Approval' : 'Rejection'} Snapshot: +

+

+ By: {version.snapshotData.approverName || version.snapshotData.approverEmail || 'Unknown'} +

+ {version.snapshotData.comments && ( +

+ Comments: {version.snapshotData.comments.substring(0, 100)} + {version.snapshotData.comments.length > 100 ? '...' : ''} +

+ )} + {version.snapshotData.rejectionReason && ( +

+ Rejection Reason: {version.snapshotData.rejectionReason.substring(0, 100)} + {version.snapshotData.rejectionReason.length > 100 ? '...' : ''} +

+ )} +
+ )} + {version.snapshotType === 'WORKFLOW' && version.snapshotData && version.snapshotData.comments && ( +
+

Approval Comment:

+

+ {version.snapshotData.comments.substring(0, 100)} {version.snapshotData.comments.length > 100 ? '...' : ''}

- )} - {version.snapshotData.rejectionReason && ( -

- Rejection Reason: {version.snapshotData.rejectionReason.substring(0, 100)} - {version.snapshotData.rejectionReason.length > 100 ? '...' : ''} -

- )} -
- )} - {version.snapshotType === 'WORKFLOW' && version.snapshotData && version.snapshotData.comments && ( -
-

Approval Comment:

-

- {version.snapshotData.comments.substring(0, 100)} - {version.snapshotData.comments.length > 100 ? '...' : ''} -

-
- )} +
+ )} +
+ ))} +
+ )} +
+ )} + + {/* Active Approver - SLA Time Tracking (Only show for current active step) */} + {isActive && approval?.sla && ( +
+
+ Due by: + + {approval.sla.deadline ? formatDateDDMMYYYY(approval.sla.deadline, true) : 'Not set'} + +
+ + {/* Current Approver - Time Tracking */} +
= 100 ? 'bg-red-50 border-red-200' : + (approval.sla.percentageUsed || 0) >= 75 ? 'bg-orange-50 border-orange-200' : + (approval.sla.percentageUsed || 0) >= 50 ? 'bg-amber-50 border-amber-200' : + 'bg-green-50 border-green-200' + }`}> +

+ + Current Approver - Time Tracking {isPaused && '(Paused)'} +

+ +
+
+ Time elapsed since assigned: + {approval.sla.elapsedText || '0 hours'} +
+
+ Time used: + + {approval.sla.elapsedText || '0 hours'} / {formatHoursMinutes(step.tatHours)} allocated +
- ))} -
- )} -
- )} - - {/* Active Approver - SLA Time Tracking (Only show for current active step) */} - {isActive && approval?.sla && ( -
-
- Due by: - - {approval.sla.deadline ? formatDateDDMMYYYY(approval.sla.deadline, true) : 'Not set'} - -
- - {/* Current Approver - Time Tracking */} -
= 100 ? 'bg-red-50 border-red-200' : - (approval.sla.percentageUsed || 0) >= 75 ? 'bg-orange-50 border-orange-200' : - (approval.sla.percentageUsed || 0) >= 50 ? 'bg-amber-50 border-amber-200' : - 'bg-green-50 border-green-200' - }`}> -

- - Current Approver - Time Tracking {isPaused && '(Paused)'} -

- -
-
- Time elapsed since assigned: - {approval.sla.elapsedText || '0 hours'}
-
- Time used: - - {approval.sla.elapsedText || '0 hours'} / {formatHoursMinutes(step.tatHours)} allocated - + + {/* Progress Bar */} +
+ {(() => { + const percentUsed = approval.sla.percentageUsed || 0; + const getActiveIndicatorColor = () => { + if (isPaused) return 'bg-gray-500'; + if (percentUsed >= 100) return 'bg-red-600'; + if (percentUsed >= 75) return 'bg-orange-500'; + if (percentUsed >= 50) return 'bg-amber-500'; + return 'bg-green-600'; + }; + const getActiveTextColor = () => { + if (isPaused) return 'text-gray-600'; + if (percentUsed >= 100) return 'text-red-600'; + if (percentUsed >= 75) return 'text-orange-600'; + if (percentUsed >= 50) return 'text-amber-600'; + return 'text-green-600'; + }; + return ( + <> + +
+ + Progress: {Math.min(100, percentUsed)}% of TAT used + + + {approval.sla.remainingText || '0 hours'} remaining + +
+ + ); + })()} + + {approval.sla.status === 'breached' && ( +

+ + Deadline Breached +

+ )} + {approval.sla.status === 'critical' && ( +

+ + Approaching Deadline +

+ )}
- - {/* Progress Bar */} -
- {(() => { - const percentUsed = approval.sla.percentageUsed || 0; - const getActiveIndicatorColor = () => { - if (isPaused) return 'bg-gray-500'; - if (percentUsed >= 100) return 'bg-red-600'; - if (percentUsed >= 75) return 'bg-orange-500'; - if (percentUsed >= 50) return 'bg-amber-500'; - return 'bg-green-600'; - }; - const getActiveTextColor = () => { - if (isPaused) return 'text-gray-600'; - if (percentUsed >= 100) return 'text-red-600'; - if (percentUsed >= 75) return 'text-orange-600'; - if (percentUsed >= 50) return 'text-amber-600'; - return 'text-green-600'; - }; - return ( - <> - -
- - Progress: {Math.min(100, percentUsed)}% of TAT used - - - {approval.sla.remainingText || '0 hours'} remaining - -
- - ); - })()} - - {approval.sla.status === 'breached' && ( -

- - Deadline Breached -

- )} - {approval.sla.status === 'critical' && ( -

- - Approaching Deadline -

- )} -
-
- )} + )} - {/* IO Organization Details (Department Lead Approval) - Show when step is approved and has IO details */} - {(() => { - // Check if this is Department Lead Approval step by step title or levelName (handles step shifts) - // Use levelName from the original step data if available, otherwise check title - const stepLevelName = (step as any).levelName || (step as any).level_name; - const isDeptLeadStep = - (stepLevelName && stepLevelName.toLowerCase().includes('department lead')) || - (step.title && step.title.toLowerCase().includes('department lead')); - // Only show when step is approved and has IO details - return isDeptLeadStep && step.status === 'approved' && step.ioDetails && step.ioDetails.ioNumber && ( -
+ {/* IO Organization Details (Department Lead Approval) - Show when step is approved and has IO details */} + {(() => { + // Check if this is Department Lead Approval step by step title or levelName (handles step shifts) + // Use levelName from the original step data if available, otherwise check title + const stepLevelName = (step as any).levelName || (step as any).level_name; + const isDeptLeadStep = + (stepLevelName && stepLevelName.toLowerCase().includes('department lead')) || + (step.title && step.title.toLowerCase().includes('department lead')); + // Only show when step is approved and has IO details + return isDeptLeadStep && step.status === 'approved' && step.ioDetails && step.ioDetails.ioNumber && ( +
+
+ +

+ IO Organisation Details +

+
+
+
+ IO Number: + + {step.ioDetails.ioNumber} + +
+ {step.ioDetails.blockedAmount !== undefined && step.ioDetails.blockedAmount > 0 && ( +
+ Blocked Amount: + + ₹{step.ioDetails.blockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + +
+ )} + {step.ioDetails.remainingBalance !== undefined && step.ioDetails.remainingBalance !== null && ( +
+ Remaining Balance: + + ₹{step.ioDetails.remainingBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + +
+ )} +
+ Organised by {step.ioDetails.organizedBy || step.approver || 'N/A'} on{' '} + {step.ioDetails.organizedAt + ? formatDateSafe(step.ioDetails.organizedAt) + : (step.approvedAt ? formatDateSafe(step.approvedAt) : 'N/A') + } +
+
+
+ ); + })()} + + {/* DMS Processing Details (Step 6) */} + {step.step === 6 && step.dmsDetails && step.dmsDetails.dmsNumber && ( +
- -

- IO Organisation Details + +

+ DMS Processing Details

- IO Number: + DMS Number: - {step.ioDetails.ioNumber} + {step.dmsDetails.dmsNumber}
- {step.ioDetails.blockedAmount !== undefined && step.ioDetails.blockedAmount > 0 && ( -
- Blocked Amount: - - ₹{step.ioDetails.blockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} - + {step.dmsDetails.dmsRemarks && ( +
+

DMS Remarks:

+

{step.dmsDetails.dmsRemarks}

)} - {step.ioDetails.remainingBalance !== undefined && step.ioDetails.remainingBalance !== null && ( -
- Remaining Balance: - - ₹{step.ioDetails.remainingBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} - + {step.dmsDetails.pushedAt && ( +
+ Pushed by {step.dmsDetails.pushedBy} on{' '} + {formatDateSafe(step.dmsDetails.pushedAt)}
)} -
- Organised by {step.ioDetails.organizedBy || step.approver || 'N/A'} on{' '} - {step.ioDetails.organizedAt - ? formatDateSafe(step.ioDetails.organizedAt) - : (step.approvedAt ? formatDateSafe(step.approvedAt) : 'N/A') - } -
- ); - })()} + )} - {/* DMS Processing Details (Step 6) */} - {step.step === 6 && step.dmsDetails && step.dmsDetails.dmsNumber && ( -
-
- -

- DMS Processing Details -

-
-
-
- DMS Number: - - {step.dmsDetails.dmsNumber} - -
- {step.dmsDetails.dmsRemarks && ( -
-

DMS Remarks:

-

{step.dmsDetails.dmsRemarks}

-
- )} - {step.dmsDetails.pushedAt && ( -
- Pushed by {step.dmsDetails.pushedBy} on{' '} - {formatDateSafe(step.dmsDetails.pushedAt)} -
- )} -
-
- )} - - {/* Action Buttons */} - {/* Only show action buttons if: + {/* Action Buttons */} + {/* Only show action buttons if: 1. Step is active (pending/in_progress and matches currentStep) 2. AND current user is the approver for this step (or is dealer for dealer steps) */} - {(() => { - // Find the step level from approvalFlow to verify user is the approver - const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step); - const stepApproverEmail = (stepLevel?.approverEmail || '').toLowerCase(); - const isUserApproverForThisStep = stepApproverEmail && userEmail === stepApproverEmail; - - // For dealer steps (1 and 5), also check if user is dealer - const isDealerStep = step.step === 1 || - (stepLevel?.levelName && stepLevel.levelName.toLowerCase().includes('dealer')); - const isUserAuthorized = isUserApproverForThisStep || (isDealerStep && isDealer); - - // Step must be active AND user must be authorized - return isActive && isUserAuthorized; - })() && ( -
- {/* Step 1: Submit Proposal Button - Only for dealer or step 1 approver */} - {step.step === 1 && (isDealer || isStep1Approver) && ( - - )} + {(() => { + // Find the step level from approvalFlow to verify user is the approver + const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step); + const stepApproverEmail = (stepLevel?.approverEmail || '').toLowerCase(); + const isUserApproverForThisStep = stepApproverEmail && userEmail === stepApproverEmail; - {/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */} - {/* Use initiatorStepNumber to handle cases where approvers are added between steps */} - {/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */} - {/* Use initiatorStepNumber to handle cases where approvers are added between steps */} - {step.step === initiatorStepNumber && (isInitiator || isStep2Approver) && ( - - )} + // For dealer steps (1 and 5), also check if user is dealer + const isDealerStep = step.step === 1 || + (stepLevel?.levelName && stepLevel.levelName.toLowerCase().includes('dealer')); + const isUserAuthorized = isUserApproverForThisStep || (isDealerStep && isDealer); - {/* Initiator Action Step: Show action buttons (REVISE, REOPEN) - Direct actions, no modal */} - {(() => { - // Find the step level from approvalFlow - const stepLevelForInitiator = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step); - const stepLevelName = (stepLevelForInitiator?.levelName || step.title || '').toLowerCase(); - const isInitiatorActionStep = stepLevelName.includes('initiator action'); - const isUserInitiator = isInitiator || (userEmail === initiatorEmail); - - if (!isInitiatorActionStep || !isUserInitiator) return null; - - const handleDirectAction = async (action: 'REVISE' | 'REOPEN') => { - try { - if (!request?.id && !request?.requestId) { - throw new Error('Request ID not found'); - } - const requestId = request.requestId || request.id; - - // Call action directly without modal - comments are optional - await handleInitiatorAction(requestId, action, { reason: '' }); - toast.success(`Action "${action === 'REVISE' ? 'Revision Requested' : 'Request Reopened'}" performed successfully`); - handleRefresh(); - } catch (error: any) { - console.error('Failed to perform initiator action:', error); - const errorMessage = error?.response?.data?.message || error?.message || 'Failed to perform action'; - toast.error(errorMessage); - } - }; - - return ( -
+ // Step must be active AND user must be authorized + return isActive && isUserAuthorized; + })() && ( +
+ {/* Step 1: Submit Proposal Button - Only for dealer or step 1 approver */} + {step.step === 1 && (isDealer || isStep1Approver) && ( - -
- ); - })()} - - {/* Department Lead Step: Approve and Organise IO - Find dynamically by levelName */} - {(() => { - // Find Department Lead step dynamically (handles step shifts) - const deptLeadStepLevel = approvalFlow.find((l: any) => { - const levelName = (l.levelName || '').toLowerCase(); - return levelName.includes('department lead'); - }); - - // Check if this is the Department Lead step - const isDeptLeadStep = deptLeadStepLevel && - (step.step === (deptLeadStepLevel.step || deptLeadStepLevel.levelNumber || deptLeadStepLevel.level_number)); - - if (!isDeptLeadStep) return null; - - // Check if user is the Department Lead approver - const deptLeadApproverEmail = (deptLeadStepLevel?.approverEmail || '').toLowerCase(); - const isDeptLeadApprover = deptLeadApproverEmail && userEmail === deptLeadApproverEmail; - - if (!(isDeptLeadApprover || isStep3Approver || isCurrentApprover)) return null; - - // Check if IO number is available (same way as IO tab and modal) - const internalOrder = request?.internalOrder || request?.internal_order; - const ioNumber = internalOrder?.ioNumber || internalOrder?.io_number || request?.ioNumber || ''; - const hasIONumber = ioNumber && ioNumber.trim() !== ''; - - return ( -
- + )} + + {/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */} + {/* Use initiatorStepNumber to handle cases where approvers are added between steps */} + {/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */} + {/* Use initiatorStepNumber to handle cases where approvers are added between steps */} + {step.step === initiatorStepNumber && (isInitiator || isStep2Approver) && ( + - {!hasIONumber && ( -

- Please add an IO number in the IO tab before approving this step. -

- )} -
- ); - })()} + )} - {/* Step 5 (or shifted step): Upload Completion Documents - Only for dealer */} - {/* Check if dealer is the approver for this step (handles step shifts) */} - {(() => { - // Find the step level from approvalFlow to verify dealer is the approver - const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step); - const stepApproverEmail = (stepLevel?.approverEmail || '').toLowerCase(); - // Check if dealer is the approver for this step - const isDealerForThisStep = isDealer && stepApproverEmail === dealerEmail; - // Check if this is the Dealer Completion Documents step - // by checking if the levelName contains "Dealer Completion" or "Completion Documents" - const levelName = (stepLevel?.levelName || step.title || '').toLowerCase(); - const isDealerCompletionStep = levelName.includes('dealer completion') || - levelName.includes('completion documents'); - return isDealerForThisStep && isDealerCompletionStep; - })() && ( - - )} + {/* Initiator Action Step: Show action buttons (REVISE, REOPEN) - Direct actions, no modal */} + {(() => { + // Find the step level from approvalFlow + const stepLevelForInitiator = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step); + const stepLevelName = (stepLevelForInitiator?.levelName || step.title || '').toLowerCase(); + const isInitiatorActionStep = stepLevelName.includes('initiator action'); + const isUserInitiator = isInitiator || (userEmail === initiatorEmail); - {/* Requestor Claim Approval: Push to DMS - Find dynamically by levelName (handles step shifts) */} - {(() => { - // Find Requestor Claim Approval step dynamically (handles step shifts) - const requestorClaimStepLevel = approvalFlow.find((l: any) => { - const levelName = (l.levelName || '').toLowerCase(); - return levelName.includes('requestor claim') || levelName.includes('requestor - claim'); - }); - - // Check if this is the Requestor Claim Approval step - const isRequestorClaimStep = requestorClaimStepLevel && - (step.step === (requestorClaimStepLevel.step || requestorClaimStepLevel.levelNumber || requestorClaimStepLevel.level_number)); - - if (!isRequestorClaimStep) return null; - - // Check if user is the initiator or the Requestor Claim Approval approver - const requestorClaimApproverEmail = (requestorClaimStepLevel?.approverEmail || '').toLowerCase(); - const isRequestorClaimApprover = requestorClaimApproverEmail && userEmail === requestorClaimApproverEmail; - - if (!(isInitiator || isRequestorClaimApprover)) return null; - - return ( - - ); - })()} + if (!isInitiatorActionStep || !isUserInitiator) return null; - {/* 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 step8ApproverEmail = (step8Level?.approverEmail || '').toLowerCase(); - const isStep8Approver = step8ApproverEmail && userEmail === step8ApproverEmail; - // Also check if user has finance role - const userRole = (user as any)?.role?.toUpperCase() || ''; - const isFinanceUser = userRole === 'FINANCE' || userRole === 'ADMIN'; - return isStep8Approver || isFinanceUser; - })() && ( - - )} + const handleDirectAction = async (action: 'REVISE' | 'REOPEN') => { + try { + if (!request?.id && !request?.requestId) { + throw new Error('Request ID not found'); + } + const requestId = request.requestId || request.id; - {/* Additional Approvers: Show approve/reject buttons for steps that don't have specific workflow actions */} - {(() => { - // Check if this is an additional approver step (not one of the fixed workflow steps) - const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step); - const levelName = (stepLevel?.levelName || step.title || '').toLowerCase(); - const isAdditionalApprover = levelName.includes('additional approver'); - - // Check if this step doesn't have any of the specific workflow action buttons above - const hasSpecificWorkflowAction = - step.step === 1 || - step.step === initiatorStepNumber || - (() => { + // Call action directly without modal - comments are optional + await handleInitiatorAction(requestId, action, { reason: '' }); + toast.success(`Action "${action === 'REVISE' ? 'Revision Requested' : 'Request Reopened'}" performed successfully`); + handleRefresh(); + } catch (error: any) { + console.error('Failed to perform initiator action:', error); + const errorMessage = error?.response?.data?.message || error?.message || 'Failed to perform action'; + toast.error(errorMessage); + } + }; + + return ( +
+ + +
+ ); + })()} + + {/* Department Lead Step: Approve and Organise IO - Find dynamically by levelName */} + {(() => { + // Find Department Lead step dynamically (handles step shifts) const deptLeadStepLevel = approvalFlow.find((l: any) => { - const ln = (l.levelName || '').toLowerCase(); - return ln.includes('department lead'); + const levelName = (l.levelName || '').toLowerCase(); + return levelName.includes('department lead'); }); - return deptLeadStepLevel && + + // Check if this is the Department Lead step + const isDeptLeadStep = deptLeadStepLevel && (step.step === (deptLeadStepLevel.step || deptLeadStepLevel.levelNumber || deptLeadStepLevel.level_number)); - })() || - (() => { + + if (!isDeptLeadStep) return null; + + // Check if user is the Department Lead approver + const deptLeadApproverEmail = (deptLeadStepLevel?.approverEmail || '').toLowerCase(); + const isDeptLeadApprover = deptLeadApproverEmail && userEmail === deptLeadApproverEmail; + + if (!(isDeptLeadApprover || isStep3Approver || isCurrentApprover)) return null; + + // Check if IO number is available (same way as IO tab and modal) + const internalOrder = request?.internalOrder || request?.internal_order; + const ioNumber = internalOrder?.ioNumber || internalOrder?.io_number || request?.ioNumber || ''; + const hasIONumber = ioNumber && ioNumber.trim() !== ''; + + return ( +
+ + {!hasIONumber && ( +

+ Please add an IO number in the IO tab before approving this step. +

+ )} +
+ ); + })()} + + {/* Step 5 (or shifted step): Upload Completion Documents - Only for dealer */} + {/* Check if dealer is the approver for this step (handles step shifts) */} + {(() => { + // Find the step level from approvalFlow to verify dealer is the approver const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step); const stepApproverEmail = (stepLevel?.approverEmail || '').toLowerCase(); + // Check if dealer is the approver for this step const isDealerForThisStep = isDealer && stepApproverEmail === dealerEmail; - const ln = (stepLevel?.levelName || step.title || '').toLowerCase(); - const isDealerCompletionStep = ln.includes('dealer completion') || ln.includes('completion documents'); + // Check if this is the Dealer Completion Documents step + // by checking if the levelName contains "Dealer Completion" or "Completion Documents" + const levelName = (stepLevel?.levelName || step.title || '').toLowerCase(); + const isDealerCompletionStep = levelName.includes('dealer completion') || + levelName.includes('completion documents'); return isDealerForThisStep && isDealerCompletionStep; - })() || - (() => { + })() && ( + + )} + + {/* Requestor Claim Approval: Push to DMS - Find dynamically by levelName (handles step shifts) */} + {(() => { + // Find Requestor Claim Approval step dynamically (handles step shifts) const requestorClaimStepLevel = approvalFlow.find((l: any) => { - const ln = (l.levelName || '').toLowerCase(); - return ln.includes('requestor claim') || ln.includes('requestor - claim'); + const levelName = (l.levelName || '').toLowerCase(); + return levelName.includes('requestor claim') || levelName.includes('requestor - claim'); }); - return requestorClaimStepLevel && + + // Check if this is the Requestor Claim Approval step + const isRequestorClaimStep = 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 - // Similar to the requestor approval step - if (isAdditionalApprover || !hasSpecificWorkflowAction) { - const levelId = stepLevel?.levelId || stepLevel?.level_id; - - // Show review modal with both approve and reject options - if (levelId) { - const levelName = stepLevel?.levelName || stepLevel?.level_name || step.title || 'Approval Level'; - const approverName = stepLevel?.approverName || stepLevel?.approver_name || step.approver || 'Approver'; + + if (!isRequestorClaimStep) return null; + + // Check if user is the initiator or the Requestor Claim Approval approver + const requestorClaimApproverEmail = (requestorClaimStepLevel?.approverEmail || '').toLowerCase(); + const isRequestorClaimApprover = requestorClaimApproverEmail && userEmail === requestorClaimApproverEmail; + + if (!(isInitiator || isRequestorClaimApprover)) return null; + return ( ); - } - - // Fallback: Direct API call if levelId not available (shouldn't happen in normal flow) - return ( - <> + })()} + + {/* 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 step8ApproverEmail = (step8Level?.approverEmail || '').toLowerCase(); + const isStep8Approver = step8ApproverEmail && userEmail === step8ApproverEmail; + // Also check if user has finance role + const userRole = (user as any)?.role?.toUpperCase() || ''; + const isFinanceUser = userRole === 'FINANCE' || userRole === 'ADMIN'; + return isStep8Approver || isFinanceUser; + })() && ( - - - ); - } - return null; - })()} -
- )} + )} - {/* Approved Date */} - {step.approvedAt && ( -

- Approved on {formatDateSafe(step.approvedAt)} -

- )} -
-
-
- ); - })} -
- - + {/* Additional Approvers: Show approve/reject buttons for steps that don't have specific workflow actions */} + {(() => { + // Check if this is an additional approver step (not one of the fixed workflow steps) + const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step); + const levelName = (stepLevel?.levelName || step.title || '').toLowerCase(); + const isAdditionalApprover = levelName.includes('additional approver'); - {/* Dealer Proposal Submission Modal */} - setShowProposalModal(false)} - onSubmit={handleProposalSubmit} - dealerName={dealerName} - activityName={activityName} - requestId={request?.id || request?.requestId} - previousProposalData={versionHistory?.find(v => v.snapshotType === 'PROPOSAL')?.snapshotData} - documentPolicy={documentPolicy} - /> + // Check if this step doesn't have any of the specific workflow action buttons above + const hasSpecificWorkflowAction = + step.step === 1 || + step.step === initiatorStepNumber || + (() => { + const deptLeadStepLevel = approvalFlow.find((l: any) => { + const ln = (l.levelName || '').toLowerCase(); + return ln.includes('department lead'); + }); + return deptLeadStepLevel && + (step.step === (deptLeadStepLevel.step || deptLeadStepLevel.levelNumber || deptLeadStepLevel.level_number)); + })() || + (() => { + 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; - {/* Initiator Proposal Approval Modal */} - { - setShowApprovalModal(false); - }} - onApprove={handleProposalApprove} - onReject={handleProposalReject} - onRequestRevision={handleProposalRevision} - proposalData={proposalData} - dealerName={dealerName} - activityName={activityName} - requestId={request?.id || request?.requestId} - request={request} - previousProposalData={(() => { - const proposalSnapshots = versionHistory?.filter(v => v.snapshotType === 'PROPOSAL') || []; - // Since history is sorted descending (most recent first): - // proposalSnapshots[0] is the current proposal being reviewed - // proposalSnapshots[1] is the previous proposal (last iteration - 1) - return proposalSnapshots.length > 1 ? proposalSnapshots[1].snapshotData : null; - })()} - /> + // Show "Review Request" button for additional approvers or steps without specific workflow actions + // Similar to the requestor approval step + if (isAdditionalApprover || !hasSpecificWorkflowAction) { + const levelId = stepLevel?.levelId || stepLevel?.level_id; - {/* Dept Lead IO Approval Modal */} - setShowIOApprovalModal(false)} - onApprove={handleIOApproval} - onReject={handleIORejection} - requestTitle={request?.title} - requestId={request?.id || request?.requestId} - preFilledIONumber={request?.internalOrder?.ioNumber || request?.internalOrder?.io_number || request?.internal_order?.ioNumber || request?.internal_order?.io_number || undefined} - preFilledBlockedAmount={request?.internalOrder?.ioBlockedAmount || request?.internalOrder?.io_blocked_amount || request?.internal_order?.ioBlockedAmount || request?.internal_order?.io_blocked_amount || undefined} - preFilledRemainingBalance={request?.internalOrder?.ioRemainingBalance || request?.internalOrder?.io_remaining_balance || request?.internal_order?.ioRemainingBalance || request?.internal_order?.io_remaining_balance || undefined} - /> + // Show review modal with both approve and reject options + if (levelId) { + const levelName = stepLevel?.levelName || stepLevel?.level_name || step.title || 'Approval Level'; + const approverName = stepLevel?.approverName || stepLevel?.approver_name || step.approver || 'Approver'; + return ( + + ); + } - {/* Dealer Completion Documents Modal */} - setShowCompletionModal(false)} - onSubmit={handleCompletionSubmit} - dealerName={dealerName} - activityName={activityName} - requestId={request?.id || request?.requestId} - documentPolicy={documentPolicy} - /> - - {/* DMS Push Modal */} - setShowDMSPushModal(false)} - onPush={handleDMSPush} - completionDetails={{ - activityCompletionDate: request?.completionDetails?.activityCompletionDate || request?.completionDetails?.activity_completion_date, - numberOfParticipants: request?.completionDetails?.numberOfParticipants || request?.completionDetails?.number_of_participants, - closedExpenses: request?.completionExpenses || request?.completion_expenses || request?.completionDetails?.closedExpenses || request?.completionDetails?.closed_expenses, - totalClosedExpenses: request?.budgetTracking?.closedExpenses || request?.budgetTracking?.closed_expenses || request?.completionDetails?.totalClosedExpenses || request?.completionDetails?.total_closed_expenses, - completionDescription: request?.completionDetails?.completionDescription || request?.completionDetails?.completion_description, - }} - ioDetails={{ - ioNumber: request?.internalOrder?.ioNumber || request?.internalOrder?.io_number || request?.internal_order?.ioNumber || request?.internal_order?.io_number, - blockedAmount: request?.internalOrder?.ioBlockedAmount || request?.internalOrder?.io_blocked_amount || request?.internal_order?.ioBlockedAmount || request?.internal_order?.io_blocked_amount, - availableBalance: request?.internalOrder?.ioAvailableBalance || request?.internalOrder?.io_available_balance || request?.internal_order?.ioAvailableBalance || request?.internal_order?.io_available_balance, - remainingBalance: request?.internalOrder?.ioRemainingBalance || request?.internalOrder?.io_remaining_balance || request?.internal_order?.ioRemainingBalance || request?.internal_order?.io_remaining_balance, - }} - completionDocuments={completionDocumentsData} - requestTitle={request?.title} - requestNumber={request?.requestNumber || request?.request_number || request?.id} - /> - - {/* Credit Note from SAP Modal (Step 8) */} - setShowCreditNoteModal(false)} - onDownload={async () => { - // TODO: Implement download functionality - toast.info('Download functionality will be implemented'); - }} - onSendToDealer={async () => { - try { - const requestId = request?.requestId || request?.id; - if (!requestId) { - toast.error('Request ID not found'); - return; - } - - await sendCreditNoteToDealer(requestId); - - toast.success('Credit note sent to dealer successfully. Step 8 has been approved.'); - - // Refresh the request details to show updated status - if (onRefresh) { - onRefresh(); - } - } catch (error: any) { - console.error('Failed to send credit note to dealer:', error); - const errorMessage = error?.response?.data?.message || error?.message || 'Failed to send credit note to dealer'; - toast.error(errorMessage); - } - }} - creditNoteData={{ - creditNoteNumber: (request as any)?.creditNote?.creditNoteNumber || - (request as any)?.creditNote?.credit_note_number || - (request as any)?.claimDetails?.creditNote?.creditNoteNumber || - (request as any)?.claimDetails?.creditNoteNumber || - (request as any)?.claimDetails?.credit_note_number, - creditNoteDate: (request as any)?.creditNote?.creditNoteDate || - (request as any)?.creditNote?.credit_note_date || - (request as any)?.claimDetails?.creditNote?.creditNoteDate || - (request as any)?.claimDetails?.creditNoteDate || - (request as any)?.claimDetails?.credit_note_date, - creditNoteAmount: (request as any)?.creditNote?.creditNoteAmount ? - Number((request as any)?.creditNote?.creditNoteAmount) : - ((request as any)?.creditNote?.credit_note_amount ? - Number((request as any)?.creditNote?.credit_note_amount) : - ((request as any)?.claimDetails?.creditNote?.creditNoteAmount ? - Number((request as any)?.claimDetails?.creditNote?.creditNoteAmount) : - ((request as any)?.claimDetails?.creditNoteAmount ? - Number((request as any)?.claimDetails?.creditNoteAmount) : - ((request as any)?.claimDetails?.credit_note_amount ? - Number((request as any)?.claimDetails?.credit_note_amount) : undefined)))), - status: (request as any)?.creditNote?.status || - (request as any)?.claimDetails?.creditNote?.status || - ((request as any)?.creditNote?.creditNoteNumber ? 'CONFIRMED' : 'PENDING'), - }} - dealerInfo={{ - dealerName: (request as any)?.claimDetails?.dealerName || (request as any)?.claimDetails?.dealer_name, - dealerCode: (request as any)?.claimDetails?.dealerCode || (request as any)?.claimDetails?.dealer_code, - dealerEmail: (request as any)?.claimDetails?.dealerEmail || (request as any)?.claimDetails?.dealer_email, - }} - activityName={(request as any)?.claimDetails?.activityName || (request as any)?.claimDetails?.activity_name} - requestNumber={request?.requestNumber || request?.id} - requestId={request?.requestId || request?.id} - dueDate={request?.dueDate} - /> - - {/* Email Notification Template Modal */} - { - setShowEmailTemplateModal(false); - setSelectedStepForEmail(null); - }} - stepNumber={selectedStepForEmail?.stepNumber || 4} - stepName={selectedStepForEmail?.stepName || 'Activity Creation'} - requestNumber={request?.requestNumber || request?.id || request?.request_number} - recipientEmail="system@royalenfield.com" - /> - - {/* Additional Approver Review Modal */} - {selectedLevelForReview && ( - { - setShowAdditionalApproverReviewModal(false); - setSelectedLevelForReview(null); - }} - onApprove={async (comments: string) => { - try { - if (!request?.id && !request?.requestId) { - throw new Error('Request ID not found'); - } - const requestId = request.id || request.requestId; - const levelId = selectedLevelForReview.levelId; - if (!levelId) { - toast.error('Approval level not found'); - return; - } - await approveLevel(requestId, levelId, comments); - toast.success('Request approved successfully'); - handleRefresh(); - setShowAdditionalApproverReviewModal(false); - setSelectedLevelForReview(null); - } catch (error: any) { - console.error('Failed to approve:', error); - const errorMessage = error?.response?.data?.message || error?.message || 'Failed to approve request. Please try again.'; - toast.error(errorMessage); - throw error; - } - }} - onReject={async (comments: string) => { - try { - if (!request?.id && !request?.requestId) { - throw new Error('Request ID not found'); - } - const requestId = request.id || request.requestId; - const levelId = selectedLevelForReview.levelId; - if (!levelId) { - toast.error('Approval level not found'); - return; - } - await rejectLevel(requestId, levelId, 'Request rejected', comments); - toast.success('Request rejected successfully'); - handleRefresh(); - setShowAdditionalApproverReviewModal(false); - setSelectedLevelForReview(null); - } catch (error: any) { - console.error('Failed to reject:', error); - const errorMessage = error?.response?.data?.message || error?.message || 'Failed to reject request. Please try again.'; - toast.error(errorMessage); - throw error; - } - }} - requestTitle={request?.title || 'Request'} - requestDescription={request?.description || ''} - requestId={request?.id || request?.requestId} - levelName={selectedLevelForReview.levelName} - approverName={selectedLevelForReview.approverName} - /> - )} - - {/* Initiator Action Modal - Removed, actions are now direct buttons in step card */} - - {/* Version History Section */} - {versionHistory && versionHistory.length > 0 && ( - - -
- - - Revision History & Audit Trail - - -
- - Records of all revisions and actions taken on this request - -
- {showHistory && ( - -
- {versionHistory.map((item, idx) => ( -
-
-
-
-
- - Version {item.version} - - {item.snapshotType && ( - - {item.snapshotType} - - )} - {item.levelNumber && ( - - Step {item.levelNumber} - - )} -
- - {formatDateSafe(item.createdAt)} - -
-

- {item.changeReason || 'Version Update'} -

-
-
- - {item.changer?.displayName?.charAt(0) || 'U'} - -
- - By {item.changer?.displayName || item.changer?.email || 'Unknown User'} - -
- {/* Show snapshot details based on type - JSONB structure */} - {item.snapshotType === 'PROPOSAL' && item.snapshotData && ( -
-

Proposal:

- {item.snapshotData.documentUrl && ( -
- + // Fallback: Direct API call if levelId not available (shouldn't happen in normal flow) + return ( + <> + + + + ); + } + return null; + })()}
)} -

Budget: ₹{Number(item.snapshotData.totalBudget || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

- {item.snapshotData.comments && ( -

Comments: {item.snapshotData.comments.substring(0, 80)}{item.snapshotData.comments.length > 80 ? '...' : ''}

- )} -
- )} - {item.snapshotType === 'COMPLETION' && item.snapshotData && ( -
-

Completion:

- {item.snapshotData.documentUrl && ( -
- -
- )} -

Total Expenses: ₹{Number(item.snapshotData.totalExpenses || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

- {item.snapshotData.comments && ( -

Comments: {item.snapshotData.comments.substring(0, 80)}{item.snapshotData.comments.length > 80 ? '...' : ''}

- )} -
- )} - {item.snapshotType === 'INTERNAL_ORDER' && item.snapshotData && ( -
-

IO Block:

-

IO Number: {item.snapshotData.ioNumber || 'N/A'}

-

Blocked: ₹{Number(item.snapshotData.blockedAmount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

- {item.snapshotData.sapDocumentNumber && ( -

SAP Doc: {item.snapshotData.sapDocumentNumber}

- )} -
- )} - {item.snapshotType === 'APPROVE' && item.snapshotData && ( -
-

- {item.snapshotData.action === 'APPROVE' ? 'Approval' : 'Rejection'}: + + {/* Approved Date */} + {step.approvedAt && ( +

+ Approved on {formatDateSafe(step.approvedAt)}

-

By: {item.snapshotData.approverName || item.snapshotData.approverEmail || 'Unknown'}

- {item.snapshotData.levelName && ( -

Level: {item.snapshotData.levelName}

- )} - {item.snapshotData.comments && ( -

Comments: {item.snapshotData.comments.substring(0, 80)}{item.snapshotData.comments.length > 80 ? '...' : ''}

- )} - {item.snapshotData.rejectionReason && ( -

Rejection Reason: {item.snapshotData.rejectionReason.substring(0, 80)}{item.snapshotData.rejectionReason.length > 80 ? '...' : ''}

- )} -
- )} - {item.snapshotType === 'WORKFLOW' && item.snapshotData && ( -
-

Workflow:

-

Status: {item.snapshotData.status || 'N/A'}

- {item.snapshotData.currentLevel && ( -

Current Level: {item.snapshotData.currentLevel}

- )} -
- )} + )} +
- ))} -
-
- )} + ); + })} +
+ - )} - - setViewSnapshot(null)} - snapshot={viewSnapshot?.data} - type={viewSnapshot?.type || 'PROPOSAL'} - title={viewSnapshot?.title} - /> + + {/* Dealer Proposal Submission Modal */} + setShowProposalModal(false)} + onSubmit={handleProposalSubmit} + dealerName={dealerName} + activityName={activityName} + requestId={request?.id || request?.requestId} + previousProposalData={versionHistory?.find(v => v.snapshotType === 'PROPOSAL')?.snapshotData} + documentPolicy={documentPolicy} + /> + + {/* Initiator Proposal Approval Modal */} + { + setShowApprovalModal(false); + }} + onApprove={handleProposalApprove} + onReject={handleProposalReject} + onRequestRevision={handleProposalRevision} + proposalData={proposalData} + dealerName={dealerName} + activityName={activityName} + requestId={request?.id || request?.requestId} + request={request} + previousProposalData={(() => { + const proposalSnapshots = versionHistory?.filter(v => v.snapshotType === 'PROPOSAL') || []; + // Since history is sorted descending (most recent first): + // proposalSnapshots[0] is the current proposal being reviewed + // proposalSnapshots[1] is the previous proposal (last iteration - 1) + return proposalSnapshots.length > 1 ? proposalSnapshots[1].snapshotData : null; + })()} + /> + + {/* Dept Lead IO Approval Modal */} + setShowIOApprovalModal(false)} + onApprove={handleIOApproval} + onReject={handleIORejection} + requestTitle={request?.title} + requestId={request?.id || request?.requestId} + preFilledIONumber={request?.internalOrder?.ioNumber || request?.internalOrder?.io_number || request?.internal_order?.ioNumber || request?.internal_order?.io_number || undefined} + preFilledBlockedAmount={request?.internalOrder?.ioBlockedAmount || request?.internalOrder?.io_blocked_amount || request?.internal_order?.ioBlockedAmount || request?.internal_order?.io_blocked_amount || undefined} + preFilledRemainingBalance={request?.internalOrder?.ioRemainingBalance || request?.internalOrder?.io_remaining_balance || request?.internal_order?.ioRemainingBalance || request?.internal_order?.io_remaining_balance || undefined} + /> + + {/* Dealer Completion Documents Modal */} + setShowCompletionModal(false)} + onSubmit={handleCompletionSubmit} + dealerName={dealerName} + activityName={activityName} + requestId={request?.id || request?.requestId} + documentPolicy={documentPolicy} + /> + + {/* DMS Push Modal */} + setShowDMSPushModal(false)} + onPush={handleDMSPush} + completionDetails={{ + activityCompletionDate: request?.completionDetails?.activityCompletionDate || request?.completionDetails?.activity_completion_date, + numberOfParticipants: request?.completionDetails?.numberOfParticipants || request?.completionDetails?.number_of_participants, + closedExpenses: request?.completionExpenses || request?.completion_expenses || request?.completionDetails?.closedExpenses || request?.completionDetails?.closed_expenses, + totalClosedExpenses: request?.budgetTracking?.closedExpenses || request?.budgetTracking?.closed_expenses || request?.completionDetails?.totalClosedExpenses || request?.completionDetails?.total_closed_expenses, + completionDescription: request?.completionDetails?.completionDescription || request?.completionDetails?.completion_description, + }} + ioDetails={{ + ioNumber: request?.internalOrder?.ioNumber || request?.internalOrder?.io_number || request?.internal_order?.ioNumber || request?.internal_order?.io_number, + blockedAmount: request?.internalOrder?.ioBlockedAmount || request?.internalOrder?.io_blocked_amount || request?.internal_order?.ioBlockedAmount || request?.internal_order?.io_blocked_amount, + availableBalance: request?.internalOrder?.ioAvailableBalance || request?.internalOrder?.io_available_balance || request?.internal_order?.ioAvailableBalance || request?.internal_order?.io_available_balance, + remainingBalance: request?.internalOrder?.ioRemainingBalance || request?.internalOrder?.io_remaining_balance || request?.internal_order?.ioRemainingBalance || request?.internal_order?.io_remaining_balance, + }} + completionDocuments={completionDocumentsData} + requestTitle={request?.title} + requestNumber={request?.requestNumber || request?.request_number || request?.id} + /> + + {/* Credit Note from SAP Modal (Step 8) */} + setShowCreditNoteModal(false)} + onDownload={async () => { + toast.info('Download functionality will be implemented'); + }} + onSendToDealer={async () => { + try { + const requestId = request?.requestId || request?.id; + if (!requestId) { + toast.error('Request ID not found'); + return; + } + + await sendCreditNoteToDealer(requestId); + + toast.success('Credit note sent to dealer successfully. Step 8 has been approved.'); + + // Refresh the request details to show updated status + if (onRefresh) { + onRefresh(); + } + } catch (error: any) { + console.error('Failed to send credit note to dealer:', error); + const errorMessage = error?.response?.data?.message || error?.message || 'Failed to send credit note to dealer'; + toast.error(errorMessage); + } + }} + creditNoteData={{ + creditNoteNumber: (request as any)?.creditNote?.creditNoteNumber || + (request as any)?.creditNote?.credit_note_number || + (request as any)?.claimDetails?.creditNote?.creditNoteNumber || + (request as any)?.claimDetails?.creditNoteNumber || + (request as any)?.claimDetails?.credit_note_number, + creditNoteDate: (request as any)?.creditNote?.creditNoteDate || + (request as any)?.creditNote?.credit_note_date || + (request as any)?.claimDetails?.creditNote?.creditNoteDate || + (request as any)?.claimDetails?.creditNoteDate || + (request as any)?.claimDetails?.credit_note_date, + creditNoteAmount: (request as any)?.creditNote?.creditNoteAmount ? + Number((request as any)?.creditNote?.creditNoteAmount) : + ((request as any)?.creditNote?.credit_note_amount ? + Number((request as any)?.creditNote?.credit_note_amount) : + ((request as any)?.claimDetails?.creditNote?.creditNoteAmount ? + Number((request as any)?.claimDetails?.creditNote?.creditNoteAmount) : + ((request as any)?.claimDetails?.creditNoteAmount ? + Number((request as any)?.claimDetails?.creditNoteAmount) : + ((request as any)?.claimDetails?.credit_note_amount ? + Number((request as any)?.claimDetails?.credit_note_amount) : undefined)))), + status: (request as any)?.creditNote?.status || + (request as any)?.claimDetails?.creditNote?.status || + ((request as any)?.creditNote?.creditNoteNumber ? 'CONFIRMED' : 'PENDING'), + }} + dealerInfo={{ + dealerName: (request as any)?.claimDetails?.dealerName || (request as any)?.claimDetails?.dealer_name, + dealerCode: (request as any)?.claimDetails?.dealerCode || (request as any)?.claimDetails?.dealer_code, + dealerEmail: (request as any)?.claimDetails?.dealerEmail || (request as any)?.claimDetails?.dealer_email, + }} + activityName={(request as any)?.claimDetails?.activityName || (request as any)?.claimDetails?.activity_name} + requestNumber={request?.requestNumber || request?.id} + requestId={request?.requestId || request?.id} + dueDate={request?.dueDate} + /> + + {/* Email Notification Template Modal */} + { + setShowEmailTemplateModal(false); + setSelectedStepForEmail(null); + }} + stepNumber={selectedStepForEmail?.stepNumber || 4} + stepName={selectedStepForEmail?.stepName || 'Activity Creation'} + requestNumber={request?.requestNumber || request?.id || request?.request_number} + recipientEmail="system@royalenfield.com" + /> + + {/* Additional Approver Review Modal */} + {selectedLevelForReview && ( + { + setShowAdditionalApproverReviewModal(false); + setSelectedLevelForReview(null); + }} + onApprove={async (comments: string) => { + try { + if (!request?.id && !request?.requestId) { + throw new Error('Request ID not found'); + } + const requestId = request.id || request.requestId; + const levelId = selectedLevelForReview.levelId; + if (!levelId) { + toast.error('Approval level not found'); + return; + } + await approveLevel(requestId, levelId, comments); + toast.success('Request approved successfully'); + handleRefresh(); + setShowAdditionalApproverReviewModal(false); + setSelectedLevelForReview(null); + } catch (error: any) { + console.error('Failed to approve:', error); + const errorMessage = error?.response?.data?.message || error?.message || 'Failed to approve request. Please try again.'; + toast.error(errorMessage); + throw error; + } + }} + onReject={async (comments: string) => { + try { + if (!request?.id && !request?.requestId) { + throw new Error('Request ID not found'); + } + const requestId = request.id || request.requestId; + const levelId = selectedLevelForReview.levelId; + if (!levelId) { + toast.error('Approval level not found'); + return; + } + await rejectLevel(requestId, levelId, 'Request rejected', comments); + toast.success('Request rejected successfully'); + handleRefresh(); + setShowAdditionalApproverReviewModal(false); + setSelectedLevelForReview(null); + } catch (error: any) { + console.error('Failed to reject:', error); + const errorMessage = error?.response?.data?.message || error?.message || 'Failed to reject request. Please try again.'; + toast.error(errorMessage); + throw error; + } + }} + requestTitle={request?.title || 'Request'} + requestDescription={request?.description || ''} + requestId={request?.id || request?.requestId} + levelName={selectedLevelForReview.levelName} + approverName={selectedLevelForReview.approverName} + /> + )} + + {/* Initiator Action Modal - Removed, actions are now direct buttons in step card */} + + {/* Version History Section */} + {versionHistory && versionHistory.length > 0 && ( + + +
+ + + Revision History & Audit Trail + + +
+ + Records of all revisions and actions taken on this request + +
+ {showHistory && ( + +
+ {versionHistory.map((item, idx) => ( +
+
+
+
+
+ + Version {item.version} + + {item.snapshotType && ( + + {item.snapshotType} + + )} + {item.levelNumber && ( + + Step {item.levelNumber} + + )} +
+ + {formatDateSafe(item.createdAt)} + +
+

+ {item.changeReason || 'Version Update'} +

+
+
+ + {item.changer?.displayName?.charAt(0) || 'U'} + +
+ + By {item.changer?.displayName || item.changer?.email || 'Unknown User'} + +
+ {/* Show snapshot details based on type - JSONB structure */} + {item.snapshotType === 'PROPOSAL' && item.snapshotData && ( +
+

Proposal:

+ {item.snapshotData.documentUrl && ( +
+ +
+ )} +

Budget: ₹{Number(item.snapshotData.totalBudget || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

+ {item.snapshotData.comments && ( +

Comments: {item.snapshotData.comments.substring(0, 80)}{item.snapshotData.comments.length > 80 ? '...' : ''}

+ )} +
+ )} + {item.snapshotType === 'COMPLETION' && item.snapshotData && ( +
+

Completion:

+ {item.snapshotData.documentUrl && ( +
+ +
+ )} +

Total Expenses: ₹{Number(item.snapshotData.totalExpenses || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

+ {item.snapshotData.comments && ( +

Comments: {item.snapshotData.comments.substring(0, 80)}{item.snapshotData.comments.length > 80 ? '...' : ''}

+ )} +
+ )} + {item.snapshotType === 'INTERNAL_ORDER' && item.snapshotData && ( +
+

IO Block:

+

IO Number: {item.snapshotData.ioNumber || 'N/A'}

+

Blocked: ₹{Number(item.snapshotData.blockedAmount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

+ {item.snapshotData.sapDocumentNumber && ( +

SAP Doc: {item.snapshotData.sapDocumentNumber}

+ )} +
+ )} + {item.snapshotType === 'APPROVE' && item.snapshotData && ( +
+

+ {item.snapshotData.action === 'APPROVE' ? 'Approval' : 'Rejection'}: +

+

By: {item.snapshotData.approverName || item.snapshotData.approverEmail || 'Unknown'}

+ {item.snapshotData.levelName && ( +

Level: {item.snapshotData.levelName}

+ )} + {item.snapshotData.comments && ( +

Comments: {item.snapshotData.comments.substring(0, 80)}{item.snapshotData.comments.length > 80 ? '...' : ''}

+ )} + {item.snapshotData.rejectionReason && ( +

Rejection Reason: {item.snapshotData.rejectionReason.substring(0, 80)}{item.snapshotData.rejectionReason.length > 80 ? '...' : ''}

+ )} +
+ )} + {item.snapshotType === 'WORKFLOW' && item.snapshotData && ( +
+

Workflow:

+

Status: {item.snapshotData.status || 'N/A'}

+ {item.snapshotData.currentLevel && ( +

Current Level: {item.snapshotData.currentLevel}

+ )} +
+ )} +
+
+ ))} +
+ + )} + + )} + + setViewSnapshot(null)} + snapshot={viewSnapshot?.data} + type={viewSnapshot?.type || 'PROPOSAL'} + title={viewSnapshot?.title} + /> ); } diff --git a/src/utils/slaTracker.ts b/src/utils/slaTracker.ts index d8bd862..02f2396 100644 --- a/src/utils/slaTracker.ts +++ b/src/utils/slaTracker.ts @@ -16,7 +16,7 @@ let configLoaded = false; // Lazy initialization of configuration async function ensureConfigLoaded() { if (configLoaded) return; - + try { const config = await configService.getConfig(); WORK_START_HOUR = config.workingHours.START_HOUR; @@ -30,7 +30,7 @@ async function ensureConfigLoaded() { } // Initialize config on first import (non-blocking) -ensureConfigLoaded().catch(() => {}); +ensureConfigLoaded().catch(() => { }); /** * Check if current time is within working hours @@ -40,7 +40,7 @@ ensureConfigLoaded().catch(() => {}); export function isWorkingTime(date: Date = new Date(), priority: string = 'standard'): boolean { const day = date.getDay(); // 0 = Sunday, 6 = Saturday const hour = date.getHours(); - + // For standard priority: exclude weekends // For express priority: include weekends (calendar days) if (priority === 'standard') { @@ -48,14 +48,13 @@ export function isWorkingTime(date: Date = new Date(), priority: string = 'stand return false; } } - + // Working hours check (applies to both priorities) if (hour < WORK_START_HOUR || hour >= WORK_END_HOUR) { return false; } - - // TODO: Add holiday check if holiday API is available - + + return true; } @@ -66,12 +65,12 @@ export function isWorkingTime(date: Date = new Date(), priority: string = 'stand */ export function getNextWorkingTime(date: Date = new Date(), priority: string = 'standard'): Date { const result = new Date(date); - + // If already in working time, return as is if (isWorkingTime(result, priority)) { return result; } - + // For standard priority: skip weekends if (priority === 'standard') { const day = result.getDay(); @@ -86,13 +85,13 @@ export function getNextWorkingTime(date: Date = new Date(), priority: string = ' return result; } } - + // If before work hours, move to work start if (result.getHours() < WORK_START_HOUR) { result.setHours(WORK_START_HOUR, 0, 0, 0); return result; } - + // If after work hours, move to next day work start if (result.getHours() >= WORK_END_HOUR) { result.setDate(result.getDate() + 1); @@ -100,7 +99,7 @@ export function getNextWorkingTime(date: Date = new Date(), priority: string = ' // Check if next day is weekend (only for standard priority) return getNextWorkingTime(result, priority); } - + return result; } @@ -114,19 +113,19 @@ export function calculateElapsedWorkingHours(startDate: Date, endDate: Date = ne let current = new Date(startDate); const end = new Date(endDate); let elapsedMinutes = 0; - + // Move minute by minute and count only working minutes while (current < end) { if (isWorkingTime(current, priority)) { elapsedMinutes++; } current.setMinutes(current.getMinutes() + 1); - + // Safety: stop if calculating more than 1 year const hoursSoFar = elapsedMinutes / 60; if (hoursSoFar > 8760) break; } - + // Convert minutes to hours (with decimal precision) return elapsedMinutes / 60; } @@ -140,12 +139,12 @@ export function calculateElapsedWorkingHours(startDate: Date, endDate: Date = ne export function calculateRemainingWorkingHours(deadline: Date, fromDate: Date = new Date(), priority: string = 'standard'): number { const deadlineTime = new Date(deadline).getTime(); const currentTime = new Date(fromDate).getTime(); - + // If deadline has passed if (deadlineTime <= currentTime) { return 0; } - + // Calculate remaining working hours return calculateElapsedWorkingHours(fromDate, deadline, priority); } @@ -160,9 +159,9 @@ export function calculateRemainingWorkingHours(deadline: Date, fromDate: Date = export function calculateSLAProgress(startDate: Date, deadline: Date, currentDate: Date = new Date(), priority: string = 'standard'): number { const totalHours = calculateElapsedWorkingHours(startDate, deadline, priority); const elapsedHours = calculateElapsedWorkingHours(startDate, currentDate, priority); - + if (totalHours === 0) return 0; - + const progress = (elapsedHours / totalHours) * 100; return Math.min(Math.max(progress, 0), 100); // Clamp between 0 and 100 } @@ -185,17 +184,17 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date, const start = new Date(startDate); const end = new Date(deadline); const now = new Date(); - + const isWorking = isWorkingTime(now, priority); const elapsedHours = calculateElapsedWorkingHours(start, now, priority); const totalHours = calculateElapsedWorkingHours(start, end, priority); const remainingHours = Math.max(0, totalHours - elapsedHours); const progress = calculateSLAProgress(start, end, now, priority); - + let statusText = ''; if (!isWorking) { - statusText = priority === 'express' - ? 'SLA tracking paused (outside working hours)' + statusText = priority === 'express' + ? 'SLA tracking paused (outside working hours)' : 'SLA tracking paused (outside working hours/days)'; } else if (remainingHours === 0) { statusText = 'SLA deadline reached'; @@ -208,7 +207,7 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date, } else { statusText = 'On track'; } - + return { isWorkingTime: isWorking, progress, @@ -231,38 +230,38 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date, export function formatHoursMinutes(hours: number | null | undefined): string { if (hours === null || hours === undefined || hours < 0) return '0 hours'; if (hours === 0) return '0 hours'; - + const WORKING_HOURS_PER_DAY = 8; - + // If less than 1 hour, show minutes only if (hours < 1) { const m = Math.round(hours * 60); return m > 0 ? `${m}m` : '0 hours'; } - + // Calculate days and remaining hours (8 hours = 1 day) // Match backend formatTime logic from Re_Backend/src/utils/tatTimeUtils.ts const days = Math.floor(hours / WORKING_HOURS_PER_DAY); const remainingHrs = Math.floor(hours % WORKING_HOURS_PER_DAY); const minutes = Math.round((hours % 1) * 60); - + // If we have days, format with days (matching backend format) if (days > 0) { const dayLabel = days === 1 ? 'day' : 'days'; const hourLabel = remainingHrs === 1 ? 'hour' : 'hours'; const minuteLabel = minutes === 1 ? 'min' : 'm'; - + if (minutes > 0) { return `${days} ${dayLabel} ${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`; } else { return `${days} ${dayLabel} ${remainingHrs} ${hourLabel}`; } } - + // No days, just hours and minutes const hourLabel = remainingHrs === 1 ? 'hour' : 'hours'; const minuteLabel = minutes === 1 ? 'min' : 'm'; - + if (minutes > 0) { return `${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`; } else { @@ -276,13 +275,13 @@ export function formatHoursMinutes(hours: number | null | undefined): string { export function formatWorkingHours(hours: number): string { if (hours === 0) return '0h'; if (hours < 0) return '0h'; - + const totalMinutes = Math.round(hours * 60); const days = Math.floor(totalMinutes / (8 * 60)); // 8 working hours per day const remainingMinutes = totalMinutes % (8 * 60); const remainingHours = Math.floor(remainingMinutes / 60); const minutes = remainingMinutes % 60; - + if (days > 0 && remainingHours > 0 && minutes > 0) { return `${days}d ${remainingHours}h ${minutes}m`; } else if (days > 0 && remainingHours > 0) { @@ -306,14 +305,14 @@ export function getTimeUntilNextWorking(priority: string = 'standard'): string { if (isWorkingTime(new Date(), priority)) { return 'In working hours'; } - + const now = new Date(); const next = getNextWorkingTime(now, priority); const diff = next.getTime() - now.getTime(); - + const hours = Math.floor(diff / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); - + if (hours > 24) { const days = Math.floor(hours / 24); return `Resumes in ${days}d ${hours % 24}h`; diff --git a/src/utils/tokenManager.ts b/src/utils/tokenManager.ts index dcdab10..df04962 100644 --- a/src/utils/tokenManager.ts +++ b/src/utils/tokenManager.ts @@ -57,14 +57,14 @@ export const cookieUtils = { */ clearAll(): void { const cookieNames = [ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, ID_TOKEN_KEY, USER_DATA_KEY]; - + cookieNames.forEach(name => { // Remove with default path this.remove(name); - + // Remove with root path explicitly document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; - + // Remove with domain (if applicable) const hostname = window.location.hostname; if (hostname !== 'localhost' && hostname !== '127.0.0.1') { @@ -75,82 +75,60 @@ export const cookieUtils = { }, }; -/** - * Token Manager - Handles token storage and retrieval - * - * SECURITY MODES: - * - Production: Tokens stored in httpOnly cookies by backend only - * Frontend does NOT store access/refresh tokens anywhere - * All API requests rely on cookies being sent automatically - * - * - Development: Tokens stored in localStorage for debugging - * Needed because frontend/backend run on different ports - */ + export class TokenManager { /** * Store access token - * In production: No-op (backend handles via httpOnly cookies) - * In development: Store in localStorage for Authorization header */ static setAccessToken(token: string): void { - // SECURITY: In production, don't store tokens client-side - // Backend sets httpOnly cookies that are sent automatically + if (isProduction()) { return; // No-op - rely on httpOnly cookies } - + // Development only: Store for debugging and cross-port requests localStorage.setItem(ACCESS_TOKEN_KEY, token); } /** * Get access token - * In production: Returns null (cookies are sent automatically) - * In development: Returns from localStorage + * */ static getAccessToken(): string | null { - // SECURITY: In production, return null - cookies are used instead + if (isProduction()) { - return null; // API calls use cookies via withCredentials: true + return null; } - + // Development: Return from localStorage return localStorage.getItem(ACCESS_TOKEN_KEY); } /** * Store refresh token - * In production: No-op (backend handles via httpOnly cookies) - * In development: Store in localStorage */ static setRefreshToken(token: string): void { // SECURITY: In production, don't store tokens client-side if (isProduction()) { return; // No-op - rely on httpOnly cookies } - + // Development only localStorage.setItem(REFRESH_TOKEN_KEY, token); } /** * Get refresh token - * In production: Returns null (cookies are used) - * In development: Returns from localStorage */ static getRefreshToken(): string | null { // SECURITY: In production, return null - backend reads from cookie if (isProduction()) { return null; } - + return localStorage.getItem(REFRESH_TOKEN_KEY); } - /** - * Store ID token (from Okta) - needed for logout - * Stored in sessionStorage (cleared when tab closes) - */ static setIdToken(token: string): void { // ID token is needed for Okta logout, use sessionStorage (more secure than localStorage) sessionStorage.setItem(ID_TOKEN_KEY, token); @@ -183,18 +161,7 @@ export class TokenManager { } } - /** - * Clear all tokens and user data - * - * PRODUCTION MODE: - * - Clears user data from localStorage - * - Clears ID token from sessionStorage - * - Backend logout endpoint clears httpOnly cookies - * - * DEVELOPMENT MODE: - * - Clears all localStorage and sessionStorage - * - Clears client-side cookies - */ + static clearAll(): void { // CRITICAL: Set logout flag in sessionStorage FIRST (before clearing) // This flag survives the redirect and prevents auto-authentication @@ -204,7 +171,7 @@ export class TokenManager { } catch (e) { console.warn('Could not set logout flags:', e); } - + // Clear user data (stored in both modes) try { localStorage.removeItem(USER_DATA_KEY); @@ -212,7 +179,7 @@ export class TokenManager { } catch (e) { console.warn('Error clearing user data:', e); } - + // In production, httpOnly cookies are cleared by backend // Only need to clear user data above if (isProduction()) { @@ -225,7 +192,7 @@ export class TokenManager { } return; } - + // DEVELOPMENT MODE: Clear everything const authKeys = [ ACCESS_TOKEN_KEY, @@ -246,7 +213,7 @@ export class TokenManager { 'persist:auth', 'redux-persist', ]; - + authKeys.forEach(key => { try { localStorage.removeItem(key); @@ -255,14 +222,14 @@ export class TokenManager { console.warn(`Error removing ${key}:`, e); } }); - + // Clear ALL localStorage try { localStorage.clear(); } catch (e) { console.error('Error clearing localStorage:', e); } - + // Clear ALL sessionStorage except logout flags try { const keysToKeep = ['__logout_in_progress__', '__force_logout__']; @@ -277,7 +244,7 @@ export class TokenManager { } catch (e) { console.error('Error clearing sessionStorage:', e); } - + // Clear client-side cookies (development only) cookieUtils.clearAll(); } @@ -296,11 +263,7 @@ export class TokenManager { return !!this.getAccessToken(); } - /** - * Check if refresh token exists - * In production: Always returns true if user data exists - * In development: Checks localStorage - */ + static hasRefreshToken(): boolean { if (isProduction()) { return !!this.getUserData(); @@ -318,7 +281,7 @@ export class TokenManager { window.location.hostname === '' ); } - + /** * Check if we're in production mode */