diff --git a/src/components/Findings.tsx b/src/components/Findings.tsx index 9b8d3ee..ca73aea 100644 --- a/src/components/Findings.tsx +++ b/src/components/Findings.tsx @@ -5,18 +5,7 @@ import type { Slice } from '../api/charts'; import { affectedClaimsHref, recoverableClaimsHref } from '../lib/claimsLink'; import { Badge, Card, Facts } from './core'; import type { AvaDecision, AvaTraceStep, Row } from '../api/types'; -import { - firstDeadline, - headline, - money, - num, - pct, - recommendations, - shortDate, - splitRecommendation, - sqlSteps, - str, -} from '../format'; +import { firstDeadline, headline, money, num, pct, prose, recommendations, shortDate, splitRecommendation, sqlSteps, str } from '../format'; /** * Ava's findings, laid out for a person who has ninety seconds. @@ -46,8 +35,8 @@ export function Verdict({ }) { const summary = str(row.summary); if (!summary) return null; - const line = headline(summary); - const scanned = str(row.scanned_summary); + const line = prose(headline(summary)); + const scanned = prose(row.scanned_summary); const affectedHref = affectedClaimsHref(kind, slice, anchor); const recoverableHref = recoverableClaimsHref(kind, slice, anchor); const atRisk = num(row.amount_at_risk); @@ -109,7 +98,7 @@ export function Verdict({ /** The decision list. Rendered directly above the Approve / Send back / Reject buttons. */ export function Recommendations({ row }: { row: Row }) { - const recs = recommendations(row.recommendations); + const recs = recommendations(row.recommendations).map(prose); if (recs.length === 0) return null; return (
@@ -140,9 +129,9 @@ export function Recommendations({ row }: { row: Row }) { /** The paragraph version — for the reader who wants the whole account. Collapsed. */ export function FullStory({ row }: { row: Row }) { const [open, setOpen] = useState(false); - const summary = str(row.summary); - const cause = str(row.root_cause); - const secondary = str(row.secondary_findings); + const summary = prose(row.summary); + const cause = prose(row.root_cause); + const secondary = prose(row.secondary_findings); if (!summary) return null; const hasSecondary = secondary && !/^none found/i.test(secondary.trim()); @@ -244,7 +233,7 @@ export function SqlEvidence({ row, steps, decisions }: { row: Row; steps: AvaTra /** The re-check. Same shape either way, so "fixed" and "not fixed" read alike. */ export function Recheck({ row }: { row: Row }) { - const summary = str(row.recheck_summary); + const summary = prose(row.recheck_summary); if (!summary) return null; const current = num(row.current_rate); const baseline = num(row.baseline_rate); diff --git a/src/format.ts b/src/format.ts index 4cd2a3d..934e252 100644 --- a/src/format.ts +++ b/src/format.ts @@ -184,6 +184,21 @@ export function firstDeadline(recs: unknown): string | null { } /** 2026-10-02 → "Oct 2" */ +/** 2026-07-26 → "Jul 26, 2026". For prose; shortDate is for axes and chips. */ +export function longDate(iso: string | null): string { + if (!iso) return ''; + const d = new Date(iso + 'T00:00:00'); + return Number.isNaN(d.getTime()) ? iso : d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +} + +/** Ava writes dates as YYYY-MM-DD so the code can read them (deadlines, the + * spike week, the claims links). People should not have to. This rewrites + * every ISO date inside a piece of prose for display, and nothing else — + * the stored text, and every parser that reads it, still sees the ISO form. */ +export function prose(v: unknown): string { + return str(v).replace(/\b(\d{4})-(\d{2})-(\d{2})\b/g, (m) => longDate(m) || m); +} + export function shortDate(iso: string | null): string { if (!iso) return ''; const d = new Date(iso + 'T00:00:00'); diff --git a/src/lib/slides.ts b/src/lib/slides.ts index 1485c39..cb663d1 100644 --- a/src/lib/slides.ts +++ b/src/lib/slides.ts @@ -2,7 +2,7 @@ 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'; +import { headline, money, num, prose, recommendations, shortDate, splitRecommendation, str } from '../format'; /** * The deck. Six slides a manager can walk into a CFO's office with. @@ -63,7 +63,7 @@ export async function buildDeck(row: Row, data: BriefingData, slice: Slice | nul // ---- 1. What happened ------------------------------------------------------- { const s = pres.addSlide(); - chrome(s, headline(row.summary, 240), 'Summary'); + chrome(s, prose(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'], @@ -75,7 +75,7 @@ export async function buildDeck(row: Row, data: BriefingData, slice: Slice | nul 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); + const cause = prose(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' }); @@ -194,7 +194,7 @@ export async function buildDeck(row: Row, data: BriefingData, slice: Slice | nul } // ---- 5. What she recommends ------------------------------------------------- - const recs = recommendations(row.recommendations); + const recs = recommendations(row.recommendations).map(prose); if (recs.length) { const s = pres.addSlide(); chrome(s, 'Recommended actions', 'For approval'); @@ -221,7 +221,7 @@ export async function buildDeck(row: Row, data: BriefingData, slice: Slice | nul 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.', + prose(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)}` : ''}.` diff --git a/src/screens/CaseScreen.tsx b/src/screens/CaseScreen.tsx index e57beee..d81d119 100644 --- a/src/screens/CaseScreen.tsx +++ b/src/screens/CaseScreen.tsx @@ -19,7 +19,7 @@ import { Disclosure, FullStory, Recheck, Recommendations, SqlEvidence, Verdict } import { Briefing, BriefingSkeleton } from '../components/Briefing'; import { loadBriefing, sliceOf } from '../api/charts'; import type { Action } from '../api/config'; -import { dateTime, num, str } from '../format'; +import { dateTime, num, prose, str } from '../format'; /** * One investigation, for a person with ninety seconds. @@ -170,7 +170,7 @@ export function CaseScreen() {

Escalated — requires a decision

- {str(row.escalation_reason) || 'No reason recorded.'} + {prose(row.escalation_reason) || 'No reason recorded.'}

)} @@ -257,8 +257,8 @@ export function CaseScreen() { ? `${str(row.period_weeks)} weeks` : '6 weeks', }, - ...(str(row.review_note) ? [{ label: 'Reviewer note', value: str(row.review_note), wide: true }] : []), - ...(str(row.approval_note) ? [{ label: 'Approval note', value: str(row.approval_note), wide: true }] : []), + ...(str(row.review_note) ? [{ label: 'Reviewer note', value: prose(row.review_note), wide: true }] : []), + ...(str(row.approval_note) ? [{ label: 'Approval note', value: prose(row.approval_note), wide: true }] : []), ]} />
diff --git a/src/screens/QueueScreen.tsx b/src/screens/QueueScreen.tsx index ce274f2..e498fb5 100644 --- a/src/screens/QueueScreen.tsx +++ b/src/screens/QueueScreen.tsx @@ -4,7 +4,7 @@ import { useQuery, useZino } from '../api/provider'; import { FINDING_KIND, QUEUES, RECORD_VIEW, STAGE_TONE } from '../api/config'; import { Badge, Button, Empty, ErrorNote, Spinner } from '../components/core'; import type { Row } from '../api/types'; -import { dateTime, headline, money, num, str } from '../format'; +import { dateTime, headline, money, num, prose, str } from '../format'; /** * A queue, as a morning briefing. @@ -115,8 +115,8 @@ function Briefing({ rows, onRun }: { rows: Row[]; onRun: () => void }) { /** A case, led by its verdict and its money. */ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) { const state = str(row.current_state_name); - const summary = str(row.summary); - const scanned = str(row.scanned_summary); + const summary = prose(row.summary); + const scanned = prose(row.scanned_summary); const flagged = num(row.flagged_count); const kind = str(row.finding_kind); const recoverable = num(row.recoverable_amount);