Bug 558: Appointment edit view persisted in Patient 360 after Back to
Worklist. Closed as not-a-bug — the edit flow now lives inside the
unified Book Appt drawer, so the same button opens either path. Rename
makes the intent explicit:
- 'New Appt' when the caller has no upcoming appointments
- 'New / Reschedule Appt' when upcoming appointments exist (pills
inside the drawer let the agent pick which one to reschedule)
- leadAppointments now filters out past appointments — past dates
can't be rescheduled, so "14 Apr · Meena Patel" shouldn't appear in
the pill row today. Uses scheduledAt >= now.
- Click Edit pill → reschedule-confirm modal:
"Yes, reschedule" → form opens in edit mode (prefilled + editable)
"No, just view" → form opens read-only (prefilled + disabled)
- Prefill was broken — AppointmentForm's useState initializers only
run at mount, so switching pills didn't re-seed state. Added
key={editingApptId}-{apptMode} so the form fully remounts whenever
the selection or mode changes.
- Thread readOnly prop through every form control (patient name,
phone, age, gender, clinic, department, doctor, date, time slots,
chief complaint). In view mode all inputs are disabled and the
Update Appointment + Cancel Appointment buttons hide — only Close
remains.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Layout (P1-adjacent):
- context-panel switches to an edit-only layout when editingAppointment
is set. Previously AppointmentForm rendered inline BELOW the AI panel,
crushing the AI area into a ~2-line strip that made the returning-
patient summary + quick actions unusable. Edit view gets full height
with a "Back to context" button.
P2s:
- Remove Attempted sub-tab from Missed Calls worklist (Pending only).
- Add CALL_DROPPED disposition option + propagate through every
per-disposition Record<CallDisposition,...> map (incoming-call-card,
call-log, call-history, agent-detail, patient-360).
- Block SLA-gaming on unanswered calls: Book Appt / Enquiry / Transfer
buttons on active-call-card are disabled until the call reaches the
answered state (wasAnsweredRef). The disposition filter was already
in place; this closes the upstream entry.
- Data labels on performance charts: my-performance bar chart shows
value on top of each bar; donut shows {d}% slice labels; team-
performance day trend line shows per-point values.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fixed useEffect dependency bug: callState in deps caused cleanup
(disconnectSip) to fire on every state transition, killing SIP
mid-dial. Now uses useRef for callState in beforeunload handler
with empty deps array — cleanup only fires on unmount.
- sendBeacon auto-dispose now includes agentId from agent config
- Disposition modal submit now includes agentId from agent config
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: when an agent refreshes the page during or after a call,
the React state (UCID, callState, disposition modal) is wiped. The
SIP BYE event fires but no component exists to trigger the disposition
modal → no POST to /api/ozonetel/dispose → agent stuck in ACW.
Layer 1 (beforeunload warning):
Shows browser's native "Leave page?" dialog during active calls.
Agent can cancel and stay.
Layer 2 (sendBeacon auto-dispose):
UCID persisted to localStorage when call activates. On page unload,
navigator.sendBeacon fires /api/ozonetel/dispose with
CALLBACK_REQUESTED. Guaranteed delivery even during page death.
Cleared from localStorage when disposition modal submits normally.
Layer 3 lives in helix-engage-server (separate commit).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
- Appointment/enquiry forms reverted to inline rendering (not modals)
- Forms: flat scrollable section with pinned footer, no card wrapper
- Appointment form: DatePicker component, date prefilled, removed Returning Patient checkbox
- Enquiry form: removed disposition dropdown, lead status defaults to CONTACTED
- Transfer dialog: agent picker with live status, doctor list with department, select-then-connect flow
- Transfer: removed external number input, moved Cancel/Connect to pinned header row
- Button mutual exclusivity: Book Appt / Enquiry / Transfer close each other
- Patient name write-back: appointment + enquiry forms update patient fullName after save
- Caller cache invalidation: POST /api/caller/invalidate after name update
- Follow-up fix (#513): assignedAgent, patientId, date validation in createFollowUp
- Patients page: removed status filters + column, added pagination (15/page)
- Pending badge removed from call desk header
- Table resize handles visible (bg-tertiary pill)
- Sim call button: dev-only (import.meta.env.DEV)
- CallControlStrip component (reusable, not currently mounted)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Appointment form: converted from inline to modal dialog, removed Returning Patient checkbox
- Enquiry form: converted from inline to modal dialog
- Active call card: removed max-h-[50vh] scroll container, forms render as modals
- Team Performance: fallback agent list from call records when Ozonetel unavailable
- NPS/Time sections show placeholder when data unavailable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Changed $id: ID! to $id: UUID! in all update mutations (4 files)
- Removed redundant slot availability check (UI already disables booked slots)
- Book Appt and Enquiry act as toggle tabs — one closes the other
- Dialler moved from FAB to header dropdown next to status toggle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Disposition is the last step. After submission, handleReset() clears
all state and returns to worklist immediately. Removed the "Call Completed"
card, post-disposition appointment form, and "Skip" button.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Enquiry button + form available during disposition stage (not just active call)
- Skip & Return to Worklist button on post-call appointment booking
- AI chat scroll uses parentElement.scrollTop instead of scrollIntoView
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AppShell: h-screen + overflow-hidden for pinned header
- AI chat: input pinned to bottom, messages scroll independently
- Dialler: numpad grid (1-9,*,0,#) replaces text input
- Inbound calls: don't fall back to previously selected lead
- Appointment: use lead.patientId instead of leadId for FK
- Added .env.production for consistent builds
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Patient 360 page queries Patient entity with appointments, calls, leads
- Patients added to CC agent sidebar navigation
- Auto token refresh on 401 (deduplicated concurrent refreshes)
- Call desk: callDismissed flag prevents SIP race on worklist return
- Missed calls skip disposition when never answered
- Callbacks tab renamed to Leads tab
- Branch column header on missed calls tab
- F0rty2.ai link on login footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Missed call queue with FIFO auto-assignment, dedup, SLA tracking
- Status sub-tabs (Pending/Attempted/Completed/Invalid) in worklist
- missedCallId passed through disposition flow for callback tracking
- Login page redesigned: centered white card on blue background
- Disposition button changed to content-width
- NavAccountCard popover close fix on menu item click
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Agent status toggle: Ready/Break/Training/Offline with Ozonetel sync
- Global search: cross-entity search (leads + patients + appointments) via sidecar
- General enquiry form: capture caller questions during calls
- Button standard: icon-only for toggles, text+icon for primary actions
- Sidecar: agent-state endpoint, search module with platform queries
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CC Agent:
- Call transfer (CONFERENCE + KICK_CALL) with inline transfer dialog
- Recording pause/resume during active calls
- Missed calls API (Ozonetel abandonCalls)
- Call history API (Ozonetel fetchCDRDetails)
Live Call Assist:
- Deepgram Nova STT via raw WebSocket
- OpenAI suggestions every 10s with lead context
- LiveTranscript component in sidebar during calls
- Browser audio capture from remote WebRTC stream
Worklist:
- Redesigned table: clickable phones, context menu (Call/SMS/WhatsApp)
- Last interaction sub-line, source column, improved SLA
- Filtered out rows without phone numbers
- New missed call notifications
Brand:
- Logo on login page
- Blue scale rebuilt from logo blue rgb(32, 96, 160)
- FontAwesome duotone CSS variables set globally
- Profile menu icons switched to duotone
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Telephony:
- Track UCID from SIP headers and ManualDial response
- Submit disposition to Ozonetel via Set Disposition API (ends ACW)
- Fix outboundPending flag lifecycle to prevent inbound poisoning
- Fix render order: post-call UI takes priority over active state
- Pre-select disposition when appointment booked during call
Appointment form:
- Convert from slideout to inline collapsible below call card
- Fetch real doctors from platform, filter by department
- Show time slot availability grid (booked slots greyed + strikethrough)
- Double-check availability before booking
- Support edit and cancel existing appointments
UI:
- Add Force Ready button to profile menu (logout+login to clear ACW)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Store UCID from outbound dial API response in sipCallUcidAtom
- Replace direct createCall GraphQL mutation with sidecar /api/ozonetel/dispose
- Remove agent-ready call from handleReset (no longer needed)
- Clear UCID on dial error and call reset
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ActiveCallCard now handles the full post-call flow:
- Call ends → Disposition form appears (6 options + notes)
- "Appointment Booked" → Opens appointment booking slideout
- "Follow-up Needed" → Auto-creates follow-up in platform
- Other dispositions → Logs call and returns to worklist
- "Book Appt" button available during active call too
- Creates Call record in platform on disposition submit
- Removed auto-reset to idle (ActiveCallCard manages lifecycle)
- "Back to Worklist" resets SIP state via Jotai atoms
Also fixes:
- All 7 GraphQL queries corrected (LINKS subfields, field renames)
- Campaign edit button moved to bottom-right
- Avg Response Time uses Math.abs for seed data edge case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Collapsible sidebar with Jotai atom (icon-only mode, persisted to localStorage)
- 2-panel call desk: worklist (60%) + context panel (40%) with AI + Lead 360 tabs
- Inline AI call prep card — known lead summary or unknown caller script
- Active call card with compact Answer/Decline buttons
- Worklist panel with human-readable labels, priority badges, click-to-select
- Context panel auto-switches to Lead 360 when lead selected or call incoming
- Browser ringtone via Web Audio API on incoming calls
- Sonner + Untitled UI IconNotification for toast system
- apiClient pattern: centralized post/get/graphql with auto-toast on errors
- Remove duplicate avatar from top bar, hide floating widget on call desk
- Fix Link routing in collapsed sidebar (was using <a> causing full page reload)
- Fix GraphQL field names: adStatus→status, platformUrl needs subfield selection
- Silent mode for DataProvider queries to prevent toast spam
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>