feat: SSE agent state, UCID fix, maint module, QA bug fixes

- Fix outbound disposition: store UCID from dial API response (root cause of silent disposition failure)
- SSE agent state: real-time Ozonetel state drives status toggle (ready/break/calling/in-call/acw)
- Maint module with OTP-protected endpoints (force-ready, unlock-agent, backfill, fix-timestamps)
- Maint OTP modal with PinInput component, keyboard shortcuts (Ctrl+Shift+R/U/B/T)
- Force-logout via SSE: admin unlock pushes force-logout to connected browsers
- Silence JsSIP debug flood, add structured lifecycle logging ([SIP], [DIAL], [DISPOSE], [AGENT-STATE])
- Centralize date formatting with IST-aware formatters across 11 files
- Fix call history: non-overlapping aggregates (completed/missed), correct timestamp display
- Auto-dismiss CallWidget ended/failed state after 3 seconds
- Remove floating "Helix Phone" idle badge from all pages
- Fix dead code in agent-state endpoint (auto-assign was unreachable after return)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 22:03:48 +05:30
parent ae94a390df
commit 488f524f84
21 changed files with 462 additions and 107 deletions

View File

@@ -28,6 +28,8 @@ const CalendarPlus02 = faIcon(faCalendarPlus);
import { Button } from '@/components/base/buttons/button';
import { TextArea } from '@/components/base/textarea/textarea';
import { AppointmentForm } from '@/components/call-desk/appointment-form';
import { useSetAtom } from 'jotai';
import { sipCallStateAtom } from '@/state/sip-state';
import { useSip } from '@/providers/sip-provider';
import { useAuth } from '@/providers/auth-provider';
import { cx } from '@/utils/cx';
@@ -41,20 +43,6 @@ const formatDuration = (seconds: number): string => {
return `${m}:${s}`;
};
const statusDotColor: Record<string, string> = {
registered: 'bg-success-500',
connecting: 'bg-warning-500',
disconnected: 'bg-quaternary',
error: 'bg-error-500',
};
const statusLabel: Record<string, string> = {
registered: 'Ready',
connecting: 'Connecting...',
disconnected: 'Offline',
error: 'Error',
};
const dispositionOptions: Array<{
value: CallDisposition;
label: string;
@@ -101,7 +89,6 @@ const dispositionOptions: Array<{
export const CallWidget = () => {
const {
connectionStatus,
callState,
callerNumber,
isMuted,
@@ -114,6 +101,7 @@ export const CallWidget = () => {
toggleHold,
} = useSip();
const { user } = useAuth();
const setCallState = useSetAtom(sipCallStateAtom);
const [disposition, setDisposition] = useState<CallDisposition | null>(null);
const [notes, setNotes] = useState('');
@@ -182,8 +170,20 @@ export const CallWidget = () => {
}
}, [callState]);
// 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]);
const handleSaveAndClose = async () => {
if (!disposition) return;
console.log(`[CALL-WIDGET] Save & Close: disposition=${disposition} lead=${matchedLead?.id ?? 'none'}`);
setIsSaving(true);
try {
@@ -264,24 +264,16 @@ export const CallWidget = () => {
setNotes('');
};
const dotColor = statusDotColor[connectionStatus] ?? 'bg-quaternary';
const label = statusLabel[connectionStatus] ?? connectionStatus;
// Log state changes for observability
useEffect(() => {
if (callState !== 'idle') {
console.log(`[CALL-WIDGET] State: ${callState} | caller=${callerNumber ?? 'none'}`);
}
}, [callState, callerNumber]);
// Idle: collapsed pill
// Idle: nothing to show — call desk has its own status toggle
if (callState === 'idle') {
return (
<div
className={cx(
'fixed bottom-6 right-6 z-50',
'inline-flex items-center gap-2 rounded-full border border-secondary bg-primary px-4 py-2.5 shadow-lg',
'transition-all duration-300',
)}
>
<span className={cx('size-2.5 shrink-0 rounded-full', dotColor)} />
<span className="text-sm font-semibold text-secondary">{label}</span>
<span className="text-sm text-tertiary">Helix Phone</span>
</div>
);
return null;
}
// Ringing inbound
@@ -444,6 +436,7 @@ export const CallWidget = () => {
callerNumber={callerNumber}
leadName={matchedLead ? `${matchedLead.contactName?.firstName ?? ''} ${matchedLead.contactName?.lastName ?? ''}`.trim() : null}
leadId={matchedLead?.id}
patientId={matchedLead?.patientId}
onSaved={() => {
setIsAppointmentOpen(false);
setDisposition('APPOINTMENT_BOOKED');