mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 10:23:27 +00:00
- Worklist default sort descending (newest first), sortable column headers (PRIORITY, PATIENT, SLA) via React Aria - Contextual disposition: auto-selects based on in-call actions (appointment → APPOINTMENT_BOOKED, enquiry → INFO_PROVIDED, transfer → FOLLOW_UP_SCHEDULED) - Context panel redesign: collapsible AI Insight, Upcoming (appointments + follow-ups + linked patient), Recent (calls + activities) sections; auto-collapse on AI chat start - Appointments added to DataProvider with APPOINTMENTS_QUERY, Appointment type, transform - Notification bell for admin/supervisor: performance alerts (idle time, NPS, conversion thresholds) with toast on load + bell dropdown with dismiss; demo alerts as fallback - Slideout z-index fix: added z-50 to slideout ModalOverlay matching modal component Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
124 lines
6.2 KiB
TypeScript
124 lines
6.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import { faCircle, faChevronDown } from '@fortawesome/pro-duotone-svg-icons';
|
|
import { useAgentState } from '@/hooks/use-agent-state';
|
|
import type { OzonetelState } from '@/hooks/use-agent-state';
|
|
import { apiClient } from '@/lib/api-client';
|
|
import { notify } from '@/lib/toast';
|
|
import { cx } from '@/utils/cx';
|
|
|
|
type ToggleableStatus = 'ready' | 'break' | 'training';
|
|
|
|
const displayConfig: Record<OzonetelState, { label: string; color: string; dotColor: string }> = {
|
|
ready: { label: 'Ready', color: 'text-success-primary', dotColor: 'text-fg-success-primary' },
|
|
break: { label: 'Break', color: 'text-warning-primary', dotColor: 'text-fg-warning-primary' },
|
|
training: { label: 'Training', color: 'text-brand-secondary', dotColor: 'text-fg-brand-primary' },
|
|
calling: { label: 'Calling', color: 'text-brand-secondary', dotColor: 'text-fg-brand-primary' },
|
|
'in-call': { label: 'In Call', color: 'text-brand-secondary', dotColor: 'text-fg-brand-primary' },
|
|
acw: { label: 'Wrapping up', color: 'text-warning-primary', dotColor: 'text-fg-warning-primary' },
|
|
offline: { label: 'Offline', color: 'text-tertiary', dotColor: 'text-fg-quaternary' },
|
|
};
|
|
|
|
const toggleOptions: Array<{ key: ToggleableStatus; label: string; color: string; dotColor: string }> = [
|
|
{ key: 'ready', label: 'Ready', color: 'text-success-primary', dotColor: 'text-fg-success-primary' },
|
|
{ key: 'break', label: 'Break', color: 'text-warning-primary', dotColor: 'text-fg-warning-primary' },
|
|
{ key: 'training', label: 'Training', color: 'text-brand-secondary', dotColor: 'text-fg-brand-primary' },
|
|
];
|
|
|
|
type AgentStatusToggleProps = {
|
|
isRegistered: boolean;
|
|
connectionStatus: string;
|
|
};
|
|
|
|
export const AgentStatusToggle = ({ isRegistered, connectionStatus }: AgentStatusToggleProps) => {
|
|
const agentConfig = localStorage.getItem('helix_agent_config');
|
|
const agentId = agentConfig ? JSON.parse(agentConfig).ozonetelAgentId : null;
|
|
const ozonetelState = useAgentState(agentId);
|
|
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
const [changing, setChanging] = useState(false);
|
|
|
|
const handleChange = async (newStatus: ToggleableStatus) => {
|
|
setMenuOpen(false);
|
|
if (newStatus === ozonetelState) return;
|
|
setChanging(true);
|
|
|
|
try {
|
|
if (newStatus === 'ready') {
|
|
console.log('[AGENT-STATE] Changing to Ready');
|
|
const res = await apiClient.post('/api/ozonetel/agent-state', { state: 'Ready' });
|
|
console.log('[AGENT-STATE] Ready response:', JSON.stringify(res));
|
|
} else {
|
|
const pauseReason = newStatus === 'break' ? 'Break' : 'Training';
|
|
console.log(`[AGENT-STATE] Changing to Pause: ${pauseReason}`);
|
|
const res = await apiClient.post('/api/ozonetel/agent-state', { state: 'Pause', pauseReason });
|
|
console.log('[AGENT-STATE] Pause response:', JSON.stringify(res));
|
|
}
|
|
// Don't setStatus — SSE will push the real state
|
|
} catch (err) {
|
|
console.error('[AGENT-STATE] Status change failed:', err);
|
|
notify.error('Status Change Failed', 'Could not update agent status');
|
|
} finally {
|
|
setChanging(false);
|
|
}
|
|
};
|
|
|
|
// If SIP isn't connected, show connection status with user-friendly message
|
|
if (!isRegistered) {
|
|
const statusMessages: Record<string, string> = {
|
|
disconnected: 'Telephony unavailable',
|
|
connecting: 'Connecting to telephony...',
|
|
connected: 'Registering...',
|
|
error: 'Telephony error — check VPN',
|
|
};
|
|
return (
|
|
<div className="flex items-center gap-1.5 rounded-full bg-secondary px-3 py-1">
|
|
<FontAwesomeIcon icon={faCircle} className="size-2 text-fg-warning-primary animate-pulse" />
|
|
<span className="text-xs font-medium text-tertiary">{statusMessages[connectionStatus] ?? connectionStatus}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const current = displayConfig[ozonetelState] ?? displayConfig.offline;
|
|
const canToggle = ozonetelState === 'ready' || ozonetelState === 'break' || ozonetelState === 'training' || ozonetelState === 'offline';
|
|
|
|
return (
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => canToggle && setMenuOpen(!menuOpen)}
|
|
disabled={changing || !canToggle}
|
|
className={cx(
|
|
'flex items-center gap-1.5 rounded-full bg-secondary px-3 py-1 transition duration-100 ease-linear',
|
|
canToggle ? 'hover:bg-secondary_hover cursor-pointer' : 'cursor-default',
|
|
changing && 'opacity-50',
|
|
)}
|
|
>
|
|
<FontAwesomeIcon icon={faCircle} className={cx('size-2', current.dotColor)} />
|
|
<span className={cx('text-xs font-medium', current.color)}>{current.label}</span>
|
|
{canToggle && <FontAwesomeIcon icon={faChevronDown} className="size-2.5 text-fg-quaternary" />}
|
|
</button>
|
|
|
|
{menuOpen && (
|
|
<>
|
|
<div className="fixed inset-0 z-40" onClick={() => setMenuOpen(false)} />
|
|
<div className="absolute right-0 top-full z-50 mt-1 w-36 rounded-lg bg-primary shadow-lg ring-1 ring-secondary py-1">
|
|
{toggleOptions.map((opt) => (
|
|
<button
|
|
key={opt.key}
|
|
onClick={() => handleChange(opt.key)}
|
|
className={cx(
|
|
'flex w-full items-center gap-2 px-3 py-2 text-xs font-medium transition duration-100 ease-linear',
|
|
opt.key === ozonetelState ? 'bg-active' : 'hover:bg-primary_hover',
|
|
)}
|
|
>
|
|
<FontAwesomeIcon icon={faCircle} className={cx('size-2', opt.dotColor)} />
|
|
<span className={opt.color}>{opt.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|