import pptxgen from 'pptxgenjs'; import type { BriefingData, Slice } from '../api/charts'; import { RECOVERY_ORDER } from '../api/charts'; import type { Row } from '../api/types'; import { headline, money, num, recommendations, shortDate, splitRecommendation, str } from '../format'; /** * The deck. Six slides a manager can walk into a CFO's office with. * * 1. What happened — the headline sentence and three numbers * 2. The spike — weekly denials for the flagged slice, bulletin marked * 3. Where it sits — denial rate by insurer, before vs now * 4. Where the money stands — recovery buckets + expiring appeals * 5. What she recommends — the ranked list, owners and dollars * 6. How she got there — the sweep, the checks, her confidence * * Charts are NATIVE PowerPoint charts built from the same rows the console * drew, so they stay editable in PowerPoint and match the screen. Text is * Ava's own, verbatim — the deck asserts nothing the case does not. * * Colours are the validated reference slots used on screen (see Briefing.tsx). * Built in the browser with pptxgenjs; nothing leaves the user's machine. */ const INK = '0F2233'; const MUTED = '5A7089'; const FAINT = '93A6B5'; const ACCENT = '2A78D6'; const GRAY = '94A3AF'; const GOOD = '008300'; const WARN = 'EB6834'; const VIOLET = '4A3AA7'; const AI = '5B2CA0'; const BRAND = '0E7C86'; const PANEL = 'F2F7F8'; const GRID = 'E3EEF1'; const RECOVERY_COLOR: Record = { recovered: GOOD, 'appeal filed': ACCENT, 'still appealable': WARN, 'lost on appeal': VIOLET, 'window closed': GRAY, }; export async function buildDeck(row: Row, data: BriefingData, slice: Slice | null, kind = ''): Promise { const pres = new pptxgen(); pres.layout = 'LAYOUT_WIDE'; // 13.33 x 7.5 in pres.author = 'Northlake Health — Denials Desk (AI analyst: Ava)'; pres.title = `Denials briefing — case #${str(row.instance_id)}`; const caseNo = str(row.instance_id); const sliceTitle = slice ? [slice.payer, slice.department, slice.code].filter(Boolean).join(' · ') : 'Northlake Health'; const chrome = (s: pptxgen.Slide, title: string, kicker?: string) => { s.background = { color: 'FFFFFF' }; s.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 13.33, h: 0.08, fill: { color: BRAND } }); if (kicker) s.addText(kicker.toUpperCase(), { x: 0.6, y: 0.35, w: 12, h: 0.3, fontSize: 10, bold: true, color: AI, charSpacing: 1 }); s.addText(title, { x: 0.6, y: kicker ? 0.62 : 0.4, w: 12.1, h: 0.7, fontSize: 22, bold: true, color: INK, valign: 'top' }); s.addText(`Northlake Health · Denials Desk · Case #${caseNo} · AI analysis (Ava) · for manager review`, { x: 0.6, y: 7.0, w: 12, h: 0.3, fontSize: 9, color: FAINT }); }; // ---- 1. What happened ------------------------------------------------------- { const s = pres.addSlide(); chrome(s, headline(row.summary, 240), 'Summary'); const tiles: Array<[string, string, string]> = [ ['Claims denied', num(row.affected_claims)?.toLocaleString('en-US') ?? '—', INK], ['Money at stake', money(row.amount_at_risk), 'B31E23'], ['Still recoverable', money(row.recoverable_amount), '00713D'], ]; tiles.forEach(([label, value, color], i) => { const x = 0.6 + i * 4.1; s.addShape(pres.ShapeType.roundRect, { x, y: 1.9, w: 3.8, h: 1.7, fill: { color: PANEL }, line: { color: 'D5E4E8', width: 0.75 }, rectRadius: 0.08 }); s.addText(label.toUpperCase(), { x: x + 0.25, y: 2.05, w: 3.3, h: 0.3, fontSize: 10, bold: true, color: MUTED, charSpacing: 1 }); s.addText(value, { x: x + 0.25, y: 2.4, w: 3.3, h: 0.9, fontSize: 36, bold: true, color }); }); const cause = str(row.root_cause); if (cause) { s.addText('ROOT CAUSE', { x: 0.6, y: 3.95, w: 12, h: 0.3, fontSize: 10, bold: true, color: MUTED, charSpacing: 1 }); s.addText(cause, { x: 0.6, y: 4.25, w: 12.1, h: 2.5, fontSize: 13, color: INK, valign: 'top' }); } } // ---- 2. The spike ----------------------------------------------------------- const spikePts = data.weeklySlice.length ? data.weeklySlice : data.weeklyRate; if (spikePts.length) { const s = pres.addSlide(); const isSlice = data.weeklySlice.length > 0; chrome(s, isSlice ? `Weekly denied claims — ${sliceTitle}` : 'Weekly denial rate — all claims', 'Trend'); const labels = spikePts.map((p) => shortDate(p.week)); const spike = slice?.spikeStart; if (isSlice) { // Emphasis form: before the rule change in gray, after in the accent. const before = spikePts.map((p) => (spike && p.week >= spike ? 0 : p.denied)); const after = spikePts.map((p) => (spike && p.week >= spike ? p.denied : 0)); s.addChart( pres.ChartType.bar, [ { name: 'before the rule change', labels, values: before }, { name: 'after the rule change', labels, values: after }, ], { x: 0.6, y: 1.5, w: 12.1, h: 5.1, barDir: 'col', barGrouping: 'stacked', barGapWidthPct: 40, chartColors: [GRAY, ACCENT], catAxisLabelColor: MUTED, valAxisLabelColor: MUTED, catAxisLabelFontSize: 9, valAxisLabelFontSize: 9, valGridLine: { color: GRID, size: 0.5 }, catGridLine: { style: 'none' }, showLegend: true, legendPos: 'b', legendFontSize: 10, legendColor: MUTED, showValue: false, }, ); } else { s.addChart(pres.ChartType.line, [{ name: 'denial rate %', labels, values: spikePts.map((p) => p.pct) }], { x: 0.6, y: 1.5, w: 12.1, h: 5.1, chartColors: [ACCENT], lineSize: 2, lineDataSymbol: 'none', catAxisLabelColor: MUTED, valAxisLabelColor: MUTED, catAxisLabelFontSize: 9, valAxisLabelFontSize: 9, valGridLine: { color: GRID, size: 0.5 }, catGridLine: { style: 'none' }, showLegend: false, }); } if (spike) s.addText(`Payer policy effective ${shortDate(spike)}. Weeks from that date shown in blue.`, { x: 0.6, y: 6.65, w: 12, h: 0.3, fontSize: 11, color: AI, italic: true }); } // ---- 3. Where it sits ------------------------------------------------------- if (data.payers.length) { const s = pres.addSlide(); chrome(s, 'Denial rate by insurer', 'Comparison'); const labels = data.payers.map((p) => p.payer); s.addChart( pres.ChartType.bar, [ { name: '8-week baseline', labels, values: data.payers.map((p) => p.rateBase) }, { name: 'last 6 weeks', labels, values: data.payers.map((p) => p.rateRecent) }, ], { x: 0.6, y: 1.5, w: 12.1, h: 4.9, barDir: 'col', barGrouping: 'clustered', barGapWidthPct: 60, chartColors: [GRAY, ACCENT], catAxisLabelColor: MUTED, valAxisLabelColor: MUTED, catAxisLabelFontSize: 10, valAxisLabelFontSize: 9, valAxisLabelFormatCode: '0"%"', valGridLine: { color: GRID, size: 0.5 }, catGridLine: { style: 'none' }, showLegend: true, legendPos: 'b', legendFontSize: 10, legendColor: MUTED, showValue: true, dataLabelFormatCode: '0.0"%"', dataLabelColor: INK, dataLabelFontSize: 9, }, ); if (slice?.payer) { const p = data.payers.find((x) => x.payer === slice.payer); if (p) s.addText(`${p.payer}: ${p.rateBase}% → ${p.rateRecent}%. Other insurers within normal variation.`, { x: 0.6, y: 6.55, w: 12, h: 0.35, fontSize: 12, bold: true, color: INK }); } } // ---- 4. Where the money stands ---------------------------------------------- if (data.recovery.length || data.expiring.length) { const s = pres.addSlide(); chrome(s, 'Financial position', 'Recovery'); if (data.recovery.length) { const ordered = RECOVERY_ORDER.filter((b) => data.recovery.some((r) => r.bucket === b)); s.addText(sliceTitle.toUpperCase(), { x: 0.6, y: 1.45, w: 6, h: 0.3, fontSize: 10, bold: true, color: MUTED, charSpacing: 1 }); s.addChart( pres.ChartType.bar, ordered.map((b) => ({ name: b, labels: ['denied since baseline'], values: [data.recovery.find((r) => r.bucket === b)?.billed ?? 0] })), { x: 0.6, y: 1.8, w: 6.0, h: 1.4, barDir: 'bar', barGrouping: 'stacked', barGapWidthPct: 20, chartColors: ordered.map((b) => RECOVERY_COLOR[b]), catAxisHidden: true, valAxisHidden: true, showLegend: true, legendPos: 'b', legendFontSize: 9, legendColor: MUTED, valGridLine: { style: 'none' }, }, ); const rows = [ [ { text: 'Status', options: { bold: true, color: MUTED } }, { text: 'Claims', options: { bold: true, color: MUTED, align: 'right' as const } }, { text: 'Billed', options: { bold: true, color: MUTED, align: 'right' as const } }, ], ...ordered.map((b) => { const r = data.recovery.find((x) => x.bucket === b)!; return [ { text: b }, { text: String(r.claims), options: { align: 'right' as const } }, { text: money(r.billed), options: { align: 'right' as const, bold: b === 'still appealable' } }, ]; }), ]; s.addTable(rows, { x: 0.6, y: 3.4, w: 6.0, colW: [3.0, 1.2, 1.8], fontSize: 11, color: INK, border: { type: 'solid', color: GRID, pt: 0.5 }, rowH: 0.32 }); } if (data.expiring.length) { const total = data.expiring.reduce((t, e) => t + e.billed, 0); s.addText('UNFILED APPEALS EXPIRING WITHIN 14 DAYS', { x: 7.0, y: 1.45, w: 5.7, h: 0.3, fontSize: 10, bold: true, color: MUTED, charSpacing: 1 }); s.addText(money(total), { x: 7.0, y: 1.75, w: 5.7, h: 0.7, fontSize: 30, bold: true, color: WARN }); s.addChart( pres.ChartType.bar, [{ name: 'billed', labels: data.expiring.map((e) => e.payer), values: data.expiring.map((e) => e.billed) }], { x: 7.0, y: 2.5, w: 5.7, h: 3.9, barDir: 'bar', barGapWidthPct: 45, chartColors: [ACCENT], catAxisLabelColor: MUTED, catAxisLabelFontSize: 10, valAxisHidden: true, valGridLine: { style: 'none' }, catGridLine: { style: 'none' }, showValue: true, dataLabelFormatCode: '$#,##0', dataLabelColor: INK, dataLabelFontSize: 9, showLegend: false, }, ); const first = data.expiring.slice().sort((a, b) => a.firstDeadline.localeCompare(b.firstDeadline))[0]; if (first) s.addText(`First deadline: ${shortDate(first.firstDeadline)} (${first.payer}).`, { x: 7.0, y: 6.5, w: 5.7, h: 0.3, fontSize: 11, color: MUTED }); } } // ---- 5. What she recommends ------------------------------------------------- const recs = recommendations(row.recommendations); if (recs.length) { const s = pres.addSlide(); chrome(s, 'Recommended actions', 'For approval'); let y = 1.5; recs.slice(0, 5).forEach((block, i) => { const { title, body } = splitRecommendation(block); s.addShape(pres.ShapeType.ellipse, { x: 0.6, y: y + 0.05, w: 0.36, h: 0.36, fill: { color: BRAND } }); s.addText(String(i + 1), { x: 0.6, y: y + 0.05, w: 0.36, h: 0.36, fontSize: 11, bold: true, color: 'FFFFFF', align: 'center', valign: 'middle' }); s.addText( [ { text: sentence(title) || body.slice(0, 80), options: { bold: true, fontSize: 13, color: INK, breakLine: true } }, { text: body.replace(/\s+/g, ' ').slice(0, 320), options: { fontSize: 10.5, color: MUTED } }, ], { x: 1.15, y, w: 11.5, h: 1.0, valign: 'top' }, ); y += 1.05; }); } // ---- 6. How she got there ---------------------------------------------------- { const s = pres.addSlide(); chrome(s, 'Method', 'Method'); const reconciled = row.data_reconciled === true || str(row.data_reconciled) === 'true'; const conf = num(row.confidence); const bullets = [ str(row.scanned_summary) || 'Reviewed every payer, department and front desk against an 8-week baseline.', reconciled ? 'Claims data reconciled against the finance report before any analysis.' : 'Claims data could not be fully reconciled against finance — treat numbers as provisional.', kind === 'payer_policy' ? `Cause matched to the payer's own policy bulletin${slice?.spikeStart ? ` effective ${shortDate(slice.spikeStart)}` : ''}.` : '', conf != null ? `Stated confidence: ${Math.round(conf * 100)}%.` : '', 'Read-only access to Databricks (workspace.rcm). Every query is logged and reproducible from the case.', ].filter(Boolean); s.addText(bullets.map((b) => ({ text: b, options: { bullet: true, breakLine: true, fontSize: 13, color: INK, paraSpaceAfter: 8 } })), { x: 0.6, y: 1.5, w: 12.1, h: 4.8, valign: 'top' }); s.addText('AI analysis requires manager approval before any action. The analyst has no write access to any system.', { x: 0.6, y: 6.4, w: 12, h: 0.4, fontSize: 11, italic: true, color: AI }); } await pres.writeFile({ fileName: `Northlake-denials-briefing-case-${caseNo}.pptx` }); } function sentence(s: string): string { if (!s) return s; if (s !== s.toUpperCase()) return s; return s.toLowerCase().replace(/^\w/, (c) => c.toUpperCase()); }