Files
helix-engage/docs/superpowers/plans/2026-03-24-supervisor-module.md
2026-03-24 13:22:31 +05:30

532 lines
17 KiB
Markdown

# Supervisor Module Implementation Plan
> **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:** Build the supervisor module with team performance dashboard (PP-5), live call monitor (PP-6), master data pages, and admin sidebar restructure.
**Architecture:** Frontend pages query platform GraphQL directly for entity data (calls, appointments, leads, agents). Sidecar provides Ozonetel-specific data (agent time breakdown, active calls via event subscription). No hardcoded/mock data anywhere.
**Tech Stack:** React + Tailwind + ECharts (frontend), NestJS sidecar (Ozonetel integration), Fortytwo platform GraphQL
**Spec:** `docs/superpowers/specs/2026-03-24-supervisor-module.md`
---
## File Map
### Frontend (`helix-engage/src/`)
| File | Action | Responsibility |
|------|--------|----------------|
| `pages/team-performance.tsx` | Create | PP-5 full dashboard |
| `pages/live-monitor.tsx` | Create | PP-6 active call table |
| `pages/call-recordings.tsx` | Create | Calls with recordings master |
| `pages/missed-calls.tsx` | Create | Missed calls master (supervisor view) |
| `components/layout/sidebar.tsx` | Modify | Admin nav restructure |
| `main.tsx` | Modify | Add new routes |
### Sidecar (`helix-engage-server/src/`)
| File | Action | Responsibility |
|------|--------|----------------|
| `supervisor/supervisor.service.ts` | Create | Team perf aggregation + active call tracking |
| `supervisor/supervisor.controller.ts` | Create | REST endpoints |
| `supervisor/supervisor.module.ts` | Create | Module registration |
| `app.module.ts` | Modify | Import SupervisorModule |
---
## Task 1: Admin Sidebar Nav + Routes
**Files:**
- Modify: `helix-engage/src/components/layout/sidebar.tsx`
- Modify: `helix-engage/src/main.tsx`
- [ ] **Step 1: Add new icon imports to sidebar**
In `sidebar.tsx`, add to the FontAwesome imports:
```typescript
import {
// existing imports...
faRadio,
faFileAudio,
faPhoneMissed,
faChartLine,
} from '@fortawesome/pro-duotone-svg-icons';
```
Add icon wrappers:
```typescript
const IconRadio = faIcon(faRadio);
const IconFileAudio = faIcon(faFileAudio);
const IconPhoneMissed = faIcon(faPhoneMissed);
const IconChartLine = faIcon(faChartLine);
```
- [ ] **Step 2: Restructure admin nav**
Replace the admin nav section (currently has Overview + Management + Admin groups) with:
```typescript
if (role === 'admin') {
return [
{ label: 'Supervisor', items: [
{ label: 'Dashboard', href: '/', icon: IconGrid2 },
{ label: 'Team Performance', href: '/team-performance', icon: IconChartLine },
{ label: 'Live Call Monitor', href: '/live-monitor', icon: IconRadio },
]},
{ label: 'Data & Reports', items: [
{ label: 'Lead Master', href: '/leads', icon: IconUsers },
{ label: 'Patient Master', href: '/patients', icon: IconHospitalUser },
{ label: 'Appointment Master', href: '/appointments', icon: IconCalendarCheck },
{ label: 'Call Log Master', href: '/call-history', icon: IconClockRewind },
{ label: 'Call Recordings', href: '/call-recordings', icon: IconFileAudio },
{ label: 'Missed Calls', href: '/missed-calls', icon: IconPhoneMissed },
]},
{ label: 'Admin', items: [
{ label: 'Settings', href: '/settings', icon: IconGear },
]},
];
}
```
- [ ] **Step 3: Add routes in main.tsx**
Import new page components (they'll be created in later tasks — use placeholder components for now):
```typescript
import { TeamPerformancePage } from "@/pages/team-performance";
import { LiveMonitorPage } from "@/pages/live-monitor";
import { CallRecordingsPage } from "@/pages/call-recordings";
import { MissedCallsPage } from "@/pages/missed-calls";
```
Add routes:
```typescript
<Route path="/team-performance" element={<TeamPerformancePage />} />
<Route path="/live-monitor" element={<LiveMonitorPage />} />
<Route path="/call-recordings" element={<CallRecordingsPage />} />
<Route path="/missed-calls" element={<MissedCallsPage />} />
```
- [ ] **Step 4: Create placeholder pages**
Create minimal placeholder files for each new page so the build doesn't fail:
```typescript
// src/pages/team-performance.tsx
export const TeamPerformancePage = () => <div>Team Performance coming soon</div>;
// src/pages/live-monitor.tsx
export const LiveMonitorPage = () => <div>Live Call Monitor coming soon</div>;
// src/pages/call-recordings.tsx
export const CallRecordingsPage = () => <div>Call Recordings coming soon</div>;
// src/pages/missed-calls.tsx
export const MissedCallsPage = () => <div>Missed Calls coming soon</div>;
```
- [ ] **Step 5: Verify build**
```bash
cd helix-engage && npm run build
```
- [ ] **Step 6: Commit**
```bash
git add src/components/layout/sidebar.tsx src/main.tsx src/pages/team-performance.tsx src/pages/live-monitor.tsx src/pages/call-recordings.tsx src/pages/missed-calls.tsx
git commit -m "feat: admin sidebar restructure + placeholder pages for supervisor module"
```
---
## Task 2: Call Recordings Page
**Files:**
- Modify: `helix-engage/src/pages/call-recordings.tsx`
- [ ] **Step 1: Implement call recordings page**
Query platform for calls with recordings. Reuse patterns from `call-history.tsx`.
```typescript
// Query: calls where recording primaryLinkUrl is not empty
const QUERY = `{ calls(first: 100, filter: {
recording: { primaryLinkUrl: { neq: "" } }
}, orderBy: [{ startedAt: DescNullsLast }]) { edges { node {
id direction callStatus callerNumber { primaryPhoneNumber }
agentName startedAt durationSec disposition
recording { primaryLinkUrl primaryLinkLabel }
} } } }`;
```
Table columns: Agent, Caller (PhoneActionCell), Type (In/Out badge), Date, Duration, Disposition, Recording (play button).
Search by agent name or phone number. Date filter optional.
- [ ] **Step 2: Verify build**
```bash
cd helix-engage && npm run build
```
- [ ] **Step 3: Commit**
```bash
git add src/pages/call-recordings.tsx
git commit -m "feat: call recordings master page"
```
---
## Task 3: Missed Calls Page (Supervisor View)
**Files:**
- Modify: `helix-engage/src/pages/missed-calls.tsx`
- [ ] **Step 1: Implement missed calls page**
Query platform for all missed calls — no agent filter (supervisor sees all).
```typescript
const QUERY = `{ calls(first: 100, filter: {
callStatus: { eq: MISSED }
}, orderBy: [{ startedAt: DescNullsLast }]) { edges { node {
id callerNumber { primaryPhoneNumber } agentName
startedAt callsourcenumber callbackstatus missedcallcount callbackattemptedat
} } } }`;
```
Table columns: Caller (PhoneActionCell), Date/Time, Branch (`callsourcenumber`), Agent, Callback Status (badge), SLA (computed from `startedAt`).
Tabs: All | Pending (`PENDING_CALLBACK`) | Attempted (`CALLBACK_ATTEMPTED`) | Completed (`CALLBACK_COMPLETED` + `WRONG_NUMBER`).
Search by phone or agent.
- [ ] **Step 2: Verify build**
```bash
cd helix-engage && npm run build
```
- [ ] **Step 3: Commit**
```bash
git add src/pages/missed-calls.tsx
git commit -m "feat: missed calls master page for supervisors"
```
---
## Task 4: Sidecar — Supervisor Module
**Files:**
- Create: `helix-engage-server/src/supervisor/supervisor.service.ts`
- Create: `helix-engage-server/src/supervisor/supervisor.controller.ts`
- Create: `helix-engage-server/src/supervisor/supervisor.module.ts`
- Modify: `helix-engage-server/src/app.module.ts`
- [ ] **Step 1: Create supervisor service**
```typescript
// supervisor.service.ts
// Two responsibilities:
// 1. Aggregate Ozonetel agent summary across all agents
// 2. Track active calls from Ozonetel real-time events
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service';
type ActiveCall = {
ucid: string;
agentId: string;
callerNumber: string;
callType: string;
startTime: string;
status: 'active' | 'on-hold';
};
@Injectable()
export class SupervisorService implements OnModuleInit {
private readonly logger = new Logger(SupervisorService.name);
private readonly activeCalls = new Map<string, ActiveCall>();
constructor(
private platform: PlatformGraphqlService,
private ozonetel: OzonetelAgentService,
private config: ConfigService,
) {}
async onModuleInit() {
// Subscribe to Ozonetel events (fire and forget)
// Will be implemented when webhook URL is configured
this.logger.log('Supervisor service initialized');
}
// Called by webhook when Ozonetel pushes call events
handleCallEvent(event: any) {
const { action, ucid, agent_id, caller_id, call_type, event_time } = event;
if (action === 'Answered' || action === 'Calling') {
this.activeCalls.set(ucid, {
ucid, agentId: agent_id, callerNumber: caller_id,
callType: call_type, startTime: event_time, status: 'active',
});
} else if (action === 'Disconnect') {
this.activeCalls.delete(ucid);
}
}
// Called by webhook when Ozonetel pushes agent events
handleAgentEvent(event: any) {
this.logger.log(`Agent event: ${event.agentId}${event.action}`);
}
getActiveCalls(): ActiveCall[] {
return Array.from(this.activeCalls.values());
}
// Aggregate time breakdown across all agents
async getTeamPerformance(date: string): Promise<any> {
// Get all agent IDs from platform
const agentData = await this.platform.query<any>(
`{ agents(first: 20) { edges { node {
id name ozonetelagentid npsscore maxidleminutes minnpsthreshold minconversionpercent
} } } }`,
);
const agents = agentData?.agents?.edges?.map((e: any) => e.node) ?? [];
// Fetch Ozonetel summary per agent
const summaries = await Promise.all(
agents.map(async (agent: any) => {
try {
const summary = await this.ozonetel.getAgentSummary(agent.ozonetelagentid, date);
return { ...agent, timeBreakdown: summary };
} catch {
return { ...agent, timeBreakdown: null };
}
}),
);
return { date, agents: summaries };
}
}
```
- [ ] **Step 2: Create supervisor controller**
```typescript
// supervisor.controller.ts
import { Controller, Get, Post, Body, Query, Logger } from '@nestjs/common';
import { SupervisorService } from './supervisor.service';
@Controller('api/supervisor')
export class SupervisorController {
private readonly logger = new Logger(SupervisorController.name);
constructor(private readonly supervisor: SupervisorService) {}
@Get('active-calls')
getActiveCalls() {
return this.supervisor.getActiveCalls();
}
@Get('team-performance')
async getTeamPerformance(@Query('date') date?: string) {
const targetDate = date ?? new Date().toISOString().split('T')[0];
return this.supervisor.getTeamPerformance(targetDate);
}
@Post('call-event')
handleCallEvent(@Body() body: any) {
// Ozonetel pushes events here
const event = body.data ?? body;
this.logger.log(`Call event: ${event.action} ucid=${event.ucid} agent=${event.agent_id}`);
this.supervisor.handleCallEvent(event);
return { received: true };
}
@Post('agent-event')
handleAgentEvent(@Body() body: any) {
const event = body.data ?? body;
this.logger.log(`Agent event: ${event.action} agent=${event.agentId}`);
this.supervisor.handleAgentEvent(event);
return { received: true };
}
}
```
- [ ] **Step 3: Create supervisor module and register**
```typescript
// supervisor.module.ts
import { Module } from '@nestjs/common';
import { PlatformModule } from '../platform/platform.module';
import { OzonetelAgentModule } from '../ozonetel/ozonetel-agent.module';
import { SupervisorController } from './supervisor.controller';
import { SupervisorService } from './supervisor.service';
@Module({
imports: [PlatformModule, OzonetelAgentModule],
controllers: [SupervisorController],
providers: [SupervisorService],
})
export class SupervisorModule {}
```
Add to `app.module.ts`:
```typescript
import { SupervisorModule } from './supervisor/supervisor.module';
// Add to imports array
```
- [ ] **Step 4: Verify sidecar build**
```bash
cd helix-engage-server && npm run build
```
- [ ] **Step 5: Commit**
```bash
git add src/supervisor/ src/app.module.ts
git commit -m "feat: supervisor module with team performance + active calls endpoints"
```
---
## Task 5: Team Performance Dashboard (PP-5)
**Files:**
- Modify: `helix-engage/src/pages/team-performance.tsx`
This is the largest task. The page queries platform directly for calls/appointments/leads and the sidecar for time breakdown.
- [ ] **Step 1: Build the full page**
The page has 6 sections. Use `apiClient.graphql()` for platform data and `apiClient.get()` for sidecar data.
**Queries needed:**
- Calls by date range: `calls(first: 500, filter: { startedAt: { gte: "...", lte: "..." } })`
- Appointments by date range: `appointments(first: 200, filter: { scheduledAt: { gte: "...", lte: "..." } })`
- Leads: `leads(first: 200)`
- Follow-ups: `followUps(first: 200)`
- Agents with thresholds: `agents(first: 20) { ... npsscore maxidleminutes minnpsthreshold minconversionpercent }`
- Sidecar: `GET /api/supervisor/team-performance?date=YYYY-MM-DD`
**Date range logic:**
- Today: today start → now
- Week: Monday of current week → now
- Month: 1st of current month → now
- Year: Jan 1 → now
- Custom: user-selected range
**Sections to implement:**
1. Key Metrics bar (6 cards in a row)
2. Call Breakdown Trends (2 ECharts line charts side by side)
3. Agent Performance table (sortable)
4. Time Breakdown (team average + per-agent stacked bars)
5. NPS + Conversion Metrics (donut + cards)
6. Performance Alerts (threshold comparison)
Check if ECharts is already installed:
```bash
grep echarts helix-engage/package.json
```
If not, install: `npm install echarts echarts-for-react`
Follow the existing My Performance page (`my-performance.tsx`) for ECharts patterns.
- [ ] **Step 2: Verify build**
```bash
cd helix-engage && npm run build
```
- [ ] **Step 3: Test locally**
```bash
cd helix-engage && npm run dev
```
Navigate to `/team-performance` as admin user. Verify all 6 sections render with real data.
- [ ] **Step 4: Commit**
```bash
git add src/pages/team-performance.tsx package.json package-lock.json
git commit -m "feat: team performance dashboard (PP-5) with 6 data sections"
```
---
## Task 6: Live Call Monitor (PP-6)
**Files:**
- Modify: `helix-engage/src/pages/live-monitor.tsx`
- [ ] **Step 1: Build the live monitor page**
Page polls `GET /api/supervisor/active-calls` every 5 seconds.
**Structure:**
1. TopBar: "Live Call Monitor" with subtitle "Listen, whisper, or barge into active calls"
2. Three KPI cards: Active Calls, On Hold, Avg Duration
3. Active Calls table: Agent, Caller, Type, Department, Duration (live counter), Status, Actions
4. Actions: Listen / Whisper / Barge buttons — all disabled with tooltip "Coming soon — pending Ozonetel API"
5. Empty state: headphones icon + "No active calls"
Duration should be a live counter — calculated client-side from `startTime` in the active call data. Use `setInterval` to update every second.
Caller name: attempt to match `callerNumber` against leads from `useData()`. If matched, show lead name + phone. If not, show phone only.
- [ ] **Step 2: Verify build**
```bash
cd helix-engage && npm run build
```
- [ ] **Step 3: Test locally**
Navigate to `/live-monitor`. Verify empty state renders. If Ozonetel events are flowing, verify active calls appear.
- [ ] **Step 4: Commit**
```bash
git add src/pages/live-monitor.tsx
git commit -m "feat: live call monitor page (PP-6) with polling + KPI cards"
```
---
## Task 7: Local Testing + Final Verification
- [ ] **Step 1: Run both locally**
Terminal 1: `cd helix-engage-server && npm run start:dev`
Terminal 2: `cd helix-engage && npm run dev`
- [ ] **Step 2: Test admin login**
Login as admin (sanjay.marketing@globalhospital.com). Verify:
- Sidebar shows new nav structure (Supervisor + Data & Reports sections)
- Dashboard loads
- Team Performance shows data from platform
- Live Monitor shows empty state or active calls
- All master data pages load (Lead, Patient, Appointment, Call Log, Call Recordings, Missed Calls)
- [ ] **Step 3: Commit any fixes**
- [ ] **Step 4: Push to Azure**
```bash
cd helix-engage && git push origin dev
cd helix-engage-server && git push origin dev
```