northlake-health/src/screens/CaseScreen.tsx
Yashas 4087d2c2e0 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>
2026-09-16 19:17:08 +05:30

317 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { ArrowLeft, Brain, Download, Radar } from 'lucide-react';
import { useQuery, useZino } from '../api/provider';
import {
ACT,
FINDING_KIND,
NEEDS_HUMAN,
RECORD_VIEW,
STAGES,
STAGE_TONE,
actionsFor,
stageStep,
} from '../api/config';
import { Badge, Button, ErrorNote, Facts, Spinner } from '../components/core';
import { ActivityForm } from '../components/ActivityForm';
import { AvaLive } from '../components/AvaLive';
import { Disclosure, FullStory, Recheck, Recommendations, SqlEvidence, Verdict } from '../components/Findings';
import { Briefing, BriefingSkeleton } from '../components/Briefing';
import { loadBriefing, sliceOf } from '../api/charts';
import type { Action } from '../api/config';
import { dateTime, num, prose, str } from '../format';
/**
* One investigation, for a person with ninety seconds.
*
* ORDER IS THE DESIGN. What happened (one sentence, large) → how much (three
* numbers) → what she wants approved → the buttons. That is the whole
* above-the-fold. The full analysis, her evidence and the live trace sit
* behind disclosures underneath, for the reviewer who wants to check and the
* sceptic who wants to see the SQL.
*
* The exception is while Ava is still working: then the live trace IS the
* page, because there is nothing else to show yet and watching her query is
* the demo.
*
* POLLING is on while she owns the case (Investigating / Monitoring); when it
* is with a person nothing moves, so the poll stops.
*/
export function CaseScreen() {
const { id } = useParams();
const { client, user } = useZino();
const [form, setForm] = useState<Action | null>(null);
const [showWork, setShowWork] = useState(false);
const [exporting, setExporting] = useState(false);
const cases = useQuery(
() =>
client.rows(RECORD_VIEW, {
limit: 1,
filters: [{ field_key: 'instance_id', value: String(id), data_type: 'number' }],
}),
[id],
true,
6000,
);
const row = cases.data?.[0];
const state = str(row?.current_state_name);
const aiOwned = state === 'Investigating' || state === 'Monitoring';
const scanned = str(row?.scanned_summary);
const flagged = num(row?.flagged_count);
const kind = str(row?.finding_kind);
const quiet = flagged === 0 && !str(row?.summary);
const hasFindings = !!str(row?.summary);
const slice = row ? sliceOf(row) : null;
// Chart rows come from six Databricks views through the record-view API. Loaded
// once there are findings to picture; the slice keys decide which weekly line
// and which recovery breakdown are drawn.
const asOf = str(row?.review_to || row?.review_as_of).slice(0, 10) || null;
// The anchor behind "expiring within 14 days". It is the review's own
// ceiling date when the reviewer set one, otherwise the lakehouse's latest
// service date — read from the stats view, NOT from the briefing: the charts
// take seconds to load and a link rendered without the anchor would silently
// drop the deadline window and open thousands of rows instead of hundreds.
const lake = useQuery(() => client.rows('dni-rv-lakehouse-tables', { limit: 10 }), [], !asOf && hasFindings);
const anchor = asOf ?? str(lake.data?.find((r) => str(r.table_name) === 'claims')?.to_date).slice(0, 10) ?? null;
const briefing = useQuery(
() => loadBriefing(client, slice, { kind, asOf }),
[id, slice?.payer, slice?.department, slice?.code, kind, asOf],
hasFindings,
);
async function exportDeck() {
if (!row || !briefing.data) return;
setExporting(true);
try {
// pptxgenjs is ~700 KB; nobody pays for it until they ask for slides.
const { buildDeck } = await import('../lib/slides');
await buildDeck(row, briefing.data, slice, kind);
} finally {
setExporting(false);
}
}
const trace = useQuery(() => client.avaTrace(String(id)), [id], true, aiOwned ? 4000 : 0);
const decisions = useQuery(() => client.avaDecisions(String(id)), [id], true, aiOwned ? 8000 : 0);
const actions = actionsFor(state, user?.roles);
function refresh() {
cases.refetch();
trace.refetch();
decisions.refetch();
}
if (cases.loading && !cases.data) return <Spinner label="Loading case…" />;
if (cases.error) return <ErrorNote>{cases.error}</ErrorNote>;
if (!row) return <ErrorNote>Case {id} not found.</ErrorNote>;
const work = <AvaLive steps={trace.data ?? []} decisions={decisions.data ?? []} working={aiOwned} />;
return (
<div className="mx-auto max-w-5xl space-y-4">
<div className="flex flex-wrap items-center gap-3">
<Link to="/q/review" className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-ink">
<ArrowLeft className="h-4 w-4" /> Back
</Link>
<span className="tnum text-sm text-faint">Case #{str(row.instance_id)}</span>
<Badge tone={quiet ? 'slate' : (STAGE_TONE[state] ?? 'slate')} dot={aiOwned}>
{quiet ? 'No findings' : state}
</Badge>
{kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>}
<span className="ml-auto text-[11px] text-faint">
Daily review · opened {dateTime(row.created_at)}
{str(row.review_as_of) ? ` · as of ${str(row.review_as_of).slice(0, 10)}` : ''}
</span>
{hasFindings && (
<Button kind="secondary" onClick={exportDeck} busy={exporting} disabled={!briefing.data} title="Export a six-slide briefing (.pptx)">
<Download className="h-4 w-4" /> Export slides
</Button>
)}
</div>
<Rail state={state} />
{/* ---------- Ava still working: the trace is the page ---------- */}
{aiOwned && !hasFindings && (
<>
<section className="lift rounded-xl border border-ai-edge bg-ai-soft px-5 py-4">
<p className="inline-flex items-center gap-2 text-[11px] font-semibold tracking-wide text-ai uppercase">
<Brain className="h-4 w-4" /> Review in progress
</p>
<p className="mt-1.5 text-[19px] leading-snug font-semibold text-ink">
Checking all payers, departments and front desks against the 8-week baseline.
</p>
<p className="mt-1 text-sm text-muted">
Typical duration 4–5 minutes. If no change clears the materiality threshold, the case closes with no findings.
</p>
</section>
{work}
</>
)}
{/* ---------- A quiet morning ---------- */}
{quiet && (
<section className="lift rounded-xl border border-line bg-panel px-5 py-5">
<p className="inline-flex items-center gap-2 text-[11px] font-semibold tracking-wide text-faint uppercase">
<Radar className="h-4 w-4" /> Review complete
</p>
<p className="mt-1.5 text-[21px] leading-snug font-semibold text-ink">
No findings. No material change across payers, departments or front desks.
</p>
{scanned && <p className="mt-3 text-sm leading-relaxed whitespace-pre-wrap text-muted">{scanned}</p>}
</section>
)}
{/* ---------- Needs a human ---------- */}
{state === NEEDS_HUMAN && (
<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="mt-1.5 text-[17px] leading-snug font-semibold text-ink">
{prose(row.escalation_reason) || 'No reason recorded.'}
</p>
</section>
)}
{/* ---------- The findings: what happened, how much, what to approve ---------- */}
{hasFindings && (
<>
<Verdict row={row} slice={slice} kind={kind} anchor={anchor || null} />
{briefing.data ? (
<Briefing data={briefing.data} slice={slice} kind={kind} />
) : (
<BriefingSkeleton error={briefing.error} />
)}
<Recheck row={row} />
<Recommendations row={row} />
</>
)}
{/* ---------- The decision ---------- */}
{actions.length > 0 && (
<section className="lift rounded-xl border border-warn-edge bg-warn-soft px-5 py-4">
<p className="text-[11px] font-semibold tracking-wide text-warn uppercase">Review decision</p>
<div className="mt-3 flex flex-wrap items-start gap-3">
{actions.map((a) => (
<div key={a.activity}>
<Button kind={a.kind} onClick={() => setForm(a)} className="px-5 py-2.5 text-[15px]">
{a.label}
</Button>
{a.hint && <p className="mt-1 max-w-[17rem] text-[11px] text-muted">{a.hint}</p>}
</div>
))}
</div>
</section>
)}
{/* ---------- For the reader who wants to check ---------- */}
{hasFindings && (
<>
<FullStory row={row} />
<SqlEvidence row={row} steps={trace.data ?? []} decisions={decisions.data ?? []} />
{!aiOwned && (
<Disclosure
open={showWork}
onToggle={() => setShowWork(!showWork)}
icon={<Brain className="h-4 w-4 text-ai" />}
title="Activity trace"
hint="Step-by-step execution log for this review"
tone="ai"
>
<div className="p-3">{work}</div>
</Disclosure>
)}
{aiOwned && work}
</>
)}
{quiet && (
<>
<SqlEvidence row={row} steps={trace.data ?? []} decisions={decisions.data ?? []} />
<Disclosure
open={showWork}
onToggle={() => setShowWork(!showWork)}
icon={<Brain className="h-4 w-4 text-ai" />}
title="Activity trace"
hint="Step-by-step execution log — the checks performed for this no-findings result"
tone="ai"
>
<div className="p-3">{work}</div>
</Disclosure>
</>
)}
{/* ---------- Provenance ---------- */}
<section className="rounded-xl border border-line bg-panel px-5 py-4">
<Facts
cols={3}
rows={[
{ label: 'Trigger', value: str(row.review_as_of) || str(row.review_from) || str(row.review_to) ? 'Custom run (chosen dates)' : 'Daily review' },
{ label: 'Data as of', value: str(row.review_to || row.review_as_of) ? str(row.review_to || row.review_as_of).slice(0, 10) : 'Latest available' },
{
label: 'Analysis window',
value: str(row.review_from)
? `${str(row.review_from).slice(0, 10)} – ${str(row.review_to || row.review_as_of).slice(0, 10) || 'latest'}`
: row.period_weeks
? `${str(row.period_weeks)} weeks`
: '6 weeks',
},
...(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 }] : []),
]}
/>
</section>
{form && (
<ActivityForm
open
onClose={() => setForm(null)}
activityUid={form.activity}
instanceId={str(row.instance_id)}
title={form.label}
subtitle={form.hint}
submitLabel={form.label}
danger={form.kind === 'danger'}
prefill={form.activity === ACT.APPROVE ? { approval_note: defaultApprovalNote(row) } : undefined}
onDone={refresh}
/>
)}
</div>
);
}
/** The five states, as a rail. Needs Human is off the rail on purpose: it is
* not a step forward, it is the case leaving the track. */
function Rail({ state }: { state: string }) {
if (state === NEEDS_HUMAN) return null;
const at = stageStep(state);
return (
<ol className="flex flex-wrap items-center gap-1.5">
{STAGES.map((s, i) => {
const done = at > i;
const here = at === i;
return (
<li key={s} className="flex items-center gap-1.5">
<span
className={`rounded-full px-2.5 py-1 text-[11px] font-semibold ${
here ? 'bg-brand text-brand-fg' : done ? 'bg-good-soft text-good ring-1 ring-good-edge ring-inset' : 'bg-panel2 text-faint'
}`}
>
{s}
</span>
{i < STAGES.length - 1 && <span aria-hidden="true" className="h-px w-3 bg-line" />}
</li>
);
})}
</ol>
);
}
function defaultApprovalNote(row: Record<string, unknown>): string {
const recoverable = row.recoverable_amount;
return recoverable
? `Approved. Proceed with the recommended appeals and process changes. Expected recovery $${Number(recoverable).toLocaleString('en-US')}.`
: 'Approved. Proceed with the recommended actions.';
}