From 85976803a1c622429d25d213c9ce34570fdd207f Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Sat, 18 Apr 2026 05:20:55 +0530 Subject: [PATCH] =?UTF-8?q?fix:=20unify=20appointment=20data=20source=20?= =?UTF-8?q?=E2=80=94=20single=20DataProvider,=20immediate=20refresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - appointments-v2: migrated from local query/state to useData().appointments. Removed AppointmentRecord type, QUERY, fetchAppointments(), local useState. All field references updated to transformed Appointment type (appointmentStatus, patientName, patientPhone, clinicName, doctorId). - active-call-card: calls refresh() after appointment book/reschedule/cancel so pills update immediately. Also invalidates sidecar Redis cache. - One source of truth — all appointment consumers read from DataProvider. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/call-desk/active-call-card.tsx | 7 +- src/pages/appointments-v2.tsx | 130 ++++-------------- 2 files changed, 36 insertions(+), 101 deletions(-) diff --git a/src/components/call-desk/active-call-card.tsx b/src/components/call-desk/active-call-card.tsx index 4e9419a..2c14393 100644 --- a/src/components/call-desk/active-call-card.tsx +++ b/src/components/call-desk/active-call-card.tsx @@ -71,7 +71,7 @@ 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 { appointments, refresh } = useData(); const leadAppointments = useMemo(() => { const patientId = (lead as any)?.patientId; if (!patientId) return []; @@ -180,6 +180,11 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete const handleAppointmentSaved = (outcome: 'BOOKED' | 'RESCHEDULED' | 'CANCELLED') => { setAppointmentOpen(false); + refresh(); + // Invalidate sidecar's caller context cache so AI gets fresh appointment data + if (lead?.id) { + apiClient.post('/api/caller/invalidate-context', { leadId: lead.id }, { silent: true }).catch(() => {}); + } if (outcome === 'RESCHEDULED') { addActions('RESCHEDULE'); notify.success('Appointment Rescheduled'); diff --git a/src/pages/appointments-v2.tsx b/src/pages/appointments-v2.tsx index 60752c6..5fab0ad 100644 --- a/src/pages/appointments-v2.tsx +++ b/src/pages/appointments-v2.tsx @@ -1,4 +1,5 @@ -// Appointments v2 — lean table + detail side panel + reschedule + reminder +// Appointments v2 — lean table + detail side panel + reschedule +// Uses DataProvider as single source of truth for appointment data. import { useEffect, useMemo, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { @@ -12,7 +13,6 @@ import { Badge } from '@/components/base/badges/badges'; import { Input } from '@/components/base/input/input'; import { Table } from '@/components/application/table/table'; import { PaginationCardDefault } from '@/components/application/pagination/pagination'; -// TopBar replaced by inline header import { Button } from '@/components/base/buttons/button'; import { ModalOverlay, Modal, Dialog } from '@/components/application/modals/modal'; import { Select } from '@/components/base/select/select'; @@ -21,33 +21,11 @@ import { parseDate, today, getLocalTimeZone } from '@internationalized/date'; import { PhoneActionCell } from '@/components/call-desk/phone-action-cell'; import { PageHeader } from '@/components/layout/page-header'; import { formatPhone, formatDateOnly, formatTimeOnly } from '@/lib/format'; +import { useData } from '@/providers/data-provider'; import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; import { cx } from '@/utils/cx'; - -type AppointmentRecord = { - id: string; - scheduledAt: string | null; - durationMin: number | null; - appointmentType: string | null; - status: string | null; - doctorName: string | null; - department: string | null; - reasonForVisit: string | null; - patient: { - id: string; - fullName: { firstName: string; lastName: string } | null; - phones: { primaryPhoneNumber: string } | null; - } | null; - clinic: { - id?: string; - clinicName: string; - } | null; - doctor: { - id: string; - fullName?: { firstName: string; lastName: string } | null; - } | null; -}; +import type { Appointment } from '@/types/entities'; type StatusTab = 'all' | 'SCHEDULED' | 'COMPLETED' | 'CANCELLED' | 'RESCHEDULED'; @@ -69,26 +47,14 @@ const STATUS_LABELS: Record = { RESCHEDULED: 'Rescheduled', }; -const QUERY = `{ appointments(first: 200, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node { - id scheduledAt durationMin appointmentType status - doctorName department reasonForVisit - patient { id fullName { firstName lastName } phones { primaryPhoneNumber } } - clinic { id clinicName } - doctor { id fullName { firstName lastName } } -} } } }`; +const getPatientName = (appt: Appointment): string => + appt.patientName || 'Unknown'; -const getPatientName = (appt: AppointmentRecord): string => { - if (!appt.patient?.fullName) return 'Unknown'; - return `${appt.patient.fullName.firstName} ${appt.patient.fullName.lastName}`.trim() || 'Unknown'; -}; +const getPhone = (appt: Appointment): string => + appt.patientPhone ?? ''; -const getPhone = (appt: AppointmentRecord): string => - appt.patient?.phones?.primaryPhoneNumber ?? ''; - -// Can edit/reschedule: anything that isn't completed or cancelled -const canEdit = (appt: AppointmentRecord): boolean => { - return appt.status !== 'COMPLETED' && appt.status !== 'CANCELLED' && appt.status !== 'NO_SHOW'; -}; +const canEdit = (appt: Appointment): boolean => + appt.appointmentStatus !== 'COMPLETED' && appt.appointmentStatus !== 'CANCELLED' && appt.appointmentStatus !== 'NO_SHOW'; // ── Detail Panel ───────────────────────────────────────────────── const DetailRow = ({ icon, label, value }: { icon: any; label: string; value: string }) => ( @@ -106,7 +72,7 @@ const AppointmentDetailPanel = ({ onClose, onReschedule, }: { - appointment: AppointmentRecord; + appointment: Appointment; onClose: () => void; onReschedule: () => void; }) => { @@ -138,12 +104,11 @@ const AppointmentDetailPanel = ({
- - {STATUS_LABELS[appointment.status ?? ''] ?? appointment.status ?? '—'} + + {STATUS_LABELS[appointment.appointmentStatus ?? ''] ?? appointment.appointmentStatus ?? '—'}
- {/* Date & Time — 2 lines */}
@@ -159,7 +124,7 @@ const AppointmentDetailPanel = ({ - +
@@ -173,7 +138,6 @@ const AppointmentDetailPanel = ({
- {/* Reschedule confirm modal — same pattern as call desk */} { if (!open) setReschedulePromptOpen(false); }} @@ -186,7 +150,6 @@ const AppointmentDetailPanel = ({

Reschedule this appointment?

Choose "Yes, reschedule" to change the date, time, or doctor. - Choose "No, just view" to see the details without changing anything.

- {/* Department */}
Department
- {/* Date */}
Date *
- {/* Time slots */} {doctor && date && slots.length > 0 && (
Time Slot * @@ -396,7 +348,6 @@ const ReschedulePanel = ({

No available slots for this date

)} - {/* Chief Complaint */}
Chief Complaint