Files
helix-engage/docs/requirements.md
saridsa2 85364c6d69 docs: add requirements tracker and Ozonetel CDR API reference
- requirements.md: full 16-user-story tracker with verified implementation
  status, code references, Ozonetel API findings, platform capability notes,
  and implementation guides for search (includeInSearch), barge/whisper, and
  appointment notifications
- ozonetel-cdr-api-reference.md: all 42 CDR fields, 3 endpoints (detailed,
  UCID, paginated), sidecar mapping status, known gotchas (nullable fields,
  field name inconsistency, rate limits)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:53:33 +05:30

67 KiB

Helix Engage — Requirements & Implementation Tracker

Goals

Visibility and optimisation of:

  • Average Lead Response Time
  • Call → Appointment Conversion Rate (%)
  • Lead → Appointment Conversion Rate (%)
  • Missed Call Callback Time

Ozonetel CDR — Unmapped Fields & API Notes

Full reference: docs/ozonetel-cdr-api-reference.md

3 CDR Endpoints Available

Endpoint Path Sidecar Uses?
Fetch CDR Detailed GET /ca_reports/fetchCDRDetails Yes — loads all records into memory
Fetch CDR by UCID GET /ca_reports/fetchCdrByUCID No — useful for Patient 360 single-call drill-down
Fetch CDR Paginated GET /ca_reports/fetchCdrByPagination No — returns totalCount, better for high-volume days

Constraints: 2 req/min rate limit, single-day only, 15-day lookback.

Sidecar Maps 6 of 42 CDR Fields

Currently mapped: AgentID, AgentName, Type, Status, TalkTime, Disposition

Unmapped Fields — Opportunities

CDR Field Value for Relevant US Null-safe?
HandlingTime Avg call handling time per agent US-13 Can be null
TimeToAnswer Lead response time / time-to-pickup Goal KPI: Avg Lead Response Time Yes
DID Which branch/public number the patient called US-2 (gap: DID display) Yes
CampaignName Campaign association per call US-15 (gap: campaign hidden column) Yes
HoldDuration Hold time per call US-12 (agent performance) Yes
WrapupDuration Wrap time per call US-12 (agent performance) Can be null
TransferType, TransferredTo/TransferTo Transfer tracking / audit US-3 (supervisor audit) Yes
CustomerRingTime How long customer phone rang before answer Missed call analysis Yes
HangupBy Who terminated (AgentHangup/UserHangup) Call quality analysis Yes
CallAudio S3 recording URL US-15 call log master (alternative to platform recordings) Yes

Known Gotchas

  1. Nullable fields: HandlingTime, WrapupDuration, WrapUpStartTime, WrapUpEndTime can be null when agent didn't complete wrapup. Must null-guard.
  2. Field name inconsistency: TransferredTo in fetchCDRDetails vs TransferTo in pagination endpoint. WrapUpEndTime vs WrapupEndTime casing also differs.
  3. Weekly report rate limit math: 7-day range = 7 CDR calls + 7 summary calls = 14 API calls at 2/min = 7 minutes minimum. Must cache daily results.

Risk Note

The ca_reports/summaryReport endpoint used by the sidecar for agent time breakdown (TotalLoginDuration, TotalBusyTime, TotalIdleTime, TotalPauseTime, TotalWrapupTime, TotalDialTime) is not publicly documented in Ozonetel's API reference. It works but is undocumented — potential breaking change risk.


User Stories

# Story Status Done Partial Missing
1 RBAC + Call routing logic 5/6 DONE 3 app roles (admin/cc-agent/executive) mapped from platform roles at login, sidebar nav per role, agent session locking, SIP auto-registration. Call routing handled by Ozonetel ACD (not CRM). Route-level guards missing (low risk) Atreum routing TBD
2 Telephony Interface 15/15 DONE SIP/WebRTC, mute, hold, transfer (CONFERENCE+KICK_CALL), listen-in (3-way conference), click-to-call, caller ID (3 SIP headers), DID N/A (multi-tenant), call type, live duration, ringing protection, network detection, busy retry (Ozonetel Advanced Retries)
3 Advanced Call Monitoring (Supervisor) 7/14 DONE Live monitor page with real-time polling, all 6 display fields, caller name resolution, KPI cards, RBAC via sidebar. Route-level guard. Barge/whisper/listen buttons exist but disabled. Barge/whisper/listen now unblocked: API found in CA-Admin source (apiId 63 for barge, apiId 158 for mode switch). Sidecar needs to proxy dashboardApi calls. Audit logging N/A until actions exist.
4 Appointment Creation During Calls Form 13/14, Disposition PARTIAL, Reschedule 3/4, Follow-up 1/2 Form complete (13 fields, dynamic slots, edit mode, cancel), disposition pre-selects based on action, caller resolve auto-detects returning patients, edit from context panel + appointments master Disposition doesn't conditionally filter options per spec, no "redial" option, no combined status. P360 edit read-only. No follow-up field in appointment form. No round-robin follow-up reassignment.
5 General Enquiries DONE: Form 11/11, AI 13/17 Enquiry form complete. AI has real tools (lookup_patient, lookup_appointments, lookup_doctor, book_appointment, create_lead), knowledge base (clinics, doctors, packages, insurance), compliance guardrails in system prompt Chat link to P360, reschedule from chat, sensitive data guardrail, treatment FAQs, per-location services
6 Lead Worklist Worklist 10/12, Leads 10/10, Side Panel 5/10 Worklist columns/filters/scoring, leads tab with My Leads + campaign filter, AI summary + appointments with edit in side panel Campaign display in side panel, P360 link Round-robin (manual assign modal exists, auto not visible), secondary patients, linked campaigns in side panel
7 Missed Call Queue 16/16 DONE All items verified: records (phone/timestamp/DID), FIFO queue, auto-assignment on Ready state, supervisor Missed Queue tab, dedup with count, 5 callback statuses, ingestion polling every 30s, campaign filtering, lead matching
8 Patient 360 Access 8/16 DONE Full page, AI summary (display only), identifiers (no MRN), timeline, appointments (read-only), call logs, notes (display only) AI on-demand generate, lead status display, appointment click-to-edit P360 search, multi-patient, campaign/inquiry fields, Book/Note buttons not wired, appointment edit/cancel
9 Appointment Notifications 0/3 Infrastructure exists: platform WhatsApp bridge (sendMessage, sendImage), frontend templates/modal Automation trigger on appointment create, 12hr reminder cron, notification templates. CTA buttons need WhatsApp Business API (GoWA bridge is personal WA only)
10 Global Search 4/7 DONE Component built, grouped results, preview, navigation. Sidecar search is naive (50-record fetch + JS includes) — needs migration to platform search GraphQL query Wire into header, proper full-text/fuzzy search, phone format matching
11 Agent Availability Status 13/13 DONE Toggle syncs to Ozonetel AUX (blocks inbound + outbound), all derived statuses via SSE, duration tracking via summaryReport
12 Agent Performance Dashboard 4/5 DONE Daily metrics, time distribution, lead response, disposition chart Weekly/monthly range KPI benchmarks/targets
13 Supervisor Dashboard 14/14 DONE All metrics available via API + UI (time breakdown in separate section, not table columns). Bar charts shown as line/gauge.
14 Call Center Admin 7/11 DONE User CRUD (platform + onboarding UI), clinics CRUD, doctors CRUD, audit logs (platform) Supervisor team view, departments KPI config, team/supervisor hierarchy, operational config (later scope)
15 Master Data 24/26 DONE Leads, patients, appointments, call logs, column toggle, edit logs (platform), audit (platform) MRN (needs HIS), campaign/chief-complaint hidden columns
16 Data Security and Compliance 8/10 DONE RBAC (platform), audit logs (platform), rate limiting (platform), bearer auth, input validation, HTTPS, outbound webhook signing (platform) ClickHouse retention policy Incident response plan, inbound webhook verification
Predictive Analytics (later scope) NOT STARTED

US-1: RBAC + Call Routing Logic

TBD for Atreum hospital.

RBAC — VERIFIED

3 app roles mapped from platform roles at login (auth.controller.ts lines 108-117):

Platform Role App Role Sidebar Navigation
HelixEngage Manager admin Supervisor (Dashboard, Team Performance, Live Monitor) + Data & Reports + Marketing + Admin Settings
HelixEngage User + email contains cc cc-agent Call Center (Call Desk, Call History, Patients, Appointments, My Performance)
HelixEngage User (default) executive Marketing (Lead Workspace, All Leads, Patients, Appointments, Campaigns, Outreach, Analytics)

Role enforcement layers:

  1. Platform RBACRoleEntity, PermissionsService, per-workspace granular permissions (verified in US-16)
  2. Login role mapping — sidecar's auth.controller.ts reads workspaceMember.roles[].label and maps to app role
  3. Sidebar navigationsidebar.tsx getNavSections(role) shows different nav items per role (lines 61-114)
  4. Route-level guardsNOT implemented. No middleware on routes like /live-monitor, /team-performance, /settings. A cc-agent who types the URL directly can access admin pages. Low risk but should be added.
  5. Agent session locking — duplicate login detection per Ozonetel agent ID (sessionService.lockSession, lines 131-134). Blocks same agent from two devices.
  6. SIP registration — per-agent SIP config from Agent entity. Login auto-registers SIP if Agent record exists for the workspace member (line 122-139).

Call Routing — HANDLED BY OZONETEL

Call routing (which agent receives which inbound call) is configured in Ozonetel CloudAgent, not in the CRM:

  • Skill-based routing — agents assigned to skills/campaigns in Ozonetel admin portal
  • ACD (Automatic Call Distribution) — Ozonetel routes inbound calls to available agents in the campaign
  • Campaign-DID mapping — each DID maps to an Ozonetel campaign, calls to that DID route to agents assigned to that campaign
  • Per-hospital isolation — each hospital has its own Ozonetel account, own agents, own DIDs (verified in US-3)

The CRM does not control call routing. It controls agent availability (Ready/Break/Training → synced to Ozonetel AUX state, verified in US-11) which influences routing decisions.

Atreum hospital: spec says "TBD" — no specific routing requirements defined yet.


US-2: Telephony Interface

Call Center Agent should be able to make and receive calls directly within the CRM through a softphone interface with standard telephony controls (answer, click-to-call, end call, forward call) so that all patient interactions happen within the CRM without switching tools.

Success Metrics

  • Currently, agents do outgoing calls looking at excel sheets for leads — manual errors when copying mobile numbers onto dialer, switching interfaces/tabs means loss of context and info on campaigns/names/patient details. Optimise for number of clicks and page shifts.
  • When making these calls, the agents have no context of their previous interactions with the hospitals, treatments they're interested in etc. to be able to upsell or convert.

Functional Requirements

The call center agent should be able to operate with CRM as their primary work interface. The telephony solution (Ozonetel, Exotel etc.) will be integrated with the CRM to enable use from within the CRM interface.

Call Widget

On the CRM, the user will see a pop-up widget on incoming calls. Within this they will be able to:

  • Answer the incoming call through their headphones — SIP WebRTC via JsSIP (sip-client.ts)
  • Mute themselves on the call — toggleMute() in SIP client (lines 193-202)
  • Keep the caller on hold — hold()/unhold() in SIP client (lines 205-215)
  • Forward the call to a different party (clinic, doctor, doctor's assistant etc.) — transfer-dialog.tsx: lists agents (with live status polling) + doctors, uses Ozonetel CONFERENCE API
  • Listen in on the forwarded call — agent stays in 3-way conference after CONFERENCE action until KICK_CALL completes. Can hear both target and caller. Not a dedicated "listen-only" mode but achieves the spec requirement. (transfer-dialog.tsx lines 125-141: connect → "Speak privately, then complete the transfer")
  • Drop off once the call is forwarded — KICK_CALL action removes agent from conference (lines 145-161)
  • See the patient's mobile number (incoming call number) — extracted from SIP headers: X-CALLERNO, P-Asserted-Identity, Remote-Party-ID (sip-client.ts lines 246-275)
  • See the branch/clinic/number the patient is calling to (public facing customer number identification) — N/A in multi-tenant architecture. Each sidecar instance serves one hospital. The agent logged into Ramaiah's workspace will only receive Ramaiah calls. DID display is redundant and could cause confusion if shown. DID data is still available in CDR + missed call events for analytics purposes.
  • See the call type (inbound / outbound) — callDirectionRef in SIP provider
  • See the live call duration — 1-second tick via sipCallDurationAtom

Outbound Calls

  • Click on a lead or patient contact to make the outgoing call — click-to-call-button.tsxdialOutbound() via Ozonetel API. Disabled when agent not registered or already in call.
  • When making an outbound call, once they click to call, they should NOT have the option to disconnect while it is still ringing — beforeunload event listener during ringing state + 30s safety timeout (sip-provider.tsx lines 103-137, 179-182)
  • If a network issue disconnects the call, show message that the network dropped. The call should not go out of the worklist. — use-network-status.ts: tracks good|unstable|offline, counts disconnects (3+ in 2 min = unstable), window offline/online event listeners
  • When outbound call hits busy, the item stays in the worklist at the same ranking till the second attempt — Handled by Ozonetel, not CRM. Ozonetel's Advanced Retries configures retry attempts based on call status (busy, no answer) at the campaign level. Configurable: max tries/day, max days to retry, per-status retry intervals, AND/OR logic. API exists in CA-Admin sourceretryConditions is JSON-serialized and submitted as part of the campaign create/update payload to POST/PUT {ADMIN_BASE_URL}/outboundCampaign (see CampaignOutBoundForm.jsx lines 1110-1121, DispositionRetryConfig.jsx for the form UI). Base URL: https://api.cloudagent.ozonetel.com/ca-admin-Api/CloudAgentAPI.

Auth for Ozonetel admin APIs (from CA-Admin/...services/auth-service.ts + api-service.ts):

  1. Pre-login: GET https://api.cloudagent.ozonetel.com/api/auth/public-key{ publicKey, keyId }
  2. Login: POST https://api.cloudagent.ozonetel.com/auth/login with body { username: RSA_ENCRYPT(user), password: RSA_ENCRYPT(pass), keyId, ltype: "PORTAL" } → returns JWT token
  3. All API calls: headers { Authorization: "Bearer <token>", userId, userName, isSuperAdmin, dAccessType }
  4. Token stored in localStorage, expiry checked via jwt-decode

This same auth is needed for the barge-in API (apiId 63), mode switch (apiId 158), and campaign retry config.

Multi-tenant design: Each hospital has its own Ozonetel account with its own admin login, agents, and DIDs. The single-tenant sidecar model is correct by design — one Ozonetel account per sidecar per hospital. A supervisor can only barge into their own hospital's calls. No cross-tenant barge problem exists.

Credential storage: The sidecar's TelephonyConfig (telephony.defaults.ts) should be extended with Ozonetel admin credentials:

ozonetel: {
    // existing fields...
    agentId: string;
    agentPassword: string;
    did: string;
    sipId: string;
    campaignName: string;
    // NEW: Ozonetel portal admin credentials for supervisor APIs (barge, retries, campaign config)
    adminUsername: string;
    adminPassword: string;
};

Editable from the setup wizard Telephony step. Sidecar authenticates on startup:

  1. GET https://api.cloudagent.ozonetel.com/api/auth/public-key{ publicKey, keyId }
  2. POST https://api.cloudagent.ozonetel.com/auth/login with RSA-encrypted credentials → JWT
  3. Cache JWT in memory, refresh on expiry (decoded via jwt-decode)
  4. Use for all admin API calls: barge (apiId 63), mode switch (apiId 158), campaign config, retry rules

Simpler SIP-only barge approach (avoids admin API auth entirely):

  • Supervisor registers with their own SIP extension (like agents do)
  • Use existing Ozonetel CONFERENCE API (already used in transfer-dialog.tsx) to add supervisor to active call by UCID
  • Mode switching via DTMF 4/5/6 over the SIP connection
  • This approach reuses existing SIP infrastructure and doesn't need Ozonetel admin auth at all

US-3: Advanced Call Monitoring (Supervisor)

Call Center Supervisor should be able to monitor ongoing calls using telephony features such as call barge-in and whisper so that they can guide agents in real time and maintain service quality.

Real-Time Active Calls View — VERIFIED

live-monitor.tsx — polls /api/supervisor/active-calls every 5s, updates duration counters every 1s. KPI cards: Active Calls, On Hold, Avg Duration. Caller name resolved from leads by phone match.

  • Agent name — call.agentId in table (line 132)
  • Agent status (on call) — status column shows active / on-hold with color badges (line 147-149)
  • Patient contact number — shown with caller name resolution from leads (lines 72-83, 134-139)
  • Call start timestamp — used for duration calc (line 21-24)
  • Call duration — live updating via formatDuration(call.startTime) + tick state (line 144)
  • Call type (inbound / outbound) — "In"/"Out" badge from call.callType (lines 126-127, 141)

Supervisor Actions on Active Calls

  • Listen: Button exists, disabled (line 153: title="Coming soon — pending Ozonetel API")
  • Whisper: Button exists, disabled (line 154: same tooltip)
  • Barge In: Button exists, disabled (line 155: title="Coming soon — requires supervisor SIP extension")

Ozonetel implementation (verified from CloudAgent source in cloudagent/):

Barge/whisper/snoop are triggered from the Ozonetel admin dashboard UI. The mechanism is:

  1. Admin clicks Barge/Whisper/Listen in Ozonetel dashboard
  2. Ozonetel server bridges the audio at the telephony layer
  3. Agent's CloudAgent app receives agentBarginStart / agentBarginEnd WebSocket events (constants.js lines 59-60)
  4. Agent-side code handles screen recording state on barge (websocket.service.js lines 403-457)

Barge-in API found in Ozonetel CA-Admin source code (CA-Admin/cloudagent.ozonetel.com/static/js/services/api-service.ts):

// Barge-in API (line 827-842)
POST https://api.cloudagent.ozonetel.com/dashboardApi/monitor/api
{
    apiId: 63,
    ucid: "<UCID of active call>",
    action: "CALL_BARGEIN",
    isSip: true | false,      // SIP mode or Normal (PSTN)
    phoneno: "<supervisor phone or SIP extension>",
    agentNumber: "<agent phone number>",
    cbURL: "<callback URL>"
}

// Barge-in mode switch (Listen/Whisper/Bargein) via Redis (line 844-856)
POST https://api.cloudagent.ozonetel.com/dashboardApi/monitor/api
{
    apiId: 158,
    Action: "listen" | "training" | "bargein",  // training = whisper
    AgentId: "<agent ID>",
    Sip: "<SIP extension>"
}

// SIP subscribe for barge number (line 880-890)
POST https://api.cloudagent.ozonetel.com/ca-admin-Api/CloudAgentAPI/endpoint/sipnumber/sipSubscribe
{
    apiId: 139,
    sipURL: "<SIP URL>"
}

Flow (from BargeInDrawer.tsx):

  1. Supervisor enters their phone number or SIP extension
  2. Clicks "Call" → bargeIn() API called with UCID + agent number + supervisor number
  3. Ozonetel bridges the supervisor into the active call
  4. Once connected, supervisor can switch between Listen / Training (whisper) / Bargein modes via bargeInRedis() API (apiId: 158)
  5. Agent-side receives agentBarginStart / agentBarginEnd WebSocket events

Two barge types supported:

  • Normal — Ozonetel calls the supervisor's phone number (PSTN)
  • SIP — supervisor connects via SIP extension (shows Listen/Training/Bargein tabs after connect)

Mode switching (SIP barge) uses DTMF tones (from BargeinDrawerSip.tsx lines 522-526, 658-660):

  • Listen = DTMF 4
  • Training/Whisper = DTMF 5
  • Barge-in = DTMF 6

Once supervisor is connected via SIP, handleChangeTab calls sendSIPDTMF(newValue) which sends kSip.sendDTMF(dtmf).

Implementation Reference for Helix Engage

Source files to study:

File Path What it does
bargeIn() API CA-Admin/cloudagent.ozonetel.com/static/js/services/api-service.ts:827-842 POST to dashboardApi with apiId 63, UCID, action CALL_BARGEIN
bargeInRedis() API CA-Admin/...api-service.ts:844-856 POST with apiId 158, Action listen/training/bargein
bargeInPhoneNumber() API CA-Admin/...api-service.ts:880-890 SIP subscribe endpoint
BargeInDrawer.tsx CA-Admin/.../components/BargeInDrawer/BargeInDrawer.tsx Normal (PSTN) barge UI — enters phone, calls bargeIn API
BargeinDrawerSip.tsx CA-Admin/.../components/BargeinDrawerSip/BargeinDrawerSip.tsx SIP barge UI — uses kSip library, DTMF for mode switch
kSip utility CA-Admin/.../utils/ksip.tsx SIP client wrapper (register, hangup, sendDTMF, status)
Agent WebSocket events cloudagent/.../services/websocket.service.js:367-370 Agent receives agentBarginStart/agentBarginEnd
WEB_EVENTS constants cloudagent/.../constants.js:59-60 Event name definitions

Implementation steps:

  1. Sidecar: proxy POST /api/supervisor/barge → calls Ozonetel dashboardApi/monitor/api with apiId 63 (needs Ozonetel admin session token — check how CA-Admin authenticates)
  2. Sidecar: proxy POST /api/supervisor/barge-mode → calls apiId 158 for mode switch (or use SIP DTMF approach which doesn't need API)
  3. Frontend (live-monitor.tsx): replace disabled buttons (lines 153-155) with a barge drawer component. On click: collect supervisor SIP extension → call sidecar barge API → on connect show Listen/Whisper/Bargein tabs → tab switch sends DTMF 4/5/6
  4. Frontend (agent side): listen for Ozonetel WebSocket agentBarginStart/agentBarginEnd events → show "Supervisor monitoring" indicator in call UI
  5. Auth: Ozonetel admin credentials stored in TelephonyConfig (adminUsername, adminPassword). Sidecar authenticates via RSA-encrypted login → JWT. See credential storage section above for full flow. Each hospital has its own Ozonetel account — no cross-tenant concerns.

Constraints

  • Restrict call monitoring capabilities to supervisors and admins only — sidebar navigation only shows Live Monitor for role === 'admin' (sidebar.tsx line 62-67). No route-level guard on /live-monitor — a cc-agent who types the URL directly could access it. Low risk since they'd need to know the URL.
  • Call recording should include the entire call including supervisor participation — not applicable until barge/whisper is implemented

Audit Logging

Not applicable until barge/whisper is implemented. No supervisor intervention actions to log yet.

  • Supervisor ID, Agent ID, Call ID
  • Intervention type (listen / whisper / barge-in)
  • Timestamp
  • Available in call audit logs for quality review

US-4: Appointment Creation During Calls

Call Center Agent should be able to create patient appointments within the CRM during a call with all the necessary details so that patient intent is captured immediately and consultations can be scheduled without delay.

Appointment Form Fields

  • Caller name
  • Patient details (option to say "same as the caller")
    • Patient name *
    • Patient contact
    • Caller's relationship to patient *
    • Option to choose from existing secondary patients linked to this mobile number
  • Date of the appointment
  • Clinic name / branch name
  • Specialty / department
  • Doctor name
  • Chief complaint
  • Date and time slot
  • Age and gender of the patient (optional)
  • Returning patient (Y/N) — handled by system
  • Source or referral details
  • Agent notes
  • Follow up (Y/N) — if Y: date and time of follow up — exists in enquiry form, not in appointment form

Disposition Logic — VERIFIED

The disposition modal (disposition-modal.tsx) shows 6 options always. Context-aware pre-selection via suggestedDisposition:

  • Appointment booked → setSuggestedDisposition('APPOINTMENT_BOOKED') (active-call-card.tsx line 141)
  • Enquiry logged → setSuggestedDisposition('INFO_PROVIDED') (line 332)
  • Follow-up created → setSuggestedDisposition('FOLLOW_UP_SCHEDULED') (line 307)

6 disposition options (always shown, not conditionally filtered per spec):

  • APPOINTMENT_BOOKED — "Appointment Booked"
  • FOLLOW_UP_SCHEDULED — "Follow-up Needed"
  • INFO_PROVIDED — "Info Provided"
  • NO_ANSWER — "No Answer"
  • WRONG_NUMBER — "Wrong Number"
  • CALLBACK_REQUESTED — "Not Interested"

Gap vs spec: The spec defines 5 different disposition flows based on call outcome (picked+appointment, picked+no appointment, picked+enquiry, not picked, both). The implementation shows all 6 options always with a pre-selected suggestion. No conditional filtering of options. No "call dropped → option to redial". No combined "appointment booked and enquiry made" status. Simpler but doesn't match spec exactly.

Notes field: Optional notes textarea included. callerDisconnected flag changes the header text ("Call ended" vs "End Call?"). Dismiss action differs: if caller disconnected, dismiss resets; if not, it keeps the call active.

Returning Patient Handling

  • If returning patient registered under the same mobile number, access patient 360 (or by MRN when EMR integration exists) — /api/caller/resolve auto-detects
  • If different patient with unregistered mobile number:
    • System will not identify the patient — no summary shown
    • Agent can search by registered number or name in patient master to get profile
    • Agent can make additions or changes (appointment changes etc.)
    • System automatically captures new incoming number and links to the patient profile (feasibility TBD)

Rescheduling / Cancellation

  • If call is from registered number, agent can edit appointment from the AI patient summary (right side panel) — context-panel Edit button → opens AppointmentForm in edit mode with pre-filled fields (line 192 of context-panel.tsx)
  • Agent can find appointment in patient 360 and edit — P360 appointment rows are read-only (no click handler, verified in US-8)
  • Agent can find appointment in appointment master list and edit — appointments.tsx page
  • Cancel appointment — handleCancel() in appointment-form.tsx (line 342) → GraphQL mutation sets status: 'CANCELLED'

Scheduled Follow-ups

  • Follow-ups display in agent's worklist — worklist.service.ts line 77 fetches by assignedAgent, displayed in worklist Follow-ups tab
  • Auto-assigned based on availability (round robin) — not implemented. Follow-up stays assigned to the agent who created it (assignedAgent: agentName in enquiry-form.tsx line 177). No round-robin reassignment on the follow-up date.

US-5: General Enquiries

Call Center Agent should be able to capture contact details and inquiry information for callers who are not existing leads so that potential patients can be added to the marketing database for future engagement. Then they should be able to address the query in full detail.

Enquiry Form Fields

  • Name of the caller *
  • Name of the patient * (option to say same as caller)
  • Relationship with the patient (optional)
  • Source / referral info *
  • Query asked *
  • Existing patient? (Yes / No) *
  • Registered mobile number
  • Relevant department (optional)
  • Relevant doctor (optional)
  • Is follow up needed (Y/N) *
    • Date and time of follow up
  • Disposition (as detailed in US-4)

The information collected is automatically added to the patient 360 (IDed by contact number), accessible to both marketing and call center teams.

If there are inbound/outbound calls to the patient thus recorded, the system will automatically surface the patient 360 and give a short summary of past interactions (detailed in worklist section).

AI Assistant for Agents — VERIFIED: Tool-calling implemented, NOT Phase 2

The AI chat backend (ai-chat.controller.ts) uses Vercel AI SDK streamText with real tools. Not a simple chat wrapper.

Static Info (Knowledge Base — cached 5 min, rebuilt from platform GraphQL)

  • Clinic info, location, specialties, doctors, treatments available — buildKnowledgeBase() fetches clinics (name, address, weekday/Saturday/Sunday hours, walk-in, online booking), doctors (name, department, specialty, fees, visit slots, clinics), health packages (name, price, discounted price, tests, inclusions)
  • Treatment FAQs, lab test preparation instructions — not in KB
  • Insurance policies, visiting hours — insurance partners (insurer name, TPA, settlement type, plan types) + clinic hours in KB
  • Pricing ranges (if hospital permits) — consultation fees + package prices in KB
  • Services available by location — clinics have address but no per-location service mapping

Dynamic Info

  • Doctor availability — lookup_doctor tool queries doctors with DOCTOR_VISIT_SLOTS_FRAGMENT (day + time slots per doctor per clinic)
  • Current packages and offers available — health packages with discounted prices in KB

Appointment Info

  • Using registered mobile number, surface appointment info — lookup_patient tool searches leads by phone, returns patientId, then lookup_appointments tool fetches appointments by patientId (doctor, department, date, status, reason)
  • Link opens details with ability to reschedule or cancel — tool returns data but no clickable link/action in chat UI
  • 100% accuracy architecture — tools use platform GraphQL queries (structured data), not LLM hallucination

Patient Info (by mobile number or MRN)

  • Summary of previous interactions if existing patient — lookup_patient returns aiSummary, aiSuggestedAction, status, contactAttempts, lastContacted
  • Query further on dates/appointments linked to that patient ID — lookup_appointments tool takes patientId
  • Example: "Patient cancelled 2 appointments booked in the last month with Dr Sharma, cardiology." — achievable via lookup_appointments → LLM summarizes
  • Patient 360 link — no clickable link rendered in chat messages

Compliance Guardrails

  • Must not give medical advice — system prompt: "NEVER give medical advice, diagnosis, or treatment recommendations" (ai.defaults.ts line 120) + "Never quote prices. No medical advice. For clinical questions, defer to a doctor" (line 103)
  • Must not give medical diagnosis — covered by same prompt rule
  • Must not give any sensitive hospital or doctor data — not explicitly enforced. Doctor fees, schedules, and clinic addresses are in the knowledge base and shared freely.

Additional AI Capabilities Found (not in original spec)

  • book_appointment tool — AI can book appointments directly via GraphQL mutation
  • create_lead tool — AI can create leads for new callers
  • Supervisor mode (ctx.type === 'supervisor') with separate tools: get_agent_performance, get_campaign_stats, get_call_summary, get_sla_breaches
  • Caller context injection — when on a call, AI knows who the agent is talking to
  • Quick action buttons from theme tokens

Data Access to the LLM

  • Hospital database (static info) — read-only API access
  • Appointment database
  • Patient 360 database

Accuracy Architecture

For instances needing 100% accuracy:

  1. Agent provides mobile number or MRN
  2. LLM must call a tool
  3. Tool runs SQL query
  4. Tool returns structured data
  5. LLM summarizes the returned data

The model must NOT answer without using the lookup tool. If no results found, default answer: "no info".


US-6 & US-7: Worklist (Leads + Missed Calls)

Worklist Overview

Each user will have a worklist auto-assigned based on set priorities. This includes leads from marketing campaigns and scheduled follow-ups.

Priorities are taken from hospital management. The call centre admin/supervisor can change these:

  • Set rankings for different tasks (missed calls, campaigns, follow-ups etc.) — scoring.ts + priority-config-panel.tsx
  • Set SLAs for each task type
  • Within campaigns, rank different campaigns
  • Rank campaign sources (Meta, website etc.) based on lead quality

Worklist Table Columns

  • Mobile number
  • Name of patient
  • SLA
  • Call type (lead / missed call / follow up / second attempt etc.)
  • Campaign (e.g., "cervical cancer check" — empty for non-applicable items)
  • Timestamp of creation (lead creation, missed call, follow-up date etc.)
  • Call source number (branch DID the customer called)
  • CTA to call

Table Actions

  • Click on patient name → side panel opens with patient summary
    • Existing patient: full summary (see below)
    • New patient: basic info summary (see below)
  • Click-to-call CTA on row actions (accessible even when side panel is open)
  • Filter by task type: missed calls, follow-ups, leads, campaign names

Side Panel — AI Summary (full height, toggle between AI Summary and Ask AI)

Existing Patient Summary

  1. Name + Age + Gender — lead header shows fullName, linked patient shows name + patientType badge (context-panel.tsx lines 126-146, 217-227)
  2. (P2) Secondary patients tagged to primary contact — not implemented
  3. 2-3 line AI summary — lead.aiSummary + lead.aiSuggestedAction in AI Insight section (lines 152-163)
    • P0: Summary of earlier interactions (calls and messages) — optimise for stated intent and negative sentiments
    • P1: Known treatments currently taking (dependent on EMR integration)
  4. Surface linked campaigns (with header + timestamp) — not shown in context panel. Lead utmCampaign/campaignId not rendered.
  5. Surface live appointments: clinic + doctor name + date — Upcoming section shows appointments with doctor name, department, date, status (lines 172-197)
    • Ability to edit appointment (opens appointment section pre-filled) — Edit button on each appointment row, opens AppointmentForm with editingAppointment state (line 192, 65)
  6. "View more" → full patient 360 — no link to Patient 360 from context panel

New Patient Summary

  1. Name (age, gender if available) — lead header shows name (line 135)
  2. AI summary — same aiSummary section
  3. Campaigns they've shown interest in and sources (FB, website etc.) — not shown in context panel
  4. Linked campaigns (with header + timestamp) — not shown

US-6: Leads from Marketing (Detail)

Call Center Agent should be able to view leads assigned to them (round robin assignment) with relevant campaign, inquiry, and patient details in their worklist so that they can contact patients with appropriate context while maintaining role-based access restrictions.

Leads Tab

  • Agent sees all leads assigned to them by marketing team — all-leads.tsx has "My Leads" tab (line 29, filters by user.name), "New" tab (NEW status), "All" tab
  • Filter leads by campaign (e.g., IVF leads) — campaignFilter state (line 61), campaign pills rendered with counts (lines 293-327), includes "No Campaign" filter

Round Robin Assignment Columns

  • Contact number (sortable) — key lead/patient identifier
  • Patient name (sortable)
  • Email (sortable, optional)
  • Campaign ID (sortable)
  • Campaign name
  • SLA status
  • Age of the lead (from creation date) — visually highlighted when beyond limit (age-indicator.tsx)
  • Timestamp of lead creation

Leads sorted by assigned priority.

Lead Actions

  • Click-to-call CTA
  • Patient summary panel appears (returning customer history, current campaign interest)
  • All call widget features available
  • Can create appointments or note/address general enquiries
  • Once click-to-call, no option to disconnect while ringing
  • Network disconnect shows message; call stays in worklist

Scheduled Follow-ups

  • Auto-assigned to agent on the day/time of follow-up based on availability (round robin) — PARTIAL (backend)

US-7: Missed Call Queue (Detail)

Call Center Agent should be able to receive missed calls when available, assigned by the system based on FIFO priority so that missed patient calls are addressed promptly.

Missed Call Record

When all agents are busy, create a missed call record capturing:

  • Caller mobile number
  • Timestamp
  • Call source number (which branch etc.)

Queue Behavior

  • Place record into Missed Call Queue (FIFO)
  • Exhaustive missed call list accessible to supervisor
  • Once click-to-call (callback), no option to disconnect while ringing
  • For Ramaiah demo: all missed calls populated in agent worklist

Auto-Assignment

  • When agent becomes available (call ends or status → Active), check if Missed Call Queue is non-empty — ozonetel-agent.controller.ts line 82: when agent state changes to Ready, calls missedQueue.assignNext(agentId). Mutex lock prevents race conditions.
  • Auto-assign the highest-ageing missed call to the agent — missed-queue.service.ts line 172-221: assignNext() queries oldest unassigned PENDING_CALLBACK call (orderBy: startedAt AscNullsLast), assigns to agent via updateCall mutation.
  • Assigned missed call appears at top of agent's worklist tagged "Missed Call"
  • Unassigned missed calls (all agents busy) shown on supervisor dashboard — team-dashboard.tsx line 41: "Missed Queue" tab with count of MISSED calls.

Duplicate Handling

  • Same number calls multiple times before callback → merge into single missed call record
  • Update missed call count — missedcallcount field with (Nx) badge
  • Update latest timestamp

Callback SLA Tracking

Each missed call should track:

  • Time since missed call
  • Callback status:
    • Pending callback
    • Callback attempted
    • Callback completed
    • Invalid / unreachable
    • Wrong number

US-8: Patient 360 Access

Call Center Agent should be able to view a patient's 360-degree profile (full page, not the right-hand panel summary) for returning patients so that conversations can be informed by past interactions, appointments, and inquiries.

AI Summary

  • Summarises all patient info in ~2 sentences — displays leadInfo.aiSummary from linked lead (line 415-419)
  • Generated on demand in patient 360 (economical) — no generate button or API call on P360 page; displays whatever aiSummary the lead already has. Not truly on-demand.
  • During calls (inbound/outbound), auto-generate updated summary → surface in side panel (context-panel, not P360)
  • Retrieve profile by registered mobile number, MRN, or patient name — page accessed only via /patient/:id route (line 266: useParams). No search input on P360 page. Navigation is from patients list or worklist side panel.
  • Display key identifiers: patient name, age (computed from DOB), gender, phone, email, patientType badge — lines 347-385. No MRN (requires HIS).

Multiple Patients Under Same Number

  • Individual appointments separately listed within patient 360 — not implemented. Single patient fetched by ID.
  • Show relationship of primary contact to connected patients — not implemented

Patient 360 Content

  • Chronological timeline of all interactions — Timeline tab (line 502-522) shows activities from linked leads: calls, WhatsApp, SMS, email, notes, status changes, assignments, appointment bookings, follow-ups. 15 activity types supported (lines 30-46).
  • All past and upcoming appointments — Appointments tab (line 464-480) with doctor name, department, duration, reason, status badges (COMPLETED/SCHEDULED/CONFIRMED/CANCELLED/NO_SHOW/RESCHEDULED). Sorted desc by scheduledAt.
    • Click on appointment to see full details and edit — not implemented. Appointments rendered as read-only rows (AppointmentRow component has no click handler).
  • Marketing lead information — PARTIAL. GraphQL fetches source, status, interestedService (line 79-81). UI shows source as badge (line 382) and interestedService (line 407). status is fetched but never rendered. campaignName and inquiryType not in query.
  • Call logs — Calls tab (line 483-499) with direction badge, agent name, duration, disposition. Sorted desc by startedAt.
  • Notes — Notes tab (line 525-562) with add note form (textarea + button) and note history from lead activities filtered by NOTE_ADDED.

Actions from Patient 360

  • Click-to-call — ClickToCallButton with phone number (line 424)
  • Appointment booking — "Book Appointment" button (line 426) — button exists but no onClick handler wired
  • Appointment editing / rescheduling / cancellation — not implemented on P360 page. No edit/cancel actions on appointment rows.
  • Adding interaction notes — Note textarea + "Add Note" button (line 537) — button has no onClick handler wired (isDisabled logic only, no submit)

US-9: Appointment Notifications

Call Center Agent should be able to trigger automated WhatsApp confirmations and reminders for booked appointments so that patients receive timely communication and appointment adherence improves.

  • When appointment is created, patient gets automated WhatsApp message confirming with appointment details
  • 12 hours before the appointment slot, patient gets a reminder
  • (With HIS integration) Send reminder with CTA to confirm or cancel → instantly updates HIS and frees up slot

What Exists (Infrastructure)

Platform WhatsApp bridge — CONFIRMED:

  • WhatsAppBridgeClientService provides sendMessage(deviceId, to, text), sendImage(), typing indicators
  • GoWA bridge deployed as Docker container on VPS + EC2 (port 4042)
  • Device management: QR login, phone pairing, connection status
  • Inbound webhook handler for incoming WhatsApp messages

Frontend:

  • whatsapp-send-modal.tsx — manual bulk send with template variable substitution ({{name}}, {{first_name}}, {{service}})
  • Template list, preview, mockup components
  • APPOINTMENT_REMINDER follow-up type exists in mock data

What's Missing (3 items)

1. Appointment confirmation trigger — when the sidecar's appointment form creates an appointment via platform GraphQL, no code calls WhatsAppBridgeClientService.sendMessage(). Needs:

  • Sidecar endpoint or platform database-event-trigger that fires on appointment creation
  • Template: "Hi {{name}}, your appointment with {{doctorName}} at {{clinic}} on {{date}} at {{time}} is confirmed."
  • Phone number from the caller/patient record

2. 12-hour reminder scheduler — no cron job or BullMQ worker polls upcoming appointments. Needs:

  • A cron job (sidecar or platform) that runs every hour
  • Queries appointments where scheduledAt is between 11-13 hours from now
  • Sends reminder via WhatsAppBridgeClientService.sendMessage()
  • Tracks "reminder sent" flag to avoid duplicates

3. Appointment notification templates — existing templates are for marketing leads, not appointment confirmations/reminders. Needs:

  • Confirmation template
  • Reminder template
  • (Later) Cancellation/reschedule template with CTA buttons (requires WhatsApp Business API, not personal WhatsApp via GoWA bridge)

Architecture Decision

The GoWA bridge uses personal WhatsApp (whatsmeow protocol). For template messages with CTA buttons (confirm/cancel), you'd need WhatsApp Business API (via providers like Gupshup, Twilio, or Meta directly). Current bridge supports text + image only — no interactive buttons.

Capability GoWA Bridge (current) WhatsApp Business API (needed for CTA)
Send text message Yes Yes
Send image Yes Yes
Template variables Manual in code Native template system
CTA buttons (confirm/cancel) No Yes
Delivery receipts Limited Yes
Cost Free (personal WA) Per-message pricing

Call Center Agent should be able to perform a global search across patients, leads, and appointments so that they can quickly locate the correct record during or before a call without navigating through multiple modules.

  • Global search bar accessible from CRM header — available from any screen — component built (global-search.tsx) but NOT wired into header navigation
  • Search by: patient name, registered mobile number, MRN, appointment ID, lead phone number — PARTIAL: current sidecar implementation is naive (see below)
  • Support partial matching and fuzzy search on names and phone numbers — PARTIAL: current impl uses JS .includes(), not proper full-text search
  • Group results by entity type (Patients, Leads, Appointments)
  • Each result shows summary preview: patient name, mobile number, MRN, appointment date, or lead source
  • Selecting a result navigates directly to the corresponding module (Patient 360, Appointment Detail etc.)
  • If no results found, notify the agent — "Try a different search term"

Implementation Note — Current Search is Naive, Needs Migration

Current sidecar search.controller.ts implementation:

  1. Fetches leads(first: 50), patients(first: 50), appointments(first: 50) — 3 separate platform GraphQL calls
  2. Filters client-side with .toLowerCase().includes(q) — substring match only
  3. Returns top 5 per entity
  4. Auth: uses platform.apiKey (server-to-server Bearer token) — auth is in place

Problems:

  • Only searches first 50 records per entity — misses everything beyond that
  • No relevance ranking
  • No full-text/fuzzy matching — just JS substring includes()
  • 3 parallel GraphQL calls instead of 1

Fix: sidecar should proxy to the platform's search GraphQL query. The platform provides full-text search via PostgreSQL tsvector/tsquery:

# Platform search query — single call, cross-object, relevance-ranked
query {
  search(
    searchInput: "ramesh"
    limit: 20
    includedObjectNameSingulars: ["patient", "lead", "appointment"]
  ) {
    edges {
      node {
        recordId
        objectNameSingular   # "patient", "lead", "appointment"
        objectLabelSingular  # "Patient", "Lead", "Appointment"
        label                # display text
        imageUrl
        tsRankCD             # relevance score
      }
      cursor
    }
    pageInfo { endCursor hasNextPage }
  }
}

Platform search features: accent-insensitive (unaccent_immutable()), prefix matching (:*), relevance ranking (ts_rank_cd), searchable field types: FULL_NAME, PHONES (multiple international formats), EMAILS, TEXT, ADDRESS, UUID. Cursor-based pagination, RBAC on results.

Sidecar should: keep GET /api/search?q= endpoint, replace the 3-query fetch+filter with a single platform search query, normalize the response for the frontend. Frontend stays unchanged.

Verified Live (EC2 ramaiah.engage.healix360.net, 2026-04-12)

# Platform search works — returns doctor, doctorVisitSlot with relevance ranking
POST /graphql  Authorization: Bearer <user-jwt>
{ search(searchInput: "bhagyalakshmi", limit: 10) { edges { node { recordId objectNameSingular label tsRankCD } } } }8 results (7 doctorVisitSlot + 1 doctor), ranked by ts_rank_cd

# BUT: appointment not found by doctor name
{ search(searchInput: "bhagyalakshmi", includedObjectNameSingulars: ["appointment"]) ... }
→ EMPTY — appointment has doctorName="Dr. Bhagyalakshmi. M" but searchVector doesn't index doctorName

# Patients have empty name fields, leads are "Unknown Caller" → search won't match

Key issue: custom SDK entity fields (doctorName, department, contactPhone) are not in the searchVector. The platform's searchVector is a PostgreSQL GENERATED ALWAYS AS (expression) STORED column. By default only the name field is included in the expression. SDK fields added via app sync are not added to the expression.

Fix Option A: Direct DB Patch (immediate, per-deployment)

Patch the asExpression in core.fieldMetadata for each entity's searchVector field. Survives restarts, resyncs, migrations. Does NOT survive new installations or new hospital onboarding — must be re-run each time.

-- 1. Find the searchVector field metadata for the appointment entity
SELECT fm.id, fm.settings
FROM core."fieldMetadata" fm
JOIN core."objectMetadata" om ON fm."objectMetadataId" = om.id
WHERE om."nameSingular" = 'appointment'
  AND fm.name = 'searchVector';

-- 2. Update asExpression to include doctorName, department, patientName
UPDATE core."fieldMetadata"
SET settings = jsonb_set(
  settings::jsonb,
  '{asExpression}',
  to_jsonb(
    'to_tsvector(''simple'', ' ||
    'COALESCE(public.unaccent_immutable("name"), '''') || '' '' || ' ||
    'COALESCE(public.unaccent_immutable("doctorName"), '''') || '' '' || ' ||
    'COALESCE(public.unaccent_immutable("department"), '''')'  ||
    ')'
  )
)
WHERE id = '<searchVector-field-id-from-step-1>';

-- 3. Apply to the workspace table (ALTER the generated column)
-- Run workspace:sync-metadata or trigger a migration to apply
-- Alternatively, direct ALTER TABLE on the workspace schema:
ALTER TABLE workspace_<id>."appointment"
  DROP COLUMN "searchVector",
  ADD COLUMN "searchVector" tsvector GENERATED ALWAYS AS (
    to_tsvector('simple',
      COALESCE(public.unaccent_immutable("name"), '') || ' ' ||
      COALESCE(public.unaccent_immutable("doctorName"), '') || ' ' ||
      COALESCE(public.unaccent_immutable("department"), '')
    )
  ) STORED;

-- Repeat for lead (add contactPhone, contactName fields)
-- Repeat for patient (add fullName fields)

Survival matrix:

Scenario Survives?
Server restart / redeploy Yes
App resync (yarn sync) Yes — sync doesn't touch searchVector
DB migration Yes
New workspace / new hospital onboarding No — new object gets default name-only expression
Fresh installation No

Fix Option B: Platform Enhancement — includeInSearch on FieldManifest (durable)

Add includeInSearch flag to SDK field definitions. After app sync creates fields, recompute the searchVector expression to include flagged fields. Survives all scenarios including new installations and onboarding.

Code changes (4 files):

1. fortytwo-shared/src/application/fieldManifestType.ts — add flag

export type FieldManifest<T extends FieldMetadataType = ...> = SyncableEntityOptions & {
  type: T;
  name: string;
  label: string;
  description?: string;
  icon?: string;
  defaultValue?: FieldMetadataDefaultValue<T>;
  options?: FieldMetadataOptions<T>;
  settings?: FieldMetadataSettings<T>;
  isNullable?: boolean;
  includeInSearch?: boolean;  // ← NEW: include this field in the searchVector
};

2. fortytwo-server/src/engine/core-modules/application/application-sync.service.ts — after field sync, recompute searchVector

After syncFieldsWithoutRelations completes for a newly created object, add:

// After all fields are synced for a new object:
const searchableFields = objectToCreate.fields
  .filter((f): f is FieldManifest => !('relation' in f))
  .filter(f => f.includeInSearch && isSearchableFieldType(f.type))
  .map(f => ({ name: computeMetadataNameFromLabelOrThrow(f.label), type: f.type }));

if (searchableFields.length > 0) {
  // Always include the name/label identifier field
  const allSearchFields = [
    { name: 'name', type: FieldMetadataType.TEXT },
    ...searchableFields,
  ];
  await this.recomputeSearchVectorForObject(createdObject.id, allSearchFields, workspaceId);
}

3. New util or method: recomputeSearchVectorForObject

Same pattern as existing recomputeSearchVectorFieldAfterLabelIdentifierUpdate:

  • Find the searchVector FlatFieldMetadata for the object
  • Call getTsVectorColumnExpressionFromFields(allSearchFields)
  • Update the field metadata's settings.asExpression
  • The workspace migration runner will pick up the change and ALTER TABLE the generated column

4. SDK app entity definitions — mark fields

// In helix-engage SDK app, e.g. appointment object:
defineObject({
  nameSingular: 'appointment',
  ...
  fields: [
    { name: 'doctorName', type: FieldType.TEXT, label: 'Doctor Name', includeInSearch: true },
    { name: 'department', type: FieldType.TEXT, label: 'Department', includeInSearch: true },
    { name: 'chiefComplaint', type: FieldType.TEXT, label: 'Chief Complaint' },  // not searched
  ],
});

// lead object:
defineObject({
  nameSingular: 'lead',
  ...
  fields: [
    { name: 'contactPhone', type: FieldType.PHONES, label: 'Contact Phone', includeInSearch: true },
    { name: 'interestedService', type: FieldType.TEXT, label: 'Interested Service', includeInSearch: true },
  ],
});

Lifecycle:

  1. SDK app defines fields with includeInSearch: true
  2. yarn syncsynchronizeFromManifest → creates object → syncs fields → recomputes searchVector
  3. Workspace migration alters the generated column in Postgres
  4. Platform search now indexes those fields automatically on every INSERT/UPDATE

Requires platform deployment: Yes. Changes to fortytwo-shared + fortytwo-server.

  1. Now: Option A (DB patch) on EC2 Ramaiah to unblock search immediately
  2. Next sprint: Option B (platform enhancement) for durability across all deployments

Additional fixes (both options)

  • Ensure patient name/fullName is populated on creation (currently empty for some records — caller resolver creates patients with empty names)
  • Replace sidecar 3-query naive search with platform search GraphQL query proxy
  • Wire GlobalSearch component into header navigation

US-11: Agent Availability Status

Call Center Agent should be able to set their availability status so that supervisors and the system can manage call routing and workload appropriately.

Agent-Set Statuses

  • Available — on login
  • Break — lunch, nature calls etc.
    • No inbound calls — CRM toggle calls Ozonetel changeAgentState with state: 'Pause', pauseReason: 'Break' → agent enters AUX mode → Ozonetel ACD stops routing inbound calls to paused agents
    • No outbound calls — outbound blocking implemented in SIP provider
    • Attempting outbound shows popup: "Change status to Active to place calls"
  • Training — meetings, trainings, official purposes
    • Same behavior as Break

System-Derived Statuses

  • Available — On Call (talking on call) — calling, in-call states via SSE
  • Available — Idle (not on call, available to take calls) — ready state
  • Break
  • Training
  • Offline (on logout — not manually settable)
  • Wrap time (call time + time to finish disposition) — acw state

Tracking

  • Duration per day within each status tracked by system — server-side via Ozonetel getAgentSummary (TotalBusyTime, TotalIdleTime, TotalPauseTime, TotalWrapupTime, TotalDialTime). Displayed in my-performance TimeBar. No client-side real-time timer (not required — Ozonetel is source of truth).
  • Viewable by supervisor, admin, or agent (own KPI dashboard)

US-12: Agent Performance Dashboard

Call Center Agent should be able to view their daily and historical performance metrics so that they can monitor productivity and meet defined KPIs.

Daily Summary Metrics

  • Total calls handled
  • Inbound calls received
  • Outbound calls made
  • Wrap time
  • Total active call duration
  • Average time to call back missed calls — not directly shown. CDR TimeToAnswer field could feed this metric
  • No. of missed calls pending & completed
  • Call-to-appointment conversion rate
  • Lead-to-contact rate

Time Distribution Metrics (per day/month)

  • Active time (time spent on calls) — stacked TimeBar component
  • Wrap time (post-call work)
  • Break time
  • Idle time

Lead Response Metrics

  • Average time to contact assigned marketing leads
  • Number of leads contacted and pending — disposition breakdown (ECharts)

Time Range Selection

  • Daily (today/yesterday + date picker)
  • Weekly, monthly periods — not implemented in my-performance
  • Evaluate trends over time

KPI Benchmarks

  • Display configured KPI targets for the agent's role beside current performance

Implementation Notes — Closing US-12 Gaps

1. Avg missed call callback time — CDR TimeToAnswer is available per call. Sidecar already fetches CDR at GET /api/ozonetel/performance?date=&agentId=. Add computation:

# Existing sidecar call (already fetches full CDR):
GET /api/ozonetel/performance?date=2026-04-12&agentId=ramaiahadmin

# CDR record already contains:
{
  "TimeToAnswer": "00:00:12",   // ← not yet mapped
  "HandlingTime": "00:03:45",   // ← not yet mapped
  "WrapupDuration": "00:00:30", // ← not yet mapped
  "DID": "918041763400",        // ← not yet mapped (US-2 branch display)
  "CampaignName": "Inbound_918041763400"  // ← not yet mapped (US-15)
}

# Sidecar change: add to the agentCdr processing in performance endpoint:
const timeToAnswers = agentCdr
  .filter(c => c.TimeToAnswer && c.TimeToAnswer !== '00:00:00')
  .map(c => parseHMS(c.TimeToAnswer));
const avgCallbackTimeSec = avg(timeToAnswers);
# Return: { ...existing, avgCallbackTimeSec }

2. Weekly/monthly range — call the same fetchCDR + getAgentSummary for each day in range, aggregate:

# Frontend: date range picker sends fromDate + toDate
GET /api/ozonetel/performance?fromDate=2026-04-06&toDate=2026-04-12&agentId=ramaiahadmin

# Sidecar: loop days in range, aggregate CDR + summaries
# Note: Ozonetel CDR API limits to 15 days lookback, max 2 req/min
# For monthly: cache daily results, or use Ozonetel Agent Call Summary Report UI export

3. KPI benchmarks — Agent entity on platform already has maxIdleMinutes, minNpsThreshold, minConversion fields (used by use-performance-alerts.ts). Frontend needs to render these as target lines on charts:

# Already available in agent config (fetched at login):
GET /api/supervisor/team-performance?date=2026-04-12
→ agents[].maxIdleMinutes, agents[].minNpsThreshold, agents[].minConversion

# Frontend: overlay target line on ECharts (my-performance.tsx)
# e.g. markLine: { data: [{ yAxis: agent.minConversion, name: 'Target' }] }

US-13: Supervisor Dashboard

Call Center Supervisor should be able to view team-level operational metrics so that they can manage productivity, identify performance gaps, and improve call center efficiency.

Team-Level Summary (bar charts — days, weeks, month, year, custom dates)

  • Total calls handled — line chart (not bar)
  • Inbound calls received
  • Outbound calls made
  • Missed calls

Agent Performance Table (filter by: today, week, month, custom dates; sortable)

  • Calls handled per agent (inbound vs missed vs follow-up vs outbound)
  • Average call handling time — API provides totalBusyTime per agent via summary endpoint; also computable per-call from CDR HandlingTime field. Shown in time breakdown section, not main table column
  • Wrap time — API provides totalWrapupTime; shown in time breakdown section, not main table column
  • Active time — API provides totalBusyTime + totalDialTime; shown in time breakdown section, not main table column
  • Idle time
  • Break time — API provides totalPauseTime; shown in time breakdown section, not main table column
  • Agent NPS — NPS gauge + per-agent column
  • Conversion metrics: call-to-appointment conversion rate, lead-to-contact rate

Threshold Alerts

  • Highlight agents/metrics outside defined thresholds (high lead response time, low conversion, excessive idle time) — use-performance-alerts.ts with configurable thresholds

AI Assistant for Supervisor

  • Natural language queries about call centre operations, team performance, lead response — ai-chat-panel.tsx with supervisor context
  • Examples:
    • "How many calls were handled today?"
    • "Which agents have the highest appointment conversions?"
    • "How many leads are pending contact?"

US-14: Call Center Admin

Should be able to remove and add new agents or change the supervisor or change the admin along with everything the supervisor can do. Set KPIs to the agents. Add, edit or remove clinics, doctors.

User Management

  • Create, edit, deactivate, or remove user accounts for agents and supervisors — PLATFORM-PROVIDED (createWorkspaceMember, updateWorkspaceMember, deleteUserFromWorkspace with soft-delete). Onboarding UI built: setup-wizard.tsx (6-step wizard), team-settings.tsx (standalone management), employee-create-form.tsx (auto-generated temp passwords, role assignment). No convenience view for supervisors to manage their own team yet.
  • Assign agents to supervisors or teams; update assignments when org structure changes — PARTIAL: role assignment works (updateWorkspaceMemberRole), but platform has no team/supervisor hierarchy — flat RBAC only

KPI Configuration

  • Configure performance KPIs: lead response time targets, call handling benchmarks, conversion targets, occupancy thresholds

Master Data Management

  • Add, edit, deactivate clinic locations (name, address) — clinics.tsx with full CRUD (ACTIVE, TEMPORARILY_CLOSED, PERMANENTLY_CLOSED)
  • Add, edit, deactivate doctor profiles (name, department, specialties, clinic association) — doctors.tsx with full CRUD
  • Manage hospital departments and specialties — PARTIAL (predefined dropdown in doctor form, no custom dept creation)

Operational Configuration (later scope)

  • Call center working hours
  • Lead assignment rules
  • Missed call callback priorities
  • Escalation thresholds

Audit Logs — PLATFORM-PROVIDED

  • User creation, role changes, KPI updates, doctor/clinic modifications — platform AuditService tracks OBJECT_RECORD_CREATED/UPDATED/DELETED events via ClickHouse
  • Track for compliance and accountability

US-15: Master Data

Call Center Admin / Supervisor should be able to access and manage master data records so that they can manage patient communication history, audit call interactions, monitor missed call follow-ups, and maintain accurate patient records.

Lead Master

  • All leads assigned to call center with click-to-call access — all-leads.tsx with lead-table.tsx + click-to-call-button.tsx

Patient Contact Master → Patient 360 Master

Columns:

  • Patient ID
  • Patient name
  • Age
  • Gender
  • Last interaction timestamp
  • MRN (where available) — requires HIS integration mapping

Actions:

  • Search by mobile number, name, MRN
  • Click row → patient 360
  • Click-to-call on row

Appointment Master

Each appointment has a system-generated appointment ID.

Columns:

  • Date and time
  • Patient name
  • Registered mobile number
  • Status: booked, cancelled, rescheduled, no-show, completed — status tabs implemented
    • Booked, cancelled, rescheduled — from HIS or call center actions
    • No-show and completed — from HIS
  • Appointment ID
  • (hidden column) Campaign and source association — CDR CampaignName field available but not yet mapped
  • (hidden column) Chief complaint

Table Actions:

  • Filter by statuses, branches, doctors, departments
  • Sort by date and time
  • Edit appointment → appointment booking module (change date/time/doctor/clinic/complaint/specialty)
  • View appointment → full details from booking form + edit logs with agent ID — view exists, edit logs available via platform OBJECT_RECORD_UPDATED_EVENT in ClickHouse
  • Search by appointment ID or patient name

Appointment Status Definitions

  • Completed: appointment has elapsed (scheduled for past date). Drilldown: "show" / "no show" (subject to HIS integration)
  • Cancelled / Rescheduled: from call center actions or HIS actions
  • Filters: mutually exclusive, no combinations. Rescheduled shows only for Live and Rescheduled appointments.

Call Log Master

Columns:

  • System-generated call ID
  • Call recording — with player
  • Call duration
  • Patient name / ID
  • Disposition
  • SLA at the time of the call
  • Agent ID / name
  • Timestamp

Additional Capabilities

  • Data search and filtering — all master pages support search/filter
  • Audit and activity logs — PLATFORM-PROVIDED via AuditService (object create/update/delete events in ClickHouse)
  • Role-based access control — 3 roles (admin, cc-agent, executive)

US-16: Data Security and Compliance

Incident Response

  • Incident response plan (what happens if there's a breach)

Encryption

  • Encryption at rest (DB, backups) — backend responsibility, not visible in frontend
  • Encryption in transit (HTTPS / TLS 1.2+) — all API calls use HTTPS

Audit Logs — PLATFORM-PROVIDED

AuditService in fortytwo-eap-core tracks object create/update/delete, workspace events, and pageviews via ClickHouse. Event-based analytics, not transaction-level audit.

  • Data access (who viewed what) — platform AuditService (engine/core-modules/audit/)
  • Edits / deletes — platform tracks object-record-created/updated/deleted events
  • Logins (success + failure) — platform auth module
  • Logs should be immutable and retained (6-12 months minimum) — ClickHouse retention policy TBD

RBAC — PLATFORM-PROVIDED

RoleEntity, PermissionsService, SettingsPermissionGuard, CustomPermissionGuard — granular per-workspace role-based permissions.

  • Role-based access control — platform (engine/metadata-modules/role/, engine/guards/)

API & Integration Security

  • Authenticated APIs (OAuth2 / API keys with rotation) — Bearer token auth with auto-refresh (api-client.ts)
  • Rate limiting + abuse protection — PLATFORM-PROVIDED: ThrottlerModule with token bucket via Redis (200 req/workspace/min tracking, 10 submissions/IP/15min public forms)
  • Input validation (prevent injection attacks) — form validation + GraphQL server-side
  • Webhook signing (outbound) — PLATFORM-PROVIDED: HMAC SHA256 on outbound webhooks (X-Twenty-Webhook-Signature)
  • Inbound webhook verification — no HMAC verification on incoming webhooks (e.g., from Ozonetel)

Later Scope: Predictive Analytics

Call Center Supervisor should be able to view predictive analytics that model the impact of response time and missed-call callback speed on conversion probability so that they can proactively identify response-time thresholds and staffing actions needed to maximize patient acquisition.