From 91002b11c435d093afad2c5046759b0ef6dd76b8 Mon Sep 17 00:00:00 2001 From: Yashas Date: Thu, 17 Sep 2026 11:39:52 +0530 Subject: [PATCH] =?UTF-8?q?fix:=20deep=20links=20reproduce=20Ava's=20count?= =?UTF-8?q?s=20=E2=80=94=20slice=20identity,=20window=20ceiling,=20cohort?= =?UTF-8?q?=20dates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/api/charts.ts | 10 ++++-- src/components/Briefing.tsx | 2 +- src/components/Findings.tsx | 11 ++++--- src/format.ts | 8 +++-- src/lib/claimsLink.ts | 61 +++++++++++++++++++++++------------- src/lib/slides.ts | 2 +- src/screens/CaseScreen.tsx | 12 +++++-- src/screens/ClaimsScreen.tsx | 5 ++- 8 files changed, 74 insertions(+), 37 deletions(-) diff --git a/src/api/charts.ts b/src/api/charts.ts index 8334590..87984f5 100644 --- a/src/api/charts.ts +++ b/src/api/charts.ts @@ -38,6 +38,9 @@ export interface Slice { payer: string; department: string; code: string; + site: string; + desk: string; + provider: string; /** ISO date of the week the change started, if Ava named one. */ spikeStart: string | null; } @@ -46,8 +49,11 @@ export function sliceOf(row: Row): Slice | null { const payer = str(row.slice_payer); const department = str(row.slice_department); const code = str(row.slice_reason_code); - if (!payer && !department && !code) return null; - return { payer, department, code, spikeStart: str(row.spike_start).slice(0, 10) || null }; + const site = str(row.slice_site); + 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 { diff --git a/src/components/Briefing.tsx b/src/components/Briefing.tsx index 186f9dd..bfd00c9 100644 --- a/src/components/Briefing.tsx +++ b/src/components/Briefing.tsx @@ -98,7 +98,7 @@ export function BriefingSkeleton({ error }: { error?: string | null }) { export function Briefing({ data, slice, kind = '' }: { data: BriefingData; slice: Slice | null; kind?: string }) { const { theme } = useTheme(); 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); // A replayed morning: the lakehouse views are anchored on the data's LATEST diff --git a/src/components/Findings.tsx b/src/components/Findings.tsx index ae5c5b0..e34e4c1 100644 --- a/src/components/Findings.tsx +++ b/src/components/Findings.tsx @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'; import { AlertTriangle, CheckCircle2, ChevronDown, ExternalLink, FileSearch } from 'lucide-react'; import type { Slice } from '../api/charts'; import { affectedClaimsHref, recoverableClaimsHref } from '../lib/claimsLink'; +import type { LinkDates } from '../lib/claimsLink'; import { Badge, Card, Facts } from './core'; import type { AvaDecision, AvaTraceStep, Row } from '../api/types'; import { firstDeadline, headline, money, num, pct, prose, recommendations, sentences, shortDate, splitRecommendation, sqlSteps, str, workStatement } from '../format'; @@ -26,23 +27,23 @@ export function Verdict({ row, slice = null, kind = '', - anchor = null, + dates = { anchor: null, ceiling: null, submittedTo: null }, }: { row: Row; slice?: Slice | null; kind?: string; - anchor?: string | null; + dates?: LinkDates; }) { const summary = str(row.summary); if (!summary) return null; const line = prose(headline(summary)); const scanned = prose(row.scanned_summary); - const affectedHref = affectedClaimsHref(kind, slice, anchor); - const recoverableHref = recoverableClaimsHref(kind, slice, anchor); + const affectedHref = affectedClaimsHref(kind, slice, dates); + const recoverableHref = recoverableClaimsHref(kind, slice, dates); const atRisk = num(row.amount_at_risk); const recoverable = num(row.recoverable_amount); const affected = num(row.affected_claims); - const deadline = firstDeadline(row.recommendations); + const deadline = firstDeadline(row.recommendations, dates.anchor); const confidence = num(row.confidence); const reconciled = row.data_reconciled === true || str(row.data_reconciled) === 'true'; diff --git a/src/format.ts b/src/format.ts index 0779d21..6924813 100644 --- a/src/format.ts +++ b/src/format.ts @@ -176,10 +176,12 @@ export function headline(summary: unknown, maxChars = 220): string { /** The earliest date Ava mentioned in her recommendations — usually the first * appeal deadline. Null if she didn't name one. */ -export function firstDeadline(recs: unknown): string | null { - const m = str(recs).match(/\b(2026-\d{2}-\d{2})\b/g); +export function firstDeadline(recs: unknown, notBefore: string | null = null): string | null { + const m = str(recs).match(/\b(20\d{2}-\d{2}-\d{2})\b/g); 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; } diff --git a/src/lib/claimsLink.ts b/src/lib/claimsLink.ts index 6e8a441..6344518 100644 --- a/src/lib/claimsLink.ts +++ b/src/lib/claimsLink.ts @@ -7,10 +7,23 @@ import { addDays, isDenialKind } from '../api/charts'; * Ava writes "216 claims" and "172 still appealable"; these build the * /claims URL whose filters reproduce those rows. Every filter here is a * column the record view declares (11_claims_explorer.sql), and the date - * bounds compile to inclusive ranges on the lakehouse. The anchor is the - * data's latest date (or the replay date) — without it we still link, just - * without the "inside the window" bound. + * bounds compile to inclusive ranges on the lakehouse. + * + * 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 { s.background = { color: 'FFFFFF' }; diff --git a/src/screens/CaseScreen.tsx b/src/screens/CaseScreen.tsx index d81d119..1554388 100644 --- a/src/screens/CaseScreen.tsx +++ b/src/screens/CaseScreen.tsx @@ -74,7 +74,13 @@ export function CaseScreen() { // 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 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( () => loadBriefing(client, slice, { kind, asOf }), [id, slice?.payer, slice?.department, slice?.code, kind, asOf], @@ -122,7 +128,7 @@ export function CaseScreen() { {kind && FINDING_KIND[kind] && {FINDING_KIND[kind]}} - 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)}` : ''} {hasFindings && ( @@ -178,7 +184,7 @@ export function CaseScreen() { {/* ---------- The findings: what happened, how much, what to approve ---------- */} {hasFindings && ( <> - + {briefing.data ? ( ) : ( diff --git a/src/screens/ClaimsScreen.tsx b/src/screens/ClaimsScreen.tsx index 6c3cfd0..2af93f2 100644 --- a/src/screens/ClaimsScreen.tsx +++ b/src/screens/ClaimsScreen.tsx @@ -106,6 +106,9 @@ export function ClaimsScreen() { if (n <= 1) next.delete('page'); else next.set('page', String(n)); 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) => ((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 ? (

No claims match these filters.

) : ( -
+