feat(supervisor): team-performance reads AgentSession first, Ozonetel as fallback

Phase 3 — wire the dashboard to the new metrics path without touching
the frontend. getTeamPerformance now:

1. Fetches AgentSession rows for the given IST date (keyed by agent UUID)
2. For each agent: uses AgentSession data rendered as HH:MM:SS if a row
   exists, otherwise falls back to Ozonetel summaryReport
3. Returns timeBreakdownSource so the frontend can optionally show which
   source was used

Frontend continues to parse the existing HH:MM:SS shape via parseTime()
— no UI change needed. Historical dates without AgentSession rows still
render via Ozonetel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 07:14:16 +05:30
parent 6de1989536
commit 9472f83cd8

View File

@@ -274,6 +274,11 @@ export class SupervisorService implements OnModuleInit {
); );
const agents = agentData?.agents?.edges?.map((e: any) => e.node) ?? []; const agents = agentData?.agents?.edges?.map((e: any) => e.node) ?? [];
// Fetch AgentSession rows for this date — the authoritative source
// for time breakdowns now that Phase 2 ingest is live. Keyed by
// agentId (UUID on platform) so we can match back by agent.id.
const sessionByAgentId = await this.fetchAgentSessionsByDate(date);
// Fetch CDR for the entire account for this date (one call, not per-agent) // Fetch CDR for the entire account for this date (one call, not per-agent)
let allCdr: any[] = []; let allCdr: any[] = [];
try { try {
@@ -282,12 +287,23 @@ export class SupervisorService implements OnModuleInit {
this.logger.warn(`Failed to fetch CDR for ${date}: ${err}`); this.logger.warn(`Failed to fetch CDR for ${date}: ${err}`);
} }
// Fetch Ozonetel time summary per agent + compute call metrics from CDR // Merge AgentSession → timeBreakdown (Ozonetel shape for UI compat);
// fall back to Ozonetel summary when no session row exists.
const summaries = await Promise.all( const summaries = await Promise.all(
agents.map(async (agent: any) => { agents.map(async (agent: any) => {
if (!agent.ozonetelAgentId) return { ...agent, timeBreakdown: null, calls: null }; if (!agent.ozonetelAgentId) return { ...agent, timeBreakdown: null, calls: null };
try { try {
const summary = await this.ozonetel.getAgentSummary(agent.ozonetelAgentId, date); let timeBreakdown: any = null;
let source: 'AGENT_SESSION' | 'OZONETEL_SUMMARY' | 'NONE' = 'NONE';
const session = sessionByAgentId.get(agent.id);
if (session) {
timeBreakdown = this.sessionToTimeBreakdown(session);
source = 'AGENT_SESSION';
} else {
timeBreakdown = await this.ozonetel.getAgentSummary(agent.ozonetelAgentId, date);
if (timeBreakdown) source = 'OZONETEL_SUMMARY';
}
// Filter CDR to this agent // Filter CDR to this agent
const agentCdr = allCdr.filter( const agentCdr = allCdr.filter(
@@ -301,7 +317,8 @@ export class SupervisorService implements OnModuleInit {
return { return {
...agent, ...agent,
timeBreakdown: summary, timeBreakdown,
timeBreakdownSource: source,
calls: { total: totalCalls, inbound, outbound, answered, missed }, calls: { total: totalCalls, inbound, outbound, answered, missed },
}; };
} catch (err) { } catch (err) {
@@ -323,6 +340,51 @@ export class SupervisorService implements OnModuleInit {
return { date, agents: summaries, teamTotals }; return { date, agents: summaries, teamTotals };
} }
// Pull AgentSession rows for the given IST date, keyed by agent UUID so
// getTeamPerformance can look them up per-agent.
private async fetchAgentSessionsByDate(date: string): Promise<Map<string, any>> {
const map = new Map<string, any>();
try {
const data = await this.platform.query<any>(
`{ agentSessions(first: 100, filter: { date: { eq: "${date}" } }) {
edges { node {
agentId loginDurationS busyTimeS idleTimeS pauseTimeS
wrapupTimeS dialTimeS avgHandlingTimeS source lastSyncedAt
} }
} }`,
);
const edges = data?.agentSessions?.edges ?? [];
for (const e of edges) {
if (e.node?.agentId) map.set(e.node.agentId, e.node);
}
} catch (err) {
this.logger.warn(`[PERF] Failed to fetch AgentSession rows for ${date}: ${err}`);
}
return map;
}
// Render AgentSession seconds in the HH:MM:SS shape the frontend expects
// (matches Ozonetel's summary so team-performance.tsx can parseTime() it
// without changing the page code).
private sessionToTimeBreakdown(session: any): any {
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}:${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}`;
};
return {
totalLoginTime: hms(session.loginDurationS),
totalBusyTime: hms(session.busyTimeS),
totalIdleTime: hms(session.idleTimeS),
totalPauseTime: hms(session.pauseTimeS),
totalWrapupTime: hms(session.wrapupTimeS),
totalDialTime: hms(session.dialTimeS),
avgHandlingTime: hms(session.avgHandlingTimeS),
};
}
// --- Barge session management --- // --- Barge session management ---
getBargeSession(agentId: string) { getBargeSession(agentId: string) {