diff --git a/src/components/call-desk/active-call-card.tsx b/src/components/call-desk/active-call-card.tsx index 831be5d..2d975eb 100644 --- a/src/components/call-desk/active-call-card.tsx +++ b/src/components/call-desk/active-call-card.tsx @@ -56,6 +56,18 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete console.log(`[ACTIVE-CALL-CARD] Mounted: state=${callState} direction=${callDirectionRef.current} ucid=${callUcid ?? 'none'} lead=${lead?.id ?? 'none'} phone=${callerPhone}`); }, []); // eslint-disable-line react-hooks/exhaustive-deps + // Sync UCID to localStorage so sendBeacon can fire auto-dispose on page refresh. + // Cleared on disposition submit (handleDisposition below) or when call resets to idle. + useEffect(() => { + if (callUcid) { + localStorage.setItem('helix_active_ucid', callUcid); + } + return () => { + // Don't clear on unmount if disposition hasn't fired — the + // beforeunload handler in SipProvider needs it + }; + }, [callUcid]); + // Detect caller disconnect: call was active and ended without agent pressing End useEffect(() => { if (wasAnsweredRef.current && !dispositionOpen && (callState === 'ended' || callState === 'failed')) { @@ -115,6 +127,9 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete } } + // Clear persisted UCID — disposition was submitted, no need for sendBeacon fallback + localStorage.removeItem('helix_active_ucid'); + notify.success('Call Logged', `Disposition: ${disposition.replace(/_/g, ' ').toLowerCase()}`); handleReset(); }; diff --git a/src/providers/sip-provider.tsx b/src/providers/sip-provider.tsx index 9926898..d5c9ca3 100644 --- a/src/providers/sip-provider.tsx +++ b/src/providers/sip-provider.tsx @@ -89,15 +89,43 @@ export const SipProvider = ({ children }: PropsWithChildren) => { // No auto-reset — the ActiveCallCard handles post-call flow (disposition → appointment → done) // and resets to idle via the "Back to Worklist" button - // Cleanup on unmount + page unload + // Layer 1: Warn on page refresh/close during active call + // Layer 2: Fire sendBeacon to auto-dispose if user confirms leave + // These two layers protect against the "agent refreshes mid-call → stuck in ACW" bug. + // Layer 3 (server-side ACW timeout) lives in supervisor.service.ts. useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + const ucid = localStorage.getItem('helix_active_ucid'); + + // Layer 1: Show browser "Leave page?" dialog during active calls + if (callState === 'active' || callState === 'ringing-in' || callState === 'ringing-out') { + e.preventDefault(); + e.returnValue = ''; + } + + // Layer 2: Fire disposition beacon if there's an active UCID + // sendBeacon is guaranteed to fire even during page unload + if (ucid) { + const payload = JSON.stringify({ + ucid, + disposition: 'CALLBACK_REQUESTED', + autoDisposed: true, + }); + navigator.sendBeacon('/api/ozonetel/dispose', new Blob([payload], { type: 'application/json' })); + localStorage.removeItem('helix_active_ucid'); + } + }; + const handleUnload = () => disconnectSip(); - window.addEventListener('beforeunload', handleUnload); + + window.addEventListener('beforeunload', handleBeforeUnload); + window.addEventListener('unload', handleUnload); return () => { - window.removeEventListener('beforeunload', handleUnload); + window.removeEventListener('beforeunload', handleBeforeUnload); + window.removeEventListener('unload', handleUnload); disconnectSip(); }; - }, []); + }, [callState]); return <>{children}; };