Compare commits
2 Commits
1c1c120202
...
76e2eddd05
| Author | SHA1 | Date | |
|---|---|---|---|
| 76e2eddd05 | |||
| b2377b4645 |
@ -4,7 +4,7 @@ import { Shell } from './layout/Shell';
|
|||||||
import { Login } from './pages/Login';
|
import { Login } from './pages/Login';
|
||||||
import { QueueScreen } from './screens/QueueScreen';
|
import { QueueScreen } from './screens/QueueScreen';
|
||||||
import { CaseScreen } from './screens/CaseScreen';
|
import { CaseScreen } from './screens/CaseScreen';
|
||||||
import { NewInvestigationScreen } from './screens/NewInvestigationScreen';
|
import { RunReviewScreen } from './screens/RunReviewScreen';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routes.
|
* Routes.
|
||||||
@ -24,7 +24,7 @@ export function App() {
|
|||||||
<Route element={<Shell />}>
|
<Route element={<Shell />}>
|
||||||
<Route path="/q/:q" element={<QueueScreen />} />
|
<Route path="/q/:q" element={<QueueScreen />} />
|
||||||
<Route path="/c/:id" element={<CaseScreen />} />
|
<Route path="/c/:id" element={<CaseScreen />} />
|
||||||
<Route path="/new" element={<NewInvestigationScreen />} />
|
<Route path="/review" element={<RunReviewScreen />} />
|
||||||
{/* Awaiting Review is the landing page, not "all cases": it is the
|
{/* Awaiting Review is the landing page, not "all cases": it is the
|
||||||
queue with work in it, and the one the console exists to serve. */}
|
queue with work in it, and the one the console exists to serve. */}
|
||||||
<Route path="/" element={<Navigate to="/q/review" replace />} />
|
<Route path="/" element={<Navigate to="/q/review" replace />} />
|
||||||
|
|||||||
@ -30,8 +30,9 @@ export const DECISIONS_VIEW = 'dni-rv-ava-decisions';
|
|||||||
// hardcoded: each activity's fields are read from view-service at open time.
|
// hardcoded: each activity's fields are read from view-service at open time.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
export const ACT = {
|
export const ACT = {
|
||||||
START: 'dni-act-start', // INIT
|
REVIEW: 'dni-act-review', // INIT — the daily sweep. The ONLY way a case is born.
|
||||||
FINDINGS: 'dni-act-findings', // Ava
|
FINDINGS: 'dni-act-findings', // Ava
|
||||||
|
NOTHING: 'dni-act-nothing', // Ava — a quiet morning; closes the case itself
|
||||||
APPROVE: 'dni-act-approve',
|
APPROVE: 'dni-act-approve',
|
||||||
CHANGES: 'dni-act-changes',
|
CHANGES: 'dni-act-changes',
|
||||||
REJECT: 'dni-act-reject',
|
REJECT: 'dni-act-reject',
|
||||||
@ -133,7 +134,9 @@ export const STAGE_ACTIONS: Record<string, Action[]> = {
|
|||||||
label: 'Send Back to Ava',
|
label: 'Send Back to Ava',
|
||||||
roles: ['revenue_cycle_manager'],
|
roles: ['revenue_cycle_manager'],
|
||||||
kind: 'secondary',
|
kind: 'secondary',
|
||||||
hint: 'She re-runs the investigation with your note',
|
// The only way a person asks Ava anything: in the context of something
|
||||||
|
// she already found. There is deliberately no "ask a question" door.
|
||||||
|
hint: 'She re-runs the review with your note',
|
||||||
},
|
},
|
||||||
{ activity: ACT.REJECT, label: 'Reject', roles: ['revenue_cycle_manager'], kind: 'danger' },
|
{ activity: ACT.REJECT, label: 'Reject', roles: ['revenue_cycle_manager'], kind: 'danger' },
|
||||||
],
|
],
|
||||||
@ -187,3 +190,25 @@ export const LAKEHOUSE = {
|
|||||||
schema: 'rcm',
|
schema: 'rcm',
|
||||||
tables: ['claims', 'payers', 'departments', 'payer_policy_updates', 'finance_monthly_summary'],
|
tables: ['claims', 'payers', 'departments', 'payer_policy_updates', 'finance_monthly_summary'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sweep's finding, as Ava classifies it. Shown as a chip so the queue reads
|
||||||
|
* "what kind of problem" without opening the case.
|
||||||
|
*/
|
||||||
|
export const FINDING_KIND: Record<string, string> = {
|
||||||
|
payer_policy: 'Payer rule change',
|
||||||
|
front_desk: 'Front-desk / registration',
|
||||||
|
coding: 'Coding',
|
||||||
|
documentation: 'Documentation',
|
||||||
|
medical_necessity: 'Medical necessity',
|
||||||
|
timely_filing: 'Timely filing',
|
||||||
|
expiring_appeals: 'Appeals about to expire',
|
||||||
|
other: 'Other',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The demo's quiet-morning replay. The data is static, so every sweep finds the
|
||||||
|
* same August spike; anchoring a run on this date lets Ava genuinely answer
|
||||||
|
* "nothing to report" — one quiet morning next to one $1.09M morning.
|
||||||
|
*/
|
||||||
|
export const QUIET_MORNING_AS_OF = '2026-07-26';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
|
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
|
||||||
import { LogOut, Search } from 'lucide-react';
|
import { LogOut, Radar } from 'lucide-react';
|
||||||
import { useZino } from '../api/provider';
|
import { useZino } from '../api/provider';
|
||||||
import { QUEUES, prettyRole } from '../api/config';
|
import { QUEUES, prettyRole } from '../api/config';
|
||||||
import { NorthlakeLockup } from '../components/NorthlakeLogo';
|
import { NorthlakeLockup } from '../components/NorthlakeLogo';
|
||||||
@ -29,9 +29,11 @@ export function Shell() {
|
|||||||
<NorthlakeLockup />
|
<NorthlakeLockup />
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-3">
|
<div className="ml-auto flex items-center gap-3">
|
||||||
<Button kind="primary" onClick={() => navigate('/new')}>
|
{/* The only way a case is born. Nobody asks Ava a question; she
|
||||||
<Search className="h-4 w-4" />
|
decides what is worth a person's time. */}
|
||||||
Ask Ava
|
<Button kind="primary" onClick={() => navigate('/review')}>
|
||||||
|
<Radar className="h-4 w-4" />
|
||||||
|
Run daily review
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import { ArrowLeft, Brain, Clock, User } from 'lucide-react';
|
import { ArrowLeft, Brain, Clock, Radar, User } from 'lucide-react';
|
||||||
import { useQuery, useZino } from '../api/provider';
|
import { useQuery, useZino } from '../api/provider';
|
||||||
import {
|
import {
|
||||||
ACT,
|
ACT,
|
||||||
|
FINDING_KIND,
|
||||||
NEEDS_HUMAN,
|
NEEDS_HUMAN,
|
||||||
RECORD_VIEW,
|
RECORD_VIEW,
|
||||||
STAGES,
|
STAGES,
|
||||||
@ -16,7 +17,7 @@ import { ActivityForm } from '../components/ActivityForm';
|
|||||||
import { AvaLive } from '../components/AvaLive';
|
import { AvaLive } from '../components/AvaLive';
|
||||||
import { Findings, Recheck, SqlEvidence } from '../components/Findings';
|
import { Findings, Recheck, SqlEvidence } from '../components/Findings';
|
||||||
import type { Action } from '../api/config';
|
import type { Action } from '../api/config';
|
||||||
import { dateTime, str } from '../format';
|
import { dateTime, num, str } from '../format';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One investigation, end to end.
|
* One investigation, end to end.
|
||||||
@ -50,6 +51,10 @@ export function CaseScreen() {
|
|||||||
const row = cases.data?.[0];
|
const row = cases.data?.[0];
|
||||||
const state = str(row?.current_state_name);
|
const state = str(row?.current_state_name);
|
||||||
const aiOwned = state === 'Investigating' || state === 'Monitoring';
|
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 trace = useQuery(() => client.avaTrace(String(id)), [id], true, aiOwned ? 4000 : 0);
|
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 decisions = useQuery(() => client.avaDecisions(String(id)), [id], true, aiOwned ? 8000 : 0);
|
||||||
@ -81,9 +86,10 @@ export function CaseScreen() {
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<h1 className="tnum text-lg font-semibold text-ink">Case #{str(row.instance_id)}</h1>
|
<h1 className="tnum text-lg font-semibold text-ink">Case #{str(row.instance_id)}</h1>
|
||||||
<Badge tone={STAGE_TONE[state] ?? 'slate'} dot={aiOwned}>
|
<Badge tone={quiet ? 'slate' : (STAGE_TONE[state] ?? 'slate')} dot={aiOwned}>
|
||||||
{state}
|
{quiet ? 'Nothing to report' : state}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>}
|
||||||
{aiOwned ? (
|
{aiOwned ? (
|
||||||
<span className="inline-flex items-center gap-1 text-xs text-ai">
|
<span className="inline-flex items-center gap-1 text-xs text-ai">
|
||||||
<Brain className="h-3.5 w-3.5" /> Ava is working
|
<Brain className="h-3.5 w-3.5" /> Ava is working
|
||||||
@ -95,7 +101,9 @@ export function CaseScreen() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1.5 max-w-2xl text-sm text-muted">
|
<p className="mt-1.5 max-w-2xl text-sm text-muted">
|
||||||
{str(row.question) || `Scope: ${str(row.scope) || 'all payers and departments'}`}
|
Daily review — every payer, department and front desk
|
||||||
|
{str(row.review_as_of) ? `, as of ${str(row.review_as_of).slice(0, 10)}` : ''}.
|
||||||
|
Nobody asked; Ava decides what is worth your time.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -129,6 +137,28 @@ export function CaseScreen() {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* ---------------- what the sweep covered ---------------- */}
|
||||||
|
{(scanned || flagged != null) && (
|
||||||
|
<section
|
||||||
|
className={`lift rounded-lg border px-4 py-3.5 ${
|
||||||
|
quiet ? 'border-line bg-panel' : 'border-ai-edge bg-ai-soft'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Radar className={`h-4 w-4 ${quiet ? 'text-muted' : 'text-ai'}`} />
|
||||||
|
<h2 className={`text-sm font-semibold ${quiet ? 'text-ink' : 'text-ai'}`}>
|
||||||
|
{quiet ? 'Reviewed. Nothing to report.' : `Reviewed. Flagged ${flagged ?? 1}.`}
|
||||||
|
</h2>
|
||||||
|
<span className="ml-auto text-[11px] text-faint">
|
||||||
|
6 payers · 8 departments · 9 desks · every week in the data
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{scanned && (
|
||||||
|
<p className="mt-2 text-sm leading-relaxed whitespace-pre-wrap text-muted">{scanned}</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ---------------- her conclusions ---------------- */}
|
{/* ---------------- her conclusions ---------------- */}
|
||||||
<Findings row={row} />
|
<Findings row={row} />
|
||||||
<Recheck row={row} />
|
<Recheck row={row} />
|
||||||
@ -138,15 +168,13 @@ export function CaseScreen() {
|
|||||||
<AvaLive steps={trace.data ?? []} decisions={decisions.data ?? []} working={aiOwned} />
|
<AvaLive steps={trace.data ?? []} decisions={decisions.data ?? []} working={aiOwned} />
|
||||||
|
|
||||||
{/* ---------------- the ask, in full ---------------- */}
|
{/* ---------------- the ask, in full ---------------- */}
|
||||||
<Card title="The request" subtitle="What was handed to Ava">
|
<Card title="This review" subtitle="How the case came to exist">
|
||||||
<div className="px-4 py-4">
|
<div className="px-4 py-4">
|
||||||
<Facts
|
<Facts
|
||||||
rows={[
|
rows={[
|
||||||
{ label: 'Scope', value: str(row.scope) || '—' },
|
{ label: 'Started by', value: str(row.review_as_of) ? 'Replay of a past morning' : 'Daily review' },
|
||||||
{ label: 'Payer', value: str(row.payer) || 'All' },
|
{ label: 'Anchored on', value: str(row.review_as_of) ? str(row.review_as_of).slice(0, 10) : 'Latest data' },
|
||||||
{ label: 'Department', value: str(row.department) || 'All' },
|
{ label: 'Look-back', value: row.period_weeks ? `${str(row.period_weeks)} weeks` : '6 weeks' },
|
||||||
{ label: 'Look-back', value: row.period_weeks ? `${str(row.period_weeks)} weeks` : '—' },
|
|
||||||
{ label: 'Question', value: str(row.question) || '—', wide: true },
|
|
||||||
...(str(row.review_note)
|
...(str(row.review_note)
|
||||||
? [{ label: 'Sent back with', value: str(row.review_note), wide: true }]
|
? [{ label: 'Sent back with', value: str(row.review_note), wide: true }]
|
||||||
: []),
|
: []),
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { Brain, Database } from 'lucide-react';
|
|
||||||
import { ActivityForm } from '../components/ActivityForm';
|
|
||||||
import { Card } from '../components/core';
|
|
||||||
import { ACT, LAKEHOUSE } from '../api/config';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ask Ava a question.
|
|
||||||
*
|
|
||||||
* The form is the INIT activity's own schema, read live from view-service — so
|
|
||||||
* the scope picker, the payer list and the look-back are whatever the workflow
|
|
||||||
* says they are, not a copy maintained here.
|
|
||||||
*
|
|
||||||
* The page around the form exists to set the expectation: the manager is not
|
|
||||||
* running a report, he is handing work to an analyst who will take a few
|
|
||||||
* minutes and come back with a recommendation.
|
|
||||||
*/
|
|
||||||
export function NewInvestigationScreen() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto max-w-2xl space-y-4">
|
|
||||||
<header>
|
|
||||||
<h1 className="text-xl font-semibold text-ink">Ask Ava</h1>
|
|
||||||
<p className="mt-0.5 text-sm text-muted">
|
|
||||||
Hand her a question about denials. She investigates and comes back with a cause, a
|
|
||||||
dollar figure and a recommendation for you to approve.
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<Card className="px-4 py-4">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<span className="rounded-full bg-ai-soft p-2 text-ai ring-1 ring-ai-edge ring-inset">
|
|
||||||
<Brain className="h-4 w-4" />
|
|
||||||
</span>
|
|
||||||
<div className="text-sm text-muted">
|
|
||||||
<p className="font-semibold text-ink">What she will do</p>
|
|
||||||
<ol className="mt-1.5 list-decimal space-y-0.5 pl-4">
|
|
||||||
<li>Reconcile the claims data against the finance report before trusting it</li>
|
|
||||||
<li>Baseline the denial rate, then slice by payer, reason code, department and desk</li>
|
|
||||||
<li>Find where the change concentrates and when it started</li>
|
|
||||||
<li>Look for the cause in the payer's own policy bulletins</li>
|
|
||||||
<li>Count what is still inside the appeal window and what it is worth</li>
|
|
||||||
</ol>
|
|
||||||
<p className="mt-2.5 inline-flex items-center gap-1.5 text-xs text-faint">
|
|
||||||
<Database className="h-3.5 w-3.5" />
|
|
||||||
Read-only against {LAKEHOUSE.platform} · {LAKEHOUSE.catalog}.{LAKEHOUSE.schema}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<ActivityForm
|
|
||||||
open={open}
|
|
||||||
onClose={() => {
|
|
||||||
setOpen(false);
|
|
||||||
navigate('/q/investigating');
|
|
||||||
}}
|
|
||||||
activityUid={ACT.START}
|
|
||||||
title="Start an investigation"
|
|
||||||
subtitle="Ava picks this up as soon as you submit"
|
|
||||||
submitLabel="Send to Ava"
|
|
||||||
onDone={(r) => {
|
|
||||||
if (r.instance_id) navigate(`/c/${r.instance_id}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Brain, Clock, User } from 'lucide-react';
|
import { Brain, Clock, User } from 'lucide-react';
|
||||||
import { useQuery, useZino } from '../api/provider';
|
import { useQuery, useZino } from '../api/provider';
|
||||||
import { QUEUES, RECORD_VIEW, STAGE_TONE } from '../api/config';
|
import { FINDING_KIND, QUEUES, RECORD_VIEW, STAGE_TONE } from '../api/config';
|
||||||
import { Badge, Card, Empty, ErrorNote, Spinner } from '../components/core';
|
import { Badge, Card, Empty, ErrorNote, Spinner } from '../components/core';
|
||||||
import type { Row } from '../api/types';
|
import type { Row } from '../api/types';
|
||||||
import { dateTime, money, num, str } from '../format';
|
import { dateTime, money, num, str } from '../format';
|
||||||
@ -79,9 +79,12 @@ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
|
|||||||
const summary = str(row.summary);
|
const summary = str(row.summary);
|
||||||
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 scope = str(row.scope);
|
|
||||||
const question = str(row.question);
|
|
||||||
const aiOwned = state === 'Investigating' || state === 'Monitoring';
|
const aiOwned = state === 'Investigating' || state === 'Monitoring';
|
||||||
|
const scanned = str(row.scanned_summary);
|
||||||
|
const flagged = num(row.flagged_count);
|
||||||
|
const kind = str(row.finding_kind);
|
||||||
|
// A sweep that found nothing: closed by Ava herself, never on a desk.
|
||||||
|
const quiet = flagged === 0 && !summary;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="cursor-pointer transition-colors hover:border-brand/50" >
|
<Card className="cursor-pointer transition-colors hover:border-brand/50" >
|
||||||
@ -90,9 +93,10 @@ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
|
|||||||
<span className="tnum text-[13px] font-semibold text-ink">
|
<span className="tnum text-[13px] font-semibold text-ink">
|
||||||
Case #{str(row.instance_id)}
|
Case #{str(row.instance_id)}
|
||||||
</span>
|
</span>
|
||||||
<Badge tone={tone} dot={aiOwned}>
|
<Badge tone={quiet ? 'slate' : tone} dot={aiOwned}>
|
||||||
{state}
|
{quiet ? 'Nothing to report' : state}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>}
|
||||||
{aiOwned ? (
|
{aiOwned ? (
|
||||||
<span className="inline-flex items-center gap-1 text-[11px] text-ai">
|
<span className="inline-flex items-center gap-1 text-[11px] text-ai">
|
||||||
<Brain className="h-3 w-3" /> Ava
|
<Brain className="h-3 w-3" /> Ava
|
||||||
@ -109,7 +113,7 @@ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-2 line-clamp-2 text-sm text-ink">
|
<p className="mt-2 line-clamp-2 text-sm text-ink">
|
||||||
{summary || question || `Scope: ${scope || 'all payers'}`}
|
{summary || (quiet ? scanned : '') || 'Ava is reviewing every payer, department and desk…'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{(atRisk || recoverable) && (
|
{(atRisk || recoverable) && (
|
||||||
|
|||||||
104
src/screens/RunReviewScreen.tsx
Normal file
104
src/screens/RunReviewScreen.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Brain, Database, Moon, Radar } from 'lucide-react';
|
||||||
|
import { useZino } from '../api/provider';
|
||||||
|
import { ACT, LAKEHOUSE, QUIET_MORNING_AS_OF, WORKFLOW } from '../api/config';
|
||||||
|
import { Button, Card, ErrorNote } from '../components/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the daily review, now.
|
||||||
|
*
|
||||||
|
* There is no form. This is the same activity a scheduler performs at 6am;
|
||||||
|
* the button exists so a room does not have to wait until 6am. Nothing is
|
||||||
|
* asked of Ava — she decides what, if anything, is worth a person's time.
|
||||||
|
*
|
||||||
|
* The second button anchors the sweep on a date before the August spike. The
|
||||||
|
* demo data is static, so without it every run flags the same thing; with it,
|
||||||
|
* one run can genuinely come back "nothing to report" — which is what most
|
||||||
|
* mornings look like, and the contrast is the point.
|
||||||
|
*/
|
||||||
|
export function RunReviewScreen() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { client } = useZino();
|
||||||
|
const [busy, setBusy] = useState<'now' | 'quiet' | null>(null);
|
||||||
|
const [err, setErr] = useState<string | null>(null);
|
||||||
|
|
||||||
|
async function run(asOf?: string) {
|
||||||
|
setBusy(asOf ? 'quiet' : 'now');
|
||||||
|
setErr(null);
|
||||||
|
try {
|
||||||
|
// The activity's one field is optional; omit it entirely for a live run
|
||||||
|
// so the workflow sees a genuinely empty submission.
|
||||||
|
const res = await client.startInstance(
|
||||||
|
WORKFLOW,
|
||||||
|
ACT.REVIEW,
|
||||||
|
asOf ? { review_as_of__review: asOf } : {},
|
||||||
|
);
|
||||||
|
if (res.instance_id) navigate(`/c/${res.instance_id}`);
|
||||||
|
else navigate('/q/investigating');
|
||||||
|
} catch (e) {
|
||||||
|
setErr((e as { message?: string })?.message ?? 'Could not start the review');
|
||||||
|
setBusy(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-2xl space-y-4">
|
||||||
|
<header>
|
||||||
|
<h1 className="text-xl font-semibold text-ink">Daily review</h1>
|
||||||
|
<p className="mt-0.5 text-sm text-muted">
|
||||||
|
Ava looks at every payer, department and front desk, and only opens a case if
|
||||||
|
something clears the bar. Most mornings she reports nothing.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{err && <ErrorNote>{err}</ErrorNote>}
|
||||||
|
|
||||||
|
<Card className="px-4 py-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="rounded-full bg-ai-soft p-2 text-ai ring-1 ring-ai-edge ring-inset">
|
||||||
|
<Brain className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 flex-1 text-sm text-muted">
|
||||||
|
<p className="font-semibold text-ink">What she checks</p>
|
||||||
|
<ul className="mt-1.5 list-disc space-y-0.5 pl-4">
|
||||||
|
<li>That the claims data reconciles with the finance report</li>
|
||||||
|
<li>Denial rate vs baseline — by payer, reason code, department, front desk, week</li>
|
||||||
|
<li>Money about to fall out of an appeal window unfiled</li>
|
||||||
|
</ul>
|
||||||
|
<p className="mt-2 text-xs text-faint">
|
||||||
|
The bar: a denied count up more than 40% on meaningful volume, or more than $100K
|
||||||
|
expiring. Below it, she watches and says nothing.
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 inline-flex items-center gap-1.5 text-xs text-faint">
|
||||||
|
<Database className="h-3.5 w-3.5" />
|
||||||
|
Read-only against {LAKEHOUSE.platform} · {LAKEHOUSE.catalog}.{LAKEHOUSE.schema}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 flex flex-wrap items-start gap-3 border-t border-line pt-4">
|
||||||
|
<div>
|
||||||
|
<Button kind="primary" onClick={() => run()} busy={busy === 'now'} disabled={busy !== null}>
|
||||||
|
<Radar className="h-4 w-4" />
|
||||||
|
Run the review now
|
||||||
|
</Button>
|
||||||
|
<p className="mt-1 max-w-[16rem] text-[11px] text-faint">
|
||||||
|
Normally fires at 6am on its own. This is the same activity.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button kind="secondary" onClick={() => run(QUIET_MORNING_AS_OF)} busy={busy === 'quiet'} disabled={busy !== null}>
|
||||||
|
<Moon className="h-4 w-4" />
|
||||||
|
Replay a quiet morning
|
||||||
|
</Button>
|
||||||
|
<p className="mt-1 max-w-[16rem] text-[11px] text-faint">
|
||||||
|
Anchored on {QUIET_MORNING_AS_OF}, before anything went wrong — to see what
|
||||||
|
“nothing to report” looks like.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user