refactor: centralise outbound dial into useSip().dialOutbound()

- Single dialOutbound() in sip-provider handles all outbound state:
  callState, callerNumber, outboundPending, API call, error recovery
- ClickToCallButton, PhoneActionCell, Dialler all use dialOutbound()
- Removed direct Jotai atom manipulation from calling components
- Removed setOutboundPending imports from components
- SIP disconnects on provider unmount + auth logout
- Dialler input is now editable (type or numpad)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 18:49:10 +05:30
parent 13e81ba9fb
commit 710609dfee
5 changed files with 48 additions and 55 deletions

View File

@@ -10,7 +10,8 @@ import {
sipCallStartTimeAtom,
sipCallUcidAtom,
} from '@/state/sip-state';
import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient } from '@/state/sip-manager';
import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient, setOutboundPending } from '@/state/sip-manager';
import { apiClient } from '@/lib/api-client';
import type { SIPConfig } from '@/types/sip';
const getSipConfig = (): SIPConfig => {
@@ -85,11 +86,14 @@ 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 page unload
// Cleanup on unmount + page unload
useEffect(() => {
const handleUnload = () => disconnectSip();
window.addEventListener('beforeunload', handleUnload);
return () => window.removeEventListener('beforeunload', handleUnload);
return () => {
window.removeEventListener('beforeunload', handleUnload);
disconnectSip();
};
}, []);
return <>{children}</>;
@@ -98,7 +102,7 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
// Hook for components to access SIP actions + state
export const useSip = () => {
const [connectionStatus] = useAtom(sipConnectionStatusAtom);
const [callState] = useAtom(sipCallStateAtom);
const [callState, setCallState] = useAtom(sipCallStateAtom);
const [callerNumber, setCallerNumber] = useAtom(sipCallerNumberAtom);
const [callUcid] = useAtom(sipCallUcidAtom);
const [isMuted, setIsMuted] = useAtom(sipIsMutedAtom);
@@ -110,6 +114,24 @@ export const useSip = () => {
setCallerNumber(phoneNumber);
}, [setCallerNumber]);
// Ozonetel outbound dial — single path for all outbound calls
const dialOutbound = useCallback(async (phoneNumber: string): Promise<void> => {
setCallState('ringing-out');
setCallerNumber(phoneNumber);
setOutboundPending(true);
const safetyTimeout = setTimeout(() => setOutboundPending(false), 30000);
try {
await apiClient.post('/api/ozonetel/dial', { phoneNumber });
} catch {
clearTimeout(safetyTimeout);
setOutboundPending(false);
setCallState('idle');
setCallerNumber(null);
throw new Error('Dial failed');
}
}, [setCallState, setCallerNumber]);
const answer = useCallback(() => getSipClient()?.answer(), []);
const reject = useCallback(() => getSipClient()?.reject(), []);
const hangup = useCallback(() => getSipClient()?.hangup(), []);
@@ -147,6 +169,7 @@ export const useSip = () => {
connect: () => connectSip(getSipConfig()),
disconnect: disconnectSip,
makeCall,
dialOutbound,
answer,
reject,
hangup,