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:
2026-04-15 13:11:31 +05:30
parent 72cb192447
commit ffb8bcb6ad
2 changed files with 111 additions and 10 deletions

View File

@@ -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