fix: deep links reproduce Ava's counts — slice identity, window ceiling, cohort dates

- slice now carries site / desk / provider (14_slice_dims.sql), so a front-desk
  or provider finding opens the right rows, not every CO-27 in the hospital
- every link is capped at the review's ceiling date (review_to / as-of); a
  custom-window run no longer opens claims Ava never looked at
- slow-payer link filters to claims SENT 31–60 days before the latest submitted
  date (from the stats view), matching her 1,352 instead of all 2,753 pending
- 'first deadline' ignores dates before the review (it was picking the spike week)
- header says 'Custom run' when dates were chosen; page changes scroll to top
  and dim the table while loading

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-09-17 11:39:52 +05:30
parent b4bff014b3
commit 91002b11c4
8 changed files with 74 additions and 37 deletions

View File

@ -38,6 +38,9 @@ export interface Slice {
payer: string; payer: string;
department: string; department: string;
code: string; code: string;
site: string;
desk: string;
provider: string;
/** ISO date of the week the change started, if Ava named one. */ /** ISO date of the week the change started, if Ava named one. */
spikeStart: string | null; spikeStart: string | null;
} }
@ -46,8 +49,11 @@ export function sliceOf(row: Row): Slice | null {
const payer = str(row.slice_payer); const payer = str(row.slice_payer);
const department = str(row.slice_department); const department = str(row.slice_department);
const code = str(row.slice_reason_code); const code = str(row.slice_reason_code);
if (!payer && !department && !code) return null; const site = str(row.slice_site);
return { payer, department, code, spikeStart: str(row.spike_start).slice(0, 10) || null }; const desk = str(row.slice_desk);
const provider = str(row.slice_provider);
if (!payer && !department && !code && !site && !desk && !provider) return null;
return { payer, department, code, site, desk, provider, spikeStart: str(row.spike_start).slice(0, 10) || null };
} }
export interface WeekPoint { export interface WeekPoint {

View File

@ -98,7 +98,7 @@ export function BriefingSkeleton({ error }: { error?: string | null }) {
export function Briefing({ data, slice, kind = '' }: { data: BriefingData; slice: Slice | null; kind?: string }) { export function Briefing({ data, slice, kind = '' }: { data: BriefingData; slice: Slice | null; kind?: string }) {
const { theme } = useTheme(); const { theme } = useTheme();
const c = HUES[theme]; const c = HUES[theme];
const title = slice ? [slice.payer, slice.department, slice.code].filter(Boolean).join(' · ') : 'the hospital'; const title = slice ? [slice.payer, slice.department, slice.provider, slice.desk && `desk ${slice.desk}`, slice.code].filter(Boolean).join(' · ') : 'the hospital';
const denial = isDenialKind(kind); const denial = isDenialKind(kind);
// A replayed morning: the lakehouse views are anchored on the data's LATEST // A replayed morning: the lakehouse views are anchored on the data's LATEST

View File

@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { AlertTriangle, CheckCircle2, ChevronDown, ExternalLink, FileSearch } from 'lucide-react'; import { AlertTriangle, CheckCircle2, ChevronDown, ExternalLink, FileSearch } from 'lucide-react';
import type { Slice } from '../api/charts'; import type { Slice } from '../api/charts';
import { affectedClaimsHref, recoverableClaimsHref } from '../lib/claimsLink'; import { affectedClaimsHref, recoverableClaimsHref } from '../lib/claimsLink';
import type { LinkDates } 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 { firstDeadline, headline, money, num, pct, prose, recommendations, sentences, shortDate, splitRecommendation, sqlSteps, str, workStatement } from '../format'; import { firstDeadline, headline, money, num, pct, prose, recommendations, sentences, shortDate, splitRecommendation, sqlSteps, str, workStatement } from '../format';
@ -26,23 +27,23 @@ export function Verdict({
row, row,
slice = null, slice = null,
kind = '', kind = '',
anchor = null, dates = { anchor: null, ceiling: null, submittedTo: null },
}: { }: {
row: Row; row: Row;
slice?: Slice | null; slice?: Slice | null;
kind?: string; kind?: string;
anchor?: string | null; dates?: LinkDates;
}) { }) {
const summary = str(row.summary); const summary = str(row.summary);
if (!summary) return null; if (!summary) return null;
const line = prose(headline(summary)); const line = prose(headline(summary));
const scanned = prose(row.scanned_summary); const scanned = prose(row.scanned_summary);
const affectedHref = affectedClaimsHref(kind, slice, anchor); const affectedHref = affectedClaimsHref(kind, slice, dates);
const recoverableHref = recoverableClaimsHref(kind, slice, anchor); const recoverableHref = recoverableClaimsHref(kind, slice, dates);
const atRisk = num(row.amount_at_risk); const atRisk = num(row.amount_at_risk);
const recoverable = num(row.recoverable_amount); const recoverable = num(row.recoverable_amount);
const affected = num(row.affected_claims); const affected = num(row.affected_claims);
const deadline = firstDeadline(row.recommendations); const deadline = firstDeadline(row.recommendations, dates.anchor);
const confidence = num(row.confidence); const confidence = num(row.confidence);
const reconciled = row.data_reconciled === true || str(row.data_reconciled) === 'true'; const reconciled = row.data_reconciled === true || str(row.data_reconciled) === 'true';

View File

@ -176,10 +176,12 @@ export function headline(summary: unknown, maxChars = 220): string {
/** The earliest date Ava mentioned in her recommendations — usually the first /** The earliest date Ava mentioned in her recommendations — usually the first
* appeal deadline. Null if she didn't name one. */ * appeal deadline. Null if she didn't name one. */
export function firstDeadline(recs: unknown): string | null { export function firstDeadline(recs: unknown, notBefore: string | null = null): string | null {
const m = str(recs).match(/\b(2026-\d{2}-\d{2})\b/g); const m = str(recs).match(/\b(20\d{2}-\d{2}-\d{2})\b/g);
if (!m) return null; if (!m) return null;
const sorted = [...new Set(m)].sort(); // A recommendation also names past dates ("since 2026-07-06"); a deadline
// is the earliest date that is still ahead of the review.
const sorted = [...new Set(m)].filter((d) => !notBefore || d >= notBefore).sort();
return sorted[0] ?? null; return sorted[0] ?? null;
} }

View File

@ -7,10 +7,23 @@ import { addDays, isDenialKind } from '../api/charts';
* Ava writes "216 claims" and "172 still appealable"; these build the * Ava writes "216 claims" and "172 still appealable"; these build the
* /claims URL whose filters reproduce those rows. Every filter here is a * /claims URL whose filters reproduce those rows. Every filter here is a
* column the record view declares (11_claims_explorer.sql), and the date * column the record view declares (11_claims_explorer.sql), and the date
* bounds compile to inclusive ranges on the lakehouse. The anchor is the * bounds compile to inclusive ranges on the lakehouse.
* data's latest date (or the replay date) — without it we still link, just *
* without the "inside the window" bound. * Dates come from the case, not from the charts:
* ceiling review_to / review_as_of — the last day Ava looked at. When
* set, every link is capped there, or the count grows past hers.
* anchor the ceiling, else the lakehouse's latest service date; the
* appeal-window bound ("still inside the window", "expiring
* within 14 days").
* submittedTo the ceiling, else the latest submitted_date; the slow-payer
* cohort is claims SENT 31–60 days before it.
*/ */
export interface LinkDates {
anchor: string | null;
ceiling: string | null;
submittedTo: string | null;
}
export type ClaimsQuery = Partial<Record< export type ClaimsQuery = Partial<Record<
'payer' | 'department' | 'code' | 'category' | 'status' | 'appeal' | 'site' | 'desk' | 'provider' | 'cpt' 'payer' | 'department' | 'code' | 'category' | 'status' | 'appeal' | 'site' | 'desk' | 'provider' | 'cpt'
| 'from' | 'to' | 'sub_from' | 'sub_to' | 'dl_from' | 'dl_to' | 'page', | 'from' | 'to' | 'sub_from' | 'sub_to' | 'dl_from' | 'dl_to' | 'page',
@ -24,42 +37,48 @@ export function claimsHref(q: ClaimsQuery): string {
return s ? `/claims?${s}` : '/claims'; return s ? `/claims?${s}` : '/claims';
} }
/** The identity part of a slice — everything Ava named. */
function who(slice: Slice): ClaimsQuery {
return {
payer: slice.payer || undefined,
department: slice.department || undefined,
code: slice.code || undefined,
site: slice.site || undefined,
desk: slice.desk || undefined,
provider: slice.provider || undefined,
};
}
/** The rows behind "claims denied" / "affected". */ /** The rows behind "claims denied" / "affected". */
export function affectedClaimsHref(kind: string, slice: Slice | null, anchor: string | null): string | null { export function affectedClaimsHref(kind: string, slice: Slice | null, d: LinkDates): string | null {
if (kind === 'expiring_appeals') { if (kind === 'expiring_appeals') {
// Without the anchor the 14-day window cannot be expressed, and the link // Without the anchor the 14-day window cannot be expressed, and the link
// would open every unfiled denial ever. Show no link instead. // would open every unfiled denial ever. Show no link instead.
if (!anchor) return null; if (!d.anchor) return null;
return claimsHref({ status: 'denied', appeal: 'not_filed', dl_from: anchor, dl_to: addDays(anchor, 14) }); return claimsHref({ status: 'denied', appeal: 'not_filed', dl_from: d.anchor, dl_to: addDays(d.anchor, 14) });
} }
if (kind === 'slow_payer') { if (kind === 'slow_payer') {
return slice?.payer ? claimsHref({ payer: slice.payer, status: 'pending' }) : null; if (!slice?.payer || !d.submittedTo) return null;
return claimsHref({ payer: slice.payer, status: 'pending', sub_from: addDays(d.submittedTo, -60), sub_to: addDays(d.submittedTo, -31) });
} }
if (kind === 'underpayment') { if (kind === 'underpayment') {
return slice?.payer ? claimsHref({ payer: slice.payer, status: 'partial', from: slice.spikeStart ?? undefined }) : null; return slice?.payer ? claimsHref({ payer: slice.payer, status: 'partial', from: slice.spikeStart ?? undefined, to: d.ceiling ?? undefined }) : null;
} }
if (!slice || !isDenialKind(kind)) return null; if (!slice || !isDenialKind(kind)) return null;
return claimsHref({ return claimsHref({ ...who(slice), status: 'denied', from: slice.spikeStart ?? undefined, to: d.ceiling ?? undefined });
payer: slice.payer || undefined,
department: slice.department || undefined,
code: slice.code || undefined,
status: 'denied',
from: slice.spikeStart ?? undefined,
});
} }
/** The rows behind "still recoverable": the affected set, unfiled and inside its window. */ /** The rows behind "still recoverable": the affected set, unfiled and inside its window. */
export function recoverableClaimsHref(kind: string, slice: Slice | null, anchor: string | null): string | null { export function recoverableClaimsHref(kind: string, slice: Slice | null, d: LinkDates): string | null {
if (kind === 'expiring_appeals') return affectedClaimsHref(kind, slice, anchor); if (kind === 'expiring_appeals') return affectedClaimsHref(kind, slice, d);
if (kind === 'slow_payer' || kind === 'underpayment') return null; if (kind === 'slow_payer' || kind === 'underpayment') return null;
if (!slice || !isDenialKind(kind)) return null; if (!slice || !isDenialKind(kind)) return null;
return claimsHref({ return claimsHref({
payer: slice.payer || undefined, ...who(slice),
department: slice.department || undefined,
code: slice.code || undefined,
status: 'denied', status: 'denied',
appeal: 'not_filed', appeal: 'not_filed',
from: slice.spikeStart ?? undefined, from: slice.spikeStart ?? undefined,
dl_from: anchor ?? undefined, to: d.ceiling ?? undefined,
dl_from: d.anchor ?? undefined,
}); });
} }

View File

@ -50,7 +50,7 @@ export async function buildDeck(row: Row, data: BriefingData, slice: Slice | nul
pres.title = `Denials briefing — case #${str(row.instance_id)}`; pres.title = `Denials briefing — case #${str(row.instance_id)}`;
const caseNo = 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 sliceTitle = slice ? [slice.payer, slice.department, slice.provider, slice.desk && `desk ${slice.desk}`, slice.code].filter(Boolean).join(' · ') : 'Northlake Health';
const chrome = (s: pptxgen.Slide, title: string, kicker?: string) => { const chrome = (s: pptxgen.Slide, title: string, kicker?: string) => {
s.background = { color: 'FFFFFF' }; s.background = { color: 'FFFFFF' };

View File

@ -74,7 +74,13 @@ export function CaseScreen() {
// take seconds to load and a link rendered without the anchor would silently // 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. // 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 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 claimsStats = lake.data?.find((r) => str(r.table_name) === 'claims');
const linkDates = {
ceiling: asOf,
anchor: asOf ?? (str(claimsStats?.to_date).slice(0, 10) || null),
submittedTo: asOf ?? (str(claimsStats?.submitted_to).slice(0, 10) || null),
};
const customRun = !!(str(row?.review_as_of) || str(row?.review_from) || str(row?.review_to));
const briefing = useQuery( const briefing = useQuery(
() => loadBriefing(client, slice, { kind, asOf }), () => loadBriefing(client, slice, { kind, asOf }),
[id, slice?.payer, slice?.department, slice?.code, kind, asOf], [id, slice?.payer, slice?.department, slice?.code, kind, asOf],
@ -122,7 +128,7 @@ export function CaseScreen() {
</Badge> </Badge>
{kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>} {kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>}
<span className="ml-auto text-[11px] text-faint"> <span className="ml-auto text-[11px] text-faint">
Daily review · opened {dateTime(row.created_at)} {customRun ? 'Custom run' : 'Daily review'} · opened {dateTime(row.created_at)}
{str(row.review_as_of) ? ` · as of ${str(row.review_as_of).slice(0, 10)}` : ''} {str(row.review_as_of) ? ` · as of ${str(row.review_as_of).slice(0, 10)}` : ''}
</span> </span>
{hasFindings && ( {hasFindings && (
@ -178,7 +184,7 @@ export function CaseScreen() {
{/* ---------- The findings: what happened, how much, what to approve ---------- */} {/* ---------- The findings: what happened, how much, what to approve ---------- */}
{hasFindings && ( {hasFindings && (
<> <>
<Verdict row={row} slice={slice} kind={kind} anchor={anchor || null} /> <Verdict row={row} slice={slice} kind={kind} dates={linkDates} />
{briefing.data ? ( {briefing.data ? (
<Briefing data={briefing.data} slice={slice} kind={kind} /> <Briefing data={briefing.data} slice={slice} kind={kind} />
) : ( ) : (

View File

@ -106,6 +106,9 @@ export function ClaimsScreen() {
if (n <= 1) next.delete('page'); if (n <= 1) next.delete('page');
else next.set('page', String(n)); else next.set('page', String(n));
navigate(`/claims?${next}`); navigate(`/claims?${next}`);
// The pager sits under the table; without this the new page loads with the
// viewport still at the bottom, which reads as "nothing changed".
window.scrollTo({ top: 0, behavior: 'smooth' });
} }
const opts = (col: string, labels?: Record<string, string>) => const opts = (col: string, labels?: Record<string, string>) =>
((options[col] ?? []) as unknown[]).map(String).filter(Boolean).map((v) => ({ value: v, label: labels?.[v] ?? v })); ((options[col] ?? []) as unknown[]).map(String).filter(Boolean).map((v) => ({ value: v, label: labels?.[v] ?? v }));
@ -223,7 +226,7 @@ export function ClaimsScreen() {
) : rows.length === 0 ? ( ) : rows.length === 0 ? (
<p className="px-5 py-10 text-center text-sm text-faint">No claims match these filters.</p> <p className="px-5 py-10 text-center text-sm text-faint">No claims match these filters.</p>
) : ( ) : (
<div className="overflow-x-auto"> <div className={`overflow-x-auto transition-opacity ${q.loading ? 'opacity-50' : ''}`} aria-busy={q.loading}>
<table className="w-full text-[13px]"> <table className="w-full text-[13px]">
<thead> <thead>
<tr className="text-left text-[11px] font-semibold tracking-wide text-faint uppercase"> <tr className="text-left text-[11px] font-semibold tracking-wide text-faint uppercase">