# 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`](./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. [x] **Platform RBAC** — `RoleEntity`, `PermissionsService`, per-workspace granular permissions (verified in US-16) 2. [x] **Login role mapping** — sidecar's `auth.controller.ts` reads `workspaceMember.roles[].label` and maps to app role 3. [x] **Sidebar navigation** — `sidebar.tsx` `getNavSections(role)` shows different nav items per role (lines 61-114) 4. [ ] **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. 5. [x] **Agent session locking** — duplicate login detection per Ozonetel agent ID (`sessionService.lockSession`, lines 131-134). Blocks same agent from two devices. 6. [x] **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: - [x] Answer the incoming call through their headphones — SIP WebRTC via JsSIP (`sip-client.ts`) - [x] Mute themselves on the call — `toggleMute()` in SIP client (lines 193-202) - [x] Keep the caller on hold — `hold()`/`unhold()` in SIP client (lines 205-215) - [x] 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 - [x] 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") - [x] Drop off once the call is forwarded — KICK_CALL action removes agent from conference (lines 145-161) - [x] 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) - [x] 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. - [x] See the call type (inbound / outbound) — `callDirectionRef` in SIP provider - [x] See the live call duration — 1-second tick via `sipCallDurationAtom` #### Outbound Calls - [x] 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. - [x] 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) - [x] 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 - [x] 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](https://docs.ozonetel.com/docs/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** — `retryConditions` 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 ", 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: ```typescript 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. - [x] Agent name — `call.agentId` in table (line 132) - [x] Agent status (on call) — status column shows `active` / `on-hold` with color badges (line 147-149) - [x] Patient contact number — shown with caller name resolution from leads (lines 72-83, 134-139) - [x] Call start timestamp — used for duration calc (line 21-24) - [x] Call duration — live updating via `formatDuration(call.startTime)` + tick state (line 144) - [x] 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`):** ```typescript // Barge-in API (line 827-842) POST https://api.cloudagent.ozonetel.com/dashboardApi/monitor/api { apiId: 63, ucid: "", action: "CALL_BARGEIN", isSip: true | false, // SIP mode or Normal (PSTN) phoneno: "", agentNumber: "", cbURL: "" } // 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: "", Sip: "" } // SIP subscribe for barge number (line 880-890) POST https://api.cloudagent.ozonetel.com/ca-admin-Api/CloudAgentAPI/endpoint/sipnumber/sipSubscribe { apiId: 139, sipURL: "" } ``` **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 - [x] 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 - [x] Caller name - [x] 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 - [x] Date of the appointment - [x] Clinic name / branch name - [x] Specialty / department - [x] Doctor name - [x] Chief complaint - [x] Date and time slot - [x] Age and gender of the patient (optional) - [x] Returning patient (Y/N) — handled by system - [x] Source or referral details - [x] 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`: - [x] Appointment booked → `setSuggestedDisposition('APPOINTMENT_BOOKED')` (active-call-card.tsx line 141) - [x] Enquiry logged → `setSuggestedDisposition('INFO_PROVIDED')` (line 332) - [x] 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 - [x] If returning patient registered under the same mobile number, access patient 360 (or by MRN when EMR integration exists) — `/api/caller/resolve` auto-detects - [x] 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 - [x] 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) - [x] Agent can find appointment in appointment master list and edit — `appointments.tsx` page - [x] Cancel appointment — `handleCancel()` in `appointment-form.tsx` (line 342) → GraphQL mutation sets `status: 'CANCELLED'` ### Scheduled Follow-ups - [x] 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 - [x] Name of the caller * - [x] Name of the patient * (option to say same as caller) - [x] Relationship with the patient (optional) - [x] Source / referral info * - [x] Query asked * - [x] Existing patient? (Yes / No) * - [x] Registered mobile number - [x] Relevant department (optional) - [x] Relevant doctor (optional) - [x] Is follow up needed (Y/N) * - Date and time of follow up - [x] 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) - [x] 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 - [x] Insurance policies, visiting hours — insurance partners (insurer name, TPA, settlement type, plan types) + clinic hours in KB - [x] 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 - [x] Doctor availability — `lookup_doctor` tool queries doctors with `DOCTOR_VISIT_SLOTS_FRAGMENT` (day + time slots per doctor per clinic) - [x] Current packages and offers available — health packages with discounted prices in KB #### Appointment Info - [x] 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 - [x] **100% accuracy architecture** — tools use platform GraphQL queries (structured data), not LLM hallucination #### Patient Info (by mobile number or MRN) - [x] Summary of previous interactions if existing patient — `lookup_patient` returns `aiSummary`, `aiSuggestedAction`, `status`, `contactAttempts`, `lastContacted` - [x] Query further on dates/appointments linked to that patient ID — `lookup_appointments` tool takes patientId - [x] 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 - [x] 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) - [x] 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: - [x] Set rankings for different tasks (missed calls, campaigns, follow-ups etc.) — `scoring.ts` + `priority-config-panel.tsx` - [x] Set SLAs for each task type - [x] Within campaigns, rank different campaigns - [x] Rank campaign sources (Meta, website etc.) based on lead quality ### Worklist Table Columns - [x] Mobile number - [x] Name of patient - [x] SLA - [x] Call type (lead / missed call / follow up / second attempt etc.) - [x] Campaign (e.g., "cervical cancer check" — empty for non-applicable items) - [x] Timestamp of creation (lead creation, missed call, follow-up date etc.) - [x] Call source number (branch DID the customer called) - [x] CTA to call ### Table Actions - [x] Click on patient name → side panel opens with patient summary - Existing patient: full summary (see below) - New patient: basic info summary (see below) - [x] Click-to-call CTA on row actions (accessible even when side panel is open) - [x] 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. [x] 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. [x] 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. [x] Surface live appointments: clinic + doctor name + date — Upcoming section shows appointments with doctor name, department, date, status (lines 172-197) - [x] 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. [x] Name (age, gender if available) — lead header shows name (line 135) 2. [x] 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 - [x] 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 - [x] 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 - [x] Contact number (sortable) — key lead/patient identifier - [x] Patient name (sortable) - [x] Email (sortable, optional) - [x] Campaign ID (sortable) - [x] Campaign name - [x] SLA status - [x] Age of the lead (from creation date) — visually highlighted when beyond limit (`age-indicator.tsx`) - [x] Timestamp of lead creation Leads sorted by assigned priority. ### Lead Actions - [x] Click-to-call CTA - [x] Patient summary panel appears (returning customer history, current campaign interest) - [x] All call widget features available - [x] Can create appointments or note/address general enquiries - [x] Once click-to-call, no option to disconnect while ringing - [x] 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: - [x] Caller mobile number - [x] Timestamp - [x] Call source number (which branch etc.) ### Queue Behavior - [x] Place record into Missed Call Queue (FIFO) - [x] Exhaustive missed call list accessible to supervisor - [x] Once click-to-call (callback), no option to disconnect while ringing - [x] For Ramaiah demo: all missed calls populated in agent worklist ### Auto-Assignment - [x] 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. - [x] 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. - [x] Assigned missed call appears at top of agent's worklist tagged "Missed Call" - [x] 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 - [x] Same number calls multiple times before callback → merge into single missed call record - [x] Update missed call count — `missedcallcount` field with `(Nx)` badge - [x] Update latest timestamp ### Callback SLA Tracking Each missed call should track: - [x] Time since missed call - [x] 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 - [x] 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. - [x] 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/:id` route** (line 266: `useParams`). No search input on P360 page. Navigation is from patients list or worklist side panel. - [x] 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 - [x] 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). - [x] 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. - [x] Call logs — Calls tab (line 483-499) with direction badge, agent name, duration, disposition. Sorted desc by startedAt. - [x] 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 - [x] Click-to-call — `ClickToCallButton` with phone number (line 424) - [x] 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. - [x] 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 | --- ## 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 - [x] Group results by entity type (Patients, Leads, Appointments) - [x] Each result shows summary preview: patient name, mobile number, MRN, appointment date, or lead source - [x] Selecting a result navigates directly to the corresponding module (Patient 360, Appointment Detail etc.) - [x] 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`: ```graphql # 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) ```bash # Platform search works — returns doctor, doctorVisitSlot with relevance ranking POST /graphql Authorization: Bearer { 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. ```sql -- 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 = ''; -- 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_."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 ```typescript export type FieldManifest = SyncableEntityOptions & { type: T; name: string; label: string; description?: string; icon?: string; defaultValue?: FieldMetadataDefaultValue; options?: FieldMetadataOptions; settings?: FieldMetadataSettings; 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: ```typescript // 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 ```typescript // 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 sync` → `synchronizeFromManifest` → 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`. ### Recommended Approach 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 - [x] **Available** — on login - [x] **Break** — lunch, nature calls etc. - [x] 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 - [x] No outbound calls — outbound blocking implemented in SIP provider - [x] Attempting outbound shows popup: "Change status to Active to place calls" - [x] **Training** — meetings, trainings, official purposes - Same behavior as Break ### System-Derived Statuses - [x] Available — On Call (talking on call) — `calling`, `in-call` states via SSE - [x] Available — Idle (not on call, available to take calls) — `ready` state - [x] Break - [x] Training - [x] Offline (on logout — not manually settable) - [x] Wrap time (call time + time to finish disposition) — `acw` state ### Tracking - [x] 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). - [x] 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 - [x] Total calls handled - [x] Inbound calls received - [x] Outbound calls made - [x] Wrap time - [x] Total active call duration - [ ] Average time to call back missed calls — not directly shown. CDR `TimeToAnswer` field could feed this metric - [x] No. of missed calls pending & completed - [x] Call-to-appointment conversion rate - [x] Lead-to-contact rate ### Time Distribution Metrics (per day/month) - [x] Active time (time spent on calls) — stacked TimeBar component - [x] Wrap time (post-call work) - [x] Break time - [x] Idle time ### Lead Response Metrics - [x] Average time to contact assigned marketing leads - [x] Number of leads contacted and pending — disposition breakdown (ECharts) ### Time Range Selection - [x] 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) - [x] Total calls handled — line chart (not bar) - [x] Inbound calls received - [x] Outbound calls made - [x] Missed calls ### Agent Performance Table (filter by: today, week, month, custom dates; sortable) - [x] Calls handled per agent (inbound vs missed vs follow-up vs outbound) - [x] 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 - [x] Wrap time — API provides `totalWrapupTime`; shown in time breakdown section, not main table column - [x] Active time — API provides `totalBusyTime` + `totalDialTime`; shown in time breakdown section, not main table column - [x] Idle time - [x] Break time — API provides `totalPauseTime`; shown in time breakdown section, not main table column - [x] Agent NPS — NPS gauge + per-agent column - [x] Conversion metrics: call-to-appointment conversion rate, lead-to-contact rate ### Threshold Alerts - [x] 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 - [x] Natural language queries about call centre operations, team performance, lead response — `ai-chat-panel.tsx` with supervisor context - [x] 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 - [x] 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 - [x] Add, edit, deactivate clinic locations (name, address) — `clinics.tsx` with full CRUD (ACTIVE, TEMPORARILY_CLOSED, PERMANENTLY_CLOSED) - [x] 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 - [x] User creation, role changes, KPI updates, doctor/clinic modifications — platform AuditService tracks `OBJECT_RECORD_CREATED/UPDATED/DELETED` events via ClickHouse - [x] 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 - [x] 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: - [x] Patient ID - [x] Patient name - [x] Age - [x] Gender - [x] Last interaction timestamp - [ ] MRN (where available) — requires HIS integration mapping Actions: - [x] Search by mobile number, name, MRN - [x] Click row → patient 360 - [x] Click-to-call on row ### Appointment Master Each appointment has a system-generated appointment ID. Columns: - [x] Date and time - [x] Patient name - [x] Registered mobile number - [x] 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 - [x] Appointment ID - [ ] (hidden column) Campaign and source association — CDR `CampaignName` field available but not yet mapped - [ ] (hidden column) Chief complaint Table Actions: - [x] Filter by statuses, branches, doctors, departments - [x] Sort by date and time - [x] Edit appointment → appointment booking module (change date/time/doctor/clinic/complaint/specialty) - [x] 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 - [x] 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: - [x] System-generated call ID - [x] Call recording — with player - [x] Call duration - [x] Patient name / ID - [x] Disposition - [x] SLA at the time of the call - [x] Agent ID / name - [x] Timestamp ### Additional Capabilities - [x] Data search and filtering — all master pages support search/filter - [x] Audit and activity logs — PLATFORM-PROVIDED via AuditService (object create/update/delete events in ClickHouse) - [x] 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 - [x] 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. - [x] Data access (who viewed what) — platform AuditService (`engine/core-modules/audit/`) - [x] Edits / deletes — platform tracks object-record-created/updated/deleted events - [x] 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. - [x] Role-based access control — platform (`engine/metadata-modules/role/`, `engine/guards/`) ### API & Integration Security - [x] Authenticated APIs (OAuth2 / API keys with rotation) — Bearer token auth with auto-refresh (`api-client.ts`) - [x] Rate limiting + abuse protection — PLATFORM-PROVIDED: ThrottlerModule with token bucket via Redis (200 req/workspace/min tracking, 10 submissions/IP/15min public forms) - [x] Input validation (prevent injection attacks) — form validation + GraphQL server-side - [x] 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.