Pair to worklist.service.ts change. Unknown caller → appointment booked (creates Patient) → caller rings back → resolver links Lead↔Patient. But the frontend sometimes found the lead in the worklist cache mid-30s-poll and that row's patientId hadn't refreshed yet — so leadAppointments filter (keyed on lead.patientId) came up empty and the Book Appt pill for the prior appointment didn't render.
Now: when the worklist row is used, overlay the resolver's patientId if the cached row's is missing. Belt-and-braces with the sidecar fix.
- drop the header Back button (cosmetic; useNavigate + ArrowLeft icon
removed with it)
- Export CSV now downloads the currently-filtered list — respects tab,
search, campaign filter and active sort order. Headers: Phone /
First/Last Name / Email / Source / Status / Campaign / Assigned
Agent / First Contact / Last Contact / Created / Age (days).
- csv-utils: rowsToCsv + downloadCsv helpers. Values quoted, embedded
quotes escaped, leading =/+/-/@ prefixed with a single quote to
defeat CSV injection when opened in Excel. UTF-8 BOM on the blob
so Excel recognises non-ASCII names/addresses.
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).
Ramaiah's product team owns their setup; end-user admins shouldn't see a dead-end Settings nav + Resume Setup banner. Flag is read from /api/config/ui-flags at app boot.
- use-ui-flags: module-scoped cache + useUiFlags hook + getUiFlags
helper for non-component callers
- main.tsx: /setup redirects when managed; RequireSelfServeSetup
guard blocks /settings/*
- resume-setup-banner: suppressed when managed
- login.tsx: skip first-run /setup redirect when managed
- settings.tsx: remove orphan popup-modal scaffolding left over
from an earlier 'contact product team' approach
- section-card: support onClick-or-href (kept for future use)
Two related fixes:
1. KPIs were capped at 100. The data-provider's entity queries were hardcoded to first: 100; on Global the supervisor dashboard showed 'Total Calls: 100' this week while the AI assistant (which paginates) reported 182. Converted each query to a cursor-aware builder, added a generic fetchAll(rootField, builder) that loops until hasNextPage=false (capped at 25 pages × 200 as a runaway guard). Page size bumped 100→200 to cut round-trips on active tenants.
2. Every 30s background poll flipped loading=true, flashing a 'Loading...' overlay across supervisor surfaces. hasLoadedRef guards the flag so only the initial fetch triggers the loading state.
QA flagged Team Dashboard vs Team Performance as repetitive. Retire Team Performance from the sidebar; move its unique surfaces (rich agent table, time breakdown, NPS/Conversion, Performance Alerts) into Team Dashboard below the existing KPI row.
- supervisor-rollup: new shared module — useSupervisorRollup hook +
RichAgentTable / TimeBreakdown / NpsConversion / PerformanceAlerts
- Time Breakdown rendered as a table (Agent / Active / Wrap / Idle
/ Break / Total + Team-average header row) — QA flagged the old
stacked-bar tiles as misleading because per-agent totals varied
wildly and width comparison was meaningless
- team-dashboard: tabs replaced with stacked sections; everything
scroll-visible so supervisors don't hunt across surfaces
- sidebar: remove 'Team Performance' entry (route kept for backup)
and drop the now-unused IconChartLine wiring
QA ask: leads should be the default view on campaign detail, not behind tab navigation, with campaign metrics (budget / funnel / source) kept visible alongside.
- drop the Overview/Leads tabs
- render LeadTable filtered to campaignLeads on the left
- Campaign Details card + Conversion Funnel + Source Breakdown
pinned on the right as a sticky sidebar
- hero: remove 'View Leads' button (was duplicate nav now)
- LeadActivitySlideout wired for row click-through
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>
- 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>
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>
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>
- 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>
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>
usePerformanceAlerts now fetches /api/supervisor/performance-alerts
every 60s instead of computing client-side. Dismiss + dismiss-all hit
the sidecar so state survives reload. Toast fires when new alerts arrive.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. notification-bell: drop the DEMO_ALERTS fallback (Riya Mehta etc.).
Empty state ("No active alerts") shows when the live computation
returns nothing — which is the truthful state until thresholds are
set on Agent records.
2. use-performance-alerts: bucket calls by c.agentId === agent.id when
the relation is set; fall back to legacy agentName matching only for
un-enriched rows. Fixes conversion% calc going to 0 after backfill.
3. agent-table: Link target uses agent.id (UUID or "legacy:NAME") so
the URL is a stable identifier instead of a display string.
4. agent-detail: parse the route param into UUID vs legacy:NAME, filter
calls by c.agentId or c.agentName accordingly, and resolve display
name via the platform Agents list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Agent Performance table on the team dashboard was bucketing by raw
call.agentName — the field that holds Ozonetel's transfer-chain string
("RamaiahAdmin -> GlobalHealthX") and collides for distinct AgentIDs
that share a Full Name. Result: 7 rows for 3 real agents.
Now buckets by call.agent.id when the CDR enrichment has populated it,
falls back to legacy agentName grouping otherwise. Calls without any
agent info are dropped from the agent rollup (instead of being
collapsed under "Unknown").
Pulls agent { id name ozonetelAgentId } + transferredTo + transferType
on CALLS_QUERY.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prefer call.agent.id (set by CDR enrichment) over call.agentName string
matching. Falls back to the raw agentName only when the row hasn't been
enriched yet. Eliminates the "RamaiahAdmin -> GlobalHealthX" transfer-chain
rows and the display-name collisions (two distinct AgentIDs with the same
Full Name).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
Appointments page was using department for the Branch column. Now fetches
doctor.clinic.clinicName from the GraphQL query and displays that. Search
filter also updated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
The logout POST to /auth/logout was getting cancelled when the page
navigated to /login before the fetch completed. keepalive: true ensures
the request survives page unload so the Redis session lock is released.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Left panel: KPI cards + clickable call table (row selection highlights).
Right panel (380px): caller context (name, phone, source, AI summary,
appointments) + BargeControls component. Fetches lead data by phone match
on selection. Auto-clears when selected call ends. Removed disabled
Listen/Whisper/Barge buttons — replaced with integrated barge drawer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Separate from agent sip-client.ts — different lifecycle (on-demand per
barge session, not persistent). Auto-answers incoming Ozonetel calls.
DTMF mode switching: 4=listen, 5=whisper, 6=barge. Event-driven with
registered/callConnected/callEnded events. Audio via hidden <audio> element.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Admin username + password inputs in the Ozonetel section for supervisor
barge/whisper/listen access. Follows existing masked password pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
#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>
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>
disconnectSip() now guards against disconnect when outboundPending
or outboundActive is true. Accepts force=true for intentional
disconnects (logout, page unload, component unmount). Prevents
React re-render cycles from killing the SIP WebSocket mid-dial,
which was causing the call to drop and disposition modal to not appear.
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>
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>
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>
- 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>
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>
Replaces the Phase 2 StepPlaceholder with six dedicated wizard step
components, each wrapping the corresponding Phase 3/4 form. The parent
setup-wizard.tsx is now a thin dispatcher that owns shell state +
markSetupStepComplete wiring; each step owns its own data load, form
state, validation, and save action.
- src/components/setup/wizard-step-types.ts — shared
WizardStepComponentProps shape
- src/components/setup/wizard-step-identity.tsx — minimal brand form
(hospital name + logo URL) hitting /api/config/theme, links out to
/branding for full customisation
- src/components/setup/wizard-step-clinics.tsx — ClinicForm + createClinic
mutation, always presents an empty "add new" form
- src/components/setup/wizard-step-doctors.tsx — DoctorForm with clinic
dropdown, blocks with an inline warning when no clinics exist yet
- src/components/setup/wizard-step-team.tsx — InviteMemberForm with real
roles fetched from getRoles, sends invitations via sendInvitations
- src/components/setup/wizard-step-telephony.tsx — loads masked config
from /api/config/telephony, validates required Ozonetel fields on save
- src/components/setup/wizard-step-ai.tsx — loads AI config, clamps
temperature 0..2, doesn't auto-advance (last step, admin taps Finish)
- src/pages/setup-wizard.tsx — dispatches to the right step component
based on activeStep, passes a WizardStepComponentProps bundle
Each step calls onComplete(step) after a successful save, which updates
the shared SetupState so the left-nav badges reflect the new status
immediately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the three remaining Pattern B placeholder routes
(/settings/telephony, /settings/ai, /settings/widget) with real forms
backed by the sidecar config endpoints introduced in Phase 1. Each page
loads the current config on mount, round-trips edits via PUT, and
supports reset-to-defaults. Changes take effect immediately since the
TelephonyConfigService / AiConfigService / WidgetConfigService all keep
in-memory caches that all consumers read through.
- src/components/forms/telephony-form.tsx — Ozonetel + SIP + Exotel
sections; honours the '***masked***' sentinel for secrets
- src/components/forms/ai-form.tsx — provider/model/temperature/prompt
with per-provider model suggestions
- src/components/forms/widget-form.tsx — enabled/url/embed toggles plus
an allowedOrigins chip list
- src/pages/telephony-settings.tsx — loads masked config, marks the
telephony wizard step complete when all required Ozonetel fields
are filled
- src/pages/ai-settings.tsx — clamps temperature to 0..2 on save,
marks the ai wizard step complete on successful save
- src/pages/widget-settings.tsx — uses the admin endpoint
(/api/config/widget/admin), exposes rotate-key + copy-to-clipboard
for the site key, and separates the read-only key card from the
editable config card
- src/main.tsx — swaps the three placeholder routes for the real pages
- src/pages/settings-placeholder.tsx — removed; no longer referenced
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces Phase 2 placeholder routes for /settings/clinics and
/settings/doctors with real list + add/edit slideouts backed directly by
the platform's ClinicCreateInput / DoctorCreateInput mutations. Rewrites
/settings/team to fetch roles via getRoles and let admins invite members
(sendInvitations) and change roles (updateWorkspaceMemberRole).
- src/components/forms/clinic-form.tsx — reusable form + GraphQL input
transformer, handles address/phone/email composite types
- src/components/forms/doctor-form.tsx — reusable form with clinic
dropdown and currency conversion for consultation fees
- src/components/forms/invite-member-form.tsx — multi-email chip input
with comma-to-commit UX (AriaTextField doesn't expose onKeyDown)
- src/pages/clinics.tsx — list + slideout using ClinicForm, marks the
clinics setup step complete on first successful add
- src/pages/doctors.tsx — list + slideout with parallel clinic fetch,
disabled-state when no clinics exist, marks doctors step complete
- src/pages/team-settings.tsx — replaces email-pattern role inference
with real getRoles + in-row role Select, adds invite slideout, marks
team step complete on successful invitation
- src/main.tsx — routes /settings/clinics and /settings/doctors to real
pages instead of SettingsPlaceholder stubs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2 of hospital onboarding & self-service plan
(docs/superpowers/plans/2026-04-06-hospital-onboarding-self-service.md
checked in here for the helix-engage repo).
Frontend foundations for the staff-portal Settings hub and 6-step setup
wizard. Backend was Phase 1 (helix-engage-server commit).
New shared components (src/components/setup/):
- wizard-shell.tsx — fullscreen layout with left step navigator, progress
bar, and Skip-for-now affordance
- wizard-step.tsx — single-step wrapper with Mark Complete + Prev/Next/
Finish navigation, completion badge
- section-card.tsx — Settings hub card with title/description/icon, links
to a section page, optional status badge mirroring setup-state
New pages:
- pages/setup-wizard.tsx — top-level /setup route, fullscreen (no AppShell),
loads setup-state from sidecar, renders the active step. Each step has a
placeholder body for now; Phase 5 swaps placeholders for real form
components from the matching settings pages. Already functional end-to-end:
Mark Complete writes to PUT /api/config/setup-state/steps/<step>, Skip
posts to /dismiss, Finish navigates to /.
- pages/team-settings.tsx — moved the existing workspace member listing out
of the old monolithic settings.tsx into its own /settings/team route. No
functional change; Phase 3 will add the invite form + role editor here.
- pages/settings-placeholder.tsx — generic "Coming in Phase X" stub used by
routes for clinics, doctors, telephony, ai, widget until those pages land.
Modified pages:
- pages/settings.tsx — rewritten as the Settings hub (the new /settings
route). Renders SectionCards in 3 groups (Hospital identity, Care
delivery, Channels & automation) with completion badges sourced from
/api/config/setup-state. The hub links to existing pages (/branding,
/rules) and to placeholder pages for the not-yet-built sections.
- pages/login.tsx — after successful login, calls getSetupState() and
redirects to /setup if wizardRequired. Failures fall through to / so an
older sidecar without the setup-state endpoint still works.
- components/layout/sidebar.tsx — collapsed the Configuration group
(Rules Engine + Branding standalone entries) into the single Settings
entry that opens the hub. Removes the IconSlidersUp import that's no
longer used.
New types and helpers (src/lib/setup-state.ts):
- SetupState / SetupStepName / SetupStepStatus types mirroring the sidecar
shape
- SETUP_STEP_NAMES constant + SETUP_STEP_LABELS map (title + description
per step) — single source of truth used by the wizard, hub, and any
future surface that wants to render step metadata
- getSetupState / markSetupStepComplete / markSetupStepIncomplete /
dismissSetupWizard / resetSetupState helpers wrapping the api-client
Other:
- lib/api-client.ts — added apiClient.put() helper for the setup-state
step update mutations (PUT was the only verb missing from the existing
get/post/graphql helpers)
- main.tsx — registered new routes:
/setup (fullscreen, no AppShell)
/settings (the hub, replaces old settings.tsx)
/settings/team (moved member listing)
/settings/clinics (placeholder, Phase 3)
/settings/doctors (placeholder, Phase 3)
/settings/telephony (placeholder, Phase 4)
/settings/ai (placeholder, Phase 4)
/settings/widget (placeholder, Phase 4)
Tested via npx tsc --noEmit and npm run build (clean, only pre-existing
chunk-size and dynamic-import warnings unrelated to this change).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fetches /api/config/widget from the sidecar and injects widget.js only
when enabled + embed.loginPage + key are all set. Falls back to VITE_API_URL
as the script host when cfg.url is empty (default for fresh configs).
Replaces an earlier draft that read VITE_WIDGET_KEY + VITE_WIDGET_URL from
build-time env — widget config lives in data/widget.json on the sidecar now
and is admin-editable via PUT /api/config/widget, so no rebuild is needed
to toggle or rotate it.
Never blocks login on a widget-config failure — the fetch is fire-and-forget
and errors just log a warning.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>