mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
feat: supervisor module — team performance + active calls endpoints
- SupervisorService: aggregates Ozonetel agent summary across all agents, tracks active calls from real-time events - GET /api/supervisor/team-performance — per-agent time breakdown + thresholds - GET /api/supervisor/active-calls — current active call map - POST /api/supervisor/call-event — Ozonetel event webhook - POST /api/supervisor/agent-event — Ozonetel agent event webhook Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { HealthModule } from './health/health.module';
|
||||
import { WorklistModule } from './worklist/worklist.module';
|
||||
import { CallAssistModule } from './call-assist/call-assist.module';
|
||||
import { SearchModule } from './search/search.module';
|
||||
import { SupervisorModule } from './supervisor/supervisor.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -30,6 +31,7 @@ import { SearchModule } from './search/search.module';
|
||||
WorklistModule,
|
||||
CallAssistModule,
|
||||
SearchModule,
|
||||
SupervisorModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
37
src/supervisor/supervisor.controller.ts
Normal file
37
src/supervisor/supervisor.controller.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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];
|
||||
this.logger.log(`Team performance: date=${targetDate}`);
|
||||
return this.supervisor.getTeamPerformance(targetDate);
|
||||
}
|
||||
|
||||
@Post('call-event')
|
||||
handleCallEvent(@Body() body: any) {
|
||||
const event = body.data ?? body;
|
||||
this.logger.log(`Call event: ${event.action} ucid=${event.ucid ?? event.monitorUCID} agent=${event.agent_id ?? event.agentID}`);
|
||||
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 ?? event.agent_id}`);
|
||||
this.supervisor.handleAgentEvent(event);
|
||||
return { received: true };
|
||||
}
|
||||
}
|
||||
12
src/supervisor/supervisor.module.ts
Normal file
12
src/supervisor/supervisor.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
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 {}
|
||||
86
src/supervisor/supervisor.service.ts
Normal file
86
src/supervisor/supervisor.service.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
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() {
|
||||
this.logger.log('Supervisor service initialized');
|
||||
}
|
||||
|
||||
handleCallEvent(event: any) {
|
||||
const action = event.action;
|
||||
const ucid = event.ucid ?? event.monitorUCID;
|
||||
const agentId = event.agent_id ?? event.agentID;
|
||||
const callerNumber = event.caller_id ?? event.callerID;
|
||||
const callType = event.call_type ?? event.Type;
|
||||
const eventTime = event.event_time ?? event.eventTime ?? new Date().toISOString();
|
||||
|
||||
if (!ucid) return;
|
||||
|
||||
if (action === 'Answered' || action === 'Calling') {
|
||||
this.activeCalls.set(ucid, {
|
||||
ucid, agentId, callerNumber,
|
||||
callType, startTime: eventTime, status: 'active',
|
||||
});
|
||||
this.logger.log(`Active call: ${agentId} ↔ ${callerNumber} (${ucid})`);
|
||||
} else if (action === 'Disconnect') {
|
||||
this.activeCalls.delete(ucid);
|
||||
this.logger.log(`Call ended: ${ucid}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleAgentEvent(event: any) {
|
||||
this.logger.log(`Agent event: ${event.agentId ?? event.agent_id} → ${event.action}`);
|
||||
}
|
||||
|
||||
getActiveCalls(): ActiveCall[] {
|
||||
return Array.from(this.activeCalls.values());
|
||||
}
|
||||
|
||||
async getTeamPerformance(date: string): Promise<any> {
|
||||
// Get all agents 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 time summary per agent
|
||||
const summaries = await Promise.all(
|
||||
agents.map(async (agent: any) => {
|
||||
if (!agent.ozonetelagentid) return { ...agent, timeBreakdown: null };
|
||||
try {
|
||||
const summary = await this.ozonetel.getAgentSummary(agent.ozonetelagentid, date);
|
||||
return { ...agent, timeBreakdown: summary };
|
||||
} catch (err) {
|
||||
this.logger.warn(`Failed to get summary for ${agent.ozonetelagentid}: ${err}`);
|
||||
return { ...agent, timeBreakdown: null };
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return { date, agents: summaries };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user