/** * Scenario detail — params + the latest persisted MC projection. * * Reuses FanChart from the What-If page. If the scenario has no MC run * yet, prompts the user to run /recompute. */ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Link, useNavigate, useParams } from 'react-router-dom'; import { api, lifeEventsApi, type Scenario, type SimulateRequest } from '@/api/client'; import { ApiError } from '@/api/client'; import { FanChart } from '@/components/FanChart'; import { GoalsSection } from '@/components/GoalsSection'; import { LifeEventsSection } from '@/components/LifeEventsSection'; import { gbp, pct } from '@/lib/format'; export function ScenarioDetail() { const params = useParams<{ id: string }>(); const id = Number(params.id); const navigate = useNavigate(); const qc = useQueryClient(); const scen = useQuery({ queryKey: ['scenarios', id], queryFn: () => api.scenarios.get(id), enabled: Number.isFinite(id), }); const proj = useQuery({ queryKey: ['scenarios', id, 'projection'], queryFn: () => api.scenarios.projection(id), enabled: Number.isFinite(id), retry: (count, err) => { // Don't retry the 404 — it's the "no run yet" empty state. if (err instanceof ApiError && err.status === 404) return false; return count < 2; }, }); const del = useMutation({ mutationFn: () => api.scenarios.delete(id), onSuccess: () => { qc.invalidateQueries({ queryKey: ['scenarios'] }); navigate('/scenarios'); }, }); const sim = useMutation({ mutationFn: (req: SimulateRequest) => api.simulate(req), }); const onDelete = () => { if (!confirm(`Delete scenario "${scen.data?.name ?? id}"? This can't be undone.`)) return; del.mutate(); }; const onRunNow = async (s: Scenario) => { // Pull events fresh so the run reflects whatever the user just edited. const events = await lifeEventsApi.list(s.id); sim.mutate({ jurisdiction: s.jurisdiction, strategy: s.strategy, leave_uk_year: s.leave_uk_year, spending_gbp: s.spending_gbp, nw_seed_gbp: s.nw_seed_gbp, savings_per_year_gbp: s.savings_per_year_gbp, horizon_years: s.horizon_years, n_paths: 5000, seed: 42, life_events: events.map((e) => ({ year_start: e.year_start, year_end: e.year_end, delta_gbp_per_year: e.delta_gbp_per_year, one_time_amount_gbp: e.one_time_amount_gbp, enabled: e.enabled, })), }); }; if (!Number.isFinite(id)) { return
Invalid scenario id.
; } if (scen.isLoading) returnLoading…
; if (scen.isError || !scen.data) { return ({s.kind} · {s.jurisdiction} · {s.strategy} · leave UK y{s.leave_uk_year} ·{' '} {s.glide_path} glide · {s.horizon_years}y horizon
{s.description &&{s.description}
}No projection yet.
Run python -m fire_planner recompute-all or{' '}
POST /recompute to fill in MC projections for all scenarios.
Loading projection…
) : (