diff --git a/src/pages/live-monitor.tsx b/src/pages/live-monitor.tsx index 4938012..7de058e 100644 --- a/src/pages/live-monitor.tsx +++ b/src/pages/live-monitor.tsx @@ -1,12 +1,14 @@ import { useEffect, useMemo, useState } from 'react'; 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 { Badge } from '@/components/base/badges/badges'; -import { Button } from '@/components/base/buttons/button'; import { Table } from '@/components/application/table/table'; +import { BargeControls } from '@/components/call-desk/barge-controls'; import { apiClient } from '@/lib/api-client'; import { useData } from '@/providers/data-provider'; +import { formatShortDate } from '@/lib/format'; +import { cx } from '@/utils/cx'; type ActiveCall = { ucid: string; @@ -17,6 +19,18 @@ type ActiveCall = { 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 seconds = Math.max(0, Math.floor((Date.now() - new Date(startTime).getTime()) / 1000)); 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 }) => ( -
- -

{value}

-

{label}

+
+ +

{value}

+

{label}

); @@ -36,13 +50,23 @@ export const LiveMonitorPage = () => { const [activeCalls, setActiveCalls] = useState([]); const [loading, setLoading] = useState(true); const [tick, setTick] = useState(0); + const [selectedCall, setSelectedCall] = useState(null); + const [callerContext, setCallerContext] = useState(null); + const [contextLoading, setContextLoading] = useState(false); const { leads } = useData(); // Poll active calls every 5 seconds useEffect(() => { const fetchCalls = () => { apiClient.get('/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(() => {}) .finally(() => setLoading(false)); }; @@ -50,9 +74,9 @@ export const LiveMonitorPage = () => { fetchCalls(); const interval = setInterval(fetchCalls, 5000); return () => clearInterval(interval); - }, []); + }, [selectedCall?.ucid]); - // Tick every second to update duration counters + // Tick every second for duration display useEffect(() => { const interval = setInterval(() => setTick(t => t + 1), 1000); return () => clearInterval(interval); @@ -82,97 +106,256 @@ export const LiveMonitorPage = () => { 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 ( <> - + -
- {/* KPI Cards */} -
-
- - - +
+ {/* Left panel — KPIs + call list */} +
+ {/* KPI Cards */} +
+
+ + + +
+
+ + {/* Active Calls Table */} +
+

Active Calls

+ + {loading ? ( +
+

Loading...

+
+ ) : activeCalls.length === 0 ? ( +
+ +

No active calls

+

Active calls will appear here in real-time

+
+ ) : ( + + + + + + + + + + {(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 ( + handleSelectCall(call)} + > + + {call.agentId} + + +
+ {callerName && {callerName}} + {call.callerNumber} +
+
+ + {typeLabel} + + + {formatDuration(call.startTime)} + + + + {call.status} + + +
+ ); + }} +
+
+ )}
- {/* Active Calls Table */} -
-

Active Calls

- - {loading ? ( -
-

Loading...

+ {/* Right panel — context + barge controls */} +
+ {!selectedCall ? ( +
+ +

Select a call to monitor

+

Click on any active call to see context and connect

- ) : activeCalls.length === 0 ? ( -
- -

No active calls

-

Active calls will appear here in real-time

+ ) : contextLoading ? ( +
+

Loading caller context...

) : ( - - - - - - - - - - - {(call) => { - const callerName = resolveCallerName(call.callerNumber); - const typeLabel = call.callType === 'InBound' ? 'In' : 'Out'; - const typeColor = call.callType === 'InBound' ? 'blue' : 'brand'; +
+ {/* Caller header */} +
+
+
+ {(callerContext?.name ?? '?')[0].toUpperCase()} +
+
+

{callerContext?.name}

+

{callerContext?.phone}

+
+ {callerContext?.patientType && ( + + {callerContext.patientType === 'RETURNING' ? 'Returning' : 'New'} + + )} +
- return ( - - - {call.agentId} - - -
- {callerName && {callerName}} - {call.callerNumber} + {/* Source + status */} + {(callerContext?.source || callerContext?.status) && ( +
+ {callerContext.source && ( + {callerContext.source.replace(/_/g, ' ')} + )} + {callerContext.status && ( + {callerContext.status.replace(/_/g, ' ')} + )} +
+ )} + + {callerContext?.interestedService && ( +

Interested in: {callerContext.interestedService}

+ )} +
+ + {/* AI Summary */} + {callerContext?.aiSummary && ( +
+
+ + AI Insight +
+

{callerContext.aiSummary}

+
+ )} + + {/* Appointments */} + {callerContext?.appointments && callerContext.appointments.length > 0 && ( +
+
+ + Appointments +
+
+ {callerContext.appointments.map(appt => ( +
+
+ {appt.doctorName ?? 'Appointment'} + {appt.department && {appt.department}} + {appt.scheduledAt && ( + — {formatShortDate(appt.scheduledAt)} + )}
- - - {typeLabel} - - - {formatDuration(call.startTime)} - - - - {call.status} + + {(appt.status ?? 'Scheduled').replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, c => c.toUpperCase())} - - -
- - - -
-
- - ); - }} - -
+
+ ))} +
+
+ )} + + {/* Call info */} +
+
+ + Current Call +
+
+
Agent: {selectedCall.agentId}
+
Type: {selectedCall.callType === 'InBound' ? 'Inbound' : 'Outbound'}
+
Duration: {formatDuration(selectedCall.startTime)}
+
Status: {selectedCall.status}
+
+
+ + {/* Barge Controls */} +
+ { + // Keep selection visible but controls reset to idle/ended + }} + /> +
+
)}
- - {/* Monitoring hint */} - {activeCalls.length > 0 && ( -
-
- -

Select "Listen" on any active call to start monitoring

-

Agent will not be notified during listen mode

-
-
- )}
);