fix: custom run is one of two modes — replay a morning, or analyse a period
The three-date form let 'data as of' and 'window to' overlap and resolved the clash by a hidden rule. Now a two-card choice: Replay (one date; 6 weeks before it vs the 8 before those) or Period (from–to vs the 8 weeks before it). Each mode sends only its own fields; the preview names the exact periods. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
77cf90e40b
commit
f86a87af38
@ -21,14 +21,36 @@ import { shortDate } from '../format';
|
||||
* the same thing; anchored before the August spike, a run can genuinely come
|
||||
* back "nothing to report" — which is what most mornings look like.
|
||||
*/
|
||||
function ModeCard({ active, onClick, title, body }: { active: boolean; onClick: () => void; title: string; body: string }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-pressed={active}
|
||||
className={`rounded-lg border px-3.5 py-3 text-left transition-colors ${
|
||||
active ? 'border-brand bg-brand/10 ring-1 ring-brand ring-inset' : 'border-line hover:border-brand/60'
|
||||
}`}
|
||||
>
|
||||
<span className="block text-sm font-semibold text-ink">{title}</span>
|
||||
<span className="mt-1 block text-xs leading-relaxed text-muted">{body}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function RunReviewScreen() {
|
||||
const navigate = useNavigate();
|
||||
const { client } = useZino();
|
||||
const [busy, setBusy] = useState<'now' | 'custom' | null>(null);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [asOf, setAsOf] = useState('');
|
||||
const [from, setFrom] = useState('');
|
||||
const [to, setTo] = useState('');
|
||||
// Two custom runs, never mixed. "Replay" = one date, the morning to stand on;
|
||||
// "Period" = from–to, the span to analyse. Each sends only its own fields.
|
||||
const [mode, setMode] = useState<'replay' | 'period'>('replay');
|
||||
const [asOfInput, setAsOf] = useState('');
|
||||
const [fromInput, setFrom] = useState('');
|
||||
const [toInput, setTo] = useState('');
|
||||
const asOf = mode === 'replay' ? asOfInput : '';
|
||||
const from = mode === 'period' ? fromInput : '';
|
||||
const to = mode === 'period' ? toInput : '';
|
||||
|
||||
// The data's own span, so a window cannot be set where there is nothing to
|
||||
// compare: a start before the first claim leaves no baseline, and a run on
|
||||
@ -47,9 +69,9 @@ export function RunReviewScreen() {
|
||||
const baseTo = recentFrom ? addDays(recentFrom, -1) : '';
|
||||
const dateErr =
|
||||
from && effectiveTo && from > effectiveTo
|
||||
? 'The window must start before it ends.'
|
||||
: to && asOf && to > asOf
|
||||
? '"Window to" cannot be after "data as of".'
|
||||
? 'The period must start before it ends.'
|
||||
: mode === 'period' && (!from || !to)
|
||||
? null
|
||||
: dataFrom && baseFrom && baseFrom < dataFrom
|
||||
? `The 8-week baseline before ${shortDate(recentFrom)} needs data from ${shortDate(baseFrom)}, but the data begins ${shortDate(dataFrom)}. Start the window on or after ${shortDate(addDays(dataFrom, 56))}.`
|
||||
: null;
|
||||
@ -61,11 +83,10 @@ export function RunReviewScreen() {
|
||||
// Every field on the activity is optional; send only the ones the
|
||||
// reviewer set, so a plain run is a genuinely empty submission.
|
||||
const data: Record<string, string> = {};
|
||||
if (custom) {
|
||||
// "Window to" is the ceiling; "data as of" only matters when no window end is given.
|
||||
if (asOf && !to) data.review_as_of__review = asOf;
|
||||
if (from) data.review_from__review = from;
|
||||
if (to) data.review_to__review = to;
|
||||
if (custom && mode === 'replay') data.review_as_of__review = asOf;
|
||||
if (custom && mode === 'period') {
|
||||
data.review_from__review = from;
|
||||
data.review_to__review = to;
|
||||
}
|
||||
const res = await client.startInstance(WORKFLOW, ACT.REVIEW, data);
|
||||
if (res.instance_id) navigate(`/c/${res.instance_id}`);
|
||||
@ -137,30 +158,47 @@ export function RunReviewScreen() {
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-semibold text-ink">Custom run</p>
|
||||
<p className="mt-0.5 text-sm text-muted">
|
||||
Replay a past morning, or point the review at a specific period. Leave a date blank to
|
||||
use the default.
|
||||
Choose one of the two.
|
||||
{dataFrom && dataTo ? ` Data available ${shortDate(dataFrom)} – ${shortDate(dataTo)}, ${dataTo.slice(0, 4)}.` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<ModeCard
|
||||
active={mode === 'replay'}
|
||||
onClick={() => setMode('replay')}
|
||||
title="Replay a past morning"
|
||||
body="One date. The review runs as if it were that morning: the 6 weeks before it, compared with the 8 weeks before those. Anything after the date is ignored."
|
||||
/>
|
||||
<ModeCard
|
||||
active={mode === 'period'}
|
||||
onClick={() => setMode('period')}
|
||||
title="Analyse a specific period"
|
||||
body="A start and an end. That span is compared with the 8 weeks before it starts. Anything after the end is ignored."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{mode === 'replay' ? (
|
||||
<div className="mt-4 sm:max-w-xs">
|
||||
<Label>Review as of</Label>
|
||||
<Input type="date" value={asOfInput} onChange={setAsOf} />
|
||||
<p className="mt-1 text-[11px] text-faint">Try {shortDate(QUIET_MORNING_AS_OF)} for a morning with no findings.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label>Data as of</Label>
|
||||
<Input type="date" value={asOf} onChange={setAsOf} />
|
||||
<p className="mt-1 text-[11px] text-faint">Ignore anything after this date. Try {shortDate(QUIET_MORNING_AS_OF)} for a quiet morning.</p>
|
||||
<Label>From</Label>
|
||||
<Input type="date" value={fromInput} onChange={setFrom} />
|
||||
<p className="mt-1 text-[11px] text-faint">First day of the period.</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Window from</Label>
|
||||
<Input type="date" value={from} onChange={setFrom} />
|
||||
<p className="mt-1 text-[11px] text-faint">Start of the period Ava analyses.</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Window to</Label>
|
||||
<Input type="date" value={to} onChange={setTo} />
|
||||
<p className="mt-1 text-[11px] text-faint">End of the period. Defaults to "data as of".</p>
|
||||
<Label>To</Label>
|
||||
<Input type="date" value={toInput} onChange={setTo} />
|
||||
<p className="mt-1 text-[11px] text-faint">Last day. Up to 12 weeks after "From".</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-3 text-xs text-muted">
|
||||
{dateErr ? (
|
||||
@ -184,7 +222,7 @@ export function RunReviewScreen() {
|
||||
kind="secondary"
|
||||
onClick={() => run(true)}
|
||||
busy={busy === 'custom'}
|
||||
disabled={busy !== null || !!dateErr || (!asOf && !from && !to)}
|
||||
disabled={busy !== null || !!dateErr || (mode === 'replay' ? !asOf : !from || !to)}
|
||||
>
|
||||
<CalendarRange className="h-4 w-4" />
|
||||
Run with these dates
|
||||
|
||||
Loading…
Reference in New Issue
Block a user