mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
- Replace mock DataProvider with real GraphQL queries through sidecar - Add queries.ts and transforms.ts for platform field name mapping - Migrate SIP state from React Context to Jotai atoms (React 19 compat) - Add singleton SIP manager to survive StrictMode remounts - Remove hardcoded Olivia/Sienna accounts from nav menu - Add password eye toggle, remember me checkbox, forgot password link - Fix worklist hook to transform platform field names - Add seed scripts for clinics, health packages, lab tests - Update test harness for new doctor→clinic relation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
4.5 KiB
TypeScript
136 lines
4.5 KiB
TypeScript
import { useEffect, useCallback, type PropsWithChildren } from 'react';
|
|
import { useAtom, useSetAtom } from 'jotai';
|
|
import {
|
|
sipConnectionStatusAtom,
|
|
sipCallStateAtom,
|
|
sipCallerNumberAtom,
|
|
sipIsMutedAtom,
|
|
sipIsOnHoldAtom,
|
|
sipCallDurationAtom,
|
|
sipCallStartTimeAtom,
|
|
} from '@/state/sip-state';
|
|
import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient } from '@/state/sip-manager';
|
|
import type { SIPConfig } from '@/types/sip';
|
|
|
|
const DEFAULT_CONFIG: SIPConfig = {
|
|
displayName: import.meta.env.VITE_SIP_DISPLAY_NAME ?? 'Helix Agent',
|
|
uri: import.meta.env.VITE_SIP_URI ?? '',
|
|
password: import.meta.env.VITE_SIP_PASSWORD ?? '',
|
|
wsServer: import.meta.env.VITE_SIP_WS_SERVER ?? '',
|
|
stunServers: 'stun:stun.l.google.com:19302',
|
|
};
|
|
|
|
export const SipProvider = ({ children }: PropsWithChildren) => {
|
|
const [, setConnectionStatus] = useAtom(sipConnectionStatusAtom);
|
|
const [callState, setCallState] = useAtom(sipCallStateAtom);
|
|
const setCallerNumber = useSetAtom(sipCallerNumberAtom);
|
|
const setCallDuration = useSetAtom(sipCallDurationAtom);
|
|
const setCallStartTime = useSetAtom(sipCallStartTimeAtom);
|
|
|
|
// Register Jotai setters so the singleton SIP manager can update atoms
|
|
useEffect(() => {
|
|
registerSipStateUpdater({
|
|
setConnectionStatus,
|
|
setCallState,
|
|
setCallerNumber,
|
|
});
|
|
}, [setConnectionStatus, setCallState, setCallerNumber]);
|
|
|
|
// Auto-connect SIP on mount
|
|
useEffect(() => {
|
|
connectSip(DEFAULT_CONFIG);
|
|
}, []);
|
|
|
|
// Call duration timer
|
|
useEffect(() => {
|
|
if (callState === 'active') {
|
|
const start = new Date();
|
|
setCallStartTime(start);
|
|
const interval = window.setInterval(() => {
|
|
setCallDuration(Math.floor((Date.now() - start.getTime()) / 1000));
|
|
}, 1000);
|
|
return () => clearInterval(interval);
|
|
}
|
|
setCallDuration(0);
|
|
setCallStartTime(null);
|
|
}, [callState, setCallDuration, setCallStartTime]);
|
|
|
|
// Auto-reset to idle after ended/failed
|
|
useEffect(() => {
|
|
if (callState === 'ended' || callState === 'failed') {
|
|
const timer = setTimeout(() => {
|
|
setCallState('idle');
|
|
setCallerNumber(null);
|
|
}, 2000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [callState, setCallState, setCallerNumber]);
|
|
|
|
// Cleanup on page unload
|
|
useEffect(() => {
|
|
const handleUnload = () => disconnectSip();
|
|
window.addEventListener('beforeunload', handleUnload);
|
|
return () => window.removeEventListener('beforeunload', handleUnload);
|
|
}, []);
|
|
|
|
return <>{children}</>;
|
|
};
|
|
|
|
// Hook for components to access SIP actions + state
|
|
export const useSip = () => {
|
|
const [connectionStatus] = useAtom(sipConnectionStatusAtom);
|
|
const [callState] = useAtom(sipCallStateAtom);
|
|
const [callerNumber, setCallerNumber] = useAtom(sipCallerNumberAtom);
|
|
const [isMuted, setIsMuted] = useAtom(sipIsMutedAtom);
|
|
const [isOnHold, setIsOnHold] = useAtom(sipIsOnHoldAtom);
|
|
const [callDuration] = useAtom(sipCallDurationAtom);
|
|
|
|
const makeCall = useCallback((phoneNumber: string) => {
|
|
getSipClient()?.call(phoneNumber);
|
|
setCallerNumber(phoneNumber);
|
|
}, [setCallerNumber]);
|
|
|
|
const answer = useCallback(() => getSipClient()?.answer(), []);
|
|
const reject = useCallback(() => getSipClient()?.reject(), []);
|
|
const hangup = useCallback(() => getSipClient()?.hangup(), []);
|
|
|
|
const toggleMute = useCallback(() => {
|
|
if (isMuted) {
|
|
getSipClient()?.unmute();
|
|
} else {
|
|
getSipClient()?.mute();
|
|
}
|
|
setIsMuted(!isMuted);
|
|
}, [isMuted, setIsMuted]);
|
|
|
|
const toggleHold = useCallback(() => {
|
|
if (isOnHold) {
|
|
getSipClient()?.unhold();
|
|
} else {
|
|
getSipClient()?.hold();
|
|
}
|
|
setIsOnHold(!isOnHold);
|
|
}, [isOnHold, setIsOnHold]);
|
|
|
|
return {
|
|
connectionStatus,
|
|
callState,
|
|
callerNumber,
|
|
isMuted,
|
|
isOnHold,
|
|
callDuration,
|
|
isRegistered: connectionStatus === 'registered',
|
|
isInCall: ['ringing-in', 'ringing-out', 'active'].includes(callState),
|
|
ozonetelStatus: 'logged-in' as const,
|
|
ozonetelError: null as string | null,
|
|
connect: () => connectSip(DEFAULT_CONFIG),
|
|
disconnect: disconnectSip,
|
|
makeCall,
|
|
answer,
|
|
reject,
|
|
hangup,
|
|
toggleMute,
|
|
toggleHold,
|
|
};
|
|
};
|