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>
164 lines
5.7 KiB
JavaScript
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();
|
|
}
|