fix: dates in Ava's prose read as 'Jul 26, 2026', not 2026-07-26
Display-only rewrite (format.prose) on every surface that shows her text — headline, work statement, full story, recommendations, re-check, queue cards, slides, notes. Stored text keeps ISO dates so deadline and spike parsers still read them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
75483f347e
commit
4087d2c2e0
@ -5,18 +5,7 @@ import type { Slice } from '../api/charts';
|
|||||||
import { affectedClaimsHref, recoverableClaimsHref } from '../lib/claimsLink';
|
import { affectedClaimsHref, recoverableClaimsHref } from '../lib/claimsLink';
|
||||||
import { Badge, Card, Facts } from './core';
|
import { Badge, Card, Facts } from './core';
|
||||||
import type { AvaDecision, AvaTraceStep, Row } from '../api/types';
|
import type { AvaDecision, AvaTraceStep, Row } from '../api/types';
|
||||||
import {
|
import { firstDeadline, headline, money, num, pct, prose, recommendations, shortDate, splitRecommendation, sqlSteps, str } from '../format';
|
||||||
firstDeadline,
|
|
||||||
headline,
|
|
||||||
money,
|
|
||||||
num,
|
|
||||||
pct,
|
|
||||||
recommendations,
|
|
||||||
shortDate,
|
|
||||||
splitRecommendation,
|
|
||||||
sqlSteps,
|
|
||||||
str,
|
|
||||||
} from '../format';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ava's findings, laid out for a person who has ninety seconds.
|
* Ava's findings, laid out for a person who has ninety seconds.
|
||||||
@ -46,8 +35,8 @@ export function Verdict({
|
|||||||
}) {
|
}) {
|
||||||
const summary = str(row.summary);
|
const summary = str(row.summary);
|
||||||
if (!summary) return null;
|
if (!summary) return null;
|
||||||
const line = headline(summary);
|
const line = prose(headline(summary));
|
||||||
const scanned = str(row.scanned_summary);
|
const scanned = prose(row.scanned_summary);
|
||||||
const affectedHref = affectedClaimsHref(kind, slice, anchor);
|
const affectedHref = affectedClaimsHref(kind, slice, anchor);
|
||||||
const recoverableHref = recoverableClaimsHref(kind, slice, anchor);
|
const recoverableHref = recoverableClaimsHref(kind, slice, anchor);
|
||||||
const atRisk = num(row.amount_at_risk);
|
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. */
|
/** The decision list. Rendered directly above the Approve / Send back / Reject buttons. */
|
||||||
export function Recommendations({ row }: { row: Row }) {
|
export function Recommendations({ row }: { row: Row }) {
|
||||||
const recs = recommendations(row.recommendations);
|
const recs = recommendations(row.recommendations).map(prose);
|
||||||
if (recs.length === 0) return null;
|
if (recs.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<section className="lift rounded-xl border border-line bg-panel">
|
<section className="lift rounded-xl border border-line bg-panel">
|
||||||
@ -140,9 +129,9 @@ export function Recommendations({ row }: { row: Row }) {
|
|||||||
/** The paragraph version — for the reader who wants the whole account. Collapsed. */
|
/** The paragraph version — for the reader who wants the whole account. Collapsed. */
|
||||||
export function FullStory({ row }: { row: Row }) {
|
export function FullStory({ row }: { row: Row }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const summary = str(row.summary);
|
const summary = prose(row.summary);
|
||||||
const cause = str(row.root_cause);
|
const cause = prose(row.root_cause);
|
||||||
const secondary = str(row.secondary_findings);
|
const secondary = prose(row.secondary_findings);
|
||||||
if (!summary) return null;
|
if (!summary) return null;
|
||||||
const hasSecondary = secondary && !/^none found/i.test(secondary.trim());
|
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. */
|
/** The re-check. Same shape either way, so "fixed" and "not fixed" read alike. */
|
||||||
export function Recheck({ row }: { row: Row }) {
|
export function Recheck({ row }: { row: Row }) {
|
||||||
const summary = str(row.recheck_summary);
|
const summary = prose(row.recheck_summary);
|
||||||
if (!summary) return null;
|
if (!summary) return null;
|
||||||
const current = num(row.current_rate);
|
const current = num(row.current_rate);
|
||||||
const baseline = num(row.baseline_rate);
|
const baseline = num(row.baseline_rate);
|
||||||
|
|||||||
@ -184,6 +184,21 @@ export function firstDeadline(recs: unknown): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 2026-10-02 → "Oct 2" */
|
/** 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 {
|
export function shortDate(iso: string | null): string {
|
||||||
if (!iso) return '';
|
if (!iso) return '';
|
||||||
const d = new Date(iso + 'T00:00:00');
|
const d = new Date(iso + 'T00:00:00');
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import pptxgen from 'pptxgenjs';
|
|||||||
import type { BriefingData, Slice } from '../api/charts';
|
import type { BriefingData, Slice } from '../api/charts';
|
||||||
import { RECOVERY_ORDER } from '../api/charts';
|
import { RECOVERY_ORDER } from '../api/charts';
|
||||||
import type { Row } from '../api/types';
|
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.
|
* 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 -------------------------------------------------------
|
// ---- 1. What happened -------------------------------------------------------
|
||||||
{
|
{
|
||||||
const s = pres.addSlide();
|
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]> = [
|
const tiles: Array<[string, string, string]> = [
|
||||||
['Claims denied', num(row.affected_claims)?.toLocaleString('en-US') ?? '—', INK],
|
['Claims denied', num(row.affected_claims)?.toLocaleString('en-US') ?? '—', INK],
|
||||||
['Money at stake', money(row.amount_at_risk), 'B31E23'],
|
['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(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 });
|
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) {
|
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('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' });
|
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 -------------------------------------------------
|
// ---- 5. What she recommends -------------------------------------------------
|
||||||
const recs = recommendations(row.recommendations);
|
const recs = recommendations(row.recommendations).map(prose);
|
||||||
if (recs.length) {
|
if (recs.length) {
|
||||||
const s = pres.addSlide();
|
const s = pres.addSlide();
|
||||||
chrome(s, 'Recommended actions', 'For approval');
|
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 reconciled = row.data_reconciled === true || str(row.data_reconciled) === 'true';
|
||||||
const conf = num(row.confidence);
|
const conf = num(row.confidence);
|
||||||
const bullets = [
|
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.',
|
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'
|
kind === 'payer_policy'
|
||||||
? `Cause matched to the payer's own policy bulletin${slice?.spikeStart ? ` effective ${shortDate(slice.spikeStart)}` : ''}.`
|
? `Cause matched to the payer's own policy bulletin${slice?.spikeStart ? ` effective ${shortDate(slice.spikeStart)}` : ''}.`
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { Disclosure, FullStory, Recheck, Recommendations, SqlEvidence, Verdict }
|
|||||||
import { Briefing, BriefingSkeleton } from '../components/Briefing';
|
import { Briefing, BriefingSkeleton } from '../components/Briefing';
|
||||||
import { loadBriefing, sliceOf } from '../api/charts';
|
import { loadBriefing, sliceOf } from '../api/charts';
|
||||||
import type { Action } from '../api/config';
|
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.
|
* One investigation, for a person with ninety seconds.
|
||||||
@ -170,7 +170,7 @@ export function CaseScreen() {
|
|||||||
<section className="lift rounded-xl border border-bad-edge bg-bad-soft px-5 py-4">
|
<section className="lift rounded-xl border border-bad-edge bg-bad-soft px-5 py-4">
|
||||||
<p className="text-[11px] font-semibold tracking-wide text-bad uppercase">Escalated — requires a decision</p>
|
<p className="text-[11px] font-semibold tracking-wide text-bad uppercase">Escalated — requires a decision</p>
|
||||||
<p className="mt-1.5 text-[17px] leading-snug font-semibold text-ink">
|
<p className="mt-1.5 text-[17px] leading-snug font-semibold text-ink">
|
||||||
{str(row.escalation_reason) || 'No reason recorded.'}
|
{prose(row.escalation_reason) || 'No reason recorded.'}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
@ -257,8 +257,8 @@ export function CaseScreen() {
|
|||||||
? `${str(row.period_weeks)} weeks`
|
? `${str(row.period_weeks)} weeks`
|
||||||
: '6 weeks',
|
: '6 weeks',
|
||||||
},
|
},
|
||||||
...(str(row.review_note) ? [{ label: 'Reviewer note', value: str(row.review_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: str(row.approval_note), wide: true }] : []),
|
...(str(row.approval_note) ? [{ label: 'Approval note', value: prose(row.approval_note), wide: true }] : []),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useQuery, useZino } from '../api/provider';
|
|||||||
import { FINDING_KIND, QUEUES, RECORD_VIEW, STAGE_TONE } from '../api/config';
|
import { FINDING_KIND, QUEUES, RECORD_VIEW, STAGE_TONE } from '../api/config';
|
||||||
import { Badge, Button, Empty, ErrorNote, Spinner } from '../components/core';
|
import { Badge, Button, Empty, ErrorNote, Spinner } from '../components/core';
|
||||||
import type { Row } from '../api/types';
|
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.
|
* 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. */
|
/** A case, led by its verdict and its money. */
|
||||||
function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
|
function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
|
||||||
const state = str(row.current_state_name);
|
const state = str(row.current_state_name);
|
||||||
const summary = str(row.summary);
|
const summary = prose(row.summary);
|
||||||
const scanned = str(row.scanned_summary);
|
const scanned = prose(row.scanned_summary);
|
||||||
const flagged = num(row.flagged_count);
|
const flagged = num(row.flagged_count);
|
||||||
const kind = str(row.finding_kind);
|
const kind = str(row.finding_kind);
|
||||||
const recoverable = num(row.recoverable_amount);
|
const recoverable = num(row.recoverable_amount);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user