feat(call-desk): lock patient name field behind explicit edit + confirm

Fixes the long-standing bug where the Appointment and Enquiry forms
silently overwrote existing patients' names with whatever happened to
be in the form's patient-name input. Before this change, an agent who
accidentally typed over the pre-filled name (or deliberately typed a
different name while booking on behalf of a relative) would rename
the patient across the entire workspace on save. The corruption
cascaded into past appointments, lead history, the AI summary, and
the Redis caller-resolution cache. This was the root cause of the
"Priya Sharma shows as Satya Sharma" incident on staging.

Root cause: appointment-form.tsx:249-278 and enquiry-form.tsx:107-117
fired updatePatient + updateLead.contactName unconditionally on every
save. Nothing distinguished "stub patient with no name yet" from
"existing patient whose name just needs this appointment booked".

Fix — lock-by-default with explicit unlock:

- src/components/modals/edit-patient-confirm-modal.tsx (new):
  generic reusable confirmation modal for any destructive edit to a
  patient's record. Accepts title/description/confirmLabel with
  sensible defaults so the call-desk forms can pass a name-specific
  description, and any future page that needs a "are you sure you
  want to change this patient field?" confirm can reuse it without
  building its own modal. Styled to match the sign-out confirmation
  in sidebar.tsx — warning circle, primary-destructive confirm button.

- src/components/call-desk/appointment-form.tsx:
  - New state: isNameEditable (default false when leadName is
    non-empty; true for first-time callers with no prior name to
    protect) + editConfirmOpen.
  - Name input renders disabled + shows an Edit button next to it
    when locked.
  - Edit button opens EditPatientConfirmModal. Confirm unlocks the
    field for the rest of the form session.
  - Save logic gates updatePatient / updateLead.contactName behind
    `isNameEditable && trimmedName.length > 0 && trimmedName !==
    initialLeadName`. Empty / same-as-initial values never trigger
    the rename chain, even if the field was unlocked.
  - On a real rename, fires POST /api/lead/:id/enrich to regenerate
    the AI summary against the corrected identity (phone passed in
    the body so the sidecar also invalidates the caller-resolution
    cache). Non-rename saves just invalidate the cache via the
    existing /api/caller/invalidate endpoint so status +
    lastContacted updates propagate.
  - Bundled fix: renamed `leadStatus: 'APPOINTMENT_SET'` →
    `status: 'APPOINTMENT_SET'` and `lastContactedAt` →
    `lastContacted` in the updateLead payload. The old field names
    are rejected by the staging platform schema and were causing the
    "Query failed: Field leadStatus is not defined by type
    LeadUpdateInput" toast on every appointment save.

- src/components/call-desk/enquiry-form.tsx:
  - Same lock + Edit + modal pattern as the appointment form.
  - Added leadName prop (the form previously didn't receive one).
  - Gated updatePatient behind the nameChanged check.
  - Gated lead.contactName in updateLead behind the same check.
  - Hooks the enrich endpoint on rename; cache invalidate otherwise.
  - Status + interestedService + source still update on every save
    (those are genuinely about this enquiry, not identity).

- src/components/call-desk/active-call-card.tsx: passes
  leadName={fullName || null} to EnquiryForm so the form can
  pre-populate + lock by default.

Behavior summary:
- New caller, no prior name: field unlocked, agent types, save runs
  the full chain (correct — this IS the name).
- Existing caller, agent leaves name alone: field locked, Save
  creates appointment/enquiry + updates lead status/lastContacted +
  invalidates cache. Zero risk of patient/lead rename.
- Existing caller, agent clicks Edit, confirms modal, changes name,
  Save: full rename chain runs — updatePatient + updateLead +
  /api/lead/:id/enrich + cache invalidate. The only code path that
  can mutate a linked patient's name, and it requires two explicit
  clicks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 13:54:22 +05:30
parent a287a97fe4
commit efe67dc28b
4 changed files with 277 additions and 29 deletions

View File

@@ -306,6 +306,7 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
isOpen={enquiryOpen}
onOpenChange={setEnquiryOpen}
callerPhone={callerPhone}
leadName={fullName || null}
leadId={lead?.id ?? null}
patientId={(lead as any)?.patientId ?? null}
agentName={user.name}