import { useState, useEffect } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPhone, faUserDoctor, faHeadset, faShieldCheck, faMagnifyingGlass } from '@fortawesome/pro-duotone-svg-icons'; import { faIcon } from '@/lib/icon-wrapper'; import { Input } from '@/components/base/input/input'; import { Button } from '@/components/base/buttons/button'; import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; import { cx } from '@/utils/cx'; const SearchIcon = faIcon(faMagnifyingGlass); type TransferTarget = { id: string; name: string; type: 'agent' | 'supervisor' | 'doctor'; department?: string; phoneNumber: string; status?: 'ready' | 'busy' | 'offline' | 'on-call' | 'break'; }; type TransferDialogProps = { ucid: string; currentAgentId?: string; onClose: () => void; onTransferred: () => void; }; const statusConfig: Record = { ready: { label: 'Ready', dotClass: 'bg-success-solid' }, 'on-call': { label: 'On Call', dotClass: 'bg-error-solid' }, 'in-call': { label: 'On Call', dotClass: 'bg-error-solid' }, busy: { label: 'Busy', dotClass: 'bg-warning-solid' }, acw: { label: 'Wrapping', dotClass: 'bg-warning-solid' }, break: { label: 'Break', dotClass: 'bg-tertiary' }, training: { label: 'Training', dotClass: 'bg-tertiary' }, offline: { label: 'Offline', dotClass: 'bg-quaternary' }, }; const typeIcons = { agent: faHeadset, supervisor: faShieldCheck, doctor: faUserDoctor, }; export const TransferDialog = ({ ucid, currentAgentId, onClose, onTransferred }: TransferDialogProps) => { const [targets, setTargets] = useState([]); const [search, setSearch] = useState(''); const [loading, setLoading] = useState(true); const [transferring, setTransferring] = useState(false); const [selectedTarget, setSelectedTarget] = useState(null); const [connectedTarget, setConnectedTarget] = useState(null); // Fetch transfer targets useEffect(() => { const fetchTargets = async () => { try { const [agentsRes, doctorsRes] = await Promise.all([ apiClient.graphql(`{ agents(first: 20) { edges { node { id name ozonetelagentid sipextension } } } }`), apiClient.graphql(`{ doctors(first: 20) { edges { node { id name department phone { primaryPhoneNumber } } } } }`), ]); const agents: TransferTarget[] = (agentsRes.agents?.edges ?? []) .map((e: any) => e.node) .filter((a: any) => a.ozonetelagentid !== currentAgentId) .map((a: any) => ({ id: a.id, name: a.name, type: 'agent' as const, phoneNumber: `0${a.sipextension}`, status: 'offline' as const, })); const doctors: TransferTarget[] = (doctorsRes.doctors?.edges ?? []) .map((e: any) => e.node) .filter((d: any) => d.phone?.primaryPhoneNumber) .map((d: any) => ({ id: d.id, name: d.name, type: 'doctor' as const, department: d.department?.replace(/_/g, ' '), phoneNumber: `0${d.phone.primaryPhoneNumber}`, })); setTargets([...agents, ...doctors]); } catch (err) { console.warn('Failed to fetch transfer targets:', err); } finally { setLoading(false); } }; fetchTargets(); }, [currentAgentId]); // Subscribe to agent state via SSE for live status useEffect(() => { const agentTargets = targets.filter(t => t.type === 'agent'); if (agentTargets.length === 0) return; // Poll agent states from the supervisor endpoint const fetchStates = async () => { for (const agent of agentTargets) { try { const res = await apiClient.get(`/api/supervisor/agent-state/${agent.phoneNumber.replace(/^0/, '')}`, { silent: true }); if (res?.state) { setTargets(prev => prev.map(t => t.id === agent.id ? { ...t, status: res.state } : t, )); } } catch { /* best effort */ } } }; fetchStates(); const interval = setInterval(fetchStates, 10000); return () => clearInterval(interval); }, [targets.length]); const filtered = search.trim() ? targets.filter(t => t.name.toLowerCase().includes(search.toLowerCase()) || (t.department ?? '').toLowerCase().includes(search.toLowerCase())) : targets; const agents = filtered.filter(t => t.type === 'agent'); const doctors = filtered.filter(t => t.type === 'doctor'); const handleConnect = async () => { const target = selectedTarget; if (!target) return; setTransferring(true); try { await apiClient.post('/api/ozonetel/call-control', { action: 'CONFERENCE', ucid, conferenceNumber: target.phoneNumber, }); setConnectedTarget(target); notify.success('Connected', `Speaking with ${target.name}. Click Complete to transfer.`); } catch { notify.error('Transfer Failed', `Could not connect to ${target.name}`); } finally { setTransferring(false); } }; const handleComplete = async () => { if (!connectedTarget) return; setTransferring(true); try { await apiClient.post('/api/ozonetel/call-control', { action: 'KICK_CALL', ucid, conferenceNumber: connectedTarget.phoneNumber, }); notify.success('Transferred', `Call transferred to ${connectedTarget.name}`); onTransferred(); } catch { notify.error('Transfer Failed', 'Could not complete transfer'); } finally { setTransferring(false); } }; const handleCancel = async () => { if (!connectedTarget) { onClose(); return; } // Disconnect the third party, keep the caller setTransferring(true); try { await apiClient.post('/api/ozonetel/call-control', { action: 'KICK_CALL', ucid, conferenceNumber: connectedTarget.phoneNumber, }); setConnectedTarget(null); notify.info('Cancelled', 'Transfer cancelled, caller reconnected'); } catch { notify.error('Error', 'Could not disconnect third party'); } finally { setTransferring(false); } }; // Connected state — show target + complete/cancel buttons if (connectedTarget) { return (

Connected to {connectedTarget.name}

Speak privately, then complete the transfer

); } // Target selection return (
{/* Search + actions — pinned */}
{/* Scrollable target list */}
{loading ? (

Loading...

) : ( <> {/* Agents */} {agents.length > 0 && (

Agents

{agents.map(agent => { const st = statusConfig[agent.status ?? 'offline'] ?? statusConfig.offline; const isSelected = selectedTarget?.id === agent.id; return ( ); })}
)} {/* Doctors */} {doctors.length > 0 && (

Doctors

{doctors.map(doc => { const isSelected = selectedTarget?.id === doc.id; return ( ); })}
)} {filtered.length === 0 && !loading && (

No matching targets

)} )}
); };