# 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** ```typescript @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/search` endpoint** Queries leads, patients, and appointments in parallel via platform GraphQL. Returns grouped results. ```typescript @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"** ```typescript ``` 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** — `changeAgentState` 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.