collabSupport/services/renderers/callReportRenderer.js
jmcqueen a25fc08fe2 Rename /voicereport to /callreport with scoped user and phone targets.
Add store/email/phone filtering, richer call-line formatting, CDR feed pagination and queueing, and split Jira poller enrichment into testable modules.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 10:10:03 -04:00

164 lines
5.7 KiB
JavaScript

// services/renderers/callReportRenderer.js
import { CALL_BUCKETS } from '../callReport/groupCdrCalls.js';
import { formatCallBlock } from '../callReport/formatCallLine.js';
import { collapseAaBursts } from '../callReport/collapseDisplayCalls.js';
import { callReportEnvInt } from '../callReport/env.js';
function fmtTime(iso) {
if (!iso) return '—';
try {
return new Date(iso).toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
} catch {
return String(iso);
}
}
function renderOutcomeBreakdown(outcomes) {
const entries = Object.entries(outcomes || {});
if (!entries.length) return '_No outcome data._';
return entries
.sort((a, b) => b[1] - a[1])
.map(([k, v]) => `- **${k}:** ${v}`)
.join('\n');
}
function renderReachSummary(summary) {
const lines = [];
if (summary.inbound > 0) {
lines.push(`- **Inbound reached a phone:** ${summary.inboundReachedPhone} of ${summary.inbound}`);
if (summary.inboundAaOnly > 0) {
lines.push(`- **Inbound stopped at auto-attendant:** ${summary.inboundAaOnly}`);
}
}
if (summary.outbound > 0) {
lines.push(`- **Outbound connected:** ${summary.outboundConnected} of ${summary.outbound}`);
}
if (summary.abnormal > 0) {
lines.push(`- **Abnormal outcomes:** ${summary.abnormal}`);
}
return lines.join('\n');
}
function renderCallSection(title, calls, { timeZone, includeWan, maxRows, detail, collapseAa = false }) {
if (!calls?.length) return '';
const displayCalls = collapseAa ? collapseAaBursts(calls, { timeZone }) : calls;
const show = displayCalls.slice(0, maxRows);
let md = `\n### ${title} (${calls.length})\n`;
for (const item of show) {
if (item.kind === 'burst') {
md += `${item.summary}\n\n`;
continue;
}
const { line1, line2 } = formatCallBlock(item, { timeZone, includeWan, detail });
md += `${line1}\n`;
if (line2) md += `${line2}\n`;
md += '\n';
}
if (displayCalls.length > maxRows) {
md += `_${displayCalls.length - maxRows} more in this section — use Control Hub for full export._\n`;
}
return md;
}
function renderReportTitle(target) {
if (!target) return 'Call report';
if (target.kind === 'store') return `Call report — Store ${target.storeNum}`;
return `Call report — ${target.label}`;
}
function renderScopeLine(target) {
if (!target || target.kind === 'store') return null;
if (target.kind === 'user') return `- **Scope:** user ${target.email || target.label}`;
if (target.kind === 'number') return `- **Scope:** number ${target.label}`;
return null;
}
/**
* @param {object} report from collectCallReport
* @param {{ detail?: boolean }} opts
*/
export function renderCallReportMarkdown(report, opts = {}) {
if (!report?.ok) {
const target = report?.target || report?.store;
let md = `**Call report** — ${target?.label || target?.storeNum || '?'}\n\n`;
md += `_${report?.reason || report?.window?.reason || 'Unable to build report.'}_\n`;
return md.trim();
}
const { store, target: reportTarget, window, cdr, prisma, joined } = report;
const target = reportTarget || store;
const summary = joined.summary || {};
const buckets = joined.buckets || {};
const maxRows = callReportEnvInt('MAX_DETAIL_ROWS', 25);
const detail = Boolean(opts.detail);
const timeZone = window.timeZone;
let md = `**${renderReportTitle(target)}**\n\n`;
md += `- **Date:** ${window.label} (${window.mode})\n`;
md += `- **Timezone:** ${window.timeZone}\n`;
md += `- **Window:** ${fmtTime(window.startTime)}${fmtTime(window.endTime)}\n`;
md += `- **Location:** ${store.locationName}\n`;
if (store.dialNumber) md += `- **Main:** ${store.dialNumber}\n`;
const scopeLine = renderScopeLine(target);
if (scopeLine) md += `${scopeLine}\n`;
md += '\n**Call volume (CDR)**\n';
if (!cdr.available) {
md += `_CDR unavailable:_ ${cdr.reason || 'unknown'}\n`;
} else {
md += `- **Total calls:** ${summary.total ?? 0}\n`;
md += `- **Inbound:** ${summary.inbound ?? 0} | **Outbound:** ${summary.outbound ?? 0}\n`;
md += `- **CDR legs fetched:** ${cdr.rawCount ?? '—'} → **${summary.total ?? 0}** correlated call(s)\n`;
const reach = renderReachSummary(summary);
if (reach) md += `${reach}\n`;
if (target?.filter && summary.total === 0) {
md += '_No calls matched scope in this window._\n';
}
md += '\n**Call outcomes**\n';
md += `${renderOutcomeBreakdown(summary.outcomes)}\n`;
}
md += '\n**WAN voice (Prisma DPI)**\n';
if (!prisma.available) {
md += `_Unavailable:_ ${prisma.reason || 'not configured'}\n`;
} else {
const m = prisma.appAudio?.mos;
md += `- **App:** ${prisma.appAudio.appName}\n`;
if (m?.min != null && m?.max != null) {
md += `- **Worst MOS in window:** ${m.min} | **Best MOS:** ${m.max}\n`;
} else if (m?.min != null) {
md += `- **Worst MOS in window:** ${m.min}\n`;
}
if (prisma.appAudio?.detailsUrl) {
md += `- [View in Prisma SCM](${prisma.appAudio.detailsUrl})\n`;
}
}
if (cdr.available) {
const sectionOpts = { timeZone, includeWan: true, maxRows, detail };
md += renderCallSection(
'Inbound — reached a phone',
buckets[CALL_BUCKETS.inboundReachedPhone],
sectionOpts,
);
md += renderCallSection(
'Inbound — stopped at auto-attendant',
buckets[CALL_BUCKETS.inboundAaOnly],
{ ...sectionOpts, collapseAa: true },
);
md += renderCallSection(
'Outbound — connected',
buckets[CALL_BUCKETS.outboundConnected],
sectionOpts,
);
md += renderCallSection(
'Other / unanswered',
buckets[CALL_BUCKETS.other],
sectionOpts,
);
}
md += `\n*Generated in ${report.elapsedMs}ms*`;
return md.trim();
}