Files
helix-engage/src/components/call-desk/call-widget.tsx
saridsa2 e6b2208077 feat: disposition modal, persistent top bar, pagination, QA fixes
- 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>
2026-03-30 14:44:48 +05:30

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;
};