diff --git a/src/app.module.ts b/src/app.module.ts index 5c55f4a..603bf32 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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 {} diff --git a/src/supervisor/supervisor.controller.ts b/src/supervisor/supervisor.controller.ts new file mode 100644 index 0000000..03e0e2d --- /dev/null +++ b/src/supervisor/supervisor.controller.ts @@ -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 }; + } +} diff --git a/src/supervisor/supervisor.module.ts b/src/supervisor/supervisor.module.ts new file mode 100644 index 0000000..f0ffad1 --- /dev/null +++ b/src/supervisor/supervisor.module.ts @@ -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 {} diff --git a/src/supervisor/supervisor.service.ts b/src/supervisor/supervisor.service.ts new file mode 100644 index 0000000..56facea --- /dev/null +++ b/src/supervisor/supervisor.service.ts @@ -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(); + + 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 { + // Get all agents from platform + const agentData = await this.platform.query( + `{ 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 }; + } +}