mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
feat: call-desk refresh — disposition modal, active-call UI, worklist + perf updates
- Call-desk: active-call-card supervisor presence badges, incoming-call-card polish, transfer-dialog, call-log - Disposition modal: auto-lock based on actions taken, not-interested split - Forms: appointment-form + enquiry-form improvements (placeholder handling, phone format) - Worklist-panel: pagination awareness, filter chips - Pages: all-leads/patients/patient-360/missed-calls/team-performance/call-history/appointments polish - SIP: sip-client reconnect, sip-provider + sip-manager state, agent-status-toggle spinner - Hooks: use-agent-state supervisor SSE events, use-worklist, use-performance-alerts - Types: entities.ts extended Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
} from '@/state/sip-state';
|
||||
import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient, setOutboundPending } from '@/state/sip-manager';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { notify } from '@/lib/toast';
|
||||
import type { SIPConfig } from '@/types/sip';
|
||||
|
||||
// SIP config comes exclusively from the Agent entity (stored on login).
|
||||
@@ -125,14 +126,14 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnload = () => disconnectSip(true);
|
||||
const handleUnload = () => disconnectSip(true, 'page-unload');
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
window.addEventListener('unload', handleUnload);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
window.removeEventListener('unload', handleUnload);
|
||||
disconnectSip(true); // force — component is unmounting
|
||||
disconnectSip(true, 'sip-provider-unmount'); // force — component is unmounting
|
||||
};
|
||||
}, []); // empty deps — runs once on mount, cleanup only on unmount
|
||||
|
||||
@@ -156,6 +157,17 @@ export const useSip = () => {
|
||||
|
||||
// Ozonetel outbound dial — single path for all outbound calls
|
||||
const dialOutbound = useCallback(async (phoneNumber: string): Promise<void> => {
|
||||
// Hard guard — no dial is valid when SIP isn't registered, because
|
||||
// the audio leg can't be established. Every entry point (worklist
|
||||
// row, click-to-call, phone-action-cell, patient 360, etc.) funnels
|
||||
// through this callback, so gating here is the single source of
|
||||
// truth for "can this agent place a call right now?"
|
||||
if (connectionStatus !== 'registered') {
|
||||
notify.error('Telephony unavailable', 'Cannot place call — SIP is not registered. Check your connection.');
|
||||
console.warn(`[DIAL] Blocked — SIP not registered (status=${connectionStatus})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Block outbound calls when agent is on Break or Training
|
||||
const agentCfg = localStorage.getItem('helix_agent_config');
|
||||
if (agentCfg) {
|
||||
@@ -166,7 +178,6 @@ export const useSip = () => {
|
||||
const stateRes = await fetch(`/api/supervisor/agent-state?agentId=${agentId}`);
|
||||
const stateData = await stateRes.json();
|
||||
if (stateData.state === 'break' || stateData.state === 'training') {
|
||||
const { notify } = await import('@/lib/toast');
|
||||
notify.info('Status: ' + stateData.state, 'Change status to Ready before placing calls');
|
||||
return;
|
||||
}
|
||||
@@ -204,7 +215,7 @@ export const useSip = () => {
|
||||
setCallerNumber(null);
|
||||
throw new Error('Dial failed');
|
||||
}
|
||||
}, [setCallState, setCallerNumber, setCallUcid]);
|
||||
}, [setCallState, setCallerNumber, setCallUcid, connectionStatus]);
|
||||
|
||||
const answer = useCallback(() => getSipClient()?.answer(), []);
|
||||
const reject = useCallback(() => getSipClient()?.reject(), []);
|
||||
|
||||
Reference in New Issue
Block a user