fire-planner: What-If gains the chart-first scenario editor
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
The Plan-tab editors (interactive Gantt for life events, flex spending
rules) are now available in What-If too — with local state instead of
DB persistence so users can tweak before committing to a scenario.
Architecture refactor:
- EventGantt is now a controlled component. The `scenarioId` prop +
internal useMutation/useQueryClient hooks went away; the component
takes a `persister: { create, patch, delete }` prop and delegates
every mutation through it. Plan tab wires it to lifeEventsApi +
cache invalidation; What-If wires it to React local state with
negative ids for new events.
- FlexRulesEditor is similarly controlled. Takes `rules + onChange`
instead of a `scenario` object. Plan tab wraps it with the PATCH
/scenarios/:id mutation; What-If wraps it with setFlexRules.
Backend:
- New stateless POST /scenarios/spending-profile-preview endpoint
takes base_spending_gbp + horizon + life_events + flex_rules in the
body and returns the same SpendingProfileResponse shape as the
read-only /scenarios/{id}/spending-profile endpoint. Used by
What-If to render the stacked-area chart against unsaved events.
- SpendingProfileResponse.scenario_id loosened to int | None to
support the preview variant.
Frontend:
- What-If page gains `events` + `flexRules` local state, an
EventGantt + FlexRulesEditor wired through them, and a Visx
spending-profile chart fed by /spending-profile-preview.
- Sim auto-refresh: a 600ms debounced effect re-fires /simulate
whenever the form / events / flex rules change. Manual "Run
simulation" button stays as an immediate trigger.
- "Save as scenario" still works — preserves the scenario params but
not yet the life events / flex rules (a Wave-3 follow-up could
POST them after the scenario is created).
247 pytest pass; mypy + ruff + frontend typecheck/test/build all clean.
This commit is contained in:
parent
eb0dd3ddbf
commit
70101c836c
7 changed files with 372 additions and 118 deletions
|
|
@ -427,11 +427,26 @@ class SpendingProfilePoint(BaseModel):
|
|||
|
||||
|
||||
class SpendingProfileResponse(BaseModel):
|
||||
scenario_id: int
|
||||
scenario_id: int | None = None
|
||||
horizon_years: int
|
||||
points: list[SpendingProfilePoint]
|
||||
|
||||
|
||||
class SpendingProfilePreviewRequest(BaseModel):
|
||||
"""Stateless spending-profile preview — used by the What-If page where
|
||||
the user is editing in-memory life events that aren't persisted yet.
|
||||
|
||||
No scenario_id needed; the caller supplies the baseline spending,
|
||||
horizon, and any flex rules. Flex-cut estimation against a portfolio
|
||||
fan is skipped (caller already has a /simulate response to pair
|
||||
with this if they want flex visualisation).
|
||||
"""
|
||||
base_spending_gbp: Decimal = Field(ge=0)
|
||||
horizon_years: int = Field(ge=1, le=100)
|
||||
life_events: list[LifeEventInput] = Field(default_factory=list)
|
||||
flex_rules: list[FlexRule] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SimulateRequest(BaseModel):
|
||||
"""Sync, non-persisted simulate. Used by the React UI for what-if.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue