Re_Figma_Code/src/hooks/useRequestSocket.ts

272 lines
9.0 KiB
TypeScript

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<any[]>([]);
// 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<any[]>([]);
/**
* 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
};
}