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:
2026-04-15 06:49:36 +05:30
parent 642911fa6c
commit 42e23a52ec
28 changed files with 614 additions and 246 deletions

View File

@@ -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(), []);