import { useState, useEffect, useMemo, useCallback } from 'react'; import workflowApi, { getPauseDetails } from '@/services/workflowApi'; import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase'; import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase'; import { getSocket } from '@/utils/socket'; /** * Custom Hook: useRequestDetails * * Purpose: Manages request data fetching, transformation, and state management * * Responsibilities: * - Fetches workflow details from API using request identifier (request number or UUID) * - Transforms backend data structure to frontend format * - Maps approval levels with TAT alerts * - Handles spectators and participants * - Provides refresh functionality * - Falls back to static databases when API fails * * @param requestIdentifier - Request number or UUID to fetch * @param dynamicRequests - Optional array of dynamic requests for fallback * @param user - Current authenticated user object * @returns Object containing request data, loading state, refresh function, etc. */ export function useRequestDetails( requestIdentifier: string, dynamicRequests: any[] = [], user: any ) { // State: Stores the fetched and transformed request data const [apiRequest, setApiRequest] = useState(null); // State: Indicates if data is currently being fetched const [refreshing, setRefreshing] = useState(false); // State: Loading state for initial fetch const [loading, setLoading] = useState(true); // State: Access denied information const [accessDenied, setAccessDenied] = useState<{ denied: boolean; message: string } | null>(null); // State: Stores the current approval level for the logged-in user const [currentApprovalLevel, setCurrentApprovalLevel] = useState(null); // State: Indicates if the current user is a spectator (view-only access) const [isSpectator, setIsSpectator] = useState(false); /** * Helper: Convert name/email to initials for avatar display * Example: "John Doe" → "JD", "john@email.com" → "JO" */ const toInitials = (name?: string, email?: string) => { const base = (name || email || 'NA').toString(); return base.split(' ').map(s => s[0]).filter(Boolean).join('').slice(0, 2).toUpperCase(); }; /** * Helper: Map backend status strings to frontend display format * Converts: IN_PROGRESS → in-review, PENDING → pending, etc. */ const statusMap = (s: string) => { const val = (s || '').toUpperCase(); if (val === 'IN_PROGRESS') return 'in-review'; if (val === 'PENDING') return 'pending'; if (val === 'APPROVED') return 'approved'; if (val === 'REJECTED') return 'rejected'; if (val === 'CLOSED') return 'closed'; if (val === 'SKIPPED') return 'skipped'; return (s || '').toLowerCase(); }; /** * Function: refreshDetails * * Purpose: Fetch the latest request data from backend and update all state * * Process: * 1. Fetch workflow details from API * 2. Extract and validate data arrays (approvals, participants, documents, TAT alerts) * 3. Transform approval levels with TAT alerts * 4. Map spectators and documents * 5. Filter out TAT warning activities from audit trail * 6. Update all state with transformed data * 7. Determine current user's approval level and spectator status * * Note: Wrapped in useCallback to allow use in Socket.io listeners */ const refreshDetails = useCallback(async () => { setRefreshing(true); try { // API Call: Fetch complete workflow details including approvals, documents, participants const details = await workflowApi.getWorkflowDetails(requestIdentifier); if (!details) { console.warn('[useRequestDetails] No details returned from API'); return; } // Extract: Separate data structures from API response const wf = details.workflow || {}; const approvals = Array.isArray(details.approvals) ? details.approvals : []; const participants = Array.isArray(details.participants) ? details.participants : []; const documents = Array.isArray(details.documents) ? details.documents : []; const summary = details.summary || {}; const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : []; // Debug: Log TAT alerts for monitoring if (tatAlerts.length > 0) { // TAT alerts loaded - logging removed } const currentLevel = summary?.currentLevel || wf.currentLevel || 1; /** * Transform: Map approval levels to UI format with TAT alerts * Each approval level includes: * - Display status (waiting, pending, in-review, approved, rejected, skipped) * - TAT information (hours, elapsed, remaining, percentage) * - TAT alerts specific to this level * - Approver details */ const approvalFlow = approvals.map((a: any) => { const levelNumber = a.levelNumber || 0; const levelStatus = (a.status || '').toString().toUpperCase(); const levelId = a.levelId || a.level_id; // Determine display status based on workflow progress let displayStatus = statusMap(a.status); // Future levels that haven't been reached yet show as "waiting" if (levelNumber > currentLevel && levelStatus !== 'APPROVED' && levelStatus !== 'REJECTED') { displayStatus = 'waiting'; } // Current level with pending status shows as "pending" else if (levelNumber === currentLevel && levelStatus === 'PENDING') { displayStatus = 'pending'; } // Filter: Get TAT alerts that belong to this specific approval level const levelAlerts = tatAlerts.filter((alert: any) => alert.levelId === levelId); return { step: levelNumber, levelId, role: a.levelName || a.approverName || 'Approver', status: displayStatus, approver: a.approverName || a.approverEmail, approverId: a.approverId || a.approver_id, approverEmail: a.approverEmail, tatHours: Number(a.tatHours || 0), elapsedHours: Number(a.elapsedHours || 0), remainingHours: Number(a.remainingHours || 0), tatPercentageUsed: Number(a.tatPercentageUsed || 0), // Calculate actual hours taken if level is completed actualHours: a.levelEndTime && a.levelStartTime ? Math.max(0, (new Date(a.levelEndTime).getTime() - new Date(a.levelStartTime).getTime()) / (1000 * 60 * 60)) : undefined, comment: a.comments || undefined, timestamp: a.actionDate || undefined, levelStartTime: a.levelStartTime || a.tatStartTime, tatAlerts: levelAlerts, skipReason: a.skipReason || undefined, isSkipped: levelStatus === 'SKIPPED' || a.isSkipped || false, }; }); /** * Transform: Map spectators from participants array * Spectators have view-only access to the request */ const spectators = participants .filter((p: any) => (p.participantType || p.participant_type || '').toUpperCase() === 'SPECTATOR') .map((p: any) => ({ name: p.userName || p.user_name || p.userEmail || p.user_email, role: 'Spectator', email: p.userEmail || p.user_email, avatar: toInitials(p.userName || p.user_name, p.userEmail || p.user_email), })); /** * Helper: Get participant name by userId * Used for document upload attribution */ const participantNameById = (uid?: string) => { if (!uid) return undefined; const p = participants.find((x: any) => x.userId === uid || x.user_id === uid); if (p?.userName || p?.user_name) return p.userName || p.user_name; if (wf.initiatorId === uid) return wf.initiator?.displayName || wf.initiator?.email; return uid; }; /** * Transform: Map documents with file size conversion and uploader details * Converts bytes to MB for better readability */ const mappedDocuments = documents.map((d: any) => { const sizeBytes = Number(d.fileSize || d.file_size || 0); const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(2) + ' MB'; return { documentId: d.documentId || d.document_id, name: d.originalFileName || d.fileName || d.file_name, fileType: d.fileType || d.file_type || '', size: sizeMb, sizeBytes: sizeBytes, uploadedBy: participantNameById(d.uploadedBy || d.uploaded_by), uploadedAt: d.uploadedAt || d.uploaded_at, }; }); /** * Filter: Remove TAT breach activities from audit trail * TAT warnings are already shown in approval steps, no need to duplicate in timeline */ const filteredActivities = Array.isArray(details.activities) ? details.activities.filter((activity: any) => { const activityType = (activity.type || '').toLowerCase(); return activityType !== 'sla_warning'; }) : []; /** * Fetch: Get pause details if request is paused * This is needed to show resume/retrigger buttons correctly */ let pauseInfo = null; try { pauseInfo = await getPauseDetails(wf.requestId); } catch (error) { // Pause info not available or request not paused - ignore console.debug('Pause details not available:', error); } /** * Build: Complete request object with all transformed data * This object is used throughout the UI */ const updatedRequest = { ...wf, id: wf.requestNumber || wf.requestId, requestId: wf.requestId, // UUID for API calls requestNumber: wf.requestNumber, // Human-readable number for display title: wf.title, description: wf.description, status: statusMap(wf.status), priority: (wf.priority || '').toString().toLowerCase(), approvalFlow, approvals, // Raw approvals for SLA calculations participants, documents: mappedDocuments, spectators, summary, // Backend-provided SLA summary initiator: { name: wf.initiator?.displayName || wf.initiator?.email, role: wf.initiator?.designation || undefined, department: wf.initiator?.department || undefined, email: wf.initiator?.email || undefined, phone: wf.initiator?.phone || undefined, avatar: toInitials(wf.initiator?.displayName, wf.initiator?.email), }, createdAt: wf.createdAt, updatedAt: wf.updatedAt, totalSteps: wf.totalLevels || 1, // Store both raw and clamped values - raw for completion detection, clamped for display currentStepRaw: summary?.currentLevel || wf.currentLevel || 1, currentStep: Math.min(Math.max(1, summary?.currentLevel || wf.currentLevel || 1), wf.totalLevels || 1), auditTrail: filteredActivities, conclusionRemark: wf.conclusionRemark || null, closureDate: wf.closureDate || null, pauseInfo: pauseInfo || null, // Include pause info for resume/retrigger buttons }; setApiRequest(updatedRequest); /** * Determine: Find the approval level assigned to current user * Used to show approve/reject buttons only when user is the CURRENT active approver * Conditions: * 1. User email matches approverEmail * 2. Status is PENDING or IN_PROGRESS * 3. Approval level number matches the current active level in workflow */ const userEmail = (user as any)?.email?.toLowerCase(); const newCurrentLevel = approvals.find((a: any) => { const st = (a.status || '').toString().toUpperCase(); const approverEmail = (a.approverEmail || '').toLowerCase(); const approvalLevelNumber = a.levelNumber || 0; // Only show buttons if user is assigned to the CURRENT active level // Include PAUSED status - paused level is still the current level return (st === 'PENDING' || st === 'IN_PROGRESS' || st === 'PAUSED') && approverEmail === userEmail && approvalLevelNumber === currentLevel; }); setCurrentApprovalLevel(newCurrentLevel || null); /** * Determine: Check if current user is a spectator * Spectators can only view and comment, cannot approve/reject */ const viewerId = (user as any)?.userId; if (viewerId) { const isSpec = participants.some((p: any) => (p.participantType || p.participant_type || '').toUpperCase() === 'SPECTATOR' && (p.userId || p.user_id) === viewerId ); setIsSpectator(isSpec); } else { setIsSpectator(false); } } catch (error) { console.error('[useRequestDetails] Error refreshing details:', error); alert('Failed to refresh request details. Please try again.'); } finally { setRefreshing(false); } }, [requestIdentifier, user]); // useCallback dependencies /** * Effect: Initial data fetch when component mounts or requestIdentifier changes * This is the primary data loading mechanism */ useEffect(() => { if (!requestIdentifier) { setLoading(false); return; } let mounted = true; setLoading(true); setAccessDenied(null); (async () => { try { const details = await workflowApi.getWorkflowDetails(requestIdentifier); if (!mounted || !details) { if (mounted) setLoading(false); return; } // Use the same transformation logic as refreshDetails const wf = details.workflow || {}; const approvals = Array.isArray(details.approvals) ? details.approvals : []; const participants = Array.isArray(details.participants) ? details.participants : []; const documents = Array.isArray(details.documents) ? details.documents : []; const summary = details.summary || {}; const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : []; // TAT alerts received - logging removed const priority = (wf.priority || '').toString().toLowerCase(); const currentLevel = summary?.currentLevel || wf.currentLevel || 1; // Transform approval flow (same logic as refreshDetails) const approvalFlow = approvals.map((a: any) => { const levelNumber = a.levelNumber || 0; const levelStatus = (a.status || '').toString().toUpperCase(); const levelId = a.levelId || a.level_id; let displayStatus = statusMap(a.status); // If paused, show paused status (don't change it) if (levelStatus === 'PAUSED') { displayStatus = 'paused'; } else if (levelNumber > currentLevel && levelStatus !== 'APPROVED' && levelStatus !== 'REJECTED') { displayStatus = 'waiting'; } else if (levelNumber === currentLevel && (levelStatus === 'PENDING' || levelStatus === 'IN_PROGRESS')) { displayStatus = levelStatus === 'IN_PROGRESS' ? 'in-review' : 'pending'; } const levelAlerts = tatAlerts.filter((alert: any) => alert.levelId === levelId); return { step: levelNumber, levelId, role: a.levelName || a.approverName || 'Approver', status: displayStatus, approver: a.approverName || a.approverEmail, approverId: a.approverId || a.approver_id, approverEmail: a.approverEmail, tatHours: Number(a.tatHours || 0), elapsedHours: Number(a.elapsedHours || 0), remainingHours: Number(a.remainingHours || 0), tatPercentageUsed: Number(a.tatPercentageUsed || 0), // Use backend-calculated elapsedHours (working hours) for completed approvals // Backend already calculates this correctly using calculateElapsedWorkingHours actualHours: a.elapsedHours !== undefined && a.elapsedHours !== null ? Number(a.elapsedHours) : undefined, comment: a.comments || undefined, timestamp: a.actionDate || undefined, levelStartTime: a.levelStartTime || a.tatStartTime, tatAlerts: levelAlerts, }; }); // Map spectators const spectators = participants .filter((p: any) => (p.participantType || '').toUpperCase() === 'SPECTATOR') .map((p: any) => ({ name: p.userName || p.userEmail, role: 'Spectator', avatar: toInitials(p.userName, p.userEmail), })); // Helper to get participant name by ID const participantNameById = (uid?: string) => { if (!uid) return undefined; const p = participants.find((x: any) => x.userId === uid); if (p?.userName) return p.userName; if (wf.initiatorId === uid) return wf.initiator?.displayName || wf.initiator?.email; return uid; }; // Map documents with size conversion const mappedDocuments = documents.map((d: any) => { const sizeBytes = Number(d.fileSize || 0); const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(2) + ' MB'; return { documentId: d.documentId || d.document_id, name: d.originalFileName || d.fileName, fileType: d.fileType || d.file_type || '', size: sizeMb, sizeBytes: sizeBytes, uploadedBy: participantNameById(d.uploadedBy), uploadedAt: d.uploadedAt, }; }); // Filter out TAT warnings from activities const filteredActivities = Array.isArray(details.activities) ? details.activities.filter((activity: any) => { const activityType = (activity.type || '').toLowerCase(); return activityType !== 'sla_warning'; }) : []; // Fetch pause details let pauseInfo = null; try { pauseInfo = await getPauseDetails(wf.requestId); } catch (error) { // Pause info not available or request not paused - ignore console.debug('Pause details not available:', error); } // Build complete request object const mapped = { id: wf.requestNumber || wf.requestId, requestId: wf.requestId, title: wf.title, description: wf.description, priority, status: statusMap(wf.status), summary, initiator: { name: wf.initiator?.displayName || wf.initiator?.email, role: wf.initiator?.designation || undefined, department: wf.initiator?.department || undefined, email: wf.initiator?.email || undefined, phone: wf.initiator?.phone || undefined, avatar: toInitials(wf.initiator?.displayName, wf.initiator?.email), }, createdAt: wf.createdAt, updatedAt: wf.updatedAt, totalSteps: wf.totalLevels || 1, // Store both raw and clamped values - raw for completion detection, clamped for display currentStepRaw: summary?.currentLevel || wf.currentLevel || 1, currentStep: Math.min(Math.max(1, summary?.currentLevel || wf.currentLevel || 1), wf.totalLevels || 1), approvalFlow, approvals, documents: mappedDocuments, spectators, auditTrail: filteredActivities, conclusionRemark: wf.conclusionRemark || null, closureDate: wf.closureDate || null, pauseInfo: pauseInfo || null, }; setApiRequest(mapped); // Find current user's approval level // Only show approve/reject buttons if user is the CURRENT active approver // Include PAUSED status - when paused, the paused level is still the current level const userEmail = (user as any)?.email?.toLowerCase(); const userCurrentLevel = approvals.find((a: any) => { const status = (a.status || '').toString().toUpperCase(); const approverEmail = (a.approverEmail || '').toLowerCase(); const approvalLevelNumber = a.levelNumber || 0; // Only show buttons if user is assigned to the CURRENT active level // Include PAUSED status - paused level is still the current level return (status === 'PENDING' || status === 'IN_PROGRESS' || status === 'PAUSED') && approverEmail === userEmail && approvalLevelNumber === currentLevel; }); setCurrentApprovalLevel(userCurrentLevel || null); // Check spectator status const viewerId = (user as any)?.userId; if (viewerId) { const isSpec = participants.some((p: any) => (p.participantType || '').toUpperCase() === 'SPECTATOR' && p.userId === viewerId ); setIsSpectator(isSpec); } else { setIsSpectator(false); } } catch (error: any) { console.error('[useRequestDetails] Error loading request details:', error); if (mounted) { // Check for 403 Forbidden (Access Denied) if (error?.response?.status === 403) { const message = error?.response?.data?.message || 'You do not have permission to view this request. Access is restricted to the initiator, approvers, and spectators of this request.'; setAccessDenied({ denied: true, message }); } setApiRequest(null); } } finally { if (mounted) { setLoading(false); } } })(); return () => { mounted = false; }; }, [requestIdentifier, user]); /** * Computed: Get final request object with fallback to static databases * Priority: API data → Custom DB → Claim DB → Dynamic props → null */ const request = useMemo(() => { // Primary source: API data if (apiRequest) return apiRequest; // Fallback 1: Static custom request database const customRequest = CUSTOM_REQUEST_DATABASE[requestIdentifier]; if (customRequest) return customRequest; // Fallback 2: Static claim management database const claimRequest = CLAIM_MANAGEMENT_DATABASE[requestIdentifier]; if (claimRequest) return claimRequest; // Fallback 3: Dynamic requests passed as props const dynamicRequest = dynamicRequests.find((req: any) => req.id === requestIdentifier || req.requestNumber === requestIdentifier || req.request_number === requestIdentifier ); if (dynamicRequest) return dynamicRequest; return null; }, [requestIdentifier, dynamicRequests, apiRequest]); /** * Computed: Check if current user is the request initiator * Initiators have special permissions (add approvers, skip approvers, close request) */ const isInitiator = useMemo(() => { if (!request || !user) return false; const userEmail = (user as any)?.email?.toLowerCase(); const initiatorEmail = request.initiator?.email?.toLowerCase(); return userEmail === initiatorEmail; }, [request, user]); /** * Computed: Get all existing participants for validation * Used when adding new approvers/spectators to prevent duplicates */ const existingParticipants = useMemo(() => { if (!request) return []; const participants: Array<{ email: string; participantType: string; name?: string }> = []; // Add initiator if (request.initiator?.email) { participants.push({ email: request.initiator.email.toLowerCase(), participantType: 'INITIATOR', name: request.initiator.name }); } // Add approvers from approval flow if (request.approvalFlow && Array.isArray(request.approvalFlow)) { request.approvalFlow.forEach((approval: any) => { if (approval.approverEmail) { participants.push({ email: approval.approverEmail.toLowerCase(), participantType: 'APPROVER', name: approval.approver }); } }); } // Add spectators if (request.spectators && Array.isArray(request.spectators)) { request.spectators.forEach((spectator: any) => { if (spectator.email) { participants.push({ email: spectator.email.toLowerCase(), participantType: 'SPECTATOR', name: spectator.name }); } }); } // Add from participants array if (request.participants && Array.isArray(request.participants)) { request.participants.forEach((p: any) => { const email = (p.userEmail || p.email || '').toLowerCase(); const participantType = (p.participantType || p.participant_type || '').toUpperCase(); const name = p.userName || p.user_name || p.name; if (email && participantType && !participants.find(x => x.email === email)) { participants.push({ email, participantType, name }); } }); } return participants; }, [request]); /** * Effect: Listen for real-time request updates via Socket.io * * Purpose: Auto-refresh request details when other users take actions * * Listens for: * - 'request:updated' - Any action that changes the request (approve, reject, pause, resume, skip, etc.) * * Behavior: * - Silently refreshes data in background * - Doesn't interrupt user actions * - Updates all tabs with latest data */ useEffect(() => { if (!requestIdentifier || !apiRequest) return; const socket = getSocket(); if (!socket) { console.warn('[useRequestDetails] Socket not available'); return; } console.log('[useRequestDetails] Setting up socket listener for request:', apiRequest.requestId); /** * Handler: Request updated by another user * Silently refresh to show latest changes */ const handleRequestUpdated = (data: any) => { console.log('[useRequestDetails] 📡 Received request:updated event:', data); // Verify this update is for the current request if (data?.requestId === apiRequest.requestId || data?.requestNumber === requestIdentifier) { console.log('[useRequestDetails] 🔄 Request updated remotely, refreshing silently...'); // Silent refresh - no loading state, no user interruption refreshDetails(); } else { console.log('[useRequestDetails] ⚠️ Event for different request, ignoring'); } }; // Register listener socket.on('request:updated', handleRequestUpdated); console.log('[useRequestDetails] ✅ Registered listener for request:updated'); // Cleanup on unmount return () => { console.log('[useRequestDetails] 🧹 Cleaning up socket listener'); socket.off('request:updated', handleRequestUpdated); }; }, [requestIdentifier, apiRequest, refreshDetails]); return { request, apiRequest, loading, refreshing, refreshDetails, currentApprovalLevel, isSpectator, isInitiator, existingParticipants, accessDenied }; }