fix(my-performance): LOGIN TIME uses AgentSession rollup, not Ozonetel summary

Ozonetel's summaryReport only tallies CLOSED login→logout pairs — an
agent who's still logged in reports 00:00:00, so the KPI card on My
Performance always showed 0s for the current session.

Our AgentSession rollup already caps open sessions at "now" when it
runs. Endpoint now:
  1. Triggers an on-demand rollupSessions(targetDate) to refresh the
     AgentSession row (no 15-min wait after login)
  2. Reads AgentSession and renders in the HH:MM:SS shape the frontend
     expects
  3. Falls back to Ozonetel's summaryReport when AgentSession is
     empty (brand-new agent, workspace missing AgentEvent entity)

Works transparently — same timeUtilization shape as before, frontend
unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 12:25:23 +05:30
parent 96977e84a1
commit f375e7736c

View File

@@ -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<any>(
`{ 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;
}
}
}