mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: track UCID from SIP headers for Ozonetel disposition
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ export class SIPClient {
|
|||||||
constructor(
|
constructor(
|
||||||
private config: SIPConfig,
|
private config: SIPConfig,
|
||||||
private onConnectionChange: (status: ConnectionStatus) => void,
|
private onConnectionChange: (status: ConnectionStatus) => void,
|
||||||
private onCallStateChange: (state: CallState, callerNumber?: string) => void,
|
private onCallStateChange: (state: CallState, callerNumber?: string, ucid?: string) => void,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
connect(): void {
|
connect(): void {
|
||||||
@@ -66,8 +66,10 @@ export class SIPClient {
|
|||||||
|
|
||||||
this.currentSession = session;
|
this.currentSession = session;
|
||||||
|
|
||||||
// Extract caller number — try multiple SIP headers
|
// Extract caller number and UCID — try event request first, then session
|
||||||
const callerNumber = this.extractCallerNumber(session);
|
const sipRequest = (data as any).request ?? (session as any)._request ?? null;
|
||||||
|
const callerNumber = this.extractCallerNumber(session, sipRequest);
|
||||||
|
const ucid = sipRequest?.getHeader ? sipRequest.getHeader('X-UCID') ?? null : null;
|
||||||
|
|
||||||
// Setup audio for this session
|
// Setup audio for this session
|
||||||
session.on('peerconnection', (e: PeerConnectionEvent) => {
|
session.on('peerconnection', (e: PeerConnectionEvent) => {
|
||||||
@@ -83,16 +85,16 @@ export class SIPClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
session.on('accepted', (() => {
|
session.on('accepted', (() => {
|
||||||
this.onCallStateChange('active', callerNumber);
|
this.onCallStateChange('active', callerNumber, ucid ?? undefined);
|
||||||
}) as CallListener);
|
}) as CallListener);
|
||||||
|
|
||||||
session.on('confirmed', () => {
|
session.on('confirmed', () => {
|
||||||
this.onCallStateChange('active', callerNumber);
|
this.onCallStateChange('active', callerNumber, ucid ?? undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
session.on('progress', (() => {
|
session.on('progress', (() => {
|
||||||
if (session.direction === 'outgoing') {
|
if (session.direction === 'outgoing') {
|
||||||
this.onCallStateChange('ringing-out', callerNumber);
|
this.onCallStateChange('ringing-out', callerNumber, ucid ?? undefined);
|
||||||
}
|
}
|
||||||
}) as CallListener);
|
}) as CallListener);
|
||||||
|
|
||||||
@@ -107,7 +109,7 @@ export class SIPClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (session.direction === 'incoming') {
|
if (session.direction === 'incoming') {
|
||||||
this.onCallStateChange('ringing-in', callerNumber);
|
this.onCallStateChange('ringing-in', callerNumber, ucid ?? undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -222,12 +224,14 @@ export class SIPClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractCallerNumber(session: RTCSession): string {
|
private extractCallerNumber(session: RTCSession, sipRequest?: any): string {
|
||||||
try {
|
try {
|
||||||
const request = session.direction === 'incoming' ? (session as any)._request : null;
|
const request = sipRequest ?? (session.direction === 'incoming' ? (session as any)._request : null);
|
||||||
|
console.log('[SIP] extractCallerNumber: request exists:', !!request, 'direction:', session.direction);
|
||||||
if (request) {
|
if (request) {
|
||||||
// Ozonetel sends the real caller number in X-CALLERNO header
|
// Ozonetel sends the real caller number in X-CALLERNO header
|
||||||
const xCallerNo = request.getHeader('X-CALLERNO');
|
const xCallerNo = request.getHeader ? request.getHeader('X-CALLERNO') : null;
|
||||||
|
console.log('[SIP] X-CALLERNO header:', xCallerNo);
|
||||||
if (xCallerNo) {
|
if (xCallerNo) {
|
||||||
// Remove leading 0s or country code prefix (00919... → 919...)
|
// Remove leading 0s or country code prefix (00919... → 919...)
|
||||||
const cleaned = xCallerNo.replace(/^0+/, '');
|
const cleaned = xCallerNo.replace(/^0+/, '');
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
sipIsOnHoldAtom,
|
sipIsOnHoldAtom,
|
||||||
sipCallDurationAtom,
|
sipCallDurationAtom,
|
||||||
sipCallStartTimeAtom,
|
sipCallStartTimeAtom,
|
||||||
|
sipCallUcidAtom,
|
||||||
} from '@/state/sip-state';
|
} from '@/state/sip-state';
|
||||||
import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient } from '@/state/sip-manager';
|
import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient } from '@/state/sip-manager';
|
||||||
import type { SIPConfig } from '@/types/sip';
|
import type { SIPConfig } from '@/types/sip';
|
||||||
@@ -24,6 +25,7 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
|
|||||||
const [, setConnectionStatus] = useAtom(sipConnectionStatusAtom);
|
const [, setConnectionStatus] = useAtom(sipConnectionStatusAtom);
|
||||||
const [callState, setCallState] = useAtom(sipCallStateAtom);
|
const [callState, setCallState] = useAtom(sipCallStateAtom);
|
||||||
const setCallerNumber = useSetAtom(sipCallerNumberAtom);
|
const setCallerNumber = useSetAtom(sipCallerNumberAtom);
|
||||||
|
const setCallUcid = useSetAtom(sipCallUcidAtom);
|
||||||
const setCallDuration = useSetAtom(sipCallDurationAtom);
|
const setCallDuration = useSetAtom(sipCallDurationAtom);
|
||||||
const setCallStartTime = useSetAtom(sipCallStartTimeAtom);
|
const setCallStartTime = useSetAtom(sipCallStartTimeAtom);
|
||||||
|
|
||||||
@@ -33,8 +35,9 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
|
|||||||
setConnectionStatus,
|
setConnectionStatus,
|
||||||
setCallState,
|
setCallState,
|
||||||
setCallerNumber,
|
setCallerNumber,
|
||||||
|
setCallUcid,
|
||||||
});
|
});
|
||||||
}, [setConnectionStatus, setCallState, setCallerNumber]);
|
}, [setConnectionStatus, setCallState, setCallerNumber, setCallUcid]);
|
||||||
|
|
||||||
// Auto-connect SIP on mount
|
// Auto-connect SIP on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -82,6 +85,7 @@ export const useSip = () => {
|
|||||||
const [connectionStatus] = useAtom(sipConnectionStatusAtom);
|
const [connectionStatus] = useAtom(sipConnectionStatusAtom);
|
||||||
const [callState] = useAtom(sipCallStateAtom);
|
const [callState] = useAtom(sipCallStateAtom);
|
||||||
const [callerNumber, setCallerNumber] = useAtom(sipCallerNumberAtom);
|
const [callerNumber, setCallerNumber] = useAtom(sipCallerNumberAtom);
|
||||||
|
const [callUcid] = useAtom(sipCallUcidAtom);
|
||||||
const [isMuted, setIsMuted] = useAtom(sipIsMutedAtom);
|
const [isMuted, setIsMuted] = useAtom(sipIsMutedAtom);
|
||||||
const [isOnHold, setIsOnHold] = useAtom(sipIsOnHoldAtom);
|
const [isOnHold, setIsOnHold] = useAtom(sipIsOnHoldAtom);
|
||||||
const [callDuration] = useAtom(sipCallDurationAtom);
|
const [callDuration] = useAtom(sipCallDurationAtom);
|
||||||
@@ -117,6 +121,7 @@ export const useSip = () => {
|
|||||||
connectionStatus,
|
connectionStatus,
|
||||||
callState,
|
callState,
|
||||||
callerNumber,
|
callerNumber,
|
||||||
|
callUcid,
|
||||||
isMuted,
|
isMuted,
|
||||||
isOnHold,
|
isOnHold,
|
||||||
callDuration,
|
callDuration,
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import type { SIPConfig, ConnectionStatus, CallState } from '@/types/sip';
|
|||||||
let sipClient: SIPClient | null = null;
|
let sipClient: SIPClient | null = null;
|
||||||
let connected = false;
|
let connected = false;
|
||||||
let outboundPending = false;
|
let outboundPending = false;
|
||||||
|
let outboundActive = false;
|
||||||
|
|
||||||
type StateUpdater = {
|
type StateUpdater = {
|
||||||
setConnectionStatus: (status: ConnectionStatus) => void;
|
setConnectionStatus: (status: ConnectionStatus) => void;
|
||||||
setCallState: (state: CallState) => void;
|
setCallState: (state: CallState) => void;
|
||||||
setCallerNumber: (number: string | null) => void;
|
setCallerNumber: (number: string | null) => void;
|
||||||
|
setCallUcid: (ucid: string | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
let stateUpdater: StateUpdater | null = null;
|
let stateUpdater: StateUpdater | null = null;
|
||||||
@@ -46,21 +48,34 @@ export function connectSip(config: SIPConfig): void {
|
|||||||
sipClient = new SIPClient(
|
sipClient = new SIPClient(
|
||||||
config,
|
config,
|
||||||
(status) => stateUpdater?.setConnectionStatus(status),
|
(status) => stateUpdater?.setConnectionStatus(status),
|
||||||
(state, number) => {
|
(state, number, ucid) => {
|
||||||
// Auto-answer SIP when it's a bridge from our outbound Kookoo call
|
// Auto-answer SIP when it's a bridge from our outbound call
|
||||||
if (state === 'ringing-in' && outboundPending) {
|
if (state === 'ringing-in' && outboundPending) {
|
||||||
outboundPending = false;
|
outboundPending = false;
|
||||||
|
outboundActive = true;
|
||||||
console.log('[SIP] Outbound bridge detected — auto-answering');
|
console.log('[SIP] Outbound bridge detected — auto-answering');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sipClient?.answer();
|
sipClient?.answer();
|
||||||
// Force active state in case SIP callbacks don't fire
|
|
||||||
setTimeout(() => stateUpdater?.setCallState('active'), 300);
|
setTimeout(() => stateUpdater?.setCallState('active'), 300);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
// Store UCID even for outbound bridge calls
|
||||||
|
if (ucid) stateUpdater?.setCallUcid(ucid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't overwrite caller number on outbound calls — it was set by click-to-call
|
||||||
stateUpdater?.setCallState(state);
|
stateUpdater?.setCallState(state);
|
||||||
if (number !== undefined) stateUpdater?.setCallerNumber(number ?? null);
|
if (!outboundActive && number !== undefined) {
|
||||||
|
stateUpdater?.setCallerNumber(number ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store UCID if provided
|
||||||
|
if (ucid) stateUpdater?.setCallUcid(ucid);
|
||||||
|
|
||||||
|
// Reset outbound flag when call ends
|
||||||
|
if (state === 'ended' || state === 'failed') {
|
||||||
|
outboundActive = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -72,7 +87,9 @@ export function disconnectSip(): void {
|
|||||||
sipClient = null;
|
sipClient = null;
|
||||||
connected = false;
|
connected = false;
|
||||||
outboundPending = false;
|
outboundPending = false;
|
||||||
|
outboundActive = false;
|
||||||
stateUpdater?.setConnectionStatus('disconnected');
|
stateUpdater?.setConnectionStatus('disconnected');
|
||||||
|
stateUpdater?.setCallUcid(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSipClient(): SIPClient | null {
|
export function getSipClient(): SIPClient | null {
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export const sipIsMutedAtom = atom<boolean>(false);
|
|||||||
export const sipIsOnHoldAtom = atom<boolean>(false);
|
export const sipIsOnHoldAtom = atom<boolean>(false);
|
||||||
export const sipCallDurationAtom = atom<number>(0);
|
export const sipCallDurationAtom = atom<number>(0);
|
||||||
export const sipCallStartTimeAtom = atom<Date | null>(null);
|
export const sipCallStartTimeAtom = atom<Date | null>(null);
|
||||||
|
export const sipCallUcidAtom = atom<string | null>(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user