import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Subject } from 'rxjs'; 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'; }; export type AgentOzonetelState = 'ready' | 'break' | 'training' | 'calling' | 'in-call' | 'acw' | 'offline'; type AgentStateEntry = { state: AgentOzonetelState; timestamp: string; }; @Injectable() export class SupervisorService implements OnModuleInit { private readonly logger = new Logger(SupervisorService.name); private readonly activeCalls = new Map(); private readonly agentStates = new Map(); readonly agentStateSubject = new Subject<{ agentId: string; state: AgentOzonetelState; timestamp: string }>(); 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) { const agentId = event.agentId ?? event.agent_id ?? 'unknown'; const action = event.action ?? 'unknown'; const eventData = event.eventData ?? ''; const eventTime = event.event_time ?? event.eventTime ?? new Date().toISOString(); this.logger.log(`[AGENT-STATE] ${agentId} → ${action}${eventData ? ` (${eventData})` : ''} at ${eventTime}`); const mapped = this.mapOzonetelAction(action, eventData); if (mapped) { this.agentStates.set(agentId, { state: mapped, timestamp: eventTime }); this.agentStateSubject.next({ agentId, state: mapped, timestamp: eventTime }); this.logger.log(`[AGENT-STATE] Emitted: ${agentId} → ${mapped}`); } } private mapOzonetelAction(action: string, eventData: string): AgentOzonetelState | null { switch (action) { case 'release': return 'ready'; case 'IDLE': return 'ready'; // agent available after unanswered/canceled call case 'calling': return 'calling'; case 'incall': return 'in-call'; case 'ACW': return 'acw'; case 'logout': return 'offline'; case 'AUX': // "changeMode" is the brief AUX during login — not a real pause if (eventData === 'changeMode') return null; if (eventData?.toLowerCase().includes('training')) return 'training'; return 'break'; case 'login': return null; // wait for release default: return null; } } getAgentState(agentId: string): AgentStateEntry | null { return this.agentStates.get(agentId) ?? null; } emitForceLogout(agentId: string) { this.logger.log(`[AGENT-STATE] Emitting force-logout for ${agentId}`); this.agentStates.set(agentId, { state: 'offline', timestamp: new Date().toISOString() }); // Use a special state so frontend can distinguish admin force-logout from normal Ozonetel logout this.agentStateSubject.next({ agentId, state: 'force-logout' as any, timestamp: new Date().toISOString() }); } getActiveCalls(): ActiveCall[] { return Array.from(this.activeCalls.values()); } async getTeamPerformance(date: string): Promise { // Get all agents from platform. Field names are label-derived // camelCase on the current platform schema — see // agent-config.service.ts for the canonical explanation of the // legacy lowercase names that used to exist on staging. const agentData = await this.platform.query( `{ agents(first: 20) { edges { node { id name ozonetelAgentId npsScore maxIdleMinutes minNpsThreshold minConversion } } } }`, ); 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 }; } }