diff --git a/src/components/call-desk/active-call-card.tsx b/src/components/call-desk/active-call-card.tsx index 28e98e1..b92a9bf 100644 --- a/src/components/call-desk/active-call-card.tsx +++ b/src/components/call-desk/active-call-card.tsx @@ -1,10 +1,11 @@ -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, useMemo } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPhone, faPhoneHangup, faMicrophone, faMicrophoneSlash, - faPause, faPlay, faCalendarPlus, + faPause, faPlay, faCalendarPlus, faPlus, faPenToSquare, faPhoneArrowRight, faRecordVinyl, faClipboardQuestion, } from '@fortawesome/pro-duotone-svg-icons'; +import { useData } from '@/providers/data-provider'; import { Button } from '@/components/base/buttons/button'; import { Badge } from '@/components/base/badges/badges'; import { useSetAtom } from 'jotai'; @@ -16,7 +17,7 @@ import type { CallAction } from './disposition-modal'; import { AppointmentForm } from './appointment-form'; import { TransferDialog } from './transfer-dialog'; import { EnquiryForm } from './enquiry-form'; -import { formatPhone } from '@/lib/format'; +import { formatPhone, formatShortDate } from '@/lib/format'; import { apiClient } from '@/lib/api-client'; import { useAuth } from '@/providers/auth-provider'; import { useAgentState } from '@/hooks/use-agent-state'; @@ -44,6 +45,10 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete const setCallerNumber = useSetAtom(sipCallerNumberAtom); const setCallUcid = useSetAtom(sipCallUcidAtom); const [appointmentOpen, setAppointmentOpen] = useState(false); + // Which existing appointment is being edited (null = creating a new one). + // The Book Appt drawer shows pills: [+ New] + one per upcoming appointment. + // Clicking Edit on a pill sets this; clicking + New clears it. + const [editingApptId, setEditingApptId] = useState(null); const [transferOpen, setTransferOpen] = useState(false); const [recordingPaused, setRecordingPaused] = useState(false); const [enquiryOpen, setEnquiryOpen] = useState(false); @@ -62,6 +67,28 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete }); }; + // Upcoming appointments for this caller (if returning patient) — drives + // the pill row above AppointmentForm so the agent can edit existing + // bookings in addition to creating new ones. + const { appointments } = useData(); + const leadAppointments = useMemo(() => { + const patientId = (lead as any)?.patientId; + if (!patientId) return []; + return appointments + .filter((a) => + a.patientId === patientId + && a.appointmentStatus !== 'CANCELLED' + && a.appointmentStatus !== 'NO_SHOW' + && a.appointmentStatus !== 'COMPLETED', + ) + .sort((a, b) => new Date(a.scheduledAt ?? '').getTime() - new Date(b.scheduledAt ?? '').getTime()); + }, [appointments, lead]); + + const editingAppt = useMemo( + () => (editingApptId ? leadAppointments.find((a) => a.id === editingApptId) ?? null : null), + [leadAppointments, editingApptId], + ); + const agentConfig = localStorage.getItem('helix_agent_config'); const agentIdForState = agentConfig ? (() => { try { return JSON.parse(agentConfig).ozonetelAgentId; } catch { return null; } })() : null; const { supervisorPresence } = useAgentState(agentIdForState); @@ -335,14 +362,76 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete }} /> )} + {appointmentOpen && leadAppointments.length > 0 && ( +
+ + {leadAppointments.map((appt) => ( +
+
+ + {appt.scheduledAt ? formatShortDate(appt.scheduledAt) : 'No date'} + + + {appt.doctorName ?? 'Doctor'} + +
+ +
+ ))} +
+ )} + { + setAppointmentOpen(open); + if (!open) setEditingApptId(null); + }} callerNumber={callerPhone} leadName={fullName || null} leadId={lead?.id ?? null} patientId={(lead as any)?.patientId ?? null} - onSaved={handleAppointmentSaved} + existingAppointment={editingAppt ? { + id: editingAppt.id, + scheduledAt: editingAppt.scheduledAt ?? '', + doctorName: editingAppt.doctorName ?? '', + doctorId: editingAppt.doctorId ?? undefined, + department: editingAppt.department ?? '', + clinicId: editingAppt.clinicId ?? undefined, + reasonForVisit: editingAppt.reasonForVisit ?? undefined, + status: editingAppt.appointmentStatus ?? 'SCHEDULED', + } : null} + onSaved={(outcome) => { + setEditingApptId(null); + handleAppointmentSaved(outcome); + }} /> }, [messages, onChatStart]); // Auto-fire a patient-summary request when a caller with a leadId appears - // on the panel. Resets whenever the caller changes (new incoming call) so - // each call starts fresh. The sidecar's AI agent inspects the leadId and - // replies with appointment/disposition/notes history when the caller is - // a returning patient, or a brief "net-new caller" ack otherwise. + // on the panel. Resets whenever the caller changes (new incoming call) or + // the call ends (leadId clears), so each call starts fresh. The sidecar's + // AI agent inspects the leadId and replies with appointment/disposition/ + // notes history when the caller is a returning patient. const autoFiredForLeadRef = useRef(null); useEffect(() => { const leadId = callerContext?.leadId ?? null; - if (!leadId) return; + + // Call ended or no caller — wipe the panel so the next caller's + // context doesn't bleed over and the agent isn't staring at a stale + // summary in the worklist view between calls. + if (!leadId) { + if (autoFiredForLeadRef.current !== null) { + autoFiredForLeadRef.current = null; + setMessages([]); + chatStartedRef.current = false; + } + return; + } + if (autoFiredForLeadRef.current === leadId) return; // New caller — clear any prior chat state and fire the summary prompt.