mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: Phase 1 — agent status toggle, global search, enquiry form
- Agent status toggle: Ready/Break/Training/Offline with Ozonetel sync - Global search: cross-entity search (leads + patients + appointments) via sidecar - General enquiry form: capture caller questions during calls - Button standard: icon-only for toggles, text+icon for primary actions - Sidecar: agent-state endpoint, search module with platform queries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
102
src/components/call-desk/agent-status-toggle.tsx
Normal file
102
src/components/call-desk/agent-status-toggle.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCircle, faChevronDown } from '@fortawesome/pro-duotone-svg-icons';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { notify } from '@/lib/toast';
|
||||
import { cx } from '@/utils/cx';
|
||||
|
||||
type AgentStatus = 'ready' | 'break' | 'training' | 'offline';
|
||||
|
||||
const statusConfig: Record<AgentStatus, { 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' },
|
||||
offline: { label: 'Offline', color: 'text-tertiary', dotColor: 'text-fg-quaternary' },
|
||||
};
|
||||
|
||||
type AgentStatusToggleProps = {
|
||||
isRegistered: boolean;
|
||||
connectionStatus: string;
|
||||
};
|
||||
|
||||
export const AgentStatusToggle = ({ isRegistered, connectionStatus }: AgentStatusToggleProps) => {
|
||||
const [status, setStatus] = useState<AgentStatus>(isRegistered ? 'ready' : 'offline');
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [changing, setChanging] = useState(false);
|
||||
|
||||
const handleChange = async (newStatus: AgentStatus) => {
|
||||
setMenuOpen(false);
|
||||
if (newStatus === status) return;
|
||||
setChanging(true);
|
||||
|
||||
try {
|
||||
if (newStatus === 'ready') {
|
||||
await apiClient.post('/api/ozonetel/agent-state', { state: 'Ready' });
|
||||
} else if (newStatus === 'offline') {
|
||||
await apiClient.post('/api/ozonetel/agent-logout', {
|
||||
agentId: 'global',
|
||||
password: 'Test123$',
|
||||
});
|
||||
} else {
|
||||
const pauseReason = newStatus === 'break' ? 'Break' : 'Training';
|
||||
await apiClient.post('/api/ozonetel/agent-state', { state: 'Pause', pauseReason });
|
||||
}
|
||||
setStatus(newStatus);
|
||||
} catch {
|
||||
notify.error('Status Change Failed', 'Could not update agent status');
|
||||
} finally {
|
||||
setChanging(false);
|
||||
}
|
||||
};
|
||||
|
||||
// If SIP isn't connected, show connection status
|
||||
if (!isRegistered) {
|
||||
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">{connectionStatus}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const current = statusConfig[status];
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
disabled={changing}
|
||||
className={cx(
|
||||
'flex items-center gap-1.5 rounded-full bg-secondary px-3 py-1 transition duration-100 ease-linear',
|
||||
'hover:bg-secondary_hover cursor-pointer',
|
||||
changing && 'opacity-50',
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCircle} className={cx('size-2', current.dotColor)} />
|
||||
<span className={cx('text-xs font-medium', current.color)}>{current.label}</span>
|
||||
<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">
|
||||
{(Object.entries(statusConfig) as [AgentStatus, typeof current][]).map(([key, cfg]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => handleChange(key)}
|
||||
className={cx(
|
||||
'flex w-full items-center gap-2 px-3 py-2 text-xs font-medium transition duration-100 ease-linear',
|
||||
key === status ? 'bg-active' : 'hover:bg-primary_hover',
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCircle} className={cx('size-2', cfg.dotColor)} />
|
||||
<span className={cfg.color}>{cfg.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user