import { useEffect, useState } from 'react'; import { getSocket, joinRequestRoom, leaveRequestRoom } from '@/utils/socket'; import { getWorkNotes } from '@/services/workflowApi'; import workflowApi from '@/services/workflowApi'; /** * Custom Hook: useRequestSocket * * Purpose: Manages real-time WebSocket connection for request updates * * Responsibilities: * - Establishes socket connection for the request * - Joins/leaves request-specific room * - Listens for new work notes in real-time * - Listens for TAT alerts and updates * - Merges work notes with activity timeline * - Manages unread work notes badge * - Handles socket cleanup on unmount * * @param requestIdentifier - Request number or UUID * @param apiRequest - Current request data object * @param activeTab - Currently active tab * @param user - Current authenticated user * @returns Object with merged messages, unread count, and work note attachments */ export function useRequestSocket( requestIdentifier: string, apiRequest: any, activeTab: string, user: any ) { // State: Merged array of work notes and activities, sorted chronologically const [mergedMessages, setMergedMessages] = useState([]); // State: Count of unread work notes (shows badge on Work Notes tab) const [unreadWorkNotes, setUnreadWorkNotes] = useState(0); // State: Attachments extracted from work notes for Documents tab const [workNoteAttachments, setWorkNoteAttachments] = useState([]); /** * Effect: Establish socket connection and join request room * * Process: * 1. Resolve UUID from request number if needed * 2. Initialize socket connection * 3. Join request-specific room (makes user "online" for this request) * 4. Cleanup on unmount (leave room, remove listeners) */ useEffect(() => { if (!requestIdentifier) { console.warn('[useRequestSocket] No requestIdentifier, cannot join socket room'); return; } // Socket connection initialized - logging removed let mounted = true; let actualRequestId = requestIdentifier; (async () => { try { // API Call: Fetch UUID if we have request number (socket rooms use UUID) const details = await workflowApi.getWorkflowDetails(requestIdentifier); if (details?.workflow?.requestId && mounted) { actualRequestId = details.workflow.requestId; // UUID resolved - logging removed } } catch (error) { console.error('[useRequestSocket] Failed to resolve UUID:', error); } if (!mounted) return; // Initialize: Get socket instance with base URL const socket = getSocket(); // Uses getSocketBaseUrl() helper internally if (!socket) { console.error('[useRequestSocket] Socket not available'); return; } const userId = (user as any)?.userId; /** * Handler: Join request room when socket connects * This makes the user "online" for this specific request */ const handleConnect = () => { // Socket connected - joining room joinRequestRoom(socket, actualRequestId, userId); }; // Join immediately if already connected, otherwise wait for connect event if (socket.connected) { handleConnect(); } else { socket.on('connect', handleConnect); } /** * Cleanup: Leave room and remove listeners when component unmounts * This marks user as "offline" for this request */ return () => { if (mounted) { socket.off('connect', handleConnect); leaveRequestRoom(socket, actualRequestId); // Left room - logging removed } }; })(); return () => { mounted = false; }; }, [requestIdentifier, user]); /** * Effect: Fetch and merge work notes with activities for timeline display * * Purpose: Combine work notes (real-time chat) with audit trail (system events) * to create a unified timeline view */ useEffect(() => { if (!requestIdentifier || !apiRequest) return; (async () => { try { // Fetch: Get all work notes for this request const workNotes = await getWorkNotes(requestIdentifier); const activities = apiRequest.auditTrail || []; // Merge: Combine work notes and activities const merged = [...workNotes, ...activities]; // Sort: Order by timestamp (oldest to newest) merged.sort((a, b) => { const timeA = new Date(a.createdAt || a.created_at || a.timestamp || 0).getTime(); const timeB = new Date(b.createdAt || b.created_at || b.timestamp || 0).getTime(); return timeA - timeB; }); setMergedMessages(merged); // Messages merged - logging removed } catch (error) { console.error('[useRequestSocket] Failed to fetch and merge messages:', error); } })(); }, [requestIdentifier, apiRequest]); /** * Effect: Listen for real-time work notes and TAT alerts via WebSocket * * Listens for: * 1. 'noteHandler' / 'worknote:new' - New work note added * 2. 'tat:alert' - TAT threshold reached or deadline breached */ useEffect(() => { if (!requestIdentifier) return; // Get socket using helper function (handles VITE_BASE_URL or VITE_API_BASE_URL) const socket = getSocket(); // Uses getSocketBaseUrl() helper internally if (!socket) return; /** * Handler: New work note received via WebSocket * * Actions: * 1. Increment unread badge if user is not on Work Notes tab * 2. Refresh merged messages to show new note */ const handleNewWorkNote = (_data: any) => { // New work note received - logging removed // Update unread badge (only if not viewing work notes) if (activeTab !== 'worknotes') { setUnreadWorkNotes(prev => prev + 1); } // Refresh: Re-fetch and merge messages to include new work note (async () => { try { const workNotes = await getWorkNotes(requestIdentifier); const activities = apiRequest?.auditTrail || []; const merged = [...workNotes, ...activities].sort((a, b) => { const timeA = new Date(a.createdAt || a.created_at || a.timestamp || 0).getTime(); const timeB = new Date(b.createdAt || b.created_at || b.timestamp || 0).getTime(); return timeA - timeB; }); setMergedMessages(merged); } catch (error) { console.error('[useRequestSocket] Failed to refresh messages:', error); } })(); }; /** * Handler: TAT alert received via WebSocket * * Triggered when: * - 50% TAT threshold reached * - 75% TAT threshold reached * - 100% TAT deadline breached * * Actions: * 1. Show console notification with emoji indicator * 2. Refresh request data to get updated TAT alerts * 3. Show browser notification if permission granted */ const handleTatAlert = (data: any) => { const alertEmoji = data.type === 'breach' ? '⏰' : data.type === 'threshold2' ? '⚠️' : '⏳'; // Refresh: Get updated TAT alerts from backend (async () => { try { const details = await workflowApi.getWorkflowDetails(requestIdentifier); if (details) { // Extract TAT alerts for potential future use void (Array.isArray(details.tatAlerts) ? details.tatAlerts : []); // Browser notification (if user granted permission) if ('Notification' in window && Notification.permission === 'granted') { new Notification(`${alertEmoji} TAT Alert`, { body: data.message, icon: '/favicon.ico', tag: `tat-${data.requestId}-${data.type}`, requireInteraction: false }); } } } catch (error) { console.error('[useRequestSocket] Failed to refresh after TAT alert:', error); } })(); }; // Register: Add event listeners for real-time updates socket.on('noteHandler', handleNewWorkNote); socket.on('worknote:new', handleNewWorkNote); socket.on('tat:alert', handleTatAlert); /** * Cleanup: Remove event listeners when component unmounts or dependencies change * Prevents memory leaks and duplicate listeners */ return () => { socket.off('noteHandler', handleNewWorkNote); socket.off('worknote:new', handleNewWorkNote); socket.off('tat:alert', handleTatAlert); }; }, [requestIdentifier, activeTab, apiRequest]); /** * Effect: Reset unread count when user switches to Work Notes tab * User has seen the messages, so clear the badge */ useEffect(() => { if (activeTab === 'worknotes') { setUnreadWorkNotes(0); } }, [activeTab]); return { mergedMessages, unreadWorkNotes, workNoteAttachments, setWorkNoteAttachments }; }