272 lines
9.0 KiB
TypeScript
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
|
|
};
|
|
}
|
|
|