feat: review coverage draws the comparison — this period vs compared with
REVIEWED is now key=value fields from Ava (window, baseline, counts, pending, scope, ceiling); the console renders two period tiles and one plain sentence instead of sentence-fragment chips. Older free-text rows keep the chip fallback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
400f9ec3a0
commit
245a6a8f75
@ -2,11 +2,12 @@ import { useState } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
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 type { ReviewedFacts } from '../format';
|
||||||
import { affectedClaimsHref, recoverableClaimsHref } from '../lib/claimsLink';
|
import { affectedClaimsHref, recoverableClaimsHref } from '../lib/claimsLink';
|
||||||
import type { LinkDates } 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, longDate, money, num, pct, prose, recommendations, reviewedFacts, sentences, shortDate, splitRecommendation, sqlSteps, str, workStatement } from '../format';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ava's findings, laid out for a person who has ninety seconds.
|
* Ava's findings, laid out for a person who has ninety seconds.
|
||||||
@ -95,8 +96,45 @@ export function Verdict({
|
|||||||
/** What was covered, as rows of short facts — not a paragraph. Ava writes it
|
/** What was covered, as rows of short facts — not a paragraph. Ava writes it
|
||||||
* as labelled lines; older cases wrote prose, which falls back to sentence
|
* as labelled lines; older cases wrote prose, which falls back to sentence
|
||||||
* bullets so nothing renders as a wall of text. */
|
* bullets so nothing renders as a wall of text. */
|
||||||
|
/** The comparison, drawn: this period against that one, then what was set aside. */
|
||||||
|
function Periods({ f }: { f: ReviewedFacts }) {
|
||||||
|
const span = (r: { from: string; to: string } | null) => (r ? `${longDate(r.from)} – ${longDate(r.to)}` : '—');
|
||||||
|
return (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="grid gap-px overflow-hidden rounded-lg bg-line sm:grid-cols-2">
|
||||||
|
<div className="bg-panel px-4 py-3">
|
||||||
|
<p className="text-[11px] font-semibold tracking-wide text-faint uppercase">This period</p>
|
||||||
|
<p className="mt-0.5 text-[15px] font-semibold text-ink">{span(f.window)}</p>
|
||||||
|
{f.windowClaims && <p className="tnum text-xs text-muted">{f.windowClaims} claims</p>}
|
||||||
|
</div>
|
||||||
|
<div className="bg-panel px-4 py-3">
|
||||||
|
<p className="text-[11px] font-semibold tracking-wide text-faint uppercase">Compared with</p>
|
||||||
|
<p className="mt-0.5 text-[15px] font-semibold text-ink">{span(f.baseline)}</p>
|
||||||
|
{f.baselineClaims && <p className="tnum text-xs text-muted">{f.baselineClaims} claims</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-xs text-muted">
|
||||||
|
{f.pendingExcluded && (
|
||||||
|
<>
|
||||||
|
Set aside <span className="tnum font-semibold text-ink">{f.pendingExcluded}</span> pending claims (the insurer has not answered yet)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(f.insurers || f.departments || f.desks) && (
|
||||||
|
<>
|
||||||
|
{f.pendingExcluded ? ' · ' : ''}Looked across{' '}
|
||||||
|
{[f.insurers && `${f.insurers} insurers`, f.departments && `${f.departments} departments`, f.desks && `${f.desks} desks`].filter(Boolean).join(' · ')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{f.ceiling && <> · Nothing after {longDate(f.ceiling)} was used</>}
|
||||||
|
{f.requested && <> · Requested {span(f.requested)}; the last 6 weeks of it were compared with the 8 before them</>}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function WorkStatement({ text, heading = 'Review coverage', bare = false }: { text: string; heading?: string; bare?: boolean }) {
|
function WorkStatement({ text, heading = 'Review coverage', bare = false }: { text: string; heading?: string; bare?: boolean }) {
|
||||||
const rows = workStatement(text);
|
const rows = workStatement(text);
|
||||||
|
const reviewed = rows ? reviewedFacts(rows.find((r) => r.key === 'REVIEWED')?.items ?? []) : null;
|
||||||
const TONE: Record<string, string> = {
|
const TONE: Record<string, string> = {
|
||||||
FOUND: 'text-warn', WATCHED: 'text-muted', CLEAR: 'text-good', REVIEWED: 'text-ink',
|
FOUND: 'text-warn', WATCHED: 'text-muted', CLEAR: 'text-good', REVIEWED: 'text-ink',
|
||||||
'SINCE APPROVAL': 'text-ink', ACTIONS: 'text-ink', RATE: 'text-ink', VERDICT: 'text-warn',
|
'SINCE APPROVAL': 'text-ink', ACTIONS: 'text-ink', RATE: 'text-ink', VERDICT: 'text-warn',
|
||||||
@ -104,9 +142,10 @@ function WorkStatement({ text, heading = 'Review coverage', bare = false }: { te
|
|||||||
return (
|
return (
|
||||||
<div className={bare ? '' : 'mt-3 border-t border-ai-edge/60 pt-3'}>
|
<div className={bare ? '' : 'mt-3 border-t border-ai-edge/60 pt-3'}>
|
||||||
{heading && <p className="text-[11px] font-semibold tracking-wide text-faint uppercase">{heading}</p>}
|
{heading && <p className="text-[11px] font-semibold tracking-wide text-faint uppercase">{heading}</p>}
|
||||||
|
{rows && reviewed && <Periods f={reviewed} />}
|
||||||
{rows ? (
|
{rows ? (
|
||||||
<dl className="mt-2 grid gap-y-2 text-[13px] sm:grid-cols-[9.5rem_1fr] sm:gap-x-4">
|
<dl className="mt-2 grid gap-y-2 text-[13px] sm:grid-cols-[9.5rem_1fr] sm:gap-x-4">
|
||||||
{rows.map((r) => (
|
{rows.filter((r) => !(reviewed && r.key === 'REVIEWED')).map((r) => (
|
||||||
<div key={r.key} className="contents">
|
<div key={r.key} className="contents">
|
||||||
<dt className={`font-semibold ${TONE[r.key] ?? 'text-ink'}`}>{r.label}</dt>
|
<dt className={`font-semibold ${TONE[r.key] ?? 'text-ink'}`}>{r.label}</dt>
|
||||||
<dd className="flex flex-wrap gap-x-1.5 gap-y-1">
|
<dd className="flex flex-wrap gap-x-1.5 gap-y-1">
|
||||||
|
|||||||
@ -231,6 +231,46 @@ export function workStatement(v: unknown): Array<{ key: string; label: string; i
|
|||||||
return rows.length >= 2 ? rows : null;
|
return rows.length >= 2 ? rows : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The REVIEWED row as fixed fields. Ava writes `window=2026-08-03..2026-09-13;
|
||||||
|
* window_claims=14,860; baseline=…` — this turns that into the two periods
|
||||||
|
* and the counts the console draws. Null when the row is older free text. */
|
||||||
|
export interface ReviewedFacts {
|
||||||
|
window: { from: string; to: string } | null;
|
||||||
|
windowClaims: string;
|
||||||
|
baseline: { from: string; to: string } | null;
|
||||||
|
baselineClaims: string;
|
||||||
|
pendingExcluded: string;
|
||||||
|
insurers: string;
|
||||||
|
departments: string;
|
||||||
|
desks: string;
|
||||||
|
ceiling: string | null;
|
||||||
|
requested: { from: string; to: string } | null;
|
||||||
|
}
|
||||||
|
export function reviewedFacts(items: string[]): ReviewedFacts | null {
|
||||||
|
const kv: Record<string, string> = {};
|
||||||
|
for (const it of items) {
|
||||||
|
const m = it.match(/^([a-z_]+)\s*=\s*(.+)$/i);
|
||||||
|
if (m) kv[m[1].toLowerCase()] = m[2].trim();
|
||||||
|
}
|
||||||
|
if (!kv.window && !kv.baseline) return null;
|
||||||
|
const range = (v?: string) => {
|
||||||
|
const m = v?.match(/(\d{4}-\d{2}-\d{2})\s*(?:\.\.|to|–|-)\s*(\d{4}-\d{2}-\d{2})/);
|
||||||
|
return m ? { from: m[1], to: m[2] } : null;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
window: range(kv.window),
|
||||||
|
windowClaims: kv.window_claims ?? '',
|
||||||
|
baseline: range(kv.baseline),
|
||||||
|
baselineClaims: kv.baseline_claims ?? '',
|
||||||
|
pendingExcluded: kv.pending_excluded ?? '',
|
||||||
|
insurers: kv.insurers ?? '',
|
||||||
|
departments: kv.departments ?? '',
|
||||||
|
desks: kv.desks ?? '',
|
||||||
|
ceiling: kv.ceiling ?? null,
|
||||||
|
requested: range(kv.requested),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 2026-07-26 → "Jul 26, 2026". For prose; shortDate is for axes and chips. */
|
/** 2026-07-26 → "Jul 26, 2026". For prose; shortDate is for axes and chips. */
|
||||||
export function longDate(iso: string | null): string {
|
export function longDate(iso: string | null): string {
|
||||||
if (!iso) return '';
|
if (!iso) return '';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user