// 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(); }