mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
- DispositionModal: single modal for all call endings. Dismissable (agent can resume call). Agent clicks End → modal → select reason → hangup + dispose. Caller disconnects → same modal. - One call screen: CallWidget stripped to ringing notification + auto-redirect to Call Desk. - Persistent top bar in AppShell: agent status toggle + network indicator on all pages. - Network indicator always visible (Connected/Unstable/No connection). - Pagination: Untitled UI PaginationCardDefault on Call History + Appointments (20/page). - Pinned table headers/footers: sticky column headers, scrollable body, pinned pagination. Applied to Call Desk worklist, Call History, Appointments, Call Recordings, Missed Calls. - "Patient" → "Caller" column label in Call History. - Offline → Ready toggle enabled. - Profile status dot reflects Ozonetel state. - NavAccountCard: popover placement top, View Profile + Account Settings restored. - WIP pages for /profile and /account-settings. - Enquiry form PHONE_INQUIRY → PHONE enum fix. - Force Ready / View Profile / Account Settings removed then restored properly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
112 lines
4.9 KiB
TypeScript
112 lines
4.9 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { useNavigate, useLocation } from 'react-router';
|
|
import { faPhone, faPhoneArrowDown, faPhoneXmark, faCircleCheck } from '@fortawesome/pro-duotone-svg-icons';
|
|
import { faIcon } from '@/lib/icon-wrapper';
|
|
|
|
const Phone01 = faIcon(faPhone);
|
|
const PhoneIncoming01 = faIcon(faPhoneArrowDown);
|
|
const PhoneX = faIcon(faPhoneXmark);
|
|
const CheckCircle = faIcon(faCircleCheck);
|
|
import { Button } from '@/components/base/buttons/button';
|
|
import { useSetAtom } from 'jotai';
|
|
import { sipCallStateAtom } from '@/state/sip-state';
|
|
import { useSip } from '@/providers/sip-provider';
|
|
import { cx } from '@/utils/cx';
|
|
|
|
const formatDuration = (seconds: number): string => {
|
|
const m = Math.floor(seconds / 60).toString().padStart(2, '0');
|
|
const s = (seconds % 60).toString().padStart(2, '0');
|
|
return `${m}:${s}`;
|
|
};
|
|
|
|
// CallWidget is a lightweight floating notification for calls outside the Call Desk.
|
|
// It only handles: ringing (answer/decline) + auto-redirect to Call Desk.
|
|
// All active call management (mute, hold, end, disposition) happens on the Call Desk via ActiveCallCard.
|
|
export const CallWidget = () => {
|
|
const { callState, callerNumber, callDuration, answer, reject } = useSip();
|
|
const setCallState = useSetAtom(sipCallStateAtom);
|
|
const navigate = useNavigate();
|
|
const { pathname } = useLocation();
|
|
|
|
// Auto-navigate to Call Desk when a call becomes active or outbound ringing starts
|
|
useEffect(() => {
|
|
if (pathname === '/call-desk') return;
|
|
if (callState === 'active' || callState === 'ringing-out') {
|
|
console.log(`[CALL-WIDGET] Redirecting to Call Desk (state=${callState})`);
|
|
navigate('/call-desk');
|
|
}
|
|
}, [callState, pathname, navigate]);
|
|
|
|
// Auto-dismiss ended/failed state after 3 seconds
|
|
useEffect(() => {
|
|
if (callState === 'ended' || callState === 'failed') {
|
|
const timer = setTimeout(() => {
|
|
console.log('[CALL-WIDGET] Auto-dismissing ended/failed state');
|
|
setCallState('idle');
|
|
}, 3000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [callState, setCallState]);
|
|
|
|
// Log state changes
|
|
useEffect(() => {
|
|
if (callState !== 'idle') {
|
|
console.log(`[CALL-WIDGET] State: ${callState} | caller=${callerNumber ?? 'none'}`);
|
|
}
|
|
}, [callState, callerNumber]);
|
|
|
|
if (callState === 'idle') return null;
|
|
|
|
// Ringing inbound — answer redirects to Call Desk
|
|
if (callState === 'ringing-in') {
|
|
return (
|
|
<div className={cx(
|
|
'fixed bottom-6 right-6 z-50 w-80',
|
|
'flex flex-col items-center gap-5 rounded-2xl border border-secondary bg-primary p-6 shadow-2xl',
|
|
'transition-all duration-300',
|
|
)}>
|
|
<div className="relative">
|
|
<div className="absolute inset-0 animate-ping rounded-full bg-brand-solid opacity-20" />
|
|
<div className="relative animate-bounce">
|
|
<PhoneIncoming01 className="size-10 text-fg-brand-primary" />
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col items-center gap-1">
|
|
<span className="text-xs font-bold uppercase tracking-wider text-brand-secondary">Incoming Call</span>
|
|
<span className="text-lg font-bold text-primary">{callerNumber ?? 'Unknown'}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Button size="md" color="primary" iconLeading={Phone01} onClick={() => { answer(); navigate('/call-desk'); }}>
|
|
Answer
|
|
</Button>
|
|
<Button size="md" color="primary-destructive" iconLeading={PhoneX} onClick={reject}>
|
|
Decline
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Ended / Failed — brief notification
|
|
if (callState === 'ended' || callState === 'failed') {
|
|
const isEnded = callState === 'ended';
|
|
return (
|
|
<div className={cx(
|
|
'fixed bottom-6 right-6 z-50 w-80',
|
|
'flex flex-col items-center gap-2 rounded-2xl border border-secondary bg-primary p-5 shadow-2xl',
|
|
'transition-all duration-300',
|
|
)}>
|
|
<CheckCircle className={cx('size-8', isEnded ? 'text-fg-success-primary' : 'text-fg-error-primary')} />
|
|
<span className="text-sm font-semibold text-primary">
|
|
{isEnded ? 'Call Ended' : 'Call Failed'}
|
|
{callDuration > 0 && ` \u00B7 ${formatDuration(callDuration)}`}
|
|
</span>
|
|
<span className="text-xs text-tertiary">auto-closing...</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Any other state (active, ringing-out) on a non-call-desk page — redirect handled by useEffect above
|
|
return null;
|
|
};
|