You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a recurring "Plan volgend seizoen" wizard to fdm-app that lets a farmer + advisor set
up an upcoming season — the target year is resolved dynamically, not fixed to currentYear + 1:
it's the earliest year from currentYear onward that still has an incomplete plan, so the
wizard works whether it's run on schedule in December, catching up in January/February, or even
much later in the season for a farmer who's behind — by cloning the last fully-planned season
and editing only what changes. It guides the user through fields, cultivations,
vanggewas/groenbemester, measures, the fertilizer plan and harvest estimates, while reducing
emissions, achieving the nutrient advice, and staying within legal limits. Today fdm-app only
has a one-time farm-creation wizard; this issue covers a repeatable yearly planning flow that
stays usable at any point in the year, every year going forward.
Motivation
Every winter the farmer and advisor rebuild next season's plan from scratch. Most of it is unchanged from last year. We want: "last year, minus the
re-typing, plus advice." Prefill everything from the prior season; surface rotation,
catch-crop and emission advice inline; flag legal-norm breaches; keep the user in control.
Not every farmer plans on time — some only get to it in January, February, or later — so the
wizard must recognize "the season I still need to plan" rather than assuming it's always run in
the preceding winter.
Scope (this release)
Target year is resolved dynamically, not hardcoded: starting from currentYear, the
wizard finds the earliest year that (a) has a prior year with clonable data and (b) doesn't
yet have a fully completed plan (per the same derived-progress rules used for resuming — see
"Progress, resume & pre-existing data"). In the common case that's currentYear + 1 when run
in the preceding winter; if the farmer is late and hasn't planned currentYear itself yet,
the wizard targets currentYear. The wizard is reusable indefinitely, at any time of year.
Clone-from-previous-fully-planned-year starting point; timeframe-based, no new "season" entity.
7-step linear wizard mirroring the create-farm pattern.
Measures (maatregelen) carried into the target year are reviewed: ongoing ones carry automatically, ones expiring before the target year require an explicit renew-or-lapse decision, and measures on removed fields are closed out.
Progress is derived from existing target-year data (no new progress-tracking entity); leaving
the wizard halfway and returning resumes at the first incomplete step, with completed steps
editable via back-navigation. See "Progress, resume & pre-existing data" below.
Running the wizard well into the target year (e.g. in June for a season that should have been
planned in the preceding winter) is supported, not blocked — a soft "je bent laat" banner may
be shown, but the wizard remains fully functional so a late farmer can still catch up.
Out of scope: multi-year (i.e. planning more than one year ahead in one run), non-NL regions, mobile-specific layout; auto-linking catch-crop selections to eco-regeling Measure records (Measures and catch crops stay independent this release).
Why it fits the codebase
2027 (or whichever year the target-year resolution picks) is already selectable: fdm-app/app/lib/calendar.ts (yearEnd = currentYear + 1); the dynamic resolution described above is a new small helper built on top of this and the progress-derivation logic (both need to exist before the "which year?" question can be answered).
Wizards are URL-driven multi-step routes with per-step DB save + breadcrumb header
(farm.create.*, header/create-farm.tsx) — mirror as farm.$b_id_farm.plan-next.*.
Clone via fdm-core: getCultivationPlan, getDefaultDatesOfCultivation(..., targetYear), addField/addCultivation/addFertilizerApplication/addHarvest, b_lu_croprotation.
Measures step reuses: fdm-core's getMeasuresForFarm, updateMeasure, and field-end-based
auto-close of ongoing measures; fdm-app's components/blocks/measures/* (table, columns,
add-measure-dialog) already used on farm.$b_id_farm.$calendar.measures.*.
User flow (happy path)
Dashboard card "Plan volgend seizoen ({target year})" → ① clone → ② confirm fields → ③ accept
rotation suggestions → ④ catch crops → ⑤ review/renew maatregelen → ⑥ keep last-year fertilizer
(or Gerrit) → ⑦ confirm yields → done. Every step persists; user can drop out and resume via
sidebar, and reopening later jumps to the first incomplete step — regardless of the actual
calendar date, whether that's December, February, or mid-season.
Wizard steps
Start — recap the last fully-planned year, one CTA to copy → the resolved target year.
Percelen — keep/remove carried fields, add new (atlas/RVO). Remove sets b_end and
auto-closes (m_end) any ongoing measures on that field.
Gewassen — grass continues; arable suggested from b_lu_croprotation history.
Vanggewas/groenbemester — toggle + species per arable field; verplicht on zand/NV-gebied; compliance bar appears.
Maatregelen — per carried field, list measures grouped by status: ongoing
(m_end = null, carries automatically, no action needed), expiring before the target year
(prompts the user to renew/extend into the target year or explicitly let lapse), already
ended (informational). Fields removed in step 2 show their measures as closed, non-editable
here. Adding a brand new measure for the target year in this step is a nice-to-have, not
required for this release.
Bemesting — copy-last-year tab + Gerrit tab; live N/manure/phosphate fill %. Norm values
(gebruiksnormen/derogation limits) use the newest year createFunctionsForNorms supports at
or before the target year — falling back to the last available year's norms with a clear
"voorlopig, gebaseerd op normen {last year}" label if the target year's norms aren't
published yet. Dates (default cultivation/application windows, catch-crop deadlines) always
use the real target year regardless of this fallback.
Since there is deliberately no new "season"/wizard-progress entity, step completion is derived from the target year's data itself, per step:
Percelen — done once every carried field has an explicit keep/remove decision (no field
is left in an "undecided" state) and any newly added fields are saved.
Gewassen — done once every field kept for the target year has a cultivation recorded for it.
Vanggewas/groenbemester — done once every arable field has an explicit catch-crop
toggle decision (on + species, or explicitly off) recorded.
Maatregelen — done once every field's measures have no pending expiring-before-target-year
decisions left unresolved.
Bemesting — done once every field has a fertilizer plan for the target year (copied or
Gerrit-generated).
Oogst & overzicht — done once yield estimates are confirmed; this is also the wizard's
completion signal.
This same per-step "done" data is also what resolves which year the wizard targets (see
Scope): starting at currentYear, the first year that has a clonable prior year but isn't yet
fully done across all 7 steps is the target year. No separate "which season am I planning"
flag is stored anywhere.
Behavior built on top of this:
Resuming: reopening the wizard (from the dashboard card or sidebar) jumps straight to the first step that isn't fully done per the rules above. Already-completed steps remain
reachable via back-navigation and are editable, matching the create-farm wizard's pattern.
Reusable at any time, every year: because both progress and the target year itself are
derived from data rather than a stored "which year/which step" flag, the same detection logic
keeps working whether the wizard is opened on schedule in December, catching up in
January/February, or picked up again mid-season — and whether it's a farm's first "plan
volgend seizoen" run or its fifth.
Late-running farms: if the resolved target year's timeframe is already substantially
underway (e.g. well past the typical planning window), the wizard still functions fully; a
soft, non-blocking "je bent laat" banner may be shown on the Start step as a nice-to-have.
Pre-existing data: if target-year data already exists before the wizard is opened (e.g. a
field, measure, or cultivation was added manually via the regular field/measures pages before
the user ever started the wizard), the wizard detects and reconciles it: that item is
shown as already-done/pre-filled in the relevant step (not duplicated or re-created), and the
user can edit it like any wizard-created item. There is no special "wizard-owned vs.
manually-added" flag — a field/measure/cultivation dated within the next-year timeframe counts
the same regardless of how it was created.
Entry point visibility: the dashboard entry point appears whenever a resolvable target
year exists (a clonable prior year plus an incomplete plan for some year >= currentYear);
it's hidden if there's nothing left to plan (e.g. the wizard was already fully completed for currentYear + 1 well ahead of schedule, since planning further ahead is out of scope).
Fertilizer plan follows the crop, not the field
Applications attach to a field, but the plan is crop-driven:
Crop unchanged → per-field copy of last year's plan (soil/NV-gebied differences stay).
Crop changed → discard old doses; seed from the new crop's template (same crop on another field, else nutrient advice). Potato doses never carry to wheat.
Crop-template overlay aligns fields sharing a crop; per-field overrides preserved; norms recompute per field.
Acceptance criteria
Entry point on farm dashboard + sidebar, visible whenever a resolvable target year exists (clonable prior year + an incomplete plan for currentYear or currentYear + 1).
Cloning the prior fully-planned year prefills fields, cultivations, catch crops, last-year fertilizer for the resolved target year (target year is dynamically resolved — currentYear if not yet planned, otherwise currentYear + 1 — never hardcoded to a specific year).
Fields step: keep/remove/add; removal closes tenure, no data loss to the prior year.
Cultivations step: grass continues; arable gets rotation suggestion; default dates resolve to the target year.
Catch-crop step adds catchcrop cultivations; mandatory flag on zand/NV-gebied.
Maatregelen step lists ongoing/expiring/ended measures per carried field.
Removing a field in the Percelen step auto-closes its ongoing measures (no orphaned open-ended measures on a closed field).
Measures expiring in the current year require an explicit renew-or-lapse decision before the field is considered done in this step (or are clearly flagged as pending in the final summary).
Renewed measures appear correctly in the BLN3/indicators view for the target year.
Fertilizer: unchanged crop copies per field; changed crop reseeds from template/advice; live compliance bar; over-norm flagged but overridable; Gerrit optional.
If the target year's gebruiksnormen aren't published yet, norm values fall back to the last available year (clearly labeled as provisional); date/timeframe-specific functions (default dates, application windows, catch-crop deadlines) still use the real target year, never the fallback year.
Harvest step prefills yields; summary shows legal limits + emission vs the prior year.
All steps persist; resumable at the first incomplete step (derived from data, no new progress entity); completed steps remain editable via back-navigation.
Data added for the target year outside the wizard (before or between wizard sessions) is detected and shown as pre-filled/done in the relevant step, not duplicated.
Reopening the wizard later — whether that's the next winter, or in January/February/mid-season of a target year that's still incomplete — resolves the correct target year each time, with no code change required per year and no distinction in behavior based on the calendar date.
pnpm typecheck passes.
Risks / open questions
Extend createFunctionsForNorms to each new target year as it becomes relevant (currently 2025/2026). Fallback strategy when the target year's gebruiksnormen aren't published yet: norm values (nitrogen/manure/phosphate gebruiksnormen, derogation limits) fall back to the last available year's norms (i.e. createFunctionsForNorms resolves to the newest supported year ≤ target year), so the compliance bar and dose advice still work, clearly labeled as provisional/based on last year's norms. Timeframe-specific functions (e.g. getDefaultDatesOfCultivation, application-period windows, catch-crop sowing/termination deadlines) always use the real target year regardless of norm-value availability — dates must never silently fall back to the prior year.
Gerrit slow (≈20-min cap) → copy-last-year is the default, Gerrit opt-in.
Confirm catch-crop legal obligations and rotation rules per target year (rules may change year to year).
Confirm whether the BLN eco-regeling measures catalogue itself needs a revision for the new target year (new m_ids, changed conflicts) before this ships, since getMeasuresFromCatalogue is not year-scoped.
Derived-progress detection (no dedicated progress table) must correctly distinguish "not started" vs. "intentionally skipped" per step (e.g. a field with no catch crop because it's not mandatory there) — needs careful per-step "done" predicates so the wizard doesn't get stuck re-prompting for already-decided items. This same logic now also determines which year to target, so it's on the critical path before any step UI can be built.
Define the "je bent laat" banner threshold (e.g. days/weeks into the target year) and copy — currently just a nice-to-have, not a hard requirement, but worth a product decision before implementation.
Summary
Add a recurring "Plan volgend seizoen" wizard to fdm-app that lets a farmer + advisor set
up an upcoming season — the target year is resolved dynamically, not fixed to
currentYear + 1:it's the earliest year from
currentYearonward that still has an incomplete plan, so thewizard works whether it's run on schedule in December, catching up in January/February, or even
much later in the season for a farmer who's behind — by cloning the last fully-planned season
and editing only what changes. It guides the user through fields, cultivations,
vanggewas/groenbemester, measures, the fertilizer plan and harvest estimates, while reducing
emissions, achieving the nutrient advice, and staying within legal limits. Today fdm-app only
has a one-time farm-creation wizard; this issue covers a repeatable yearly planning flow that
stays usable at any point in the year, every year going forward.
Motivation
Every winter the farmer and advisor rebuild next season's plan from scratch. Most of it is unchanged from last year. We want: "last year, minus the
re-typing, plus advice." Prefill everything from the prior season; surface rotation,
catch-crop and emission advice inline; flag legal-norm breaches; keep the user in control.
Not every farmer plans on time — some only get to it in January, February, or later — so the
wizard must recognize "the season I still need to plan" rather than assuming it's always run in
the preceding winter.
Scope (this release)
currentYear, thewizard finds the earliest year that (a) has a prior year with clonable data and (b) doesn't
yet have a fully completed plan (per the same derived-progress rules used for resuming — see
"Progress, resume & pre-existing data"). In the common case that's
currentYear + 1when runin the preceding winter; if the farmer is late and hasn't planned
currentYearitself yet,the wizard targets
currentYear. The wizard is reusable indefinitely, at any time of year.the wizard halfway and returning resumes at the first incomplete step, with completed steps
editable via back-navigation. See "Progress, resume & pre-existing data" below.
planned in the preceding winter) is supported, not blocked — a soft "je bent laat" banner may
be shown, but the wizard remains fully functional so a late farmer can still catch up.
Why it fits the codebase
fdm-app/app/lib/calendar.ts(yearEnd = currentYear + 1); the dynamic resolution described above is a new small helper built on top of this and the progress-derivation logic (both need to exist before the "which year?" question can be answered).(
farm.create.*,header/create-farm.tsx) — mirror asfarm.$b_id_farm.plan-next.*.StrategyForm/PlanTable/SummaryCards), rotationDataTable,fields-newsidebar, fdm-calculator (calculateDose, norm fillings, N/OM balance).getCultivationPlan,getDefaultDatesOfCultivation(..., targetYear),addField/addCultivation/addFertilizerApplication/addHarvest,b_lu_croprotation.fdm-core'sgetMeasuresForFarm,updateMeasure, and field-end-basedauto-close of ongoing measures;
fdm-app'scomponents/blocks/measures/*(table, columns,add-measure-dialog) already used on
farm.$b_id_farm.$calendar.measures.*.User flow (happy path)
Dashboard card "Plan volgend seizoen ({target year})" → ① clone → ② confirm fields → ③ accept
rotation suggestions → ④ catch crops → ⑤ review/renew maatregelen → ⑥ keep last-year fertilizer
(or Gerrit) → ⑦ confirm yields → done. Every step persists; user can drop out and resume via
sidebar, and reopening later jumps to the first incomplete step — regardless of the actual
calendar date, whether that's December, February, or mid-season.
Wizard steps
b_endandauto-closes (
m_end) any ongoing measures on that field.b_lu_croprotationhistory.(
m_end = null, carries automatically, no action needed), expiring before the target year(prompts the user to renew/extend into the target year or explicitly let lapse), already
ended (informational). Fields removed in step 2 show their measures as closed, non-editable
here. Adding a brand new measure for the target year in this step is a nice-to-have, not
required for this release.
(gebruiksnormen/derogation limits) use the newest year
createFunctionsForNormssupports ator before the target year — falling back to the last available year's norms with a clear
"voorlopig, gebaseerd op normen {last year}" label if the target year's norms aren't
published yet. Dates (default cultivation/application windows, catch-crop deadlines) always
use the real target year regardless of this fallback.
getHarvests; legal+emission summary; finish/PDF.Progress, resume & pre-existing data
Since there is deliberately no new "season"/wizard-progress entity, step completion is
derived from the target year's data itself, per step:
is left in an "undecided" state) and any newly added fields are saved.
toggle decision (on + species, or explicitly off) recorded.
decisions left unresolved.
Gerrit-generated).
completion signal.
This same per-step "done" data is also what resolves which year the wizard targets (see
Scope): starting at
currentYear, the first year that has a clonable prior year but isn't yetfully done across all 7 steps is the target year. No separate "which season am I planning"
flag is stored anywhere.
Behavior built on top of this:
first step that isn't fully done per the rules above. Already-completed steps remain
reachable via back-navigation and are editable, matching the create-farm wizard's pattern.
derived from data rather than a stored "which year/which step" flag, the same detection logic
keeps working whether the wizard is opened on schedule in December, catching up in
January/February, or picked up again mid-season — and whether it's a farm's first "plan
volgend seizoen" run or its fifth.
underway (e.g. well past the typical planning window), the wizard still functions fully; a
soft, non-blocking "je bent laat" banner may be shown on the Start step as a nice-to-have.
field, measure, or cultivation was added manually via the regular field/measures pages before
the user ever started the wizard), the wizard detects and reconciles it: that item is
shown as already-done/pre-filled in the relevant step (not duplicated or re-created), and the
user can edit it like any wizard-created item. There is no special "wizard-owned vs.
manually-added" flag — a field/measure/cultivation dated within the next-year timeframe counts
the same regardless of how it was created.
year exists (a clonable prior year plus an incomplete plan for some year
>= currentYear);it's hidden if there's nothing left to plan (e.g. the wizard was already fully completed for
currentYear + 1well ahead of schedule, since planning further ahead is out of scope).Fertilizer plan follows the crop, not the field
Applications attach to a field, but the plan is crop-driven:
Acceptance criteria
currentYearorcurrentYear + 1).currentYearif not yet planned, otherwisecurrentYear + 1— never hardcoded to a specific year).pnpm typecheckpasses.Risks / open questions
createFunctionsForNormsto each new target year as it becomes relevant (currently 2025/2026). Fallback strategy when the target year's gebruiksnormen aren't published yet: norm values (nitrogen/manure/phosphate gebruiksnormen, derogation limits) fall back to the last available year's norms (i.e.createFunctionsForNormsresolves to the newest supported year ≤ target year), so the compliance bar and dose advice still work, clearly labeled as provisional/based on last year's norms. Timeframe-specific functions (e.g.getDefaultDatesOfCultivation, application-period windows, catch-crop sowing/termination deadlines) always use the real target year regardless of norm-value availability — dates must never silently fall back to the prior year.m_ids, changed conflicts) before this ships, sincegetMeasuresFromCatalogueis not year-scoped.