- 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>
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
- Nullable fields:
HandlingTime,WrapupDuration,WrapUpStartTime,WrapUpEndTimecan benullwhen agent didn't complete wrapup. Must null-guard. - Field name inconsistency:
TransferredToin fetchCDRDetails vsTransferToin pagination endpoint.WrapUpEndTimevsWrapupEndTimecasing also differs. - 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:
- Platform RBAC —
RoleEntity,PermissionsService, per-workspace granular permissions (verified in US-16) - Login role mapping — sidecar's
auth.controller.tsreadsworkspaceMember.roles[].labeland maps to app role - Sidebar navigation —
sidebar.tsxgetNavSections(role)shows different nav items per role (lines 61-114) - Route-level guards — NOT 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. - Agent session locking — duplicate login detection per Ozonetel agent ID (
sessionService.lockSession, lines 131-134). Blocks same agent from two devices. - 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.tsxlines 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) —
callDirectionRefin 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.tsx→dialOutbound()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 —
beforeunloadevent 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: tracksgood|unstable|offline, counts disconnects (3+ in 2 min = unstable), windowoffline/onlineevent 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 source —
retryConditionsis JSON-serialized and submitted as part of the campaign create/update payload toPOST/PUT {ADMIN_BASE_URL}/outboundCampaign(seeCampaignOutBoundForm.jsxlines 1110-1121,DispositionRetryConfig.jsxfor 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):
- Pre-login:
GET https://api.cloudagent.ozonetel.com/api/auth/public-key→{ publicKey, keyId } - Login:
POST https://api.cloudagent.ozonetel.com/auth/loginwith body{ username: RSA_ENCRYPT(user), password: RSA_ENCRYPT(pass), keyId, ltype: "PORTAL" }→ returns JWT token - All API calls: headers
{ Authorization: "Bearer <token>", userId, userName, isSuperAdmin, dAccessType } - 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:
GET https://api.cloudagent.ozonetel.com/api/auth/public-key→{ publicKey, keyId }POST https://api.cloudagent.ozonetel.com/auth/loginwith RSA-encrypted credentials → JWT- Cache JWT in memory, refresh on expiry (decoded via
jwt-decode) - 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.agentIdin table (line 132) - Agent status (on call) — status column shows
active/on-holdwith 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:
- Admin clicks Barge/Whisper/Listen in Ozonetel dashboard
- Ozonetel server bridges the audio at the telephony layer
- Agent's CloudAgent app receives
agentBarginStart/agentBarginEndWebSocket events (constants.js lines 59-60) - 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):
- Supervisor enters their phone number or SIP extension
- Clicks "Call" →
bargeIn()API called with UCID + agent number + supervisor number - Ozonetel bridges the supervisor into the active call
- Once connected, supervisor can switch between Listen / Training (whisper) / Bargein modes via
bargeInRedis()API (apiId: 158) - Agent-side receives
agentBarginStart/agentBarginEndWebSocket 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:
- Sidecar: proxy
POST /api/supervisor/barge→ calls OzoneteldashboardApi/monitor/apiwith apiId 63 (needs Ozonetel admin session token — check how CA-Admin authenticates) - Sidecar: proxy
POST /api/supervisor/barge-mode→ calls apiId 158 for mode switch (or use SIP DTMF approach which doesn't need API) - 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 - Frontend (agent side): listen for Ozonetel WebSocket
agentBarginStart/agentBarginEndevents → show "Supervisor monitoring" indicator in call UI - 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/resolveauto-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
AppointmentFormin 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.tsxpage - Cancel appointment —
handleCancel()inappointment-form.tsx(line 342) → GraphQL mutation setsstatus: 'CANCELLED'
Scheduled Follow-ups
- Follow-ups display in agent's worklist —
worklist.service.tsline 77 fetches byassignedAgent, 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: agentNamein 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_doctortool queries doctors withDOCTOR_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_patienttool searches leads by phone, returnspatientId, thenlookup_appointmentstool 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_patientreturnsaiSummary,aiSuggestedAction,status,contactAttempts,lastContacted - Query further on dates/appointments linked to that patient ID —
lookup_appointmentstool 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_appointmenttool — AI can book appointments directly via GraphQL mutationcreate_leadtool — 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:
- Agent provides mobile number or MRN
- LLM must call a tool
- Tool runs SQL query
- Tool returns structured data
- 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
- Name + Age + Gender — lead header shows fullName, linked patient shows name + patientType badge (context-panel.tsx lines 126-146, 217-227)
- (P2) Secondary patients tagged to primary contact — not implemented
- 2-3 line AI summary —
lead.aiSummary+lead.aiSuggestedActionin 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)
- Surface linked campaigns (with header + timestamp) — not shown in context panel. Lead
utmCampaign/campaignIdnot rendered. - 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
AppointmentFormwitheditingAppointmentstate (line 192, 65)
- Ability to edit appointment (opens appointment section pre-filled) — Edit button on each appointment row, opens
- "View more" → full patient 360 — no link to Patient 360 from context panel
New Patient Summary
- Name (age, gender if available) — lead header shows name (line 135)
- AI summary — same
aiSummarysection - Campaigns they've shown interest in and sources (FB, website etc.) — not shown in context panel
- 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.tsxhas "My Leads" tab (line 29, filters byuser.name), "New" tab (NEW status), "All" tab - Filter leads by campaign (e.g., IVF leads) —
campaignFilterstate (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.tsline 82: when agent state changes toReady, callsmissedQueue.assignNext(agentId). Mutex lock prevents race conditions. - Auto-assign the highest-ageing missed call to the agent —
missed-queue.service.tsline 172-221:assignNext()queries oldest unassignedPENDING_CALLBACKcall (orderBy: startedAt AscNullsLast), assigns to agent viaupdateCallmutation. - 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.tsxline 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 —
missedcallcountfield 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.aiSummaryfrom 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)
Patient Search
- Retrieve profile by registered mobile number, MRN, or patient name — page accessed only via
/patient/:idroute (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 (
AppointmentRowcomponent has no click handler).
- Click on appointment to see full details and edit — not implemented. Appointments rendered as read-only rows (
- Marketing lead information — PARTIAL. GraphQL fetches
source,status,interestedService(line 79-81). UI showssourceas badge (line 382) andinterestedService(line 407).statusis fetched but never rendered.campaignNameandinquiryTypenot 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 —
ClickToCallButtonwith 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:
WhatsAppBridgeClientServiceprovidessendMessage(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_REMINDERfollow-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
scheduledAtis 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 |
US-10: Global Search
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:
- Fetches
leads(first: 50),patients(first: 50),appointments(first: 50)— 3 separate platform GraphQL calls - Filters client-side with
.toLowerCase().includes(q)— substring match only - Returns top 5 per entity
- 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
FlatFieldMetadatafor the object - Call
getTsVectorColumnExpressionFromFields(allSearchFields) - Update the field metadata's
settings.asExpression - The workspace migration runner will pick up the change and
ALTER TABLEthe 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:
- SDK app defines fields with
includeInSearch: true yarn sync→synchronizeFromManifest→ creates object → syncs fields → recomputes searchVector- Workspace migration alters the generated column in Postgres
- Platform search now indexes those fields automatically on every INSERT/UPDATE
Requires platform deployment: Yes. Changes to fortytwo-shared + fortytwo-server.
Recommended Approach
- Now: Option A (DB patch) on EC2 Ramaiah to unblock search immediately
- Next sprint: Option B (platform enhancement) for durability across all deployments
Additional fixes (both options)
- Ensure patient
name/fullNameis populated on creation (currently empty for some records — caller resolver creates patients with empty names) - Replace sidecar 3-query naive search with platform
searchGraphQL 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
changeAgentStatewithstate: '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"
- No inbound calls — CRM toggle calls Ozonetel
- Training — meetings, trainings, official purposes
- Same behavior as Break
System-Derived Statuses
- Available — On Call (talking on call) —
calling,in-callstates via SSE - Available — Idle (not on call, available to take calls) —
readystate - Break
- Training
- Offline (on logout — not manually settable)
- Wrap time (call time + time to finish disposition) —
acwstate
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
TimeToAnswerfield 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
totalBusyTimeper agent via summary endpoint; also computable per-call from CDRHandlingTimefield. 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.tswith configurable thresholds
AI Assistant for Supervisor
- Natural language queries about call centre operations, team performance, lead response —
ai-chat-panel.tsxwith 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,deleteUserFromWorkspacewith 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.tsxwith full CRUD (ACTIVE, TEMPORARILY_CLOSED, PERMANENTLY_CLOSED) - Add, edit, deactivate doctor profiles (name, department, specialties, clinic association) —
doctors.tsxwith 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/DELETEDevents 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.tsxwithlead-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
CampaignNamefield 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_EVENTin 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.