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
|
* the same thing; anchored before the August spike, a run can genuinely come
|
||||||
* back "nothing to report" — which is what most mornings look like.
|
* 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() {
|
export function RunReviewScreen() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { client } = useZino();
|
const { client } = useZino();
|
||||||
const [busy, setBusy] = useState<'now' | 'custom' | null>(null);
|
const [busy, setBusy] = useState<'now' | 'custom' | null>(null);
|
||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
const [asOf, setAsOf] = useState('');
|
// Two custom runs, never mixed. "Replay" = one date, the morning to stand on;
|
||||||
const [from, setFrom] = useState('');
|
// "Period" = from–to, the span to analyse. Each sends only its own fields.
|
||||||
const [to, setTo] = useState('');
|
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
|
// 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
|
// 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 baseTo = recentFrom ? addDays(recentFrom, -1) : '';
|
||||||
const dateErr =
|
const dateErr =
|
||||||
from && effectiveTo && from > effectiveTo
|
from && effectiveTo && from > effectiveTo
|
||||||
? 'The window must start before it ends.'
|
? 'The period must start before it ends.'
|
||||||
: to && asOf && to > asOf
|
: mode === 'period' && (!from || !to)
|
||||||
? '"Window to" cannot be after "data as of".'
|
? null
|
||||||
: dataFrom && baseFrom && baseFrom < dataFrom
|
: 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))}.`
|
? `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;
|
: null;
|
||||||
@ -61,11 +83,10 @@ export function RunReviewScreen() {
|
|||||||
// Every field on the activity is optional; send only the ones the
|
// Every field on the activity is optional; send only the ones the
|
||||||
// reviewer set, so a plain run is a genuinely empty submission.
|
// reviewer set, so a plain run is a genuinely empty submission.
|
||||||
const data: Record<string, string> = {};
|
const data: Record<string, string> = {};
|
||||||
if (custom) {
|
if (custom && mode === 'replay') data.review_as_of__review = asOf;
|
||||||
// "Window to" is the ceiling; "data as of" only matters when no window end is given.
|
if (custom && mode === 'period') {
|
||||||
if (asOf && !to) data.review_as_of__review = asOf;
|
data.review_from__review = from;
|
||||||
if (from) data.review_from__review = from;
|
data.review_to__review = to;
|
||||||
if (to) data.review_to__review = to;
|
|
||||||
}
|
}
|
||||||
const res = await client.startInstance(WORKFLOW, ACT.REVIEW, data);
|
const res = await client.startInstance(WORKFLOW, ACT.REVIEW, data);
|
||||||
if (res.instance_id) navigate(`/c/${res.instance_id}`);
|
if (res.instance_id) navigate(`/c/${res.instance_id}`);
|
||||||
@ -137,31 +158,48 @@ export function RunReviewScreen() {
|
|||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="font-semibold text-ink">Custom run</p>
|
<p className="font-semibold text-ink">Custom run</p>
|
||||||
<p className="mt-0.5 text-sm text-muted">
|
<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
|
Choose one of the two.
|
||||||
use the default.
|
|
||||||
{dataFrom && dataTo ? ` Data available ${shortDate(dataFrom)} – ${shortDate(dataTo)}, ${dataTo.slice(0, 4)}.` : ''}
|
{dataFrom && dataTo ? ` Data available ${shortDate(dataFrom)} – ${shortDate(dataTo)}, ${dataTo.slice(0, 4)}.` : ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||||
<div>
|
<ModeCard
|
||||||
<Label>Data as of</Label>
|
active={mode === 'replay'}
|
||||||
<Input type="date" value={asOf} onChange={setAsOf} />
|
onClick={() => setMode('replay')}
|
||||||
<p className="mt-1 text-[11px] text-faint">Ignore anything after this date. Try {shortDate(QUIET_MORNING_AS_OF)} for a quiet morning.</p>
|
title="Replay a past morning"
|
||||||
</div>
|
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."
|
||||||
<div>
|
/>
|
||||||
<Label>Window from</Label>
|
<ModeCard
|
||||||
<Input type="date" value={from} onChange={setFrom} />
|
active={mode === 'period'}
|
||||||
<p className="mt-1 text-[11px] text-faint">Start of the period Ava analyses.</p>
|
onClick={() => setMode('period')}
|
||||||
</div>
|
title="Analyse a specific period"
|
||||||
<div>
|
body="A start and an end. That span is compared with the 8 weeks before it starts. Anything after the end is ignored."
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>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>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">
|
<p className="mt-3 text-xs text-muted">
|
||||||
{dateErr ? (
|
{dateErr ? (
|
||||||
<span className="text-bad">{dateErr}</span>
|
<span className="text-bad">{dateErr}</span>
|
||||||
@ -184,7 +222,7 @@ export function RunReviewScreen() {
|
|||||||
kind="secondary"
|
kind="secondary"
|
||||||
onClick={() => run(true)}
|
onClick={() => run(true)}
|
||||||
busy={busy === 'custom'}
|
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" />
|
<CalendarRange className="h-4 w-4" />
|
||||||
Run with these dates
|
Run with these dates
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user