mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user