# 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
<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** — `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.
