For outbound calls, SIP state transitions to 'active' when the agent's
bridge connects — before the customer picks up. Ozonetel state stays
'calling' until customer answers, then goes to 'in-call'.
Now reads ozonetelState from useAgentState and computes customerAnswered
(callState=active AND ozonetelState!=calling). Action buttons (Book Appt,
Enquiry, Transfer) disabled until customerAnswered is true.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AiFloatingButton: FAB (bottom-right) opens a slide-in drawer with
the supervisor AI chat panel. Close button collapses drawer, FAB
reappears. Chat state persists across open/close and page navigation.
- app-shell: mounts FAB for admin users (isAdmin), same pattern as
CallWidget for agents.
- team-dashboard: removed inline AI panel + toggle button — replaced
by the global FAB. Dashboard content reclaims the full width.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shows prominent banner on active-call-card when network drops:
- Offline: red banner "Network connection lost — call may have dropped"
- Unstable: yellow banner "Network unstable — call quality may be affected"
Uses existing useNetworkStatus hook. Banner disappears when network recovers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Product decision: agent cannot abort outbound call while ringing.
Risk accepted — misdialled calls will connect before agent can cancel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- appointments-v2: migrated from local query/state to useData().appointments.
Removed AppointmentRecord type, QUERY, fetchAppointments(), local useState.
All field references updated to transformed Appointment type (appointmentStatus,
patientName, patientPhone, clinicName, doctorId).
- active-call-card: calls refresh() after appointment book/reschedule/cancel
so pills update immediately. Also invalidates sidecar Redis cache.
- One source of truth — all appointment consumers read from DataProvider.
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>
- 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)
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)
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>
- 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>
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>
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>
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>
- 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>