import { useEffect, useMemo, useState } from 'react'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { ArrowLeft, ChevronLeft, ChevronRight, Database, Search, X } from 'lucide-react'; import { useQuery, useZino } from '../api/provider'; import { LAKEHOUSE } from '../api/config'; import type { Tone } from '../api/config'; import type { RecordFilter, Row } from '../api/types'; import { Badge, ErrorNote, Input, Select, Spinner } from '../components/core'; import { CARC, money, num, shortDate, str } from '../format'; /** * The claims table — the lakehouse rows themselves, read-only. * * Reached three ways: from the Lakehouse overview, from the sidebar, and from * a finding's numbers ("216 claims" → the exact rows Ava counted). The URL IS * the filter set, so a link can be copied into an email and lands on the same * rows. Everything is server-side: paging, filtering, search and the total * come from /view/recordview against the Databricks-backed record view, and * the dropdown options are the distinct values the platform returns for the * current filter set. */ const PAGE = 50; /** URL param → record-view column. */ const DIMS: Array<{ key: string; col: string; label: string }> = [ { key: 'payer', col: 'payer', label: 'Insurer' }, { key: 'department', col: 'department', label: 'Department' }, { key: 'status', col: 'status', label: 'Status' }, { key: 'code', col: 'denial_reason_code', label: 'Reason code' }, { key: 'appeal', col: 'appeal_status', label: 'Appeal' }, { key: 'site', col: 'site', label: 'Site' }, { key: 'desk', col: 'registration_desk', label: 'Desk' }, { key: 'provider', col: 'provider', label: 'Provider' }, { key: 'cpt', col: 'procedure_code', label: 'CPT' }, { key: 'category', col: 'denial_category', label: 'Denial category' }, ]; const RANGES: Array<{ from: string; to: string; col: string; label: string }> = [ { from: 'from', to: 'to', col: 'service_date', label: 'Service date' }, { from: 'sub_from', to: 'sub_to', col: 'submitted_date', label: 'Submitted' }, { from: 'dl_from', to: 'dl_to', col: 'appeal_deadline', label: 'Appeal deadline' }, ]; const STATUS_TONE: Record = { paid: 'emerald', partial: 'amber', denied: 'rose', pending: 'slate' }; const APPEAL_LABEL: Record = { not_filed: 'not filed', filed: 'filed', overturned: 'overturned', upheld: 'upheld' }; export function ClaimsScreen() { const { client } = useZino(); const [params] = useSearchParams(); const navigate = useNavigate(); const page = Math.max(1, Number(params.get('page') ?? '1') || 1); const search = params.get('q') ?? ''; const [draft, setDraft] = useState(search); useEffect(() => setDraft(search), [search]); const filters = useMemo(() => { const f: RecordFilter[] = []; for (const d of DIMS) { const v = params.get(d.key); if (v) f.push({ field_key: d.col, value: v, data_type: 'string' }); } for (const r of RANGES) { const a = params.get(r.from) ?? undefined; const b = params.get(r.to) ?? undefined; if (a || b) f.push({ field_key: r.col, data_type: 'date', range: { from: a, to: b } }); } return f; }, [params]); const filtered = filters.length > 0 || !!search; const key = params.toString(); // ORDER BY A UNIQUE COLUMN. Each page is its own query against Spark, and // `service_date` has thousands of ties — with a non-unique sort key the // engine is free to order tied rows differently per execution, so page 2 // repeated rows from page 1. `claim_id` is unique and is assigned in date // order, so descending is still newest-first, and every page is disjoint. const q = useQuery( () => client.recordView('dni-rv-claims', { page, limit: PAGE, filters, search, sortBy: 'claim_id', sortDir: 'desc' }), [key], ); // The platform caps its count at 10,000; the stats view has the exact // table size, which is the number to show when nothing is filtered. const stats = useQuery(() => client.rows('dni-rv-lakehouse-tables', { limit: 10 }), [], !filtered); const exact = stats.data?.find((r) => str(r.table_name) === 'claims'); const rows: Row[] = q.data?.data ?? q.data?.records ?? []; const pg = q.data?.pagination; const total = !filtered && exact ? (num(exact.row_count) ?? null) : (pg?.total_count ?? q.data?.total ?? null); const capped = filtered && !!pg?.total_is_capped; const pages = total != null ? Math.max(1, Math.ceil(total / PAGE)) : (pg?.total_pages ?? null); const options = q.data?.config?.filter_options ?? {}; const billedOnPage = rows.reduce((t, r) => t + (num(r.billed_amount) ?? 0), 0); function go(next: URLSearchParams) { next.delete('page'); navigate(`/claims${next.toString() ? `?${next}` : ''}`); } function set(k: string, v: string) { const next = new URLSearchParams(params); if (v) next.set(k, v); else next.delete(k); go(next); } function setPage(n: number) { const next = new URLSearchParams(params); 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 })); // Active filters as removable chips, in display order. const chips: Array<{ label: string; value: string; remove: string[] }> = []; for (const d of DIMS) { const v = params.get(d.key); if (v) chips.push({ label: d.label, value: d.col === 'appeal_status' ? (APPEAL_LABEL[v] ?? v) : v, remove: [d.key] }); } for (const r of RANGES) { const a = params.get(r.from); const b = params.get(r.to); if (a || b) chips.push({ label: r.label, value: a && b ? `${shortDate(a)} – ${shortDate(b)}` : a ? `from ${shortDate(a)}` : `to ${shortDate(b)}`, remove: [r.from, r.to] }); } if (search) chips.push({ label: 'Search', value: search, remove: ['q'] }); return (
Lakehouse

Claims

Read-only · {LAKEHOUSE.platform} · {LAKEHOUSE.catalog}.{LAKEHOUSE.schema}.claims

{total == null ? '—' : `${total.toLocaleString('en-US')}${capped ? '+' : ''}`}

{filtered ? 'claims match' : 'claims in the table'} {rows.length > 0 ? ` · ${money(billedOnPage)} billed on this page` : ''}

{/* ---------- Search + filters. Options are the platform's own distinct values. ---------- */}
{ e.preventDefault(); set('q', draft.trim()); }} >
set('department', v)} options={opts('department')} placeholder="All departments" /> set('code', v)} options={opts('denial_reason_code').map((o) => ({ ...o, label: CARC[o.value] ? `${o.value} · ${CARC[o.value]}` : o.value }))} placeholder="Any reason code" /> set('site', v)} options={opts('site')} placeholder="All sites" /> set('provider', v)} options={opts('provider')} placeholder="All providers" />
Service set('from', v)} />
to set('to', v)} />
{chips.length > 0 && (
{chips.map((c) => ( ))} Clear all
)}
{q.error && {q.error}}
{q.loading && !q.data ? ( ) : rows.length === 0 ? (

No claims match these filters.

) : (
{rows.map((r) => { const status = str(r.status); const code = str(r.denial_reason_code); return ( ); })}
Claim Service Insurer Department Provider Procedure Billed Status Reason Appeal Deadline
{str(r.claim_id)} {shortDate(str(r.service_date).slice(0, 10) || null)} {str(r.payer)} {str(r.department)} {str(r.provider)} {str(r.procedure_code)} {str(r.procedure_desc)} {money(r.billed_amount)} {status} {code ? ( {code} {str(r.denial_reason_desc)} ) : ( — )} {status === 'denied' ? (APPEAL_LABEL[str(r.appeal_status)] ?? str(r.appeal_status)) : —} {status === 'denied' ? shortDate(str(r.appeal_deadline).slice(0, 10) || null) : —}
)} {pages != null && pages > 1 && (
Page {page.toLocaleString('en-US')} of {pages.toLocaleString('en-US')} {capped ? ' (count capped by the platform at 10,000)' : ''} setPage(page - 1)} label="Previous page"> = pages} onClick={() => setPage(page + 1)} label="Next page">
)}
); } function Th({ children, right }: { children: React.ReactNode; right?: boolean }) { return {children}; } function Td({ children, right, mono }: { children: React.ReactNode; right?: boolean; mono?: boolean }) { return {children}; } function PageBtn({ children, disabled, onClick, label }: { children: React.ReactNode; disabled: boolean; onClick: () => void; label: string }) { return ( ); }