mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { disconnectSocket, getSocket } from '@/lib/socket';
|
|
import type { CallDisposition } from '@/types/entities';
|
|
|
|
type CallState = 'idle' | 'ringing' | 'active' | 'completed';
|
|
|
|
type EnrichedLead = {
|
|
id: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
phone: string;
|
|
email?: string;
|
|
source?: string;
|
|
status?: string;
|
|
campaign?: string;
|
|
interestedService?: string;
|
|
age: number;
|
|
aiSummary?: string;
|
|
aiSuggestedAction?: string;
|
|
recentActivities: { activityType: string; summary: string; occurredAt: string; performedBy: string }[];
|
|
};
|
|
|
|
type IncomingCallEvent = {
|
|
callSid: string;
|
|
eventType: 'ringing' | 'answered' | 'ended';
|
|
lead: EnrichedLead | null;
|
|
callerPhone: string;
|
|
agentName: string;
|
|
timestamp: string;
|
|
};
|
|
|
|
export const useCallEvents = (agentName: string) => {
|
|
const [callState, setCallState] = useState<CallState>('idle');
|
|
const [activeLead, setActiveLead] = useState<EnrichedLead | null>(null);
|
|
const [activeCallSid, setActiveCallSid] = useState<string | null>(null);
|
|
const [callStartTime, setCallStartTime] = useState<string | null>(null);
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
const completedTimerRef = useRef<number | null>(null);
|
|
|
|
// Connect to WebSocket and register agent
|
|
useEffect(() => {
|
|
const socket = getSocket();
|
|
|
|
socket.on('connect', () => {
|
|
setIsConnected(true);
|
|
socket.emit('agent:register', agentName);
|
|
});
|
|
|
|
socket.on('disconnect', () => {
|
|
setIsConnected(false);
|
|
});
|
|
|
|
socket.on('agent:registered', (data: { agentName: string }) => {
|
|
console.log(`Registered as agent: ${data.agentName}`);
|
|
});
|
|
|
|
socket.on('call:incoming', (event: IncomingCallEvent) => {
|
|
if (event.eventType === 'ringing' || event.eventType === 'answered') {
|
|
setActiveCallSid(event.callSid);
|
|
setActiveLead(event.lead);
|
|
setCallStartTime(event.timestamp);
|
|
|
|
if (event.eventType === 'ringing') {
|
|
setCallState('ringing');
|
|
// Auto-transition to active after 1.5s (call is answered)
|
|
setTimeout(() => setCallState('active'), 1500);
|
|
} else {
|
|
setCallState('active');
|
|
}
|
|
} else if (event.eventType === 'ended') {
|
|
// Call ended from Exotel side (e.g. customer hung up)
|
|
setCallState('completed');
|
|
completedTimerRef.current = window.setTimeout(() => {
|
|
setCallState('idle');
|
|
setActiveLead(null);
|
|
setActiveCallSid(null);
|
|
setCallStartTime(null);
|
|
}, 3000);
|
|
}
|
|
});
|
|
|
|
socket.on('call:disposition:ack', () => {
|
|
// Disposition saved on server
|
|
});
|
|
|
|
socket.connect();
|
|
|
|
return () => {
|
|
if (completedTimerRef.current) {
|
|
clearTimeout(completedTimerRef.current);
|
|
}
|
|
disconnectSocket();
|
|
};
|
|
}, [agentName]);
|
|
|
|
// Send disposition to server
|
|
const sendDisposition = useCallback(
|
|
(disposition: CallDisposition, notes: string) => {
|
|
const socket = getSocket();
|
|
const duration = callStartTime
|
|
? Math.floor((Date.now() - new Date(callStartTime).getTime()) / 1000)
|
|
: 0;
|
|
|
|
socket.emit('call:disposition', {
|
|
callSid: activeCallSid,
|
|
leadId: activeLead?.id ?? null,
|
|
disposition,
|
|
notes,
|
|
agentName,
|
|
callerPhone: activeLead?.phone ?? '',
|
|
startedAt: callStartTime,
|
|
duration,
|
|
});
|
|
|
|
setCallState('completed');
|
|
completedTimerRef.current = window.setTimeout(() => {
|
|
setCallState('idle');
|
|
setActiveLead(null);
|
|
setActiveCallSid(null);
|
|
setCallStartTime(null);
|
|
}, 3000);
|
|
},
|
|
[activeCallSid, activeLead, agentName, callStartTime],
|
|
);
|
|
|
|
return {
|
|
callState,
|
|
activeLead,
|
|
activeCallSid,
|
|
isConnected,
|
|
sendDisposition,
|
|
};
|
|
};
|