Campaign Details, Conversion Funnel, Source Breakdown now render as
3-column horizontal cards above the leads table. Table gets full width.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removed redundant Campaign, Ad, Email, First Contact, Spam, Dups
columns from campaign detail LeadTable — already on the campaign page.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Suggestions moved from above chat to below (before input) — agent
reads summary first, sees suggestions after AI responds
- During streaming, the last assistant message (raw JSON) is hidden —
only the typing indicator shows. Once complete, parsed message renders.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supervisor UI pass:
- PageHeader on all supervisor pages (Team Dashboard, Live Monitor,
Call Recordings, Missed Calls, Settings)
- Notification bell moved from wasted app-shell top bar into PageHeader
(admin-only), top bar only renders for agents now
- Settings cards disabled (Clinics, Doctors, Team, Telephony, AI, Widget)
- Campaign edit button disabled
- Column toggle blank page fixed (key-based Table remount)
- Live monitor: SSE replaces 5s polling for real-time call state
- Hold/unhold status reflected in supervisor live monitor via SSE
- Call Recordings: enriched agent names (agent relation in query)
- Missed Calls: underline tabs → custom pills
- Call Recordings: TopBar → PageHeader
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- team-dashboard.tsx: replaced inline header with PageHeader (was the
actual page being rendered, not team-performance.tsx)
- call-recordings.tsx: added agent relation to GraphQL query, render
uses enriched agent.name with raw agentName fallback — matches
Call History page pattern. Search + sort also use enriched name.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PageHeader: renders NotificationBell when isAdmin — bell now appears
on every page that uses PageHeader (leads, contacts, appointments,
patients, call history, missed calls, call recordings, live monitor,
team performance, settings)
- app-shell: top bar row only renders for agents (network indicator +
status toggle). Supervisors no longer see a wasted empty row.
- Call Recordings: TopBar → PageHeader with badge + info icon
- Live Monitor: TopBar → PageHeader with badge + info icon
- Team Performance: TopBar → PageHeader with info icon
- Settings: TopBar → PageHeader with info icon
- Missed Calls: underline tabs → custom pills (consistent with all pages)
- Desktop overlay app-shell synced with same changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
- Remove New/My Leads/All Leads tabs — redundant now that contacts
are on a separate page; all leads shown as a flat list
- Remove row checkboxes (selectionMode="none") — bulk actions weren't
wired to any backend and confused QA
- Move Search + Columns + Export into the header row alongside the
title — cleaner single-row layout
- Remove BulkActionBar + AssignModal + WhatsAppSendModal + MarkSpamModal
imports and JSX — dead code without checkboxes
- LeadTable: new selectionMode prop (default "multiple" for back-compat)
- Same cleanup on Contacts page (no checkboxes)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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)
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)
Lands 40 commits from feature/barge-whisper into master as a single marker-commit for easy rollback.
Highlights:
- Call attribution chain fix (inbound transferred calls now get the right agent.id; CDR enrichment now indexes by both UCID + monitorUCID)
- Worklist patientId + Book Appt pill patientId overlay — returning callers see their prior appointment as a reschedule pill
- Supervisor Dashboard merge: Team Performance surfaces folded in, scrollable sections, time-breakdown rendered as a table
- Data-provider pagination: KPIs no longer capped at 100 rows
- Background poll no longer flashes a Loading state
- Campaign detail: leads inline, View Leads button removed
- All Leads: stray Back button gone, Export CSV wired up
- Maint OTP modal: agent picker (Locked/Free) after OTP, no more reliance on agent-config in localStorage
- Per-tenant HELIX_SETUP_MANAGED flag hides Setup nav + banner on managed workspaces
- Supervisor AI chat panel: supervisor-specific quick actions
Revert this entire batch with: git revert -m 1 <merge-sha>
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>
Seed script was writing weekdayHours / saturdayHours / sundayHours +
requiredDocuments as strings — neither exist on Clinic that way.
Switched to per-day booleans + opensAt/closesAt. requiredDocuments is
a relation, so dropped from the clinic payload.
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>