mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
fix: Book Appt pills + AI chat clears on call end
Book Appt defect (QA-559): no visible path to edit an existing appointment — the Upcoming section in the context panel collapses automatically when the AI auto-summary fires, hiding the Edit action. Fix: render appointment pills above the AppointmentForm drawer when the returning patient has upcoming appointments: [+ New] [Apr 24 · Dr. Harpreet Edit] [May 02 · Dr. Meena Edit] - Click [+ New] (default): empty form, create mode - Click Edit on a pill: form prefills with that appointment, edit mode - Closing the drawer resets the selected pill Separate defect: AI chat persisted after call ended — stale summary from the previous call stayed visible on the worklist. ai-chat-panel now wipes messages + resets the auto-fire guard when callerContext.leadId transitions to null (call dropped/released, no selected lead). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string | null>(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 && (
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingApptId(null)}
|
||||
className={cx(
|
||||
'flex items-center gap-1.5 rounded-lg border-2 px-3 py-2 text-xs font-semibold transition duration-100 ease-linear',
|
||||
!editingApptId
|
||||
? 'border-brand bg-brand-primary text-brand-secondary'
|
||||
: 'border-secondary bg-primary text-secondary hover:bg-primary_hover',
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="size-3" />
|
||||
New
|
||||
</button>
|
||||
{leadAppointments.map((appt) => (
|
||||
<div
|
||||
key={appt.id}
|
||||
className={cx(
|
||||
'flex items-center gap-2 rounded-lg border-2 px-3 py-2 text-xs',
|
||||
editingApptId === appt.id
|
||||
? 'border-brand bg-brand-primary'
|
||||
: 'border-secondary bg-primary',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-primary">
|
||||
{appt.scheduledAt ? formatShortDate(appt.scheduledAt) : 'No date'}
|
||||
</span>
|
||||
<span className="text-[11px] text-tertiary">
|
||||
{appt.doctorName ?? 'Doctor'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingApptId(appt.id)}
|
||||
className="flex items-center gap-1 rounded-md px-2 py-1 text-[11px] font-semibold text-brand-secondary hover:bg-brand-primary_hover transition duration-100 ease-linear"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPenToSquare} className="size-3" />
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AppointmentForm
|
||||
isOpen={appointmentOpen}
|
||||
onOpenChange={setAppointmentOpen}
|
||||
onOpenChange={(open) => {
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
|
||||
<EnquiryForm
|
||||
|
||||
@@ -50,14 +50,26 @@ export const AiChatPanel = ({ callerContext, onChatStart }: AiChatPanelProps) =>
|
||||
}, [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<string | null>(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.
|
||||
|
||||
Reference in New Issue
Block a user