refactor: one front door
Start Investigation is gone. The daily review is the only way a case is born — "she isn't asked" should not ship with a button for asking her. A person still steers Ava, but only about something she already found: Send Back to Ava carries the note. Removed the ask-a-question screen, route and ghost button; the case page's "request" card now says how the review came to exist (live, or a replay of a past morning) instead of echoing a question nobody typed. Backend counterpart: sm2/custom-apps/denials-investigator/09_one_front_door.sql. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
b2377b4645
commit
76e2eddd05
@ -4,7 +4,6 @@ import { Shell } from './layout/Shell';
|
||||
import { Login } from './pages/Login';
|
||||
import { QueueScreen } from './screens/QueueScreen';
|
||||
import { CaseScreen } from './screens/CaseScreen';
|
||||
import { NewInvestigationScreen } from './screens/NewInvestigationScreen';
|
||||
import { RunReviewScreen } from './screens/RunReviewScreen';
|
||||
|
||||
/**
|
||||
@ -25,7 +24,6 @@ export function App() {
|
||||
<Route element={<Shell />}>
|
||||
<Route path="/q/:q" element={<QueueScreen />} />
|
||||
<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
|
||||
queue with work in it, and the one the console exists to serve. */}
|
||||
|
||||
@ -30,8 +30,7 @@ export const DECISIONS_VIEW = 'dni-rv-ava-decisions';
|
||||
// hardcoded: each activity's fields are read from view-service at open time.
|
||||
// ---------------------------------------------------------------------------
|
||||
export const ACT = {
|
||||
REVIEW: 'dni-act-review', // INIT — the daily sweep. No question, no form.
|
||||
START: 'dni-act-start', // INIT — a human asks a specific question
|
||||
REVIEW: 'dni-act-review', // INIT — the daily sweep. The ONLY way a case is born.
|
||||
FINDINGS: 'dni-act-findings', // Ava
|
||||
NOTHING: 'dni-act-nothing', // Ava — a quiet morning; closes the case itself
|
||||
APPROVE: 'dni-act-approve',
|
||||
@ -135,7 +134,9 @@ export const STAGE_ACTIONS: Record<string, Action[]> = {
|
||||
label: 'Send Back to Ava',
|
||||
roles: ['revenue_cycle_manager'],
|
||||
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' },
|
||||
],
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
|
||||
import { LogOut, Radar, Search } from 'lucide-react';
|
||||
import { LogOut, Radar } from 'lucide-react';
|
||||
import { useZino } from '../api/provider';
|
||||
import { QUEUES, prettyRole } from '../api/config';
|
||||
import { NorthlakeLockup } from '../components/NorthlakeLogo';
|
||||
@ -29,15 +29,12 @@ export function Shell() {
|
||||
<NorthlakeLockup />
|
||||
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
{/* The sweep is the product; the question is the exception. */}
|
||||
{/* The only way a case is born. Nobody asks Ava a question; she
|
||||
decides what is worth a person's time. */}
|
||||
<Button kind="primary" onClick={() => navigate('/review')}>
|
||||
<Radar className="h-4 w-4" />
|
||||
Run daily review
|
||||
</Button>
|
||||
<Button kind="ghost" onClick={() => navigate('/new')} title="Ask Ava a specific question">
|
||||
<Search className="h-4 w-4" />
|
||||
Ask a question
|
||||
</Button>
|
||||
|
||||
{user && (
|
||||
<div className="hidden text-right sm:block">
|
||||
|
||||
@ -54,7 +54,6 @@ export function CaseScreen() {
|
||||
const scanned = str(row?.scanned_summary);
|
||||
const flagged = num(row?.flagged_count);
|
||||
const kind = str(row?.finding_kind);
|
||||
const isSweep = scanned !== '' || flagged != null || !str(row?.question);
|
||||
const quiet = flagged === 0 && !str(row?.summary);
|
||||
|
||||
const trace = useQuery(() => client.avaTrace(String(id)), [id], true, aiOwned ? 4000 : 0);
|
||||
@ -102,12 +101,9 @@ export function CaseScreen() {
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1.5 max-w-2xl text-sm text-muted">
|
||||
{str(row.question) ||
|
||||
(isSweep
|
||||
? `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.`
|
||||
: `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>
|
||||
</div>
|
||||
|
||||
@ -172,15 +168,13 @@ export function CaseScreen() {
|
||||
<AvaLive steps={trace.data ?? []} decisions={decisions.data ?? []} working={aiOwned} />
|
||||
|
||||
{/* ---------------- 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">
|
||||
<Facts
|
||||
rows={[
|
||||
{ label: 'Scope', value: str(row.scope) || '—' },
|
||||
{ label: 'Payer', value: str(row.payer) || 'All' },
|
||||
{ label: 'Department', value: str(row.department) || 'All' },
|
||||
{ label: 'Look-back', value: row.period_weeks ? `${str(row.period_weeks)} weeks` : '—' },
|
||||
{ label: 'Question', value: str(row.question) || '—', wide: true },
|
||||
{ label: 'Started by', value: str(row.review_as_of) ? 'Replay of a past morning' : 'Daily review' },
|
||||
{ label: 'Anchored on', value: str(row.review_as_of) ? str(row.review_as_of).slice(0, 10) : 'Latest data' },
|
||||
{ label: 'Look-back', value: row.period_weeks ? `${str(row.period_weeks)} weeks` : '6 weeks' },
|
||||
...(str(row.review_note)
|
||||
? [{ 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>
|
||||
);
|
||||
}
|
||||
@ -79,15 +79,12 @@ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
|
||||
const summary = str(row.summary);
|
||||
const atRisk = num(row.amount_at_risk);
|
||||
const recoverable = num(row.recoverable_amount);
|
||||
const scope = str(row.scope);
|
||||
const question = str(row.question);
|
||||
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;
|
||||
const isSweep = scanned !== '' || flagged != null;
|
||||
|
||||
return (
|
||||
<Card className="cursor-pointer transition-colors hover:border-brand/50" >
|
||||
@ -99,7 +96,6 @@ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
|
||||
<Badge tone={quiet ? 'slate' : tone} dot={aiOwned}>
|
||||
{quiet ? 'Nothing to report' : state}
|
||||
</Badge>
|
||||
{isSweep && <Badge tone="slate">daily review</Badge>}
|
||||
{kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>}
|
||||
{aiOwned ? (
|
||||
<span className="inline-flex items-center gap-1 text-[11px] text-ai">
|
||||
@ -117,10 +113,7 @@ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
|
||||
</div>
|
||||
|
||||
<p className="mt-2 line-clamp-2 text-sm text-ink">
|
||||
{summary ||
|
||||
(quiet ? scanned : '') ||
|
||||
question ||
|
||||
(isSweep ? 'Ava is reviewing every payer, department and desk…' : `Scope: ${scope || 'all payers'}`)}
|
||||
{summary || (quiet ? scanned : '') || 'Ava is reviewing every payer, department and desk…'}
|
||||
</p>
|
||||
|
||||
{(atRisk || recoverable) && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user