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>
- 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>
- 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>
- 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>
- 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>
- 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>
- Replace text buttons with icon-only (eye, SMS, WhatsApp, call)
- Remove Patients tab from CC agent sidebar (CC works with leads, not patients)
- Fix icon wrapper prop forwarding cleanup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaced all bare `FC<{ className?: string }>` and `FC<HTMLAttributes<...>>`
wrappers that only forwarded `className` with `faIcon()` from
`src/lib/icon-wrapper.ts`, ensuring props like `data-icon` needed by the
Button component's CSS selector `*:data-icon:size-5` are correctly forwarded.
Also widened `NavItemBaseProps.icon` and `NavItemType.icon` prop types to
`FC<Record<string, any>>` to stay compatible with `faIcon()` return type.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all @untitledui/icons imports across 55 files with equivalent
@fortawesome/pro-duotone-svg-icons icons, using FontAwesomeIcon wrappers
(FC<{ className?: string }>) for prop-based usage and inline replacements
for direct JSX usage. Drops unsupported Untitled UI-specific props
(strokeWidth, numeric size). TypeScript compiles clean with no errors.
Co-Authored-By: Claude Sonnet 4.6 <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>
Instead of Kookoo outbound → IVR → dial back to SIP (broken bridge),
make the call directly from JsSIP in the browser. The SIP INVITE goes
through Ozonetel's SIP server to the PSTN. No intermediary needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All Table.Head components need at least one isRowHeader prop set.
Fixed in: worklist, agent-table, settings, patients, call-history, agent-detail.
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>
Dashboard KPI:
- Fix 1534m → 25h 34m (formatMinutes helper)
- Add info icon tooltips on all KPI and metric cards
- Pass role="admin" to AI panel for manager-specific prompts
Settings:
- Add search + pagination to employee table
- Infer roles from email convention (platform roles API returns null via API key)
AI Assistant:
- Role-specific quick prompts: manager sees "Agent performance", "Missed risks"
- Agent sees "Doctor availability", "Treatment packages"
Sticky headers:
- Add overflow-hidden to campaigns and all-leads pages
Misc:
- Fix free-brands-svg-icons → pro-duotone in integrations
- Remove Follow-ups from CC agent sidebar
- Remove global search from TopBar
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>