Files
helix-engage/docs/superpowers/plans/2026-03-21-phase1-unblock.md
saridsa2 c3604377b9 feat: Phase 1 — agent status toggle, global search, enquiry form
- 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>
2026-03-21 14:21:40 +05:30

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-state endpoint

@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

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/search endpoint

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.tsx or src/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:

  1. Creates a Lead record with source: 'PHONE_INQUIRY'
  2. Creates a LeadActivity with activityType: 'ENQUIRY'
  3. 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 OzonetelchangeAgentState cannot transition from ACW. The toggle should disable during ACW and show a "Completing wrap-up..." state.
  • Search filter syntax — the platform's GraphQL like operator 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.