diff --git a/src/ozonetel/ozonetel-agent.controller.ts b/src/ozonetel/ozonetel-agent.controller.ts index c2b6ebb..cdac0fe 100644 --- a/src/ozonetel/ozonetel-agent.controller.ts +++ b/src/ozonetel/ozonetel-agent.controller.ts @@ -7,6 +7,7 @@ import { EventBusService } from '../events/event-bus.service'; import { Topics } from '../events/event-types'; import { TelephonyConfigService } from '../config/telephony-config.service'; import { SupervisorService } from '../supervisor/supervisor.service'; +import { AgentHistoryService } from '../supervisor/agent-history.service'; // Convert Ozonetel "HH:MM:SS" (or null/empty) to integer seconds. // Returns null when input is missing or all-zero. @@ -30,6 +31,7 @@ export class OzonetelAgentController { private readonly eventBus: EventBusService, private readonly supervisor: SupervisorService, private readonly agentLookup: AgentLookupService, + private readonly agentHistory: AgentHistoryService, ) {} private requireAgentId(agentId: string | undefined | null): string { @@ -405,12 +407,28 @@ export class OzonetelAgentController { const targetDate = date ?? new Date().toISOString().split('T')[0]; this.logger.log(`Performance: date=${targetDate} agent=${agent}`); - const [cdr, summary, aht] = await Promise.all([ + // Trigger an on-demand rollup for the requested date so the + // AgentSession row reflects the current open session (caps at now) + // instead of waiting up to 15 min for the background tick. Fire-and- + // forget with a short await so we don't block the whole response on + // cache-refresh tail but still hand the read a fresh row when Redpanda + // is quiet. Safe to error — AgentSession just stays stale. + await this.agentHistory.rollupSessions(targetDate).catch(() => {}); + + const [cdr, summary, aht, agentSessionBreakdown] = await Promise.all([ this.ozonetelAgent.fetchCDR({ date: targetDate }), this.ozonetelAgent.getAgentSummary(agent, targetDate), this.ozonetelAgent.getAHT(agent), + this.fetchAgentSessionTimeBreakdown(agent, targetDate), ]); + // Prefer our AgentSession rollup when present — it correctly counts + // the current OPEN session (caps at now), while Ozonetel's summaryReport + // only tallies CLOSED login→logout pairs. Fall back to Ozonetel if + // our rollup hasn't captured this agent yet (e.g., brand-new agent, + // workspace without AgentEvent entity synced). + const timeUtilization = agentSessionBreakdown ?? summary; + // Filter CDR to this agent only — fetchCDR returns all agents' calls // Use case-insensitive matching — Ozonetel field casing varies const agentLower = agent.toLowerCase(); @@ -460,7 +478,7 @@ export class OzonetelAgentController { avgHandlingTime: aht, conversionRate: totalCalls > 0 ? Math.round((appointmentsBooked / totalCalls) * 100) : 0, appointmentsBooked, - timeUtilization: summary, + timeUtilization, dispositions, }; } @@ -480,4 +498,52 @@ export class OzonetelAgentController { }; return map[disposition] ?? 'General Enquiry'; } + + // Convert our AgentSession rollup (seconds per category) into the HH:MM:SS + // shape the frontend expects — so My Performance gets LOGIN TIME with the + // current open session included, not just closed sessions from Ozonetel. + private async fetchAgentSessionTimeBreakdown(ozonetelAgentId: string, date: string): Promise<{ + totalLoginDuration: string; + totalBusyTime: string; + totalIdleTime: string; + totalPauseTime: string; + totalWrapupTime: string; + totalDialTime: string; + } | null> { + try { + const agentUuid = await this.agentLookup.resolveByOzonetelId(ozonetelAgentId); + if (!agentUuid) return null; + const data = await this.platform.query( + `{ agentSessions(first: 1, filter: { + agentId: { eq: "${agentUuid}" }, + date: { eq: "${date}" } + }) { edges { node { + loginDurationS busyTimeS idleTimeS pauseTimeS wrapupTimeS dialTimeS + } } } }`, + ); + const node = data?.agentSessions?.edges?.[0]?.node; + if (!node) return null; + const hms = (sec: number | null | undefined): string => { + const s = Math.max(0, Math.round(sec ?? 0)); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const r = s % 60; + return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${r.toString().padStart(2, '0')}`; + }; + // If the entire rollup is zero, treat as "no data yet" — fall back + // to Ozonetel's summaryReport so the KPI isn't all zeroes. + const total = (node.loginDurationS ?? 0) + (node.busyTimeS ?? 0) + (node.idleTimeS ?? 0) + (node.pauseTimeS ?? 0) + (node.wrapupTimeS ?? 0); + if (total === 0) return null; + return { + totalLoginDuration: hms(node.loginDurationS), + totalBusyTime: hms(node.busyTimeS), + totalIdleTime: hms(node.idleTimeS), + totalPauseTime: hms(node.pauseTimeS), + totalWrapupTime: hms(node.wrapupTimeS), + totalDialTime: hms(node.dialTimeS), + }; + } catch { + return null; + } + } }