mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
- Mute persists across calls: sip-manager's "ended/failed" branch now resets the Recoil sipIsMutedAtom + sipIsOnHoldAtom (previously only the SIP track was unmuted, leaving the UI icon + toggle logic in a muted state that the next call inherited). - Telephony-unavailable dial pad: call-desk.tsx dial-pad "Call" button was missing an isRegistered check in its disabled prop, so it stayed clickable when SIP was down. Button now shows "Telephony unavailable" and is disabled. - Past dates in Follow-up: enquiry-form's follow-up date input had no min constraint. Switched to a raw <input type="date"> with min set to today's ISO date. - Returning-patient AI summary during call: ai-chat-panel now auto-fires a "give me a quick summary of <caller>" request whenever the caller's leadId changes (new incoming call). Clears prior chat state so each caller starts fresh. - Remove Type column in Patients page (Badge import also pruned). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
import { SIPClient } from '@/lib/sip-client';
|
|
import type { SIPConfig, ConnectionStatus, CallState } from '@/types/sip';
|
|
|
|
// Singleton SIP client — survives React StrictMode remounts
|
|
let sipClient: SIPClient | null = null;
|
|
let connected = false;
|
|
let outboundPending = false;
|
|
let outboundActive = false;
|
|
let activeAgentId: string | null = null;
|
|
|
|
type StateUpdater = {
|
|
setConnectionStatus: (status: ConnectionStatus) => void;
|
|
setCallState: (state: CallState) => void;
|
|
setCallerNumber: (number: string | null) => void;
|
|
setCallUcid: (ucid: string | null) => void;
|
|
setIsMuted: (muted: boolean) => void;
|
|
setIsOnHold: (onHold: boolean) => void;
|
|
};
|
|
|
|
let stateUpdater: StateUpdater | null = null;
|
|
|
|
export function registerSipStateUpdater(updater: StateUpdater) {
|
|
stateUpdater = updater;
|
|
}
|
|
|
|
export function setOutboundPending(pending: boolean) {
|
|
outboundPending = pending;
|
|
}
|
|
|
|
export function isOutboundPending(): boolean {
|
|
return outboundPending;
|
|
}
|
|
|
|
export function connectSip(config: SIPConfig): void {
|
|
if (connected || sipClient?.isRegistered() || sipClient?.isConnected()) {
|
|
return;
|
|
}
|
|
|
|
if (!config.wsServer || !config.uri) {
|
|
console.warn('SIP config incomplete — wsServer and uri required');
|
|
return;
|
|
}
|
|
|
|
if (sipClient) {
|
|
sipClient.disconnect();
|
|
}
|
|
|
|
// Resolve agent identity for logging
|
|
try {
|
|
const agentCfg = JSON.parse(localStorage.getItem('helix_agent_config') ?? '{}');
|
|
activeAgentId = agentCfg.ozonetelAgentId ?? null;
|
|
const ext = config.uri?.match(/sip:(\d+)@/)?.[1] ?? 'unknown';
|
|
console.log(`[SIP] Connecting agent=${activeAgentId} ext=${ext} ws=${config.wsServer}`);
|
|
} catch {
|
|
console.log(`[SIP] Connecting uri=${config.uri}`);
|
|
}
|
|
|
|
connected = true;
|
|
stateUpdater?.setConnectionStatus('connecting');
|
|
|
|
sipClient = new SIPClient(
|
|
config,
|
|
(status) => stateUpdater?.setConnectionStatus(status),
|
|
(state, number, ucid) => {
|
|
// Auto-answer SIP when it's a bridge from our outbound call
|
|
if (state === 'ringing-in' && outboundPending) {
|
|
outboundPending = false;
|
|
outboundActive = true;
|
|
console.log('[SIP-MGR] Outbound bridge detected — auto-answering');
|
|
setTimeout(() => {
|
|
sipClient?.answer();
|
|
setTimeout(() => stateUpdater?.setCallState('active'), 300);
|
|
}, 500);
|
|
if (ucid) stateUpdater?.setCallUcid(ucid);
|
|
return;
|
|
}
|
|
|
|
console.log(`[SIP] ${activeAgentId} | state=${state} | caller=${number ?? 'none'} | ucid=${ucid ?? 'none'} | outbound=${outboundActive}`);
|
|
|
|
stateUpdater?.setCallState(state);
|
|
if (!outboundActive && number !== undefined) {
|
|
stateUpdater?.setCallerNumber(number ?? null);
|
|
}
|
|
|
|
if (ucid) stateUpdater?.setCallUcid(ucid);
|
|
|
|
if (state === 'ended' || state === 'failed') {
|
|
// Reset both the SIP track AND the Recoil state — otherwise the
|
|
// UI icon + toggle-mute branch logic stay "muted" and the next
|
|
// call opens in a confusing half-muted state.
|
|
sipClient?.unmute();
|
|
sipClient?.unhold();
|
|
stateUpdater?.setIsMuted(false);
|
|
stateUpdater?.setIsOnHold(false);
|
|
outboundActive = false;
|
|
outboundPending = false;
|
|
}
|
|
},
|
|
);
|
|
|
|
sipClient.connect();
|
|
}
|
|
|
|
export function disconnectSip(force = false, reason = 'unspecified'): void {
|
|
// Guard: don't disconnect SIP during an active or pending call
|
|
// unless explicitly forced (e.g., logout, page unload).
|
|
// This prevents React re-render cycles from killing the
|
|
// SIP WebSocket mid-dial.
|
|
if (!force && (outboundPending || outboundActive)) {
|
|
console.log(`[SIP-MGR] Disconnect blocked — call in progress (reason=${reason})`);
|
|
return;
|
|
}
|
|
console.log(`[SIP] Disconnecting agent=${activeAgentId} reason=${reason}` + (force ? ' (forced)' : ''));
|
|
sipClient?.disconnect();
|
|
sipClient = null;
|
|
connected = false;
|
|
outboundPending = false;
|
|
outboundActive = false;
|
|
activeAgentId = null;
|
|
stateUpdater?.setConnectionStatus('disconnected');
|
|
stateUpdater?.setCallUcid(null);
|
|
}
|
|
|
|
export function getSipClient(): SIPClient | null {
|
|
return sipClient;
|
|
}
|