Commit Graph

77 Commits

Author SHA1 Message Date
bdabcb2ea4 feat: consistent UI across all list pages — PhoneActionCell, custom pills, eye icon
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- PhoneActionCell: kebab always visible (SMS + WhatsApp), Call removed from menu,
  phone number always brand-colored regardless of telephony state
- LeadTable: replaced actions kebab column with eye icon (first column) for
  view activity, phone column now uses PhoneActionCell
- Worklist: React Aria Tabs replaced with custom pill buttons matching All Leads
  pattern (bg-brand-solid on selected), search lifted to call-desk.tsx header
- Appointments: underline tabs replaced with custom pills, phone in patient cell
  uses PhoneActionCell, group/row added to rows
- Patients: removed redundant HamburgerMenu column, group/row on rows
- Call Desk: search input in header row, cleaned up duplicate imports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 23:05:32 +05:30
df08bcfc19 feat: SSE-driven worklist + agent call history split + remove SOURCE column
Worklist:
- SSE stream replaces 30s poll — EventSource on /api/supervisor/worklist/stream
  triggers immediate fetchWorklist() on missed-call events
- Toast notification: 'Missed Call — {name} — needs callback'
- No polling fallback — SSE is the source of truth

Call History split by role:
- Agent: 'My Call History' — own calls only (matched by agent relation
  or chain-parsed agentName), missed calls excluded (they belong on
  the Call Desk queue), no Agent/Recording/SLA columns, phone clickable
  via PhoneActionCell instead of separate Call button
- Supervisor: 'Call History' — all calls, Agent + Recording columns visible

Worklist panel:
- SOURCE/BRANCH column removed from display (data stays on row)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:34:37 +05:30
ca482e731e feat: Contacts page + P360 for all tabs + dynamic column toggle + slot flicker fix
Contacts page:
 - New /contacts route — shows leads with source=PHONE/WALK_IN/REFERRAL
 - Leads page now excludes those sources (campaign-sourced only)
 - Sidebar: Contacts nav item added for all roles; Leads added for cc-agent
 - Same LeadTable + pagination + CSV export pattern as All Leads

P360 context panel for all worklist tabs (#6-10):
 - WorklistPanel: onSelectLead → onSelectItem (generic WorklistSelection)
 - call-desk: handleSelectItem builds ContextPanelSubject for any row type
 - ContextPanelSubject type replaces (lead as any).patientId casts
 - Highlight tracks row.id (mc-*/fu-*/lead-*) not lead.id

Dynamic column toggle (blank-screen fix):
 - missed-calls + call-recordings refactored to React Aria dynamic
   collections API (Table.Header columns={} + Table.Row columns={})
 - Fixes "Cell count must match column count" crash on column hide
 - Row-header column metadata in columnDefs instead of hardcoded JSX

Slot flickering fix (#2):
 - Removed clinic + timeSlot from slot-fetch useEffect deps (circular
   loop: effect sets clinic → clinic in deps → re-fires)
 - Memoized timeSlotSelectItems

Other:
 - GlobalSearch hidden (stale appointment state on navigation)
 - Branch column: shows campaign name from relation, falls back to DID
 - formatSource maps PHONE → "Phone"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:55:57 +05:30
c22d82f8c5 fix(dates): block past-date selection in appointment + clinic holiday pickers
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Bug 556 triggered a broader audit of every date input in the app:

 - appointment-form DatePicker now has minValue=today(getLocalTimeZone()) —
   can't book or reschedule into the past (tightens bug 555 at the
   input layer too; the past-slot filter in masterdata service still
   handles the hour-granularity)
 - clinic-form holiday date picker gets the same — can't observe a
   holiday that already passed

Audit complete:
 - enquiry-form follow-up date: already had min=today (bug 556 fix)
 - appointment-form: fixed here
 - clinic-form holidays: fixed here
 - my-performance date filter: past valid (reports over history)
 - campaign-edit start/end: past valid (historical campaigns)
2026-04-16 05:41:46 +05:30
f52722086e fix(call-desk): Book Appt button label reflects New vs Reschedule
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)
2026-04-16 05:41:33 +05:30
9d09662f16 feat(maint+ai): OTP modal agent picker + supervisor AI quick actions
Maint shortcuts (Unlock Agent / Force Ready) used to read agentId from the CC-agent's localStorage config — supervisors had no such config and the endpoint 400'd. New flow: after OTP passes, modal calls /api/maint/session-status and renders a two-bucket picker (Locked selectable / Free informational 'Already free'). Orphan locks surface with an explicit label.

- use-maint-shortcuts: agentPickerEndpoint flag on forceReady + unlockAgent
- maint-otp-modal: two-phase — OTP gate, then picker, then submit; OTP
  held in state across phases so the operator doesn't re-enter it

AI chat panel: supervisor context now shows supervisor-appropriate quick actions (Agent performance / Call summary / Campaign stats / Who needs attention?) that map 1:1 to the supervisor tool set on the sidecar. Agent flow keeps the theme-token quick actions (doctors/clinics/packages).
2026-04-15 18:56:34 +05:30
6c32d76d7e fix(appointment-form): keep saved doctor visible on edit when department filter mismatches
Edit mode prefilled clinic + department + doctorId, but the doctor
Select rendered blank because the doctor-list filter (doctors where
department === selectedDept) excluded the saved doctor. Root cause:
the Appointment.department string doesn't always match the doctor's
current department enum value.

Fix: doctorSelectItems now always includes the currently-selected
doctor as the first item, even when the department filter would
exclude them. Once the user changes department or doctor, the filter
behaves normally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:50:10 +05:30
04f559037c fix(appointment-form): filter past pills + confirm modal + view-only mode
- 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>
2026-04-15 13:39:54 +05:30
ffb8bcb6ad 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>
2026-04-15 13:11:31 +05:30
72cb192447 fix(appointments): preload clinic + keep saved time on edit
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
The appointment-edit form opened with clinic/time blank even for
well-formed appointments because the pipeline never carried clinicId
end-to-end. Four-layer audit + fix:

1. APPOINTMENTS_QUERY now fetches clinicId + clinic { id clinicName }
   + doctor.fullName (was only doctor.id).
2. transformAppointments populates real clinicId + clinicName from the
   relation instead of faking clinicName=department.
3. Appointment type gets clinicId: string | null.
4. context-panel passes clinicId through to AppointmentForm's
   existingAppointment prop; form initial-states clinic from it.

Also on edit: if the saved timeSlot isn't in the fresh slot list
(past-slot filter, schedule change, clinic mismatch) we inject it as
"HH:MM (current)" so the dropdown displays the existing value instead
of looking cleared.

Historical appointments with clinicId=null on the platform still fall
through to the auto-select-from-slot logic; a maint backfill for those
is a separate task.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:56:51 +05:30
d3cbf4d2bb fix: P2 defect batch + context-panel edit takeover
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>
2026-04-15 11:48:39 +05:30
5632f15031 fix: P1 call-desk defects batch
- Mute persists across calls: sip-manager's "ended/failed" branch now
  resets the Recoil sipIsMutedAtom + sipIsOnHoldAtom (previously only
  the SIP track was unmuted, leaving the UI icon + toggle logic in a
  muted state that the next call inherited).
- Telephony-unavailable dial pad: call-desk.tsx dial-pad "Call" button
  was missing an isRegistered check in its disabled prop, so it stayed
  clickable when SIP was down. Button now shows "Telephony unavailable"
  and is disabled.
- Past dates in Follow-up: enquiry-form's follow-up date input had no
  min constraint. Switched to a raw <input type="date"> with min set
  to today's ISO date.
- Returning-patient AI summary during call: ai-chat-panel now auto-fires
  a "give me a quick summary of <caller>" request whenever the caller's
  leadId changes (new incoming call). Clears prior chat state so each
  caller starts fresh.
- Remove Type column in Patients page (Badge import also pruned).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:38:35 +05:30
f4dcf6574f fix(enquiry-form): set name on createPatient
Patient records created from the enquiry form now get a platform title
from the typed name. Cosmetic fix — frontend was already showing the
fullName, only platform admin browsing showed "Untitled".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:32:33 +05:30
42e23a52ec feat: call-desk refresh — disposition modal, active-call UI, worklist + perf updates
- Call-desk: active-call-card supervisor presence badges, incoming-call-card polish, transfer-dialog, call-log
- Disposition modal: auto-lock based on actions taken, not-interested split
- Forms: appointment-form + enquiry-form improvements (placeholder handling, phone format)
- Worklist-panel: pagination awareness, filter chips
- Pages: all-leads/patients/patient-360/missed-calls/team-performance/call-history/appointments polish
- SIP: sip-client reconnect, sip-provider + sip-manager state, agent-status-toggle spinner
- Hooks: use-agent-state supervisor SSE events, use-worklist, use-performance-alerts
- Types: entities.ts extended

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 06:49:36 +05:30
642911fa6c fix: appointment clinic relation — save clinicId, query clinic.clinicName for branch column
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:38:11 +05:30
8bc01d1a9f fix: QA defects — phone format E.164, call history shows phone not Unknown
- appointment-form: normalize phone to +91XXXXXXXXXX before patient create
- appointment-form: use empty string not 'Unknown' for name fallback
- call-history: show formatted phone number instead of 'Unknown' when no lead

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:57:59 +05:30
d3e6934dcb fix: stop auto-creating Unknown leads on every call
Caller resolver now returns empty IDs for unrecognized numbers instead
of eagerly creating lead+patient records. Records are created when the
agent explicitly books an appointment or logs an enquiry — per PRD.

- caller-resolution.service.ts: return unresolved result, don't create
- call-desk.tsx: toast changed to 'No existing records found'
- appointment-form.tsx: create patient on save if none exists

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:22:23 +05:30
d24945a3af fix: update patient name on new callers — prevents 'Unknown' patients
When a new caller's patient record is created by the caller resolver,
the name is empty. The agent types a name in the appointment form but
the patient was never updated (Bug #527 removed all patient updates).
Now updates patient name only when the initial name was empty — existing
patients with names are not affected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:05:26 +05:30
d8f9174a55 fix: filter time slots by selected clinic/branch
Slots were fetched by doctor+date but not filtered by clinic. A doctor
visiting multiple branches showed all slots regardless of selected branch.
Now filters by clinicId when a clinic is selected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:59:10 +05:30
ee9da619c1 feat(frontend): supervisor presence indicator on agent call card
- useAgentState hook returns { state, supervisorPresence }
- SSE events: supervisor-whisper → "Supervisor coaching" (blue badge)
  supervisor-barge → "Supervisor on call" (brand badge)
  supervisor-left → badge disappears
- Listen mode is silent — no badge shown
- Updated call sites: sidebar.tsx, agent-status-toggle.tsx destructure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:16:31 +05:30
d19ca4f593 feat(frontend): barge controls component — connect, mode tabs, hangup
BargeControls component with 4 states: idle → connecting → connected → ended.
Connected state shows Listen/Whisper/Barge mode tabs (DTMF 4/5/6), live
duration counter, hang up button. Auto-cleanup on unmount. Mode changes
notify sidecar for agent SSE events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:09:37 +05:30
c044d2d143 feat: quick wins — global search, P360 actions, context panel, route guards
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Wire GlobalSearch component into app shell top bar (US-10)
- P360: Book Appointment button opens AppointmentForm (US-8)
- P360: Add Note button creates leadActivity via GraphQL (US-8)
- P360: Appointment rows clickable for edit (active statuses only) (US-8)
- P360: Display lead status badge (was fetched but not rendered) (US-8)
- Context panel: "View 360" link on linked patient → /patient/:id (US-6)
- Context panel: Display campaign info from lead.utmCampaign (US-6)
- Route guards: Admin-only routes wrapped in RequireAdmin (US-1, US-3)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:31:56 +05:30
cb4894ddc3 feat: Global E2E tests, multi-agent fixes, SIP agent tracing
- 13 Global Hospital smoke tests (CC Agent + Supervisor)
- Auto-unlock agent session in test setup via maint API
- agent-status-toggle sends agentId from localStorage (was missing)
- maint-otp-modal injects agentId from localStorage into all calls
- SIP manager logs agent identity on connect/disconnect/state changes
- seed-data.ts: added CC agent + marketing users, idempotent member
  creation, cleanup phase before seeding
- .gitignore: exclude test-results/ and playwright-report/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:12:22 +05:30
c4b6f9a438 fix: 5 bug fixes — #533 #531 #529 #527 #547
#533: Remove redundant Call History top header (duplicate TopBar)
#531: Block logout during active call (confirm dialog + UCID check)
#529: Block outbound calls when agent is on Break/Training
#527: Remove updatePatient during appointment creation (was mutating
      shared Patient entity, affecting all past appointments)
#547: SLA rules seeded via API (config issue, not code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:26:55 +05:30
951acf59c5 feat: appointment form uses master data endpoint for clinics, doctors, departments
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:31:01 +05:30
05de50f796 fix: remove hardcoded clinic list, fetch from platform dynamically
Appointment form clinic dropdown was hardcoded to 3 "Global Hospital"
branches. Replaced with a GraphQL query to { clinics } so each
workspace shows its own clinics. If no clinics are configured, the
dropdown is empty instead of showing wrong data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:46:30 +05:30
6a2fc47226 fix: SIP disconnect on callState change + dispose sends agentId
- 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>
2026-04-10 15:49:34 +05:30
fb92da113e fix: setup wizard role guard, Doctor.clinic removal, dial sends agent config
Defect 1: Setup wizard (/setup) now guarded by AdminSetupGuard —
CC agents and other non-admin roles are redirected to / instead of
seeing the setup wizard they can't complete.

Defect 3: Removed all references to Doctor.clinic (relation was
replaced by DoctorVisitSlot entity). Updated queries.ts,
appointments.tsx, transforms.ts, doctors.tsx, appointment-form.tsx.

Defect 6 (frontend side): Dial request now sends agentId and
campaignName from localStorage agent config so the sidecar dials
with the correct per-agent credentials, not global defaults.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:29:32 +05:30
72012f099c fix: three-layer ACW protection — prevent agent stuck in wrapping-up
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>
2026-04-10 12:31:45 +05:30
f57fbc1f24 feat(onboarding/phase-6): setup wizard polish, seed script alignment, doctor visit slots
- Setup wizard: 3-pane layout with right-side live previews, resume
  banner, edit/copy icons on team step, AI prompt configuration
- Forms: employee-create replaces invite-member (no email invites),
  clinic form with address/hours/payment, doctor form with visit slots
- Seed script: aligned to current SDK schema — doctors created as
  workspace members (HelixEngage Manager role), visitingHours replaced
  by doctorVisitSlot entity, clinics seeded, portalUserId linked
  dynamically, SUB/ORIGIN/GQL configurable via env vars
- Pages: clinics + doctors CRUD updated for new schema, team settings
  with temp password + role assignment
- New components: time-picker, day-selector, wizard-right-panes,
  wizard-layout-context, resume-setup-banner
- Removed: invite-member-form (replaced by employee-create-form per
  no-email-invites rule)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:37:34 +05:30
efe67dc28b 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>
2026-04-07 13:54:22 +05:30
8470dd03c7 fix: UI polish — nav labels, date picker, rules engine, error messages
- Sidebar: removed "Master" from nav labels (Leads, Patients, Appointments, Call Log)
- Appointment form: Dept + Doctor in 2-col row, Date below, disabled cascade
- DatePicker: placement="bottom start" + shouldFlip fixes popover positioning
- Team Performance: default to "Week", grid KPI cards, chart legend spacing
- Rules Engine: manual save (removed auto-debounce), Reset to Defaults uses
  DEFAULT_PRIORITY_CONFIG (no template endpoint), removed dead saveTimerRef
- Automation rules: 6 showcase cards with trigger/condition/action, replaced
  agent-specific rule with generic round-robin
- Recording analysis: friendly error message with retry instead of raw Deepgram error
- Sidebar active/hover: brand color reference for theming

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:55:16 +05:30
afd0829dc6 feat: design tokens — multi-hospital theming system
Backend (sidecar):
- ThemeService: read/write/backup/reset theme.json with versioning
- ThemeController: GET/PUT/POST /api/config/theme endpoints
- ConfigThemeModule registered in app

Frontend:
- ThemeTokenProvider: fetches theme, injects CSS variables on <html>
- Login page: logo, title, subtitle, Google/forgot toggles from tokens
- Sidebar: title, subtitle, active highlight from brand color scale
- AI chat: quick actions from tokens
- Branding settings page: 2-column layout, file upload for logo/favicon,
  single color picker with palette generation, font dropdowns, presets,
  pinned footer, versioning

Theme CSS:
- Sidebar active/hover text now references --color-brand-400

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:50:36 +05:30
c5d5e9c4f9 feat: supervisor AI tools — agent performance, campaign stats, call summary, SLA breaches
- AiChatPanel accepts context type, team dashboard passes { type: 'supervisor' }
- Supervisor system prompt: data-driven, no bias, threshold-based comparisons

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:05:20 +05:30
4598740efe feat: inline forms, transfer redesign, patient fixes, UI polish
- 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>
2026-04-02 12:14:38 +05:30
442a581c8a fix: appointment/enquiry modals + team performance fallback
- 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>
2026-04-01 17:20:59 +05:30
b90740e009 feat: rules engine — priority config UI + worklist scoring
- Rules engine spec v2 (priority vs automation rules distinction)
- Priority Rules settings page with weight sliders, SLA config, campaign/source weights
- Collapsible config sections with dynamic headers
- Live worklist preview panel with client-side scoring
- AI assistant panel (collapsible) with rules-engine-specific system prompt
- Worklist panel: score display with SLA status dots, sort by score
- Scoring library (scoring.ts) for client-side preview computation
- Sidebar: Rules Engine nav item under Configuration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:20:59 +05:30
c3c3f4b3d7 feat: worklist sorting, contextual disposition, context panel redesign, notifications
- Worklist default sort descending (newest first), sortable column headers (PRIORITY, PATIENT, SLA) via React Aria
- Contextual disposition: auto-selects based on in-call actions (appointment → APPOINTMENT_BOOKED, enquiry → INFO_PROVIDED, transfer → FOLLOW_UP_SCHEDULED)
- Context panel redesign: collapsible AI Insight, Upcoming (appointments + follow-ups + linked patient), Recent (calls + activities) sections; auto-collapse on AI chat start
- Appointments added to DataProvider with APPOINTMENTS_QUERY, Appointment type, transform
- Notification bell for admin/supervisor: performance alerts (idle time, NPS, conversion thresholds) with toast on load + bell dropdown with dismiss; demo alerts as fallback
- Slideout z-index fix: added z-50 to slideout ModalOverlay matching modal component

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:45:52 +05:30
0477064b3e wip: AI chat streaming endpoint + useChat integration (protocol mismatch)
- Server: POST /api/ai/stream using streamText with tools (lookup_patient, lookup_appointments, lookup_doctor)
- Frontend: AiChatPanel rewritten with @ai-sdk/react useChat hook
- Tool result cards: PatientCard, AppointmentCard, DoctorCard
- Streaming works server-side but useChat v1 doesn't parse AI SDK v6 toTextStreamResponse format
- Context panel layout needs redesign — context section fills entire panel, chat pushed below fold
- TODO: Fix streaming protocol, redesign panel layout with collapsible context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:44:48 +05:30
48ed300094 feat: unified context panel, remove tabs, context-aware layout
- Merged AI Assistant + Lead 360 tabs into single context-aware panel
- Context section shows: lead profile, AI insight (live from event bus), appointments, activity
- "On call with" banner only shows during active calls (isInCall prop)
- AI Chat always available at bottom of panel
- Phase 1 only — AI Chat panel needs full redesign with Vercel AI SDK tool calling (Phase 2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:44:48 +05:30
e6b2208077 feat: disposition modal, persistent top bar, pagination, QA fixes
- DispositionModal: single modal for all call endings. Dismissable (agent can resume call).
  Agent clicks End → modal → select reason → hangup + dispose. Caller disconnects → same modal.
- One call screen: CallWidget stripped to ringing notification + auto-redirect to Call Desk.
- Persistent top bar in AppShell: agent status toggle + network indicator on all pages.
- Network indicator always visible (Connected/Unstable/No connection).
- Pagination: Untitled UI PaginationCardDefault on Call History + Appointments (20/page).
- Pinned table headers/footers: sticky column headers, scrollable body, pinned pagination.
  Applied to Call Desk worklist, Call History, Appointments, Call Recordings, Missed Calls.
- "Patient" → "Caller" column label in Call History.
- Offline → Ready toggle enabled.
- Profile status dot reflects Ozonetel state.
- NavAccountCard: popover placement top, View Profile + Account Settings restored.
- WIP pages for /profile and /account-settings.
- Enquiry form PHONE_INQUIRY → PHONE enum fix.
- Force Ready / View Profile / Account Settings removed then restored properly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:44:48 +05:30
70e0f6fc3e feat: call recording analysis with Deepgram diarization + AI insights
- Deepgram pre-recorded API: transcription with diarization, sentiment, topics, summary
- OpenAI structured insights: call outcome, patient satisfaction, coaching notes, action items, compliance flags
- Slideout panel UI with audio player, speaker-labeled transcript, sentiment badge
- AI pill button in recordings table between Caller and Type columns
- Redis caching (7-day TTL) to avoid re-analyzing the same recording

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:44:48 +05:30
488f524f84 feat: SSE agent state, UCID fix, maint module, QA bug fixes
- Fix outbound disposition: store UCID from dial API response (root cause of silent disposition failure)
- SSE agent state: real-time Ozonetel state drives status toggle (ready/break/calling/in-call/acw)
- Maint module with OTP-protected endpoints (force-ready, unlock-agent, backfill, fix-timestamps)
- Maint OTP modal with PinInput component, keyboard shortcuts (Ctrl+Shift+R/U/B/T)
- Force-logout via SSE: admin unlock pushes force-logout to connected browsers
- Silence JsSIP debug flood, add structured lifecycle logging ([SIP], [DIAL], [DISPOSE], [AGENT-STATE])
- Centralize date formatting with IST-aware formatters across 11 files
- Fix call history: non-overlapping aggregates (completed/missed), correct timestamp display
- Auto-dismiss CallWidget ended/failed state after 3 seconds
- Remove floating "Helix Phone" idle badge from all pages
- Fix dead code in agent-state endpoint (auto-assign was unreachable after return)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:44:48 +05:30
710609dfee refactor: centralise outbound dial into useSip().dialOutbound()
- Single dialOutbound() in sip-provider handles all outbound state:
  callState, callerNumber, outboundPending, API call, error recovery
- ClickToCallButton, PhoneActionCell, Dialler all use dialOutbound()
- Removed direct Jotai atom manipulation from calling components
- Removed setOutboundPending imports from components
- SIP disconnects on provider unmount + auth logout
- Dialler input is now editable (type or numpad)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:49:10 +05:30
dbd8391f2c fix: UUID type mismatch, slot conflict, appt/enquiry tabs, dialler in header
- 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>
2026-03-24 10:54:56 +05:30
1df40f14ff fix: disposition returns straight to worklist — no intermediate screens
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>
2026-03-24 09:55:37 +05:30
938f2a84d8 fix: enquiry in post-call, appointment skip button, AI scroll containment
- 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>
2026-03-24 08:11:22 +05:30
5816cc0b5c fix: pinned header/chat input, numpad dialler, caller matching, appointment FK
- 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>
2026-03-23 14:41:31 +05:30
727a0728ee feat: QA fixes — Patient 360 rewrite, token refresh, call flow, UI polish
- 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>
2026-03-23 11:52:33 +05:30
744a91a1ff feat: Phase 2 — missed call queue, login redesign, button fix
- 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>
2026-03-23 09:16:53 +05:30