mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-14 20:12:25 +00:00
feat(frontend): live monitor split layout with context panel and barge
Left panel: KPI cards + clickable call table (row selection highlights). Right panel (380px): caller context (name, phone, source, AI summary, appointments) + BargeControls component. Fetches lead data by phone match on selection. Auto-clears when selected call ends. Removed disabled Listen/Whisper/Barge buttons — replaced with integrated barge drawer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faHeadset, faPhoneVolume, faPause, faClock } from '@fortawesome/pro-duotone-svg-icons';
|
import { faHeadset, faPhoneVolume, faPause, faClock, faSparkles, faCalendarCheck, faClockRotateLeft } from '@fortawesome/pro-duotone-svg-icons';
|
||||||
import { TopBar } from '@/components/layout/top-bar';
|
import { TopBar } from '@/components/layout/top-bar';
|
||||||
import { Badge } from '@/components/base/badges/badges';
|
import { Badge } from '@/components/base/badges/badges';
|
||||||
import { Button } from '@/components/base/buttons/button';
|
|
||||||
import { Table } from '@/components/application/table/table';
|
import { Table } from '@/components/application/table/table';
|
||||||
|
import { BargeControls } from '@/components/call-desk/barge-controls';
|
||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient } from '@/lib/api-client';
|
||||||
import { useData } from '@/providers/data-provider';
|
import { useData } from '@/providers/data-provider';
|
||||||
|
import { formatShortDate } from '@/lib/format';
|
||||||
|
import { cx } from '@/utils/cx';
|
||||||
|
|
||||||
type ActiveCall = {
|
type ActiveCall = {
|
||||||
ucid: string;
|
ucid: string;
|
||||||
@@ -17,6 +19,18 @@ type ActiveCall = {
|
|||||||
status: 'active' | 'on-hold';
|
status: 'active' | 'on-hold';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CallerContext = {
|
||||||
|
name: string;
|
||||||
|
phone: string;
|
||||||
|
source: string | null;
|
||||||
|
status: string | null;
|
||||||
|
interestedService: string | null;
|
||||||
|
aiSummary: string | null;
|
||||||
|
patientType: string | null;
|
||||||
|
leadId: string | null;
|
||||||
|
appointments: Array<{ id: string; scheduledAt: string; doctorName: string; department: string; status: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
const formatDuration = (startTime: string): string => {
|
const formatDuration = (startTime: string): string => {
|
||||||
const seconds = Math.max(0, Math.floor((Date.now() - new Date(startTime).getTime()) / 1000));
|
const seconds = Math.max(0, Math.floor((Date.now() - new Date(startTime).getTime()) / 1000));
|
||||||
const m = Math.floor(seconds / 60);
|
const m = Math.floor(seconds / 60);
|
||||||
@@ -25,10 +39,10 @@ const formatDuration = (startTime: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const KpiCard = ({ value, label, icon }: { value: string | number; label: string; icon: any }) => (
|
const KpiCard = ({ value, label, icon }: { value: string | number; label: string; icon: any }) => (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center rounded-xl border border-secondary bg-primary py-6">
|
<div className="flex flex-1 flex-col items-center justify-center rounded-xl border border-secondary bg-primary py-4">
|
||||||
<FontAwesomeIcon icon={icon} className="size-5 text-fg-quaternary mb-2" />
|
<FontAwesomeIcon icon={icon} className="size-4 text-fg-quaternary mb-1" />
|
||||||
<p className="text-3xl font-bold text-primary">{value}</p>
|
<p className="text-xl font-bold text-primary">{value}</p>
|
||||||
<p className="text-xs text-tertiary mt-1">{label}</p>
|
<p className="text-[11px] text-tertiary">{label}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -36,13 +50,23 @@ export const LiveMonitorPage = () => {
|
|||||||
const [activeCalls, setActiveCalls] = useState<ActiveCall[]>([]);
|
const [activeCalls, setActiveCalls] = useState<ActiveCall[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [tick, setTick] = useState(0);
|
const [tick, setTick] = useState(0);
|
||||||
|
const [selectedCall, setSelectedCall] = useState<ActiveCall | null>(null);
|
||||||
|
const [callerContext, setCallerContext] = useState<CallerContext | null>(null);
|
||||||
|
const [contextLoading, setContextLoading] = useState(false);
|
||||||
const { leads } = useData();
|
const { leads } = useData();
|
||||||
|
|
||||||
// Poll active calls every 5 seconds
|
// Poll active calls every 5 seconds
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCalls = () => {
|
const fetchCalls = () => {
|
||||||
apiClient.get<ActiveCall[]>('/api/supervisor/active-calls', { silent: true })
|
apiClient.get<ActiveCall[]>('/api/supervisor/active-calls', { silent: true })
|
||||||
.then(setActiveCalls)
|
.then(calls => {
|
||||||
|
setActiveCalls(calls);
|
||||||
|
// If selected call ended, clear selection
|
||||||
|
if (selectedCall && !calls.find(c => c.ucid === selectedCall.ucid)) {
|
||||||
|
setSelectedCall(null);
|
||||||
|
setCallerContext(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
@@ -50,9 +74,9 @@ export const LiveMonitorPage = () => {
|
|||||||
fetchCalls();
|
fetchCalls();
|
||||||
const interval = setInterval(fetchCalls, 5000);
|
const interval = setInterval(fetchCalls, 5000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, [selectedCall?.ucid]);
|
||||||
|
|
||||||
// Tick every second to update duration counters
|
// Tick every second for duration display
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => setTick(t => t + 1), 1000);
|
const interval = setInterval(() => setTick(t => t + 1), 1000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
@@ -82,97 +106,256 @@ export const LiveMonitorPage = () => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch caller context when a call is selected
|
||||||
|
const handleSelectCall = (call: ActiveCall) => {
|
||||||
|
setSelectedCall(call);
|
||||||
|
setContextLoading(true);
|
||||||
|
setCallerContext(null);
|
||||||
|
|
||||||
|
const phoneClean = call.callerNumber.replace(/\D/g, '');
|
||||||
|
|
||||||
|
// Search for lead by phone
|
||||||
|
apiClient.graphql<{ leads: { edges: Array<{ node: any }> } }>(
|
||||||
|
`{ leads(first: 5, filter: { contactPhone: { primaryPhoneNumber: { like: "%${phoneClean.slice(-10)}" } } }) { edges { node {
|
||||||
|
id contactName { firstName lastName } source status interestedService aiSummary patientId
|
||||||
|
} } } }`,
|
||||||
|
).then(async (data) => {
|
||||||
|
const lead = data.leads.edges[0]?.node;
|
||||||
|
const name = lead
|
||||||
|
? `${lead.contactName?.firstName ?? ''} ${lead.contactName?.lastName ?? ''}`.trim()
|
||||||
|
: resolveCallerName(call.callerNumber) ?? 'Unknown Caller';
|
||||||
|
|
||||||
|
let appointments: CallerContext['appointments'] = [];
|
||||||
|
if (lead?.patientId) {
|
||||||
|
try {
|
||||||
|
const apptData = await apiClient.graphql<{ appointments: { edges: Array<{ node: any }> } }>(
|
||||||
|
`{ appointments(first: 5, filter: { patientId: { eq: "${lead.patientId}" } }, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node {
|
||||||
|
id scheduledAt doctorName department status
|
||||||
|
} } } }`,
|
||||||
|
);
|
||||||
|
appointments = apptData.appointments.edges.map(e => e.node);
|
||||||
|
} catch { /* best effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
setCallerContext({
|
||||||
|
name,
|
||||||
|
phone: call.callerNumber,
|
||||||
|
source: lead?.source ?? null,
|
||||||
|
status: lead?.status ?? null,
|
||||||
|
interestedService: lead?.interestedService ?? null,
|
||||||
|
aiSummary: lead?.aiSummary ?? null,
|
||||||
|
patientType: lead?.patientId ? 'RETURNING' : 'NEW',
|
||||||
|
leadId: lead?.id ?? null,
|
||||||
|
appointments,
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
setCallerContext({
|
||||||
|
name: resolveCallerName(call.callerNumber) ?? 'Unknown Caller',
|
||||||
|
phone: call.callerNumber,
|
||||||
|
source: null, status: null, interestedService: null,
|
||||||
|
aiSummary: null, patientType: null, leadId: null, appointments: [],
|
||||||
|
});
|
||||||
|
}).finally(() => setContextLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar title="Live Call Monitor" subtitle="Listen, whisper, or barge into active calls" />
|
<TopBar title="Live Call Monitor" subtitle="Monitor, whisper, or barge into active calls" />
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* KPI Cards */}
|
{/* Left panel — KPIs + call list */}
|
||||||
<div className="px-6 pt-5">
|
<div className="flex flex-1 flex-col overflow-y-auto border-r border-secondary">
|
||||||
<div className="flex gap-4">
|
{/* KPI Cards */}
|
||||||
<KpiCard value={activeCalls.length} label="Active Calls" icon={faPhoneVolume} />
|
<div className="px-5 pt-4">
|
||||||
<KpiCard value={onHold} label="On Hold" icon={faPause} />
|
<div className="flex gap-3">
|
||||||
<KpiCard value={avgDuration} label="Avg Duration" icon={faClock} />
|
<KpiCard value={activeCalls.length} label="Active Calls" icon={faPhoneVolume} />
|
||||||
|
<KpiCard value={onHold} label="On Hold" icon={faPause} />
|
||||||
|
<KpiCard value={avgDuration} label="Avg Duration" icon={faClock} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active Calls Table */}
|
||||||
|
<div className="px-5 pt-5 pb-4">
|
||||||
|
<h3 className="text-sm font-semibold text-secondary mb-3">Active Calls</h3>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<p className="text-sm text-tertiary">Loading...</p>
|
||||||
|
</div>
|
||||||
|
) : activeCalls.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-16 text-center rounded-xl border border-secondary bg-primary">
|
||||||
|
<FontAwesomeIcon icon={faHeadset} className="size-12 text-fg-quaternary mb-4" />
|
||||||
|
<p className="text-sm font-medium text-secondary">No active calls</p>
|
||||||
|
<p className="text-xs text-tertiary mt-1">Active calls will appear here in real-time</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Table size="sm">
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Head label="Agent" isRowHeader />
|
||||||
|
<Table.Head label="Caller" />
|
||||||
|
<Table.Head label="Type" className="w-16" />
|
||||||
|
<Table.Head label="Duration" className="w-20" />
|
||||||
|
<Table.Head label="Status" className="w-24" />
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body items={activeCalls}>
|
||||||
|
{(call) => {
|
||||||
|
const callerName = resolveCallerName(call.callerNumber);
|
||||||
|
const typeLabel = call.callType === 'InBound' ? 'In' : 'Out';
|
||||||
|
const typeColor = call.callType === 'InBound' ? 'blue' : 'brand';
|
||||||
|
const isSelected = selectedCall?.ucid === call.ucid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.Row
|
||||||
|
id={call.ucid}
|
||||||
|
className={cx(
|
||||||
|
'cursor-pointer transition duration-100 ease-linear',
|
||||||
|
isSelected ? 'bg-active' : 'hover:bg-primary_hover',
|
||||||
|
)}
|
||||||
|
onAction={() => handleSelectCall(call)}
|
||||||
|
>
|
||||||
|
<Table.Cell>
|
||||||
|
<span className="text-sm font-medium text-primary">{call.agentId}</span>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<div>
|
||||||
|
{callerName && <span className="text-sm font-medium text-primary block">{callerName}</span>}
|
||||||
|
<span className="text-xs text-tertiary">{call.callerNumber}</span>
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Badge size="sm" color={typeColor} type="pill-color">{typeLabel}</Badge>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<span className="text-sm font-mono text-primary">{formatDuration(call.startTime)}</span>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Badge size="sm" color={call.status === 'on-hold' ? 'warning' : 'success'} type="pill-color">
|
||||||
|
{call.status}
|
||||||
|
</Badge>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Active Calls Table */}
|
{/* Right panel — context + barge controls */}
|
||||||
<div className="px-6 pt-6">
|
<div className="flex w-[380px] shrink-0 flex-col overflow-y-auto bg-primary">
|
||||||
<h3 className="text-sm font-semibold text-secondary mb-3">Active Calls</h3>
|
{!selectedCall ? (
|
||||||
|
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
|
||||||
{loading ? (
|
<FontAwesomeIcon icon={faHeadset} className="size-10 text-fg-quaternary" />
|
||||||
<div className="flex items-center justify-center py-12">
|
<p className="text-sm font-medium text-secondary">Select a call to monitor</p>
|
||||||
<p className="text-sm text-tertiary">Loading...</p>
|
<p className="text-xs text-tertiary">Click on any active call to see context and connect</p>
|
||||||
</div>
|
</div>
|
||||||
) : activeCalls.length === 0 ? (
|
) : contextLoading ? (
|
||||||
<div className="flex flex-col items-center justify-center py-16 text-center rounded-xl border border-secondary bg-primary">
|
<div className="flex flex-1 items-center justify-center">
|
||||||
<FontAwesomeIcon icon={faHeadset} className="size-12 text-fg-quaternary mb-4" />
|
<p className="text-sm text-tertiary">Loading caller context...</p>
|
||||||
<p className="text-sm font-medium text-secondary">No active calls</p>
|
|
||||||
<p className="text-xs text-tertiary mt-1">Active calls will appear here in real-time</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table size="sm">
|
<div className="flex flex-col gap-4 p-4">
|
||||||
<Table.Header>
|
{/* Caller header */}
|
||||||
<Table.Head label="Agent" isRowHeader />
|
<div className="rounded-xl border border-secondary bg-primary p-4">
|
||||||
<Table.Head label="Caller" />
|
<div className="flex items-center gap-3">
|
||||||
<Table.Head label="Type" className="w-16" />
|
<div className="flex size-10 items-center justify-center rounded-full bg-brand-secondary text-sm font-bold text-fg-white">
|
||||||
<Table.Head label="Duration" className="w-20" />
|
{(callerContext?.name ?? '?')[0].toUpperCase()}
|
||||||
<Table.Head label="Status" className="w-24" />
|
</div>
|
||||||
<Table.Head label="Actions" className="w-48" />
|
<div className="min-w-0 flex-1">
|
||||||
</Table.Header>
|
<p className="text-sm font-semibold text-primary truncate">{callerContext?.name}</p>
|
||||||
<Table.Body items={activeCalls}>
|
<p className="text-xs text-tertiary">{callerContext?.phone}</p>
|
||||||
{(call) => {
|
</div>
|
||||||
const callerName = resolveCallerName(call.callerNumber);
|
{callerContext?.patientType && (
|
||||||
const typeLabel = call.callType === 'InBound' ? 'In' : 'Out';
|
<Badge size="sm" color={callerContext.patientType === 'RETURNING' ? 'brand' : 'gray'} type="pill-color">
|
||||||
const typeColor = call.callType === 'InBound' ? 'blue' : 'brand';
|
{callerContext.patientType === 'RETURNING' ? 'Returning' : 'New'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
return (
|
{/* Source + status */}
|
||||||
<Table.Row id={call.ucid}>
|
{(callerContext?.source || callerContext?.status) && (
|
||||||
<Table.Cell>
|
<div className="mt-2 flex flex-wrap gap-1">
|
||||||
<span className="text-sm font-medium text-primary">{call.agentId}</span>
|
{callerContext.source && (
|
||||||
</Table.Cell>
|
<Badge size="sm" color="gray" type="pill-color">{callerContext.source.replace(/_/g, ' ')}</Badge>
|
||||||
<Table.Cell>
|
)}
|
||||||
<div>
|
{callerContext.status && (
|
||||||
{callerName && <span className="text-sm font-medium text-primary block">{callerName}</span>}
|
<Badge size="sm" color="brand" type="pill-color">{callerContext.status.replace(/_/g, ' ')}</Badge>
|
||||||
<span className="text-xs text-tertiary">{call.callerNumber}</span>
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{callerContext?.interestedService && (
|
||||||
|
<p className="mt-2 text-xs text-tertiary">Interested in: {callerContext.interestedService}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AI Summary */}
|
||||||
|
{callerContext?.aiSummary && (
|
||||||
|
<div className="rounded-xl border border-secondary bg-secondary_alt p-3">
|
||||||
|
<div className="flex items-center gap-1 mb-1">
|
||||||
|
<FontAwesomeIcon icon={faSparkles} className="size-3 text-fg-brand-primary" />
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-wider text-brand-secondary">AI Insight</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs leading-relaxed text-primary">{callerContext.aiSummary}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Appointments */}
|
||||||
|
{callerContext?.appointments && callerContext.appointments.length > 0 && (
|
||||||
|
<div className="rounded-xl border border-secondary bg-primary p-3">
|
||||||
|
<div className="flex items-center gap-1 mb-2">
|
||||||
|
<FontAwesomeIcon icon={faCalendarCheck} className="size-3 text-fg-brand-primary" />
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-wider text-tertiary">Appointments</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{callerContext.appointments.map(appt => (
|
||||||
|
<div key={appt.id} className="flex items-center gap-2 rounded-lg bg-secondary px-2.5 py-2">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<span className="text-xs font-medium text-primary">{appt.doctorName ?? 'Appointment'}</span>
|
||||||
|
{appt.department && <span className="text-[11px] text-tertiary ml-1">{appt.department}</span>}
|
||||||
|
{appt.scheduledAt && (
|
||||||
|
<span className="text-[11px] text-tertiary ml-1">— {formatShortDate(appt.scheduledAt)}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Table.Cell>
|
<Badge size="sm" color={appt.status === 'COMPLETED' ? 'success' : appt.status === 'CANCELLED' ? 'error' : 'brand'} type="pill-color">
|
||||||
<Table.Cell>
|
{(appt.status ?? 'Scheduled').replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, c => c.toUpperCase())}
|
||||||
<Badge size="sm" color={typeColor} type="pill-color">{typeLabel}</Badge>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
<span className="text-sm font-mono text-primary">{formatDuration(call.startTime)}</span>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
<Badge size="sm" color={call.status === 'on-hold' ? 'warning' : 'success'} type="pill-color">
|
|
||||||
{call.status}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</Table.Cell>
|
</div>
|
||||||
<Table.Cell>
|
))}
|
||||||
<div className="flex items-center gap-1.5">
|
</div>
|
||||||
<Button size="sm" color="secondary" isDisabled title="Coming soon — pending Ozonetel API">Listen</Button>
|
</div>
|
||||||
<Button size="sm" color="secondary" isDisabled title="Coming soon — pending Ozonetel API">Whisper</Button>
|
)}
|
||||||
<Button size="sm" color="primary-destructive" isDisabled title="Coming soon — requires supervisor SIP extension">Barge</Button>
|
|
||||||
</div>
|
{/* Call info */}
|
||||||
</Table.Cell>
|
<div className="rounded-xl border border-secondary bg-primary p-3">
|
||||||
</Table.Row>
|
<div className="flex items-center gap-1 mb-2">
|
||||||
);
|
<FontAwesomeIcon icon={faClockRotateLeft} className="size-3 text-fg-quaternary" />
|
||||||
}}
|
<span className="text-[10px] font-bold uppercase tracking-wider text-tertiary">Current Call</span>
|
||||||
</Table.Body>
|
</div>
|
||||||
</Table>
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div><span className="text-tertiary">Agent:</span> <span className="font-medium text-primary">{selectedCall.agentId}</span></div>
|
||||||
|
<div><span className="text-tertiary">Type:</span> <span className="font-medium text-primary">{selectedCall.callType === 'InBound' ? 'Inbound' : 'Outbound'}</span></div>
|
||||||
|
<div><span className="text-tertiary">Duration:</span> <span className="font-mono font-medium text-primary">{formatDuration(selectedCall.startTime)}</span></div>
|
||||||
|
<div><span className="text-tertiary">Status:</span> <span className="font-medium text-primary">{selectedCall.status}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Barge Controls */}
|
||||||
|
<div className="rounded-xl border border-secondary bg-primary p-4">
|
||||||
|
<BargeControls
|
||||||
|
ucid={selectedCall.ucid}
|
||||||
|
agentId={selectedCall.agentId}
|
||||||
|
agentNumber={selectedCall.agentId}
|
||||||
|
agentName={selectedCall.agentId}
|
||||||
|
onDisconnected={() => {
|
||||||
|
// Keep selection visible but controls reset to idle/ended
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Monitoring hint */}
|
|
||||||
{activeCalls.length > 0 && (
|
|
||||||
<div className="px-6 pt-6 pb-8">
|
|
||||||
<div className="flex flex-col items-center justify-center py-8 rounded-xl border border-secondary bg-secondary_alt text-center">
|
|
||||||
<FontAwesomeIcon icon={faHeadset} className="size-8 text-fg-quaternary mb-3" />
|
|
||||||
<p className="text-sm text-secondary">Select "Listen" on any active call to start monitoring</p>
|
|
||||||
<p className="text-xs text-tertiary mt-1">Agent will not be notified during listen mode</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user