- Agent status toggle: Ready/Break/Training/Offline with Ozonetel sync - Global search: cross-entity search (leads + patients + appointments) via sidecar - General enquiry form: capture caller questions during calls - Button standard: icon-only for toggles, text+icon for primary actions - Sidecar: agent-state endpoint, search module with platform queries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 KiB
Phase 1: Agent Status + Global Search + Enquiry Form
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Unblock supervisor features by adding agent availability toggle, give agents fast record lookup via global search, and add a general enquiry capture form for non-lead interactions.
Architecture: Agent status syncs with Ozonetel's changeAgentState API. Global search queries the platform GraphQL for leads, patients, and appointments in parallel. Enquiry form creates a Lead record with source "PHONE_INQUIRY" and captures the interaction details.
Tech Stack: NestJS sidecar (Ozonetel APIs), React 19 + Jotai, Platform GraphQL
Feature A: Agent Availability Status
The agent needs an Active/Away/Offline toggle that syncs with Ozonetel CloudAgent state.
File Map
| File | Action |
|---|---|
helix-engage/src/components/call-desk/agent-status-toggle.tsx |
Create: dropdown toggle for Ready/Pause/Offline |
helix-engage/src/pages/call-desk.tsx |
Modify: replace hardcoded "Ready" badge with AgentStatusToggle |
helix-engage-server/src/ozonetel/ozonetel-agent.controller.ts |
Modify: add POST /api/ozonetel/agent-state accepting state + pauseReason |
Task A1: Sidecar endpoint for agent state
Files:
-
Modify:
helix-engage-server/src/ozonetel/ozonetel-agent.controller.ts -
Step 1: Add
POST /api/ozonetel/agent-stateendpoint
@Post('agent-state')
async agentState(
@Body() body: { state: 'Ready' | 'Pause'; pauseReason?: string },
) {
if (!body.state) {
throw new HttpException('state required', 400);
}
this.logger.log(`Agent state change: ${this.defaultAgentId} → ${body.state}`);
try {
const result = await this.ozonetelAgent.changeAgentState({
agentId: this.defaultAgentId,
state: body.state,
pauseReason: body.pauseReason,
});
return result;
} catch (error: any) {
const message = error.response?.data?.message ?? error.message ?? 'State change failed';
return { status: 'error', message };
}
}
- Step 2: Type check and commit
feat: add agent state change endpoint
Task A2: Agent status toggle component
Files:
-
Create:
helix-engage/src/components/call-desk/agent-status-toggle.tsx -
Step 1: Create the toggle component
A dropdown button showing current status (Ready/Break/Offline) with color-coded dot. Selecting a state calls the sidecar API.
States:
- Ready (green dot) → Ozonetel state: Ready
- Break (orange dot) → Ozonetel state: Pause, pauseReason: "Break"
- Training (blue dot) → Ozonetel state: Pause, pauseReason: "Training"
- Offline (gray dot) → calls agent-logout
The component uses React Aria's Select or a simple popover.
- Step 2: Commit
feat: add agent status toggle component
Task A3: Wire into call desk
Files:
-
Modify:
helix-engage/src/pages/call-desk.tsx -
Step 1: Replace the hardcoded "Ready" BadgeWithDot with AgentStatusToggle
The current badge at line 43-49 shows SIP registration status. Replace with the new toggle that shows actual CloudAgent state AND SIP status.
- Step 2: Commit
feat: replace hardcoded Ready badge with agent status toggle
Feature B: Global Search
A search bar in the header/top-bar that searches across leads, patients, and appointments.
File Map
| File | Action |
|---|---|
helix-engage/src/components/shared/global-search.tsx |
Modify: search leads + patients + appointments via sidecar |
helix-engage-server/src/ozonetel/ozonetel-agent.controller.ts |
Modify: add GET /api/search?q= that queries platform |
helix-engage/src/components/layout/top-bar.tsx |
Modify: add GlobalSearch to the top bar |
Task B1: Sidecar search endpoint
Files:
-
Modify:
helix-engage-server/src/ozonetel/ozonetel-agent.controller.ts(or create a new search controller) -
Step 1: Add
GET /api/searchendpoint
Queries leads, patients, and appointments in parallel via platform GraphQL. Returns grouped results.
@Get('search')
async search(@Query('q') query: string) {
if (!query || query.length < 2) return { leads: [], patients: [], appointments: [] };
const authHeader = `Bearer ${this.platformApiKey}`;
// Search leads by name or phone
const [leadsResult, patientsResult, appointmentsResult] = await Promise.all([
this.platform.queryWithAuth(`{ leads(first: 5, filter: {
or: [
{ contactName: { firstName: { like: "%${query}%" } } },
{ contactPhone: { primaryPhoneNumber: { like: "%${query}%" } } }
]
}) { edges { node { id name contactName { firstName lastName } contactPhone { primaryPhoneNumber } source status } } } }`, undefined, authHeader),
this.platform.queryWithAuth(`{ patients(first: 5, filter: {
or: [
{ fullName: { firstName: { like: "%${query}%" } } },
{ phones: { primaryPhoneNumber: { like: "%${query}%" } } }
]
}) { edges { node { id name fullName { firstName lastName } phones { primaryPhoneNumber } } } } }`, undefined, authHeader),
this.platform.queryWithAuth(`{ appointments(first: 5, filter: {
doctorName: { like: "%${query}%" }
}) { edges { node { id scheduledAt doctorName department appointmentStatus patientId } } } }`, undefined, authHeader),
]).catch(() => [{ leads: { edges: [] } }, { patients: { edges: [] } }, { appointments: { edges: [] } }]);
return {
leads: leadsResult?.leads?.edges?.map((e: any) => e.node) ?? [],
patients: patientsResult?.patients?.edges?.map((e: any) => e.node) ?? [],
appointments: appointmentsResult?.appointments?.edges?.map((e: any) => e.node) ?? [],
};
}
Note: GraphQL like filter syntax may differ on the platform. May need to use contains or fetch-and-filter client-side.
- Step 2: Commit
feat: add cross-entity search endpoint
Task B2: Update GlobalSearch component
Files:
-
Modify:
helix-engage/src/components/shared/global-search.tsx -
Step 1: Wire to sidecar search endpoint
Replace the local leads-only search with a call to GET /api/search?q=. Display results grouped by entity type with icons:
- 👤 Leads — name, phone, source
- 🏥 Patients — name, phone, MRN
- 📅 Appointments — doctor, date, status
Clicking a result navigates to the appropriate detail page.
- Step 2: Commit
feat: wire global search to cross-entity sidecar endpoint
Task B3: Add search to call desk header
Files:
-
Modify:
helix-engage/src/pages/call-desk.tsxorsrc/components/layout/top-bar.tsx -
Step 1: Add GlobalSearch to the call desk header
Place next to the existing search in the worklist area, or in the top bar so it's accessible from every page.
- Step 2: Commit
feat: add global search to call desk header
Feature C: General Enquiry Form
When a caller has a question (not a lead), the agent needs a structured form to capture the interaction.
File Map
| File | Action |
|---|---|
helix-engage/src/components/call-desk/enquiry-form.tsx |
Create: inline form for capturing general enquiries |
helix-engage/src/components/call-desk/active-call-card.tsx |
Modify: add "Log Enquiry" button during active call |
Task C1: Create enquiry form
Files:
-
Create:
helix-engage/src/components/call-desk/enquiry-form.tsx -
Step 1: Create inline enquiry form
Fields (from spec US 5):
- Patient Name*
- Source/Referral*
- Query Asked* (textarea)
- Existing Patient? (Y/N)*
- If Y: Registered mobile number
- Relevant Department (optional, select from doctors list)
- Relevant Doctor (optional, filtered by department)
- Follow-up needed? (Y/N)*
- If Y: Date and time
- Disposition*
On submit:
- Creates a Lead record with
source: 'PHONE_INQUIRY' - Creates a LeadActivity with
activityType: 'ENQUIRY' - If follow-up needed, creates a FollowUp record
The form is inline (same pattern as appointment form) — shows below the call card when "Log Enquiry" is clicked.
- Step 2: Commit
feat: add general enquiry capture form
Task C2: Add "Log Enquiry" button to active call
Files:
-
Modify:
helix-engage/src/components/call-desk/active-call-card.tsx -
Step 1: Add button between "Book Appt" and "Transfer"
<Button size="sm" color="secondary"
iconLeading={...}
onClick={() => setEnquiryOpen(!enquiryOpen)}>Enquiry</Button>
Show the enquiry form inline below the call card when open (same pattern as appointment form).
- Step 2: Commit
feat: add Log Enquiry button to active call card
Task D: Deploy and verify
- Step 1: Type check both projects
- Step 2: Build and deploy sidecar
- Step 3: Build and deploy frontend
- Step 4: Test agent status toggle — switch to Break, verify badge changes, switch back to Ready
- Step 5: Test global search — search by name, phone number, verify results from leads + patients
- Step 6: Test enquiry form — during a call, click Enquiry, fill form, submit, verify Lead + Activity created
Notes
- Agent state and Ozonetel —
changeAgentStatecannot transition from ACW. The toggle should disable during ACW and show a "Completing wrap-up..." state. - Search filter syntax — the platform's GraphQL
likeoperator may not exist. Fallback: fetch first 50 records of each type and filter client-side by name/phone match. - Enquiry vs Disposition — the enquiry form is separate from the disposition form. An enquiry can be logged DURING a call (like booking an appointment), while disposition is logged AFTER the call ends.
- The 6-button problem — active call now has: Mute, Hold, Book Appt, Enquiry, Transfer, Pause Rec, End = 7 buttons. Consider grouping Book Appt + Enquiry under a "More" dropdown, or using icon-only buttons for some.