saridsa2 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

Helix Engage — Frontend

Call center CRM frontend for healthcare lead management. Built on the FortyTwo platform.

Owner: Mouli

Architecture

┌─────────────────────┐     ┌──────────────────────┐     ┌─────────────────────┐
│   helix-engage      │     │ helix-engage-server   │     │  FortyTwo Platform  │
│   (this repo)       │────▶│ (sidecar)             │────▶│  (backend)          │
│   React frontend    │     │ NestJS REST API       │     │  GraphQL API        │
│   Port 5173 (dev)   │     │ Port 4100             │     │  Port 4000          │
└─────────────────────┘     └──────────────────────┘     └─────────────────────┘
         │                           │
         │    SIP/WebRTC             │    Ozonetel CloudAgent APIs
         ▼                           ▼
   ┌───────────┐              ┌──────────────┐
   │ Ozonetel  │              │ Ozonetel     │
   │ SIP (444) │              │ REST APIs    │
   └───────────┘              └──────────────┘

Three repos:

Repo Purpose Owner
helix-engage (this) React frontend Mouli
helix-engage-server NestJS sidecar — Ozonetel + Platform bridge Karthik
helix-engage-app FortyTwo SDK app — entity schemas (Call, Lead, etc.) Shared

Getting Started

npm install
npm run dev       # http://localhost:5173
npm run build     # TypeScript check + production build

Environment Variables (set at build time or in .env)

Variable Purpose Dev Default Production
VITE_API_URL Platform GraphQL http://localhost:4000 https://engage-api.srv1477139.hstgr.cloud
VITE_SIDECAR_URL Sidecar REST API http://localhost:4100 https://engage-api.srv1477139.hstgr.cloud
VITE_SIP_URI Ozonetel SIP URI sip:523590@blr-pub-rtc4.ozonetel.com
VITE_SIP_PASSWORD SIP password 523590
VITE_SIP_WS_SERVER SIP WebSocket wss://blr-pub-rtc4.ozonetel.com:444

Production build command:

VITE_API_URL=https://engage-api.srv1477139.hstgr.cloud \
VITE_SIDECAR_URL=https://engage-api.srv1477139.hstgr.cloud \
VITE_SIP_URI=sip:523590@blr-pub-rtc4.ozonetel.com \
VITE_SIP_PASSWORD=523590 \
VITE_SIP_WS_SERVER=wss://blr-pub-rtc4.ozonetel.com:444 \
npm run build

Tech Stack

  • React 19 + TypeScript + Vite
  • Tailwind CSS 4 with semantic color tokens (text-primary, bg-brand-section — never raw colors like text-gray-900)
  • React Aria Components for accessibility (imports always prefixed Aria*)
  • Jotai for SIP/call state
  • React Context for auth, data, theme
  • FontAwesome Pro Duotone icons
  • Untitled UI component library (src/components/base/, src/components/application/)

Project Structure

src/
├── pages/                  # Route-level pages
│   ├── call-desk.tsx       # Main CC agent workspace — THE CORE PAGE
│   ├── login.tsx           # Auth page (centered card on blue bg)
│   ├── call-history.tsx    # CDR log viewer
│   ├── my-performance.tsx  # Agent KPI dashboard
│   ├── team-dashboard.tsx  # Supervisor overview
│   ├── all-leads.tsx       # Lead master table
│   └── campaigns.tsx       # Campaign listing
│
├── components/
│   ├── call-desk/          # ⚡ Call center components — WHERE MOST WORK HAPPENS
│   │   ├── active-call-card.tsx    # In-call UI + post-call disposition flow
│   │   ├── worklist-panel.tsx      # Agent task queue with tabs + sub-tabs
│   │   ├── context-panel.tsx       # AI assistant + Lead 360 sidebar
│   │   ├── disposition-form.tsx    # Post-call outcome selector
│   │   ├── appointment-form.tsx    # Book appointment during/after call
│   │   ├── agent-status-toggle.tsx # Ready/Break/Training/Offline toggle
│   │   ├── transfer-dialog.tsx     # Call transfer
│   │   ├── enquiry-form.tsx        # General enquiry capture
│   │   ├── live-transcript.tsx     # Real-time transcription (Deepgram)
│   │   └── phone-action-cell.tsx   # Click-to-call in table rows
│   ├── base/               # Untitled UI primitives (Button, Input, Select, Badge)
│   ├── application/        # Complex UI (Table, Modal, Tabs, DatePicker, Nav)
│   ├── layout/             # Sidebar — role-based navigation
│   └── dashboard/          # KPI cards, charts, missed queue widget
│
├── providers/
│   ├── sip-provider.tsx    # SIP WebRTC — call lifecycle management
│   ├── auth-provider.tsx   # User session, roles (executive/admin/cc-agent)
│   ├── data-provider.tsx   # Bulk entity loader (leads, campaigns, calls)
│   └── theme-provider.tsx  # Light/dark mode
│
├── hooks/
│   ├── use-worklist.ts     # Polls sidecar /api/worklist every 30s
│   ├── use-call-assist.ts  # Live transcript via Socket.IO
│   └── use-sip-phone.ts    # Low-level SIP.js wrapper
│
├── lib/
│   ├── api-client.ts       # REST + GraphQL client (auth, queries, sidecar calls)
│   ├── queries.ts          # Platform GraphQL query strings
│   └── format.ts           # Phone/date formatting
│
├── state/
│   └── sip-state.ts        # Jotai atoms (callState, callerNumber, isMuted, etc.)
│
└── types/
    └── entities.ts         # Lead, Patient, Call, Appointment, etc.

Troubleshooting Guide — Where to Look

"The call desk isn't working"

File: src/pages/call-desk.tsx This is the orchestrator. It uses useSip() for call state, useWorklist() for the task queue, and renders either ActiveCallCard (in-call) or WorklistPanel (idle). Start here, then drill into whichever child component is misbehaving.

"Calls aren't connecting / SIP errors"

File: src/providers/sip-provider.tsx + src/state/sip-state.ts Check VITE_SIP_* env vars. Ozonetel SIP WebSocket runs on port 444 — VPNs block it. If WebSocket hangs at "connecting", turn off VPN. Also check browser console for SIP.js registration errors.

"Worklist not loading / empty"

File: src/hooks/use-worklist.ts This polls GET /api/worklist on the sidecar every 30s. Open browser Network tab → filter for /api/worklist. Common causes: sidecar is down, auth token expired, or agent name doesn't match any assigned leads.

"Missed calls not appearing / sub-tabs empty"

File: src/components/call-desk/worklist-panel.tsx Missed calls come from the sidecar worklist response. The sub-tabs filter by callbackstatus field. If all sub-tabs are empty, the sidecar ingestion may not be running (check sidecar logs for MissedQueueService).

"Disposition / appointment not saving"

File: src/components/call-desk/active-call-card.tsxhandleDisposition() Posts to sidecar POST /api/ozonetel/dispose. Errors are caught silently (non-blocking). Check browser Network tab for the dispose request/response, then check sidecar logs.

"Login broken / Failed to fetch"

File: src/pages/login.tsx + src/lib/api-client.ts Login calls apiClient.login() → sidecar /auth/login → platform GraphQL. Most common cause: wrong VITE_API_URL (built with localhost instead of production URL). Always set env vars at build time.

"UI component looks wrong"

Files: src/components/base/ (primitives), src/components/application/ (complex) These come from the Untitled UI library. Design tokens are in src/styles/theme.css. Brand colors were rebuilt from logo blue rgb(32, 96, 160).

"Navigation / role-based access"

File: src/components/layout/sidebar.tsx Navigation groups are defined per role (admin, cc-agent, executive). Routes are registered in src/main.tsx.

Data Flow

User action
    │
    ▼
Component (e.g. ActiveCallCard)
    │
    ├──▶ Sidecar REST API (via apiClient.post/get)
    │       e.g. /api/ozonetel/dispose, /api/worklist
    │
    ├──▶ Platform GraphQL (via apiClient.graphql)
    │       e.g. leads, appointments, patients queries
    │
    └──▶ SIP.js (via useSip() hook)
            Call control: answer, hangup, mute, hold

Key pattern: The frontend talks to TWO backends:

  1. Sidecar (REST) — for Ozonetel telephony operations and worklist
  2. Platform (GraphQL) — for entity CRUD (leads, appointments, patients)

Conventions

  • File naming: kebab-case (worklist-panel.tsx)
  • Colors: Semantic tokens only (text-primary, bg-brand-section)
  • Icons: @fortawesome/pro-duotone-svg-icons + faIcon() wrapper in src/lib/icon-wrapper.ts
  • React Aria: Always prefix imports (Button as AriaButton)
  • Transitions: transition duration-100 ease-linear

Git Workflow

  • dev — active development
  • master — stable baseline
  • Always build with production env vars before deploying
Description
No description provided
Readme 3.4 MiB
Languages
TypeScript 96.1%
CSS 3.8%