Files
helix-engage/src/components/call-desk/agent-status-toggle.tsx
saridsa2 c3c3f4b3d7 feat: worklist sorting, contextual disposition, context panel redesign, notifications
- 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>
2026-03-30 14:45:52 +05:30

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