From d68b6c5ffd74488c58d6c996232432f8a800c5c6 Mon Sep 17 00:00:00 2001 From: Aki Kuivas <91662678+asku1990@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:56:32 +0300 Subject: [PATCH 1/8] docs/bej-103-creation-planning ref bej-103 --- docs/planning/trials/README.md | 21 ++- docs/planning/trials/later-ux.md | 10 +- .../trials/result-creation-guided-ux.md | 145 +++++++++++++++++ .../trials/result-fields-by-rule-window.md | 153 ++++++++++-------- 4 files changed, 258 insertions(+), 71 deletions(-) create mode 100644 docs/planning/trials/result-creation-guided-ux.md diff --git a/docs/planning/trials/README.md b/docs/planning/trials/README.md index 19878253..bb3e6cc8 100644 --- a/docs/planning/trials/README.md +++ b/docs/planning/trials/README.md @@ -19,9 +19,13 @@ on the next gate. identity, transaction, error, date-only, and Server Action backend contract. - [Result creation R2](./result-creation-r2.md) defines the full-page result form and admin UI workflow built on the approved R1 contract. -- [Rule-window-aware result fields](./result-fields-by-rule-window.md) defines - the second result-creation follow-up after R2: introduce field-set selection - for every rule window and verify the 2023+ set first. +- [Rule-window-aware result creation](./result-fields-by-rule-window.md) + defines R3A: make the existing result-create form use the event's persisted + rule window, verify the 2023+ field set, and retain a warned show-all + fallback for other windows. +- [Guided result-creation UX](./result-creation-guided-ux.md) defines R3B: + replace only the full-page result-create form with a four-step workflow + after R3A has been reviewed. - [Later UX](./later-ux.md) records deferred ideas only and does not authorize their implementation. @@ -66,6 +70,9 @@ Repository guardrails and current feature documentation: adding another result to the same event and finishing at the event workspace. - The existing trials master-detail list and existing result-edit modal remain in place for BEJ-103. +- Rule-window-aware presentation is introduced for result creation before any + result-editing redesign. Existing result editing remains unchanged through + R3A and R3B. ## Implementation order and review rules @@ -80,14 +87,17 @@ R1 (backend) ↓ R2 (UI) ↓ -R3 (rule-window field sets) +R3A (rule-window-aware creation) + ↓ +R3B (guided creation UX) ``` 1. `E1` - event workspace 2. `E2` - event creation and empty-event lifecycle 3. `R1` - manual result schema and backend 4. `R2` - manual result UI and workflow -5. `R3` - rule-window-aware result fields, after R2 review +5. `R3A` - rule-window-aware result creation, after R2 review +6. `R3B` - guided result-creation UX, after R3A review For every gate: @@ -108,6 +118,7 @@ BEJ-103 does not authorize: - changes to trial statistics or their calculation; - redesign of legacy import or Koiratietokanta ingestion; - redesign of existing result editing; +- rule-window-aware result editing; - batch entry of several unsaved dog results; - a draft/publish workflow; - autosave; diff --git a/docs/planning/trials/later-ux.md b/docs/planning/trials/later-ux.md index 76170f9a..74f4e884 100644 --- a/docs/planning/trials/later-ux.md +++ b/docs/planning/trials/later-ux.md @@ -16,6 +16,10 @@ the review rules in the [BEJ-103 planning overview](./README.md). index. - Move editing of existing results from the modal to the reusable full-page result form. +- Make existing result editing rule-window-aware, including preservation of + stored fields hidden by a narrower verified field set. +- Redesign result editing only after the R3A and R3B creation gates have been + implemented, validated, and reviewed. - Add searchable dog selection to manual result creation. - Allow inline dog creation from the result flow. - Add an explicit draft/publish state for trial events or results. @@ -27,8 +31,10 @@ the review rules in the [BEJ-103 planning overview](./README.md). - E1 and E2 retain the existing trials master-detail list. - R1 and R2 retain the existing result-edit modal. -- R2 uses a free-text registration field and saves one complete result at a - time. +- R2, R3A, and R3B use a free-text registration field and save one complete + result at a time. +- R3A and R3B do not change result editing. Older, null, and unknown rule + windows use the warned show-all fallback only in result creation. - Matching Koiratietokanta upserts are resolved by the authoritative backend behavior documented in [Result creation](./result-creation.md), without a manual reconciliation screen. diff --git a/docs/planning/trials/result-creation-guided-ux.md b/docs/planning/trials/result-creation-guided-ux.md new file mode 100644 index 00000000..41f36eab --- /dev/null +++ b/docs/planning/trials/result-creation-guided-ux.md @@ -0,0 +1,145 @@ +# Follow-up R3B — Guided Result-Creation UX + +## Status and sequencing + +This is the second creation follow-up after R2. It is planning only and does +not authorize implementation. + +R3A must be implemented, validated, reviewed, and approved before R3B begins. +R3B consumes the R3A field-set registry and changes only the full-page +result-create experience. Existing result editing remains unchanged. + +## Purpose + +Replace the long result-create form with a guided, mobile-capable workflow +that keeps event context visible, presents only the selected rule window's +fields, and provides a clear review step before the one atomic save. + +## Four-step creation flow + +The create route keeps one in-memory draft across four steps: + +1. **Perustiedot** + - Show read-only event date, place, SKL ID, and rule-window information. + - Enter registration, trial/dog metadata, owner snapshots, and + judge/signature fields. +2. **Tulos ja pisteet** + - Enter result, status, score, total, note, and era fields allowed by the + R3A field set. +3. **Lisätiedot** + - Select and enter the rule-window-specific per-era lisätiedot. +4. **Yhteenveto** + - Review a structured, print-friendly representation of the unsaved + request before submission. + +No database write occurs when changing steps. The create mutation is called +only from the summary step. + +Backward navigation is always allowed and retains the draft. Moving forward +validates the current step. Completed steps may be reopened directly; future +steps cannot be skipped. + +## Lisätiedot interaction + +- Group rows by the real PDF domains: + - Olosuhteet + - Haku + - Haukku + - Metsästysinto + - Ajo + - Muut ominaisuudet +- Support search by localized name or code, domain filtering, group + expand/collapse, expand all, selected-row chips, and clearing selections. +- Show the lisätieto code before its localized name. +- Use localized value-kind or unit guidance. Do not invent descriptive + business text that has no authoritative repository source. +- Selecting a row reveals one control per current era: + - marker fields use checkbox-style controls; + - integer and decimal fields use matching numeric input modes; and + - the unverified fallback can continue to expose text inputs where its + generic configuration requires them. +- Removing a selected row clears its unsaved era values and excludes it from + the request. +- On desktop, keep the selected-row summary visible beside the groups when + space permits. On mobile, render the same information above the stacked + group cards. +- Show the current rule-window label and technical ID as read-only context. + Show the R3A unverified warning for fallback field sets. + +## Review, save, and errors + +- The summary displays event identity, registration/basic data, result and + era values, and selected non-empty lisätiedot. +- Do not add an unsaved official-PDF preview contract. The existing official + PDF remains available only after a saved entry has an ID. +- Preserve the existing two successful submission paths: + - **Save and add another** creates one result, then resets to step 1 with + event-level judge defaults restored. + - **Save and finish** creates one result and returns to the event workspace. +- Preserve duplicate-submit protection, Server Action mutation behavior, + query invalidation/refetch, and the rule against optimistic partial cache + insertion. +- Stable backend validation errors return to the populated relevant step: + registration/basic errors to step 1, entry/era errors to step 2, and + lisätieto errors to step 3. +- Event-not-found and missing-SKL errors retain their event-level states. + Authentication and authorization retain the existing admin behavior. +- Preserve the existing dirty internal-navigation confirmation and native + refresh/tab-close protection across every step. Browser Back retains the R2 + behavior. +- Add Finnish and Swedish labels, value hints, validation messages, success + feedback, and error feedback for the redesigned creation flow. +- Add a user-visible `CHANGELOG.md` entry and update durable admin-trial + documentation when implemented. + +## Exclusions + +- No result-edit UI or rule-window-aware editing. +- No migration of the edit modal to the guided flow. +- No unsaved PDF generation or new PDF preview endpoint. +- No searchable dog picker, inline dog creation, batch creation, autosave, + draft/publish state, or reconciliation UI. +- No Prisma schema, backend write-contract, server-validation, PDF renderer, + or Koiratietokanta ingestion changes. +- No new production dependency. +- No redesign of the trials index or event workspace. + +## Acceptance criteria + +- Creation uses the R3A field set for the event's persisted rule window. +- The four steps retain one draft and enforce forward validation without + writing before the summary. +- Search, filtering, grouping, selection chips, and per-era inputs work with + both the verified current field set and the warned fallback. +- The summary represents the final create request without offering an unsaved + official PDF. +- Stable errors return to the correct populated step without losing values. +- Save-and-add-another and save-and-finish retain their existing persistence, + reset, navigation, and cache behavior. +- Dirty navigation protection works throughout the flow. +- The workflow is usable at desktop and mobile widths in Finnish and Swedish. +- The existing result-edit modal behaves exactly as before. + +## Targeted validation + +- Form-model tests for step ownership, forward validation, backward + navigation, completed-step navigation, reset, and draft retention. +- Lisätieto tests for search, filtering, grouping, selection/removal, + per-era controls, corrected input kinds, and mobile/desktop summaries. +- Summary tests proving parity with the serialized create request. +- Mutation tests for stable error-to-step mapping, success continuations, + duplicate-submit protection, invalidation/refetch, and no optimistic row. +- Navigation tests for dirty cancel, internal links, browser Back, and native + refresh/close protection. +- Regression tests for event-level missing/error states, the event workspace, + selected-event panel, and existing result-edit modal. +- Targeted web type checking, unit/component tests, and lint without cycle + lint. +- Manual desktop and mobile browser checks for all four steps and both + successful continuations when browser tooling is available. + +## Merge independence and review gate + +R3B can merge after R3A without any result-editing work. Stop after validation +and request creation-flow review. Editing remains a separately planned and +approved follow-up. diff --git a/docs/planning/trials/result-fields-by-rule-window.md b/docs/planning/trials/result-fields-by-rule-window.md index 937e7e33..b3306d98 100644 --- a/docs/planning/trials/result-fields-by-rule-window.md +++ b/docs/planning/trials/result-fields-by-rule-window.md @@ -1,108 +1,133 @@ -# Follow-up R3 — Rule-window-aware Result Fields +# Follow-up R3A — Rule-Window-Aware Result Creation ## Status and sequencing -This is the second result-creation follow-up after the current R2 implementation -and validation work. It is planning only and does not authorize implementation. +This is the first creation follow-up after R2. It is planning only and does +not authorize implementation. -R2 must be completed and reviewed before this work begins. The initial R3 -change introduces the rule-window structure and verifies only the current -2023+ field set. Exact historical field-set audits remain separate follow-up -work. +R2 must be completed and reviewed before this work begins. R3A introduces the +rule-window field-set structure only for the existing full-page result-create +form and verifies the current 2023+ field set. R3B may redesign result +creation only after R3A has been implemented, validated, and reviewed. + +Existing result editing remains unchanged. Rule-window-aware editing and edit +UX are deferred work recorded in [Later UX](./later-ux.md). ## Purpose -Make manual trial result creation and editing choose their visible score, era, -and lisätieto fields from the event's persisted `trialRuleWindowId`. This keeps -the current form aligned with the corresponding dog-trial PDF without coupling -form behavior to PDF coordinates. +Make manual trial result creation choose its visible score, era, and +lisätieto fields from the event's persisted `trialRuleWindowId`. This aligns +new 2023+ results with the corresponding dog-trial PDF before changing the +creation UI. The current mismatch is structural: -- PDF rendering already selects a renderer by rule window, while the admin form - uses one global field list. -- The 2023+ renderer does not print the current form's `tja` and `pin` score +- PDF rendering already selects a renderer by rule window, while the admin + create form uses one global field list. +- The 2023+ renderer does not print the create form's `tja` and `pin` score fields. - The current PDF consumes only part `a` for lisätieto codes `25` and `27`, while the form exposes parts `a`, `b`, and `c`. -- Lisätieto input types differ for codes `19`, `23`, `26`, and `59`. -- Historical templates contain different score and lisätieto sets. +- Create-form lisätieto input kinds differ from the renderer for codes `19`, + `23`, `26`, and `59`. +- Historical templates contain different score and lisätieto sets, but their + exact create-form configurations have not been audited. ## Scope - Add `trialRuleWindowId` to the admin event-detail contract and propagate the stored value through the DB and service mappings. -- Add a semantic admin result field-set registry covering every seeded rule +- Add a semantic result-create field-set registry covering every seeded rule window ID. +- Resolve the create field set from the event's persisted + `trialRuleWindowId`, never from the browser or by recalculating from the + event date. - Keep PDF coordinates and drawing logic inside the PDF rule-set modules. The - shared semantic configuration defines field availability and value kinds, - not layout. + create registry defines field availability and value kinds, not layout. - Configure `trw_post_20230801` as the first verified field set: - - drive visible entry score fields, era fields, and lisätieto rows from the - configuration; - - omit current-window score fields not consumed by the 2023+ PDF; - - represent codes `25` and `27` with the PDF-consumed `a` part without showing - the implementation suffix to the administrator; - - correct lisätieto value kinds to match the current renderer; and - - retain shared registration, owner, judge, result-status, trial-type, and - other metadata required by persistence or PDF generation. -- Register other known, null, and unknown rule windows through one explicitly - unverified fallback matching the current form behavior. Show a localized - warning instead of presenting the fallback as template-verified. -- Apply the selected field set to both the full-page create form and the - existing edit modal. -- Preserve hidden values when editing an existing result. Selecting a narrower - field set must not silently clear compatibility data or source-projected - lisätieto rows. + - retain the existing registration, event/result metadata, owner, judge, + status, note, total, and other persistence fields required by creation and + PDF generation; + - omit entry- and era-level `tja` and `pin` score fields; + - expose lisätieto codes `10`–`27`, `30`–`42`, and `50`–`62`; + - represent codes `25` and `27` with only their PDF-consumed `a` part, + displayed as a single row without an implementation suffix; + - use integer input for code `19`, decimal input for codes `23`, `26`, and + `59`, and retain the renderer-compatible kinds for all other rows; and + - keep the existing continuous-era behavior: start with one era and permit + additional eras without a create-form maximum, even though the current + PDF renders only eras 1 and 2. +- Register older known, null, and unknown rule windows through one explicitly + unverified fallback matching the current show-all create-form behavior. +- Show a localized warning whenever the unverified fallback is active. +- Pass field-set configuration only into the result-create flow. Shared + components must keep their current default behavior so the existing edit + modal is unaffected. - Keep backend write shapes and their existing validation semantics unchanged. - This gate controls admin presentation and does not add rule-window-specific + R3A controls create-form presentation and does not add rule-window-specific server rejection. -- Update the durable admin-trial documentation and add a user-visible - `CHANGELOG.md` entry when the behavior is implemented. +- Update durable admin-trial documentation and add a user-visible + `CHANGELOG.md` entry when implemented. ## Exclusions +- No result-create layout or navigation redesign. +- No changes to the existing result-edit modal, its visible fields, or its + serialization behavior. +- No rule-window-aware result editing or hidden-value preservation work. - No Prisma schema or migration changes. - No new production dependency. -- No PDF coordinate or template changes. -- No claim that 2005–2011 or 2011–2023 admin field sets are exact. +- No PDF coordinate, mapper, template, or rendering changes. +- No exact 2005–2011, 2011–2023, pre-2002, or 2002–2005 field-set claim. - No removal or migration of stored compatibility fields. -- No rule-window-specific validation in Koiratietokanta ingestion. -- No redesign of result creation or the existing edit modal. +- No rule-window-specific backend validation or Koiratietokanta ingestion + changes. ## Acceptance criteria - Admin event details return the event's persisted `trialRuleWindowId`. -- Create and edit resolve their field set from that stored ID rather than from - the browser date. -- A 2023+ event displays the verified score, era, and lisätieto configuration. +- Result creation resolves its field set from that stored ID. +- A 2023+ create form displays the verified score, era, and lisätieto + configuration. - Codes `25` and `27` persist with part `a`, appear as single rows, and reach the existing PDF pivot correctly. -- Current-window lisätieto value kinds match the 2023+ renderer. -- Other known, null, and unknown windows retain the existing generic editing +- Current-window lisätieto input kinds match the 2023+ renderer. +- Additional continuous eras remain available in the create form. +- Older known, null, and unknown windows retain the existing generic creation capability and display an unverified-field-set warning. -- Editing through a narrower field set preserves hidden existing values. -- Existing create/update request contracts and server validation behavior do - not change. +- Existing create request contracts and server validation behavior do not + change. +- The existing edit modal renders and submits exactly as before. ## Targeted validation -- Contract, DB-mapping, and service tests for `trialRuleWindowId` in admin event - details. -- Field-registry tests covering every seeded rule-window ID and fallback - behavior. -- 2023+ parity tests for visible score fields, era fields, lisätieto codes, +- Contract, DB-mapping, and service tests for `trialRuleWindowId` in admin + event details. +- Field-registry tests covering every seeded rule-window ID and null/unknown + fallback behavior. +- 2023+ parity tests for visible entry fields, era fields, lisätieto codes, parts, ordering, and input kinds. -- Create-form and edit-modal tests for rule selection, the fallback warning, - and hidden-value preservation. -- Regression tests for request serialization, current validation feedback, and - PDF part-`a` mapping. +- Create-form tests for persisted-rule selection, fallback warning, unlimited + continuous eras, and request serialization. +- Regression tests proving the edit modal retains its current complete field + set and request serialization. +- PDF pivot regression tests for part `a` of codes `25` and `27`. - Targeted web, contracts, server, and DB type checks and tests, plus targeted lint without cycle lint. -## Later historical audit +## Merge independence and review gate + +R3A can merge independently after R2. It changes only the semantic +presentation of result creation and leaves result editing unchanged. + +Stop after validation and request explicit review. Do not begin the guided +creation UX in R3B without separate approval. + +## Later historical and edit work + +After the creation gates are reviewed, plan result editing separately. That +work must define rule-window selection, hidden stored-value preservation, and +the edit interaction before changing the existing modal. -After R3 is reviewed, separately compare the 2005–2011 and 2011–2023 PDF -templates and renderers with stored legacy/API data. Replace the unverified -fallback for each window only after its score fields, supported era behavior, -lisätieto rows, value kinds, and preservation behavior have dedicated tests. +Historical create/edit field sets also require separate audits against the +2005–2011, 2011–2023, pre-2002, and 2002–2005 templates and source data. From 873c294d3e99caeb43defd3e953fee5112c7b73f Mon Sep 17 00:00:00 2001 From: Aki Kuivas <91662678+asku1990@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:14:18 +0300 Subject: [PATCH 2/8] docs(trials): refine rule-window creation UX plans ref bej-103 --- docs/planning/trials/README.md | 10 +- docs/planning/trials/later-ux.md | 5 + .../trials/result-creation-guided-ux.md | 145 ----------- docs/planning/trials/result-creation-ux.md | 235 ++++++++++++++++++ .../trials/result-fields-by-rule-window.md | 39 ++- 5 files changed, 280 insertions(+), 154 deletions(-) delete mode 100644 docs/planning/trials/result-creation-guided-ux.md create mode 100644 docs/planning/trials/result-creation-ux.md diff --git a/docs/planning/trials/README.md b/docs/planning/trials/README.md index bb3e6cc8..79db8678 100644 --- a/docs/planning/trials/README.md +++ b/docs/planning/trials/README.md @@ -23,9 +23,9 @@ on the next gate. defines R3A: make the existing result-create form use the event's persisted rule window, verify the 2023+ field set, and retain a warned show-all fallback for other windows. -- [Guided result-creation UX](./result-creation-guided-ux.md) defines R3B: - replace only the full-page result-create form with a four-step workflow - after R3A has been reviewed. +- [Result-creation UX](./result-creation-ux.md) defines R3B: evolve the + existing full-page create form into a clearer card-based single-page + experience after R3A has been reviewed. - [Later UX](./later-ux.md) records deferred ideas only and does not authorize their implementation. @@ -89,7 +89,7 @@ R2 (UI) ↓ R3A (rule-window-aware creation) ↓ -R3B (guided creation UX) +R3B (result-creation UX) ``` 1. `E1` - event workspace @@ -97,7 +97,7 @@ R3B (guided creation UX) 3. `R1` - manual result schema and backend 4. `R2` - manual result UI and workflow 5. `R3A` - rule-window-aware result creation, after R2 review -6. `R3B` - guided result-creation UX, after R3A review +6. `R3B` - card-based result-creation UX, after R3A review For every gate: diff --git a/docs/planning/trials/later-ux.md b/docs/planning/trials/later-ux.md index 74f4e884..9f3db1c4 100644 --- a/docs/planning/trials/later-ux.md +++ b/docs/planning/trials/later-ux.md @@ -20,6 +20,9 @@ the review rules in the [BEJ-103 planning overview](./README.md). stored fields hidden by a narrower verified field set. - Redesign result editing only after the R3A and R3B creation gates have been implemented, validated, and reviewed. +- If a guided or multi-step result flow is adopted later, implement it as a + coordinator that controls visibility of the reusable R3B cards rather than + rewriting their field presentation. - Add searchable dog selection to manual result creation. - Allow inline dog creation from the result flow. - Add an explicit draft/publish state for trial events or results. @@ -35,6 +38,8 @@ the review rules in the [BEJ-103 planning overview](./README.md). result at a time. - R3A and R3B do not change result editing. Older, null, and unknown rule windows use the warned show-all fallback only in result creation. +- R3B keeps result creation on one page; a wizard or mandatory multi-step + coordinator is not part of the current gates. - Matching Koiratietokanta upserts are resolved by the authoritative backend behavior documented in [Result creation](./result-creation.md), without a manual reconciliation screen. diff --git a/docs/planning/trials/result-creation-guided-ux.md b/docs/planning/trials/result-creation-guided-ux.md deleted file mode 100644 index 41f36eab..00000000 --- a/docs/planning/trials/result-creation-guided-ux.md +++ /dev/null @@ -1,145 +0,0 @@ -# Follow-up R3B — Guided Result-Creation UX - -## Status and sequencing - -This is the second creation follow-up after R2. It is planning only and does -not authorize implementation. - -R3A must be implemented, validated, reviewed, and approved before R3B begins. -R3B consumes the R3A field-set registry and changes only the full-page -result-create experience. Existing result editing remains unchanged. - -## Purpose - -Replace the long result-create form with a guided, mobile-capable workflow -that keeps event context visible, presents only the selected rule window's -fields, and provides a clear review step before the one atomic save. - -## Four-step creation flow - -The create route keeps one in-memory draft across four steps: - -1. **Perustiedot** - - Show read-only event date, place, SKL ID, and rule-window information. - - Enter registration, trial/dog metadata, owner snapshots, and - judge/signature fields. -2. **Tulos ja pisteet** - - Enter result, status, score, total, note, and era fields allowed by the - R3A field set. -3. **Lisätiedot** - - Select and enter the rule-window-specific per-era lisätiedot. -4. **Yhteenveto** - - Review a structured, print-friendly representation of the unsaved - request before submission. - -No database write occurs when changing steps. The create mutation is called -only from the summary step. - -Backward navigation is always allowed and retains the draft. Moving forward -validates the current step. Completed steps may be reopened directly; future -steps cannot be skipped. - -## Lisätiedot interaction - -- Group rows by the real PDF domains: - - Olosuhteet - - Haku - - Haukku - - Metsästysinto - - Ajo - - Muut ominaisuudet -- Support search by localized name or code, domain filtering, group - expand/collapse, expand all, selected-row chips, and clearing selections. -- Show the lisätieto code before its localized name. -- Use localized value-kind or unit guidance. Do not invent descriptive - business text that has no authoritative repository source. -- Selecting a row reveals one control per current era: - - marker fields use checkbox-style controls; - - integer and decimal fields use matching numeric input modes; and - - the unverified fallback can continue to expose text inputs where its - generic configuration requires them. -- Removing a selected row clears its unsaved era values and excludes it from - the request. -- On desktop, keep the selected-row summary visible beside the groups when - space permits. On mobile, render the same information above the stacked - group cards. -- Show the current rule-window label and technical ID as read-only context. - Show the R3A unverified warning for fallback field sets. - -## Review, save, and errors - -- The summary displays event identity, registration/basic data, result and - era values, and selected non-empty lisätiedot. -- Do not add an unsaved official-PDF preview contract. The existing official - PDF remains available only after a saved entry has an ID. -- Preserve the existing two successful submission paths: - - **Save and add another** creates one result, then resets to step 1 with - event-level judge defaults restored. - - **Save and finish** creates one result and returns to the event workspace. -- Preserve duplicate-submit protection, Server Action mutation behavior, - query invalidation/refetch, and the rule against optimistic partial cache - insertion. -- Stable backend validation errors return to the populated relevant step: - registration/basic errors to step 1, entry/era errors to step 2, and - lisätieto errors to step 3. -- Event-not-found and missing-SKL errors retain their event-level states. - Authentication and authorization retain the existing admin behavior. -- Preserve the existing dirty internal-navigation confirmation and native - refresh/tab-close protection across every step. Browser Back retains the R2 - behavior. -- Add Finnish and Swedish labels, value hints, validation messages, success - feedback, and error feedback for the redesigned creation flow. -- Add a user-visible `CHANGELOG.md` entry and update durable admin-trial - documentation when implemented. - -## Exclusions - -- No result-edit UI or rule-window-aware editing. -- No migration of the edit modal to the guided flow. -- No unsaved PDF generation or new PDF preview endpoint. -- No searchable dog picker, inline dog creation, batch creation, autosave, - draft/publish state, or reconciliation UI. -- No Prisma schema, backend write-contract, server-validation, PDF renderer, - or Koiratietokanta ingestion changes. -- No new production dependency. -- No redesign of the trials index or event workspace. - -## Acceptance criteria - -- Creation uses the R3A field set for the event's persisted rule window. -- The four steps retain one draft and enforce forward validation without - writing before the summary. -- Search, filtering, grouping, selection chips, and per-era inputs work with - both the verified current field set and the warned fallback. -- The summary represents the final create request without offering an unsaved - official PDF. -- Stable errors return to the correct populated step without losing values. -- Save-and-add-another and save-and-finish retain their existing persistence, - reset, navigation, and cache behavior. -- Dirty navigation protection works throughout the flow. -- The workflow is usable at desktop and mobile widths in Finnish and Swedish. -- The existing result-edit modal behaves exactly as before. - -## Targeted validation - -- Form-model tests for step ownership, forward validation, backward - navigation, completed-step navigation, reset, and draft retention. -- Lisätieto tests for search, filtering, grouping, selection/removal, - per-era controls, corrected input kinds, and mobile/desktop summaries. -- Summary tests proving parity with the serialized create request. -- Mutation tests for stable error-to-step mapping, success continuations, - duplicate-submit protection, invalidation/refetch, and no optimistic row. -- Navigation tests for dirty cancel, internal links, browser Back, and native - refresh/close protection. -- Regression tests for event-level missing/error states, the event workspace, - selected-event panel, and existing result-edit modal. -- Targeted web type checking, unit/component tests, and lint without cycle - lint. -- Manual desktop and mobile browser checks for all four steps and both - successful continuations when browser tooling is available. - -## Merge independence and review gate - -R3B can merge after R3A without any result-editing work. Stop after validation -and request creation-flow review. Editing remains a separately planned and -approved follow-up. diff --git a/docs/planning/trials/result-creation-ux.md b/docs/planning/trials/result-creation-ux.md new file mode 100644 index 00000000..5fd8ca89 --- /dev/null +++ b/docs/planning/trials/result-creation-ux.md @@ -0,0 +1,235 @@ +# Follow-up R3B — Result-Creation UX + +## Status and sequencing + +This is the second creation follow-up after R2. It is planning only and does +not authorize implementation. + +R3A must be implemented, validated, reviewed, and approved before R3B begins. +R3B consumes the R3A field-set registry and changes only the presentation of +the existing full-page result-create experience. Existing result editing +remains unchanged. + +The referenced UX screenshots are concepts for hierarchy, cards, and +lisätieto interaction. They do not specify exact geometry, field counts, +placement, wording, or a mandatory navigation model. + +## Purpose + +Evolve the current single-page result-create form into a clearer, +mobile-capable set of reusable cards. Preserve the familiar save flow and +overall navigation while improving hierarchy, spacing, descriptions, and +grouping. + +R3B owns presentation only. Field correctness, availability, ordering, +business grouping, semantic input kinds, and persistence mapping remain owned +by the R3A registry. + +## Single-page card composition + +Keep one full-page form and one in-memory draft. Do not require a wizard or +mandatory multi-step navigation. + +Compose the page from independently reusable cards such as: + +- event context; +- Perustiedot; +- Tulos ja huomautus; +- Ansiopisteet; +- Haku, Haukku ja muut; +- Tuomarit; +- Erät; +- Lisätiedot; and +- an optional informational Yhteenveto. + +The exact card boundaries may combine closely related fields when needed for +responsive layout, but must preserve the current form organization and +backend request ownership. Other than Lisätiedot, R3B changes spacing, +descriptions, visual hierarchy, and grouping rather than introducing a new +workflow. + +Cards must not own page navigation or rule-window resolution. They receive +draft values, callbacks, validation state, and resolved field configuration +through their interfaces. This keeps them reusable for future editing or for +a later optional coordinator that controls card visibility without rewriting +the cards. + +## Rule-window presentation + +- The event already owns the rule window; the administrator never selects it. +- Show only minimal localized read-only context, for example + `Säännöt: 1.8.2023 →`. +- Do not expose `trialRuleWindowId` as a normal field. The technical ID may + appear only in a tooltip, expandable debug information, or developer + diagnostics. +- For an unverified fallback, show the R3A localized warning without asking + the administrator to choose another rule window. +- Presentation components consume the R3A field set and contain no + rule-window-specific branching. + +## Lisätiedot workspace + +Lisätiedot is the primary R3B UX improvement. Replace the extremely long +scrolling matrix with a dedicated workspace inside its own card. + +The workspace supports: + +- search by code; +- search by localized name; +- filtering by business/PDF domain; +- collapsible domain groups; +- expand all and collapse all; +- a selected-row summary above the groups; +- semantic per-era controls; and +- responsive desktop and mobile layouts. + +Use the real business/PDF domains as the primary groups: + +- Olosuhteet; +- Haku; +- Haukku; +- Metsästysinto; +- Ajo; and +- Muut ominaisuudet. + +Numeric ranges may appear as secondary information but never define the +primary grouping. + +Each row presents its code, localized name, optional authoritative +description, and semantic control together. Do not invent descriptions when +no authoritative source exists; use localized value-kind or unit guidance +instead. + +Controls come from the R3A semantic input kind: + +- `marker` renders as a checkbox or equivalent boolean control; +- `integer` renders as an integer input; +- `decimal` renders as a decimal input; +- `text` renders as a text input; and +- `tri-state` is allowed only when persistence explicitly distinguishes empty, + `0`, and `1`, and renders as meaningful localized choices rather than raw + persistence values. + +Administrators must not normally enter raw persistence values such as `0` or +`1`. The UI translates semantic control state through registry persistence +mapping. + +Selected rows remain visible in the summary above the groups. Removing a +selected row clears all of its unsaved era values. Rows with no values are +omitted from the create request, preserving the existing request behavior. + +On desktop, the workspace may use adjacent filter, group, selected-row, and +editor regions when space permits. On mobile, it uses stacked groups or an +overlay/sheet while preserving the same selection and semantic controls. +These are responsive implementation choices, not separate workflows. + +## Erät and other sections + +- Keep each era visually independent in the existing card direction. +- Preserve adding and removing continuous eras and the R3A-configured visible + fields. +- Retain the current organization and validation semantics for all other + sections. +- Improve only spacing, localized descriptions, hierarchy, and grouping + outside the Lisätiedot workspace. + +## Optional summary + +A lightweight informational summary card may appear near the bottom of the +same page. It may summarize: + +- event context; +- dog/registration data; +- eras; and +- selected non-empty lisätiedot. + +The summary is not a required confirmation step and does not add an official +PDF preview. The existing official PDF remains available only after a saved +entry has an ID. + +## Save, errors, and navigation + +- Preserve the existing two successful submission paths: + - **Save and add another** creates one result, then resets the same page with + event-level judge defaults restored. + - **Save and finish** creates one result and returns to the event workspace. +- Preserve existing client/server validation, stable error mapping, + duplicate-submit protection, Server Action mutation behavior, query + invalidation/refetch, and the rule against optimistic partial cache + insertion. +- Validation and server failures keep the populated single-page form visible + and identify the relevant card or field without changing navigation. +- Preserve the existing dirty internal-navigation confirmation and native + refresh/tab-close protection. Browser Back retains the R2 behavior. +- Add Finnish and Swedish labels, value hints, validation messages, success + feedback, and error feedback for the evolved presentation. +- Add a user-visible `CHANGELOG.md` entry and update durable admin-trial + documentation when implemented. + +## Exclusions + +- No wizard or mandatory multi-step flow. +- No result-edit UI or rule-window-aware editing. +- No migration of the edit modal to the card-based create page. +- No unsaved PDF generation or new PDF preview endpoint. +- No searchable dog picker, inline dog creation, batch creation, autosave, + draft/publish state, or reconciliation UI. +- No Prisma schema, backend write-contract, server-validation, PDF renderer, + or Koiratietokanta ingestion changes. +- No new production dependency. +- No redesign of the trials index or event workspace. + +## Acceptance criteria + +- R3A remains the owner of business correctness and R3B changes presentation + only. +- The existing result-create route remains a familiar single-page form with + the same save and navigation behavior. +- Reusable cards render from the resolved R3A field set and contain no + rule-window-specific branching. +- Rule windows remain mostly invisible: administrators cannot select one, + normally see only a localized period label, and do not see the technical ID + as a standard field. +- Lisätiedot provides search, domain filtering, collapsible groups, + expand/collapse all, selected-row summary, and semantic per-era controls. +- Business/PDF domains are the primary lisätieto grouping; numeric ranges are + secondary only. +- Removing a selected lisätieto clears its unsaved values, and empty rows are + omitted from the create request. +- Erät and other sections preserve their current behavior while gaining + clearer card hierarchy and responsive presentation. +- The optional summary is informational and does not create a new confirmation + or PDF-preview step. +- Save-and-add-another, save-and-finish, validation, dirty navigation, + mutation, and cache behavior remain unchanged. +- Cards are independently reusable by future editing without implementing or + redesigning editing in R3B. +- The page is usable at desktop and mobile widths in Finnish and Swedish. +- The existing result-edit modal behaves exactly as before. + +## Targeted validation + +- Card tests for independent rendering, field-set-driven visibility, + validation presentation, and draft callbacks. +- Lisätieto tests for code/name search, domain filtering, group + expand/collapse, selected-row summary, selection removal, semantic controls, + per-era values, and omission of empty rows. +- Tests proving semantic marker and tri-state controls serialize through the + registry without exposing raw persistence values. +- Responsive component tests for desktop and mobile workspace variants. +- Regression tests for create request serialization, both success + continuations, stable errors, duplicate-submit protection, + invalidation/refetch, dirty navigation, and no optimistic row. +- Regression tests for event-level missing/error states, the event workspace, + selected-event panel, and existing result-edit modal. +- Targeted web type checking, unit/component tests, and lint without cycle + lint. +- Manual desktop and mobile browser checks for the card hierarchy, + Lisätiedot workspace, and both successful continuations when browser tooling + is available. + +## Merge independence and review gate + +R3B can merge after R3A without any result-editing work. Stop after validation +and request creation-flow review. Editing remains a separately planned and +approved follow-up. diff --git a/docs/planning/trials/result-fields-by-rule-window.md b/docs/planning/trials/result-fields-by-rule-window.md index b3306d98..386b133c 100644 --- a/docs/planning/trials/result-fields-by-rule-window.md +++ b/docs/planning/trials/result-fields-by-rule-window.md @@ -20,6 +20,12 @@ lisätieto fields from the event's persisted `trialRuleWindowId`. This aligns new 2023+ results with the corresponding dog-trial PDF before changing the creation UI. +R3A owns field correctness, not layout. Its registry is the single source of +truth for visible fields, ordering, business/PDF grouping, semantic input +kinds, and the persistence metadata needed to serialize a selected field. +Presentation components consume the resolved field set and must not branch on +specific rule-window IDs. + The current mismatch is structural: - PDF rendering already selects a renderer by rule window, while the admin @@ -38,7 +44,13 @@ The current mismatch is structural: - Add `trialRuleWindowId` to the admin event-detail contract and propagate the stored value through the DB and service mappings. - Add a semantic result-create field-set registry covering every seeded rule - window ID. + window ID. The registry defines field visibility, ordering, grouping, + semantic input kinds, and persistence mapping. +- Define semantic input kinds independently from stored values: + `marker`, `integer`, `decimal`, and `text`, plus `tri-state` only for a field + whose persistence explicitly distinguishes empty, `0`, and `1`. The + registry owns the mapping between semantic control state and persisted + values. - Resolve the create field set from the event's persisted `trialRuleWindowId`, never from the browser or by recalculating from the event date. @@ -60,6 +72,12 @@ The current mismatch is structural: - Register older known, null, and unknown rule windows through one explicitly unverified fallback matching the current show-all create-form behavior. - Show a localized warning whenever the unverified fallback is active. +- Treat rule-window selection as event-owned context. The administrator never + chooses or changes it from the result-create form. +- Show only a minimal localized read-only rule-period label, for example + `Säännöt: 1.8.2023 →`. The technical `trialRuleWindowId` may appear only in + a tooltip, expandable diagnostics, or developer output, never as a normal + user-facing form field. - Pass field-set configuration only into the result-create flow. Shared components must keep their current default behavior so the existing edit modal is unaffected. @@ -87,6 +105,14 @@ The current mismatch is structural: - Admin event details return the event's persisted `trialRuleWindowId`. - Result creation resolves its field set from that stored ID. +- The field registry is the single source of truth for create-form visibility, + ordering, grouping, semantic controls, and persistence mapping; UI + components contain no rule-window-ID branches. +- Semantic control state maps to persistence values through the registry, + including tri-state only where empty, `0`, and `1` are distinct domain + values. +- Administrators cannot select a rule window and normally see only its + localized period label. - A 2023+ create form displays the verified score, era, and lisätieto configuration. - Codes `25` and `27` persist with part `a`, appear as single rows, and reach @@ -105,10 +131,15 @@ The current mismatch is structural: event details. - Field-registry tests covering every seeded rule-window ID and null/unknown fallback behavior. +- Registry tests for semantic input kinds and control-state-to-persistence + mapping, including any explicitly configured tri-state field. - 2023+ parity tests for visible entry fields, era fields, lisätieto codes, parts, ordering, and input kinds. - Create-form tests for persisted-rule selection, fallback warning, unlimited - continuous eras, and request serialization. + continuous eras, minimal rule-period presentation, and request + serialization. +- Presentation tests proving the resolved registry configuration drives + rendering without rule-window-specific component branches. - Regression tests proving the edit modal retains its current complete field set and request serialization. - PDF pivot regression tests for part `a` of codes `25` and `27`. @@ -120,8 +151,8 @@ The current mismatch is structural: R3A can merge independently after R2. It changes only the semantic presentation of result creation and leaves result editing unchanged. -Stop after validation and request explicit review. Do not begin the guided -creation UX in R3B without separate approval. +Stop after validation and request explicit review. Do not begin the +card-based creation UX in R3B without separate approval. ## Later historical and edit work From 6298f1fa73d5edd2a6ad109fd37840a5e30f554d Mon Sep 17 00:00:00 2001 From: Aki Kuivas <91662678+asku1990@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:53:55 +0300 Subject: [PATCH 3/8] docs(trials): clarify reusable creation UX scope --- docs/planning/trials/later-ux.md | 8 +++----- docs/planning/trials/result-creation-ux.md | 14 ++++++++++++-- .../trials/result-fields-by-rule-window.md | 15 ++++++++------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/docs/planning/trials/later-ux.md b/docs/planning/trials/later-ux.md index 9f3db1c4..9161af6b 100644 --- a/docs/planning/trials/later-ux.md +++ b/docs/planning/trials/later-ux.md @@ -20,9 +20,6 @@ the review rules in the [BEJ-103 planning overview](./README.md). stored fields hidden by a narrower verified field set. - Redesign result editing only after the R3A and R3B creation gates have been implemented, validated, and reviewed. -- If a guided or multi-step result flow is adopted later, implement it as a - coordinator that controls visibility of the reusable R3B cards rather than - rewriting their field presentation. - Add searchable dog selection to manual result creation. - Allow inline dog creation from the result flow. - Add an explicit draft/publish state for trial events or results. @@ -38,8 +35,9 @@ the review rules in the [BEJ-103 planning overview](./README.md). result at a time. - R3A and R3B do not change result editing. Older, null, and unknown rule windows use the warned show-all fallback only in result creation. -- R3B keeps result creation on one page; a wizard or mandatory multi-step - coordinator is not part of the current gates. +- R3B does not require a wizard. If a guided presentation is adopted during + R3B, it must be a thin coordinator over the same reusable cards and preserve + the existing save and navigation behavior. - Matching Koiratietokanta upserts are resolved by the authoritative backend behavior documented in [Result creation](./result-creation.md), without a manual reconciliation screen. diff --git a/docs/planning/trials/result-creation-ux.md b/docs/planning/trials/result-creation-ux.md index 5fd8ca89..1f2009cd 100644 --- a/docs/planning/trials/result-creation-ux.md +++ b/docs/planning/trials/result-creation-ux.md @@ -30,6 +30,12 @@ by the R3A registry. Keep one full-page form and one in-memory draft. Do not require a wizard or mandatory multi-step navigation. +A guided or multi-step presentation is permitted, but it is not an R3B +requirement. If implementation review shows that guidance is useful, add it as +a thin coordinator that controls which reusable cards are visible. It must not +duplicate card content, create separate field models, or change the existing +save and navigation contract. + Compose the page from independently reusable cards such as: - event context; @@ -51,7 +57,7 @@ workflow. Cards must not own page navigation or rule-window resolution. They receive draft values, callbacks, validation state, and resolved field configuration through their interfaces. This keeps them reusable for future editing or for -a later optional coordinator that controls card visibility without rewriting +an optional R3B coordinator that controls card visibility without rewriting the cards. ## Rule-window presentation @@ -168,7 +174,9 @@ entry has an ID. ## Exclusions -- No wizard or mandatory multi-step flow. +- No requirement to implement a wizard or mandatory multi-step flow. Any + guided presentation must remain a thin coordinator over the same reusable + cards. - No result-edit UI or rule-window-aware editing. - No migration of the edit modal to the card-based create page. - No unsaved PDF generation or new PDF preview endpoint. @@ -185,6 +193,8 @@ entry has an ID. only. - The existing result-create route remains a familiar single-page form with the same save and navigation behavior. +- A guided presentation, if adopted, reuses the same cards and field model and + does not change persistence, save, or navigation behavior. - Reusable cards render from the resolved R3A field set and contain no rule-window-specific branching. - Rule windows remain mostly invisible: administrators cannot select one, diff --git a/docs/planning/trials/result-fields-by-rule-window.md b/docs/planning/trials/result-fields-by-rule-window.md index 386b133c..d7ca795c 100644 --- a/docs/planning/trials/result-fields-by-rule-window.md +++ b/docs/planning/trials/result-fields-by-rule-window.md @@ -22,9 +22,9 @@ creation UI. R3A owns field correctness, not layout. Its registry is the single source of truth for visible fields, ordering, business/PDF grouping, semantic input -kinds, and the persistence metadata needed to serialize a selected field. -Presentation components consume the resolved field set and must not branch on -specific rule-window IDs. +kinds, localized value hints, and the persistence metadata needed to serialize +a selected field. Presentation components consume the resolved field set and +must not branch on specific rule-window IDs. The current mismatch is structural: @@ -45,7 +45,7 @@ The current mismatch is structural: stored value through the DB and service mappings. - Add a semantic result-create field-set registry covering every seeded rule window ID. The registry defines field visibility, ordering, grouping, - semantic input kinds, and persistence mapping. + semantic input kinds, localized value hints, and persistence mapping. - Define semantic input kinds independently from stored values: `marker`, `integer`, `decimal`, and `text`, plus `tri-state` only for a field whose persistence explicitly distinguishes empty, `0`, and `1`. The @@ -106,8 +106,8 @@ The current mismatch is structural: - Admin event details return the event's persisted `trialRuleWindowId`. - Result creation resolves its field set from that stored ID. - The field registry is the single source of truth for create-form visibility, - ordering, grouping, semantic controls, and persistence mapping; UI - components contain no rule-window-ID branches. + ordering, grouping, semantic controls, localized value hints, and + persistence mapping; UI components contain no rule-window-ID branches. - Semantic control state maps to persistence values through the registry, including tri-state only where empty, `0`, and `1` are distinct domain values. @@ -132,7 +132,8 @@ The current mismatch is structural: - Field-registry tests covering every seeded rule-window ID and null/unknown fallback behavior. - Registry tests for semantic input kinds and control-state-to-persistence - mapping, including any explicitly configured tri-state field. + mapping, including localized value hints and any explicitly configured + tri-state field. - 2023+ parity tests for visible entry fields, era fields, lisätieto codes, parts, ordering, and input kinds. - Create-form tests for persisted-rule selection, fallback warning, unlimited From ae771ccbbc7afdb4ca920c9d9660eef316e7d599 Mon Sep 17 00:00:00 2001 From: Aki Kuivas <91662678+asku1990@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:57:11 +0300 Subject: [PATCH 4/8] feat(trials): make result creation rule-window aware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve the manual result-create field set from the trial event’s persisted rule window. - add trialRuleWindowId to admin event details - configure and verify the current 2023+ score, era, and lisätieto fields - hide obsolete tja and pin score fields for current rules - use Ajotaito for the current rule period - align lisätieto codes, parts, input types, and marker persistence with the PDF - retain a warned legacy fallback for older and unknown rule windows - preserve existing result-edit behavior ref bej-103 --- CHANGELOG.md | 1 + .../admin-trial-event-edit-dialog.test.tsx | 1 + .../admin-trial-selected-event-panel.test.tsx | 3 + .../admin-trial-entry-create-page-client.tsx | 21 ++- .../__tests__/entry-meta-section.test.tsx | 20 +++ .../internal/__tests__/era-section.test.tsx | 33 ++++ .../trials/internal/entry-meta-section.tsx | 141 +++++++++++---- .../admin/trials/internal/era-section.tsx | 50 +++--- .../trials/internal/lisatiedot-matrix.tsx | 52 +++++- .../__tests__/entry-create-model.test.ts | 22 +++ .../result-create-field-registry.test.ts | 80 +++++++++ .../lib/admin/trials/entry-create-model.ts | 17 +- .../web/lib/admin/trials/entry-edit-config.ts | 7 +- .../admin/trials/entry-edit-dialog-model.ts | 21 ++- apps/web/lib/admin/trials/index.ts | 1 + .../trials/result-create-field-registry.ts | 165 ++++++++++++++++++ .../lib/i18n/messages/admin/trials/manage.ts | 22 +++ docs/features/admin-trial-management.md | 16 +- .../manage/admin-trial-event-details.ts | 1 + .../get-trial-event-details.parity.test.ts | 2 + .../trials/manage/get-trial-event-details.ts | 2 + packages/db/admin/trials/manage/types.ts | 1 + .../manage/__tests__/get-trial-event.test.ts | 2 + .../admin/trials/manage/get-trial-event.ts | 1 + .../__tests__/get-trial-dog-pdf-data.test.ts | 35 ++++ 25 files changed, 650 insertions(+), 67 deletions(-) create mode 100644 apps/web/components/admin/trials/internal/__tests__/era-section.test.tsx create mode 100644 apps/web/lib/admin/trials/__tests__/result-create-field-registry.test.ts create mode 100644 apps/web/lib/admin/trials/result-create-field-registry.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c8e92c6..191007c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This project uses a user-facing changelog format. - Viimeisen koetuloksen poistaminen säilyttää ajokoetapahtuman tyhjänä, kunnes ylläpitäjä poistaa tapahtuman erikseen. - Uuden ajokoetapahtuman luomisen jälkeen siirrytään suoraan lisäämään tapahtuman ensimmäistä koetulosta. +- Ajokoetuloksen lisäyslomake näyttää 1.8.2023 alkaen käytössä olevan sääntökauden mukaiset kentät. Vanhemmilla tai tunnistamattomilla sääntökausilla kaikki yhteensopivuuskentät säilyvät näkyvissä varoituksen kanssa. ### Fixed diff --git a/apps/web/components/admin/trials/__tests__/admin-trial-event-edit-dialog.test.tsx b/apps/web/components/admin/trials/__tests__/admin-trial-event-edit-dialog.test.tsx index c532fe21..6dc0f26f 100644 --- a/apps/web/components/admin/trials/__tests__/admin-trial-event-edit-dialog.test.tsx +++ b/apps/web/components/admin/trials/__tests__/admin-trial-event-edit-dialog.test.tsx @@ -51,6 +51,7 @@ vi.mock("@/components/ui/input", () => ({ const event1 = { trialEventId: "event-1", + trialRuleWindowId: "trw_post_20230801", eventDate: "2026-04-14", eventPlace: "Helsinki", eventName: "Kevatkoe", diff --git a/apps/web/components/admin/trials/__tests__/admin-trial-selected-event-panel.test.tsx b/apps/web/components/admin/trials/__tests__/admin-trial-selected-event-panel.test.tsx index 41dd1c23..f1edacaf 100644 --- a/apps/web/components/admin/trials/__tests__/admin-trial-selected-event-panel.test.tsx +++ b/apps/web/components/admin/trials/__tests__/admin-trial-selected-event-panel.test.tsx @@ -94,6 +94,7 @@ describe("AdminTrialSelectedEventPanel", () => { React.createElement(AdminTrialSelectedEventPanel, { selectedEvent: { trialEventId: "event-1", + trialRuleWindowId: "trw_post_20230801", eventDate: "2026-04-14", eventPlace: "Helsinki", eventName: "Kevatkoe", @@ -143,6 +144,7 @@ describe("AdminTrialSelectedEventPanel", () => { React.createElement(AdminTrialSelectedEventPanel, { selectedEvent: { trialEventId: "event-1", + trialRuleWindowId: null, eventDate: "2026-04-14", eventPlace: "Helsinki", eventName: null, @@ -175,6 +177,7 @@ describe("AdminTrialSelectedEventPanel", () => { React.createElement(AdminTrialSelectedEventPanel, { selectedEvent: { trialEventId: "event-1", + trialRuleWindowId: null, eventDate: "2026-04-14", eventPlace: "Helsinki", eventName: null, diff --git a/apps/web/components/admin/trials/admin-trial-entry-create-page-client.tsx b/apps/web/components/admin/trials/admin-trial-entry-create-page-client.tsx index 8cf7f2ad..77af1c1d 100644 --- a/apps/web/components/admin/trials/admin-trial-entry-create-page-client.tsx +++ b/apps/web/components/admin/trials/admin-trial-entry-create-page-client.tsx @@ -19,6 +19,7 @@ import { getAdminTrialEventHref, getAdminTrialsHref, getNextEraNumber, + resolveResultCreateFieldSet, toCreateAdminTrialEntryRequest, } from "@/lib/admin/trials"; import { @@ -113,9 +114,13 @@ function ResultCreateForm({ const { t } = useI18n(); const router = useRouter(); const mutation = useCreateAdminTrialEntryMutation(); + const fieldSet = React.useMemo( + () => resolveResultCreateFieldSet(event.trialRuleWindowId), + [event.trialRuleWindowId], + ); const initial = React.useMemo( - () => createAdminTrialEntryCreateDraft(event), - [event], + () => createAdminTrialEntryCreateDraft(event, fieldSet), + [event, fieldSet], ); const [draft, setDraft] = React.useState(initial); const [errorText, setErrorText] = React.useState(null); @@ -164,7 +169,7 @@ function ResultCreateForm({ await mutation.mutateAsync(parsed.request); toast.success(t("admin.trials.manage.resultCreate.success")); if (intent === "another") - setDraft(createAdminTrialEntryCreateDraft(event)); + setDraft(createAdminTrialEntryCreateDraft(event, fieldSet)); else router.replace(workspaceHref); } catch (error) { const code = @@ -215,6 +220,14 @@ function ResultCreateForm({ {formatDateForFinland(event.eventDate)} • {event.eventPlace} • SKL{" "} {event.sklKoeId}

+

+ {t(fieldSet.rulePeriodMessageKey)} +

+ {!fieldSet.verified ? ( +

+ {t("admin.trials.manage.resultCreate.ruleWindowWarning")} +

+ ) : null} @@ -245,6 +258,7 @@ function ResultCreateForm({ entry: update(current.entry), })) } + visibleFields={fieldSet.entryFields} /> { expect(html).toContain("Ryhmätuomari"); expect(html).toContain("Palkintotuomari"); }); + + it("uses create-only field visibility without changing the default edit view", () => { + const html = renderToStaticMarkup( + React.createElement(EntryMetaSection, { + entryDraft, + isPending: false, + onChange: vi.fn(), + visibleFields: + resolveResultCreateFieldSet("trw_post_20230801").entryFields, + }), + ); + + expect(html).not.toContain("Tie ja estetyöskentely"); + expect(html).not.toContain("Metsästysinto"); + expect(html).not.toContain("Ajotaito / yleisvaikutelma"); + expect(html).not.toContain(" { + it("uses the current-era Ajotaito label only in configured creation", () => { + const props = { + eras: [createEmptyEraDraft(1)], + isPending: false, + onAddEra: vi.fn(), + onRemoveEra: vi.fn(), + onChangeEraField: vi.fn(), + }; + + const createHtml = renderToStaticMarkup( + React.createElement(EraSection, { + ...props, + visibleFields: + resolveResultCreateFieldSet("trw_post_20230801").eraFields, + }), + ); + const editHtml = renderToStaticMarkup( + React.createElement(EraSection, props), + ); + + expect(createHtml).toContain("ajotaito"); + expect(createHtml).not.toContain("ajotaito / yleisvaikutelma"); + expect(editHtml).toContain("ajotaito / yleisvaikutelma"); + }); +}); diff --git a/apps/web/components/admin/trials/internal/entry-meta-section.tsx b/apps/web/components/admin/trials/internal/entry-meta-section.tsx index d9172d19..711725cb 100644 --- a/apps/web/components/admin/trials/internal/entry-meta-section.tsx +++ b/apps/web/components/admin/trials/internal/entry-meta-section.tsx @@ -6,15 +6,23 @@ type Props = { entryDraft: EntryDraft; isPending: boolean; onChange: (updater: (current: EntryDraft) => EntryDraft) => void; + visibleFields?: ReadonlySet; }; -export function EntryMetaSection({ entryDraft, isPending, onChange }: Props) { +export function EntryMetaSection({ + entryDraft, + isPending, + onChange, + visibleFields, +}: Props) { function updateField(field: keyof EntryDraft, value: string) { onChange((current) => ({ ...current, [field]: value, })); } + const visible = (field: keyof EntryDraft) => + !visibleFields || visibleFields.has(field); return (
@@ -158,8 +166,8 @@ export function EntryMetaSection({ entryDraft, isPending, onChange }: Props) {

Ansiopisteet

-
- + {visibleFields ? ( +
updateField("ajoajanPisteet", value)} /> - updateField("yva", value)} - /> - - updateField("haku", value)} /> - - updateField("hauk", value)} /> - - - updateField("ansiopisteetYhteensa", value)} - /> updateField("tja", value)} + onChange={(value) => updateField("yva", value)} /> + {visible("tja") ? ( + updateField("tja", value)} + /> + ) : null} + {visible("pin") ? ( + updateField("pin", value)} + /> + ) : null} updateField("pin", value)} + onChange={(value) => updateField("ansiopisteetYhteensa", value)} /> - -
+
+ ) : ( +
+ + + updateField("hyvaksytytAjominuutit", value) + } + /> + updateField("ajoajanPisteet", value)} + /> + updateField("yva", value)} + /> + + + updateField("haku", value)} + /> + + + updateField("hauk", value)} + /> + + + updateField("ansiopisteetYhteensa", value)} + /> + {visible("tja") ? ( + updateField("tja", value)} + /> + ) : null} + {visible("pin") ? ( + updateField("pin", value)} + /> + ) : null} + +
+ )}
diff --git a/apps/web/components/admin/trials/internal/era-section.tsx b/apps/web/components/admin/trials/internal/era-section.tsx index a7821a47..f1f597b6 100644 --- a/apps/web/components/admin/trials/internal/era-section.tsx +++ b/apps/web/components/admin/trials/internal/era-section.tsx @@ -14,6 +14,7 @@ type Props = { field: Exclude, value: string, ) => void; + visibleFields?: ReadonlySet>; }; export function EraSection({ @@ -22,6 +23,7 @@ export function EraSection({ onAddEra, onRemoveEra, onChangeEraField, + visibleFields, }: Props) { return ( <> @@ -64,32 +66,40 @@ export function EraSection({ "tja", "pin", ] as const - ).map((field) => ( -
))} diff --git a/apps/web/components/admin/trials/internal/lisatiedot-matrix.tsx b/apps/web/components/admin/trials/internal/lisatiedot-matrix.tsx index f977ee7e..11cba671 100644 --- a/apps/web/components/admin/trials/internal/lisatiedot-matrix.tsx +++ b/apps/web/components/admin/trials/internal/lisatiedot-matrix.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useI18n } from "@/hooks/i18n"; import { ADMIN_TRIAL_LISATIETO_GROUP_LABELS, type AdminTrialLisatietoInputKind, @@ -26,11 +27,12 @@ export function LisatiedotMatrix({ isPending, onChangeCell, }: Props) { + const { t } = useI18n(); function sanitizeValue( kind: AdminTrialLisatietoInputKind, rawValue: string, ): string { - if (kind === "marker") { + if (kind === "marker" || kind === "tri-state") { if (rawValue === "1" || rawValue === "0" || rawValue === "") { return rawValue; } @@ -98,7 +100,9 @@ export function LisatiedotMatrix({
- {row.osa ? `${row.koodi} ${row.osa}` : row.koodi} + {row.osa && !row.hideOsaSuffix + ? `${row.koodi} ${row.osa}` + : row.koodi} {row.label} @@ -107,7 +111,24 @@ export function LisatiedotMatrix({ {sortedEras.map((era) => ( - {row.inputKind === "marker" ? ( + {row.inputKind === "marker" && + row.useSemanticControl ? ( + + onChangeCell( + row.koodi, + row.osa, + era.era, + event.target.checked ? "1" : "", + ) + } + /> + ) : row.inputKind === "marker" || + row.inputKind === "tri-state" ? ( ) : ( diff --git a/apps/web/lib/admin/trials/__tests__/entry-create-model.test.ts b/apps/web/lib/admin/trials/__tests__/entry-create-model.test.ts index 3553c35f..602fa3c1 100644 --- a/apps/web/lib/admin/trials/__tests__/entry-create-model.test.ts +++ b/apps/web/lib/admin/trials/__tests__/entry-create-model.test.ts @@ -4,9 +4,11 @@ import { createAdminTrialEntryCreateDraft, toCreateAdminTrialEntryRequest, } from "../entry-create-model"; +import { resolveResultCreateFieldSet } from "../result-create-field-registry"; const event: AdminTrialEventDetails = { trialEventId: "event-1", + trialRuleWindowId: "trw_post_20230801", eventDate: "2026-07-21", eventPlace: "Helsinki", eventName: null, @@ -120,4 +122,24 @@ describe("manual trial entry create model", () => { ); } }); + + it("serializes only the resolved 2023+ rows through registry persistence mapping", () => { + const fieldSet = resolveResultCreateFieldSet(event.trialRuleWindowId); + const draft = createAdminTrialEntryCreateDraft(event, fieldSet); + draft.registrationNo = "FI43560/18"; + + expect( + draft.lisatiedotRows.some((row) => row.koodi === "25" && row.osa === "b"), + ).toBe(false); + const marker = draft.lisatiedotRows.find((row) => row.koodi === "10"); + if (marker) marker.eraValues[1] = "0"; + + const result = toCreateAdminTrialEntryRequest("event-1", draft); + expect(result).toMatchObject({ + ok: true, + request: { + lisatiedotRows: [], + }, + }); + }); }); diff --git a/apps/web/lib/admin/trials/__tests__/result-create-field-registry.test.ts b/apps/web/lib/admin/trials/__tests__/result-create-field-registry.test.ts new file mode 100644 index 00000000..fef710ab --- /dev/null +++ b/apps/web/lib/admin/trials/__tests__/result-create-field-registry.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vitest"; +import { + resolveResultCreateFieldSet, + SEEDED_TRIAL_RULE_WINDOW_IDS, +} from "../result-create-field-registry"; + +describe("result create field registry", () => { + it("registers the verified 2023+ field set from the persisted rule window", () => { + const fieldSet = resolveResultCreateFieldSet("trw_post_20230801"); + + expect(fieldSet.id).toBe("post-2023"); + expect(fieldSet.verified).toBe(true); + expect(fieldSet.entryFields.has("tja")).toBe(false); + expect(fieldSet.entryFields.has("pin")).toBe(false); + expect(fieldSet.eraFields.has("tja")).toBe(false); + expect(fieldSet.eraFields.has("pin")).toBe(false); + }); + + it("matches the verified 2023+ lisatieto codes, parts, order and kinds", () => { + const rows = resolveResultCreateFieldSet("trw_post_20230801").lisatiedot; + const keys = rows.map((row) => `${row.koodi}:${row.osa}`); + + expect(keys).toEqual([ + ...Array.from({ length: 15 }, (_, index) => `${index + 10}:`).slice( + 0, + 15, + ), + "25:a", + "26:", + "27:a", + ...Array.from({ length: 8 }, (_, index) => `${index + 30}:`), + ...Array.from({ length: 3 }, (_, index) => `${index + 40}:`), + ...Array.from({ length: 13 }, (_, index) => `${index + 50}:`), + ]); + expect( + Object.fromEntries( + rows + .filter((row) => ["19", "23", "26", "59"].includes(row.koodi)) + .map((row) => [row.koodi, row.inputKind]), + ), + ).toEqual({ + "19": "integer", + "23": "decimal", + "26": "decimal", + "59": "decimal", + }); + expect(rows.map((row) => row.persistenceOrder)).toEqual( + [...rows] + .map((row) => row.persistenceOrder) + .sort((left, right) => left - right), + ); + }); + + it("maps semantic marker state to persistence without exposing raw values", () => { + const marker = resolveResultCreateFieldSet( + "trw_post_20230801", + ).lisatiedot.find((row) => row.koodi === "10"); + + expect(marker?.inputKind).toBe("marker"); + expect(marker?.toPersistedValue("1")).toBe("1"); + expect(marker?.toPersistedValue("0")).toBe(""); + expect(marker?.toPersistedValue("")).toBe(""); + }); + + it.each([ + ...SEEDED_TRIAL_RULE_WINDOW_IDS.filter((id) => id !== "trw_post_20230801"), + null, + "unknown-window", + ])("uses the warned show-all fallback for %s", (trialRuleWindowId) => { + const fieldSet = resolveResultCreateFieldSet(trialRuleWindowId); + + expect(fieldSet.id).toBe("unverified-fallback"); + expect(fieldSet.verified).toBe(false); + expect(fieldSet.entryFields.has("tja")).toBe(true); + expect(fieldSet.eraFields.has("pin")).toBe(true); + expect( + fieldSet.lisatiedot.some((row) => row.koodi === "25" && row.osa === "b"), + ).toBe(true); + }); +}); diff --git a/apps/web/lib/admin/trials/entry-create-model.ts b/apps/web/lib/admin/trials/entry-create-model.ts index 6bc6fa36..d81ee93c 100644 --- a/apps/web/lib/admin/trials/entry-create-model.ts +++ b/apps/web/lib/admin/trials/entry-create-model.ts @@ -16,6 +16,7 @@ import { type EraDraft, type LisatietoRowDraft, } from "./entry-edit-dialog-model"; +import type { ResultCreateFieldSet } from "./result-create-field-registry"; export type AdminTrialEntryCreateDraft = { registrationNo: string; @@ -48,6 +49,7 @@ function emptyEntry(event: AdminTrialEventDetails): AdminTrialEventEntry { export function createAdminTrialEntryCreateDraft( event: AdminTrialEventDetails, + fieldSet?: ResultCreateFieldSet, ): AdminTrialEntryCreateDraft { const entry = emptyEntry(event); const eras = [createEmptyEraDraft(1)]; @@ -55,7 +57,7 @@ export function createAdminTrialEntryCreateDraft( registrationNo: "", entry: toEntryDraft(entry), eras, - lisatiedotRows: toLisatietoRows(entry, eras), + lisatiedotRows: toLisatietoRows(entry, eras, fieldSet?.lisatiedot), }; } @@ -166,7 +168,12 @@ export function toCreateAdminTrialEntryRequest( })), lisatiedotRows: draft.lisatiedotRows .filter((row) => - eras.some((era) => (row.eraValues[era.era] ?? "").trim().length > 0), + eras.some((era) => { + const controlValue = row.eraValues[era.era] ?? ""; + const persistedValue = + row.toPersistedValue?.(controlValue) ?? controlValue; + return persistedValue.trim().length > 0; + }), ) .map((row) => ({ koodi: row.koodi, @@ -175,7 +182,11 @@ export function toCreateAdminTrialEntryRequest( jarjestys: row.jarjestys, eraValues: eras.map((era) => ({ era: era.era, - arvo: parseNullableString(row.eraValues[era.era] ?? ""), + arvo: parseNullableString( + row.toPersistedValue?.(row.eraValues[era.era] ?? "") ?? + row.eraValues[era.era] ?? + "", + ), })), })), }, diff --git a/apps/web/lib/admin/trials/entry-edit-config.ts b/apps/web/lib/admin/trials/entry-edit-config.ts index 115ba8ea..d8dd3cfa 100644 --- a/apps/web/lib/admin/trials/entry-edit-config.ts +++ b/apps/web/lib/admin/trials/entry-edit-config.ts @@ -24,7 +24,8 @@ export type AdminTrialLisatietoInputKind = | "marker" | "integer" | "decimal" - | "text"; + | "text" + | "tri-state"; export type AdminTrialLisatietoConfig = { koodi: string; @@ -34,6 +35,10 @@ export type AdminTrialLisatietoConfig = { inputKind: AdminTrialLisatietoInputKind; sortOrder: number; persistenceOrder: number; + valueHint?: "marker" | "integer" | "decimal" | "text"; + toPersistedValue?: (controlValue: string) => string; + hideOsaSuffix?: boolean; + useSemanticControl?: boolean; }; function defineLisatieto( diff --git a/apps/web/lib/admin/trials/entry-edit-dialog-model.ts b/apps/web/lib/admin/trials/entry-edit-dialog-model.ts index 7a0af752..ca664719 100644 --- a/apps/web/lib/admin/trials/entry-edit-dialog-model.ts +++ b/apps/web/lib/admin/trials/entry-edit-dialog-model.ts @@ -4,6 +4,7 @@ import { getAdminTrialLisatietoConfig, type AdminTrialLisatietoGroup, type AdminTrialLisatietoInputKind, + type AdminTrialLisatietoConfig, } from "./entry-edit-config"; export type LisatietoRowDraft = { @@ -14,6 +15,10 @@ export type LisatietoRowDraft = { group: AdminTrialLisatietoGroup; label: string; inputKind: AdminTrialLisatietoInputKind; + valueHint?: "marker" | "integer" | "decimal" | "text"; + toPersistedValue?: (controlValue: string) => string; + hideOsaSuffix?: boolean; + useSemanticControl?: boolean; sortOrder: number; eraValues: Record; }; @@ -147,6 +152,7 @@ export function toEraDrafts(entry: AdminTrialEventEntry): EraDraft[] { export function toLisatietoRows( entry: AdminTrialEventEntry, eras: EraDraft[], + configs: readonly AdminTrialLisatietoConfig[] = ADMIN_TRIAL_LISATIETO_CONFIG, ): LisatietoRowDraft[] { const eraNumbers = eras.map((era) => era.era); const values = new Map(); @@ -175,7 +181,10 @@ export function toLisatietoRows( return existing; } - const config = getAdminTrialLisatietoConfig(input.koodi, input.osa); + const config = + configs.find( + (item) => item.koodi === input.koodi && item.osa === input.osa, + ) ?? getAdminTrialLisatietoConfig(input.koodi, input.osa); const parsedCode = Number.parseInt(input.koodi, 10); const row: LisatietoRowDraft = { koodi: input.koodi, @@ -185,6 +194,14 @@ export function toLisatietoRows( group: config?.group ?? "unknown", label: config?.label ?? input.nimi ?? "Tuntematon lisätieto", inputKind: config?.inputKind ?? "text", + valueHint: + config?.valueHint ?? + (config?.inputKind === "tri-state" + ? "marker" + : (config?.inputKind ?? "text")), + toPersistedValue: config?.toPersistedValue ?? ((value) => value), + hideOsaSuffix: config?.hideOsaSuffix, + useSemanticControl: config?.useSemanticControl, sortOrder: config?.sortOrder ?? (Number.isInteger(parsedCode) ? parsedCode : Number.MAX_SAFE_INTEGER), @@ -194,7 +211,7 @@ export function toLisatietoRows( return row; } - for (const config of ADMIN_TRIAL_LISATIETO_CONFIG) { + for (const config of configs) { ensureRow({ koodi: config.koodi, osa: config.osa, diff --git a/apps/web/lib/admin/trials/index.ts b/apps/web/lib/admin/trials/index.ts index 02c2b754..9ed98e13 100644 --- a/apps/web/lib/admin/trials/index.ts +++ b/apps/web/lib/admin/trials/index.ts @@ -1,6 +1,7 @@ export * from "./trial-route"; export * from "./submit-admin-trial-event-creation"; export * from "./entry-create-model"; +export * from "./result-create-field-registry"; export { createEmptyEraDraft, getNextEraNumber, diff --git a/apps/web/lib/admin/trials/result-create-field-registry.ts b/apps/web/lib/admin/trials/result-create-field-registry.ts new file mode 100644 index 00000000..8cdb1220 --- /dev/null +++ b/apps/web/lib/admin/trials/result-create-field-registry.ts @@ -0,0 +1,165 @@ +import { + ADMIN_TRIAL_LISATIETO_CONFIG, + type AdminTrialLisatietoConfig, + type AdminTrialLisatietoInputKind, +} from "./entry-edit-config"; +import type { EntryDraft, EraDraft } from "./entry-edit-dialog-model"; + +export type ResultCreateSemanticInputKind = + | AdminTrialLisatietoInputKind + | "tri-state"; + +export type ResultCreateValueHint = "marker" | "integer" | "decimal" | "text"; + +export type ResultCreateLisatietoField = AdminTrialLisatietoConfig & { + inputKind: ResultCreateSemanticInputKind; + valueHint: ResultCreateValueHint; + toPersistedValue: (controlValue: string) => string; +}; + +export type ResultCreateFieldSet = { + id: "post-2023" | "unverified-fallback"; + verified: boolean; + rulePeriodMessageKey: + | "admin.trials.manage.resultCreate.rulePeriod.post2023" + | "admin.trials.manage.resultCreate.rulePeriod.unverified"; + entryFields: ReadonlySet; + eraFields: ReadonlySet>; + lisatiedot: readonly ResultCreateLisatietoField[]; +}; + +export const SEEDED_TRIAL_RULE_WINDOW_IDS = [ + "trw_pre_20020801", + "trw_range_2002_2005", + "trw_range_2005_2011", + "trw_post_20110801", + "trw_post_20230801", +] as const; + +const ALL_ENTRY_FIELDS = new Set([ + "koemaasto", + "koemuoto", + "koetyyppi", + "ke", + "lk", + "award", + "rank", + "points", + "koiriaLuokassa", + "hyvaksytytAjominuutit", + "ajoajanPisteet", + "haku", + "hauk", + "yva", + "hlo", + "alo", + "tja", + "pin", + "ansiopisteetYhteensa", + "tappiopisteetYhteensa", + "judge", + "huomautus", + "huomautusTeksti", + "ylituomariNumeroSnapshot", + "ryhmatuomariNimi", + "palkintotuomariNimi", + "omistajaSnapshot", + "omistajanKotikuntaSnapshot", +]); + +const ALL_ERA_FIELDS = new Set>([ + "alkoi", + "hakumin", + "ajomin", + "haku", + "hauk", + "yva", + "hlo", + "alo", + "tja", + "pin", + "huomautusTeksti", +]); + +function identity(value: string): string { + return value; +} + +function toMarkerPersistence(value: string): string { + return value === "1" ? "1" : ""; +} + +function withSemantics( + config: AdminTrialLisatietoConfig, + inputKind: ResultCreateSemanticInputKind = config.inputKind, +): ResultCreateLisatietoField { + return { + ...config, + inputKind, + valueHint: + inputKind === "tri-state" + ? "marker" + : (inputKind as ResultCreateValueHint), + toPersistedValue: inputKind === "marker" ? toMarkerPersistence : identity, + useSemanticControl: true, + }; +} + +const FALLBACK_LISATIEDOT = ADMIN_TRIAL_LISATIETO_CONFIG.map((config) => + withSemantics(config), +); + +const POST_2023_INPUT_OVERRIDES: Readonly< + Record +> = { + "19": "integer", + "23": "decimal", + "26": "decimal", + "59": "decimal", +}; + +const POST_2023_LISATIETO_CODES = new Set([ + ...Array.from({ length: 18 }, (_, index) => String(index + 10)), + ...Array.from({ length: 13 }, (_, index) => String(index + 30)), + ...Array.from({ length: 13 }, (_, index) => String(index + 50)), +]); + +const POST_2023_LISATIEDOT = ADMIN_TRIAL_LISATIETO_CONFIG.filter( + (config) => + POST_2023_LISATIETO_CODES.has(config.koodi) && + (!(config.koodi === "25" || config.koodi === "27") || config.osa === "a"), +).map((config) => ({ + ...withSemantics(config, POST_2023_INPUT_OVERRIDES[config.koodi]), + hideOsaSuffix: config.koodi === "25" || config.koodi === "27", +})); + +const POST_2023_FIELD_SET: ResultCreateFieldSet = { + id: "post-2023", + verified: true, + rulePeriodMessageKey: "admin.trials.manage.resultCreate.rulePeriod.post2023", + entryFields: new Set( + [...ALL_ENTRY_FIELDS].filter((field) => field !== "tja" && field !== "pin"), + ), + eraFields: new Set( + [...ALL_ERA_FIELDS].filter((field) => field !== "tja" && field !== "pin"), + ), + lisatiedot: POST_2023_LISATIEDOT, +}; + +const UNVERIFIED_FALLBACK_FIELD_SET: ResultCreateFieldSet = { + id: "unverified-fallback", + verified: false, + rulePeriodMessageKey: + "admin.trials.manage.resultCreate.rulePeriod.unverified", + entryFields: ALL_ENTRY_FIELDS, + eraFields: ALL_ERA_FIELDS, + lisatiedot: FALLBACK_LISATIEDOT, +}; + +export function resolveResultCreateFieldSet( + trialRuleWindowId: string | null, +): ResultCreateFieldSet { + return trialRuleWindowId === "trw_post_20230801" + ? POST_2023_FIELD_SET + : UNVERIFIED_FALLBACK_FIELD_SET; +} diff --git a/apps/web/lib/i18n/messages/admin/trials/manage.ts b/apps/web/lib/i18n/messages/admin/trials/manage.ts index 82e3ad82..8cde11ac 100644 --- a/apps/web/lib/i18n/messages/admin/trials/manage.ts +++ b/apps/web/lib/i18n/messages/admin/trials/manage.ts @@ -68,6 +68,17 @@ export const fiAdminTrialsManageMessages = { "admin.trials.manage.resultCreate.backToWorkspace": "Takaisin tapahtuman sivulle", "admin.trials.manage.resultCreate.registration": "Rekisterinumero", + "admin.trials.manage.resultCreate.rulePeriod.post2023": "Säännöt: 1.8.2023 →", + "admin.trials.manage.resultCreate.rulePeriod.unverified": + "Säännöt: historiallinen tai määrittämätön sääntökausi", + "admin.trials.manage.resultCreate.ruleWindowWarning": + "Tämän sääntökauden kenttiä ei ole vielä tarkistettu pöytäkirjaa vasten. Lomake näyttää yhteensopivuuden vuoksi kaikki nykyiset kentät.", + "admin.trials.manage.resultCreate.valueHint.marker": "Valinta", + "admin.trials.manage.resultCreate.valueHint.integer": "Kokonaisluku", + "admin.trials.manage.resultCreate.valueHint.decimal": "Desimaaliluku", + "admin.trials.manage.resultCreate.valueHint.text": "Teksti", + "admin.trials.manage.resultCreate.triState.no": "Ei", + "admin.trials.manage.resultCreate.triState.yes": "Kyllä", "admin.trials.manage.resultCreate.saveAnother": "Tallenna ja lisää seuraava", "admin.trials.manage.resultCreate.saveFinish": "Tallenna ja lopeta", "admin.trials.manage.resultCreate.cancel": "Peruuta", @@ -271,6 +282,17 @@ export const svAdminTrialsManageMessages = { "admin.trials.manage.resultCreate.backToWorkspace": "Tillbaka till evenemangssidan", "admin.trials.manage.resultCreate.registration": "Registreringsnummer", + "admin.trials.manage.resultCreate.rulePeriod.post2023": "Regler: 1.8.2023 →", + "admin.trials.manage.resultCreate.rulePeriod.unverified": + "Regler: historisk eller obestämd regelperiod", + "admin.trials.manage.resultCreate.ruleWindowWarning": + "Fälten för den här regelperioden har ännu inte verifierats mot protokollet. Formuläret visar därför alla nuvarande fält för kompatibilitet.", + "admin.trials.manage.resultCreate.valueHint.marker": "Val", + "admin.trials.manage.resultCreate.valueHint.integer": "Heltal", + "admin.trials.manage.resultCreate.valueHint.decimal": "Decimaltal", + "admin.trials.manage.resultCreate.valueHint.text": "Text", + "admin.trials.manage.resultCreate.triState.no": "Nej", + "admin.trials.manage.resultCreate.triState.yes": "Ja", "admin.trials.manage.resultCreate.saveAnother": "Spara och lägg till nästa", "admin.trials.manage.resultCreate.saveFinish": "Spara och avsluta", "admin.trials.manage.resultCreate.cancel": "Avbryt", diff --git a/docs/features/admin-trial-management.md b/docs/features/admin-trial-management.md index 25c88de6..b8cbc8aa 100644 --- a/docs/features/admin-trial-management.md +++ b/docs/features/admin-trial-management.md @@ -17,6 +17,10 @@ and follow-up admin flow redesign). - An admin can create one manual result at a time from an event workspace. The full-page form supports saving another result for the same event or finishing back at the workspace. +- Manual result creation resolves its visible score, era, and lisätieto fields + from the event's persisted `trialRuleWindowId`. The 2023+ window is verified + against its PDF field set; older, null, and unknown windows retain the + complete compatibility form with a warning. ## Main files @@ -59,7 +63,8 @@ and follow-up admin flow redesign). - Event list response: paginated event summaries (`total`, `totalPages`, `page`, `filters`, `availableYears`, `items[]`). - Event detail response: one event (`event`) with event header fields and - selected dog rows (`entries[]`). + selected dog rows (`entries[]`). It includes the event-owned + `trialRuleWindowId`; clients must not recalculate the window from the date. - Event creation requires a positive `sklKoeId`, an ISO date, and a non-empty place. Duplicate SKL IDs return `SKL_KOE_ID_CONFLICT`. - Manual result identity is `SKL:|REG:` and @@ -69,6 +74,10 @@ and follow-up admin flow redesign). to occur only once across the submitted matrix rows. - Lisätieto UI sorting is separate from its integer persisted `jarjestys`; unused create-form rows are omitted from the write payload. +- The result-create field registry owns create-only visibility, ordering, + business grouping, semantic input kinds, localized value-hint categories, + and control-to-persistence mapping. Shared edit components use their default + complete field set and do not consume create-only configuration. - Manual-result validation errors may carry safe structured field context for localized feedback without exposing raw user-entered values. - `TrialEvent.koepaiva` is a PostgreSQL `DATE`; all trial contracts serialize @@ -88,6 +97,11 @@ and follow-up admin flow redesign). confirm internal navigation and use native unload protection for refresh or close. Browser Back leaves the form without application confirmation because the App Router has no reliable asynchronous route-blocking hook. +- The result form shows only a localized rule-period label. For + `trw_post_20230801`, entry- and era-level `tja`/`pin` are hidden, lisätieto + codes 25 and 27 expose only part `a`, and codes 19, 23, 26, and 59 use their + verified semantic input kinds. Other rule windows show the full compatibility + set and an unverified-field warning. - Selected dog rows have PDF, edit, and result-delete actions. - A missing workspace event is shown explicitly and never falls back to a different event. diff --git a/packages/contracts/admin/trials/manage/admin-trial-event-details.ts b/packages/contracts/admin/trials/manage/admin-trial-event-details.ts index f0380fa2..5a5a463d 100644 --- a/packages/contracts/admin/trials/manage/admin-trial-event-details.ts +++ b/packages/contracts/admin/trials/manage/admin-trial-event-details.ts @@ -66,6 +66,7 @@ export type AdminTrialEntryEraLisatieto = { }; export type AdminTrialEventDetails = AdminTrialEventSummary & { + trialRuleWindowId: string | null; entries: AdminTrialEventEntry[]; }; diff --git a/packages/db/admin/trials/manage/__tests__/get-trial-event-details.parity.test.ts b/packages/db/admin/trials/manage/__tests__/get-trial-event-details.parity.test.ts index 728dcf2e..db95aafb 100644 --- a/packages/db/admin/trials/manage/__tests__/get-trial-event-details.parity.test.ts +++ b/packages/db/admin/trials/manage/__tests__/get-trial-event-details.parity.test.ts @@ -37,6 +37,7 @@ describe("getAdminTrialEventDetailsDb parity", () => { it("maps event entries and decimal points", async () => { trialEventFindUniqueMock.mockResolvedValue({ id: "event-1", + trialRuleWindowId: "trw_post_20230801", sklKoeId: 1001, koepaiva: new Date("2026-03-01T00:00:00.000Z"), koekunta: "Helsinki", @@ -95,6 +96,7 @@ describe("getAdminTrialEventDetailsDb parity", () => { expect(result).toEqual({ trialEventId: "event-1", + trialRuleWindowId: "trw_post_20230801", eventDate: new Date("2026-03-01T00:00:00.000Z"), eventPlace: "Helsinki", eventName: "Talvikoe", diff --git a/packages/db/admin/trials/manage/get-trial-event-details.ts b/packages/db/admin/trials/manage/get-trial-event-details.ts index 6b113c76..fda1062c 100644 --- a/packages/db/admin/trials/manage/get-trial-event-details.ts +++ b/packages/db/admin/trials/manage/get-trial-event-details.ts @@ -23,6 +23,7 @@ export async function getAdminTrialEventDetailsDb( }, select: { id: true, + trialRuleWindowId: true, sklKoeId: true, koepaiva: true, koekunta: true, @@ -117,6 +118,7 @@ export async function getAdminTrialEventDetailsDb( return { trialEventId: row.id, + trialRuleWindowId: row.trialRuleWindowId, eventDate: row.koepaiva, eventPlace: row.koekunta, eventName: row.jarjestaja, diff --git a/packages/db/admin/trials/manage/types.ts b/packages/db/admin/trials/manage/types.ts index e86675a2..079d2d1b 100644 --- a/packages/db/admin/trials/manage/types.ts +++ b/packages/db/admin/trials/manage/types.ts @@ -99,6 +99,7 @@ export type AdminTrialEntryEraLisatietoDb = { export type AdminTrialEventDetailsDb = { trialEventId: string; + trialRuleWindowId: string | null; eventDate: Date; eventPlace: string; eventName: string | null; diff --git a/packages/server/admin/trials/manage/__tests__/get-trial-event.test.ts b/packages/server/admin/trials/manage/__tests__/get-trial-event.test.ts index 2bd98309..2f1f6b13 100644 --- a/packages/server/admin/trials/manage/__tests__/get-trial-event.test.ts +++ b/packages/server/admin/trials/manage/__tests__/get-trial-event.test.ts @@ -55,6 +55,7 @@ describe("getAdminTrialEvent", () => { it("maps event and entries from db", async () => { getAdminTrialEventDetailsDbMock.mockResolvedValue({ trialEventId: "event-1", + trialRuleWindowId: "trw_post_20230801", eventDate: new Date("2026-04-14T00:00:00.000Z"), eventPlace: "Helsinki", eventName: "Talvikoe", @@ -99,6 +100,7 @@ describe("getAdminTrialEvent", () => { data: { event: { trialEventId: "event-1", + trialRuleWindowId: "trw_post_20230801", eventDate: "2026-04-14", eventPlace: "Helsinki", eventName: "Talvikoe", diff --git a/packages/server/admin/trials/manage/get-trial-event.ts b/packages/server/admin/trials/manage/get-trial-event.ts index 053e043b..2c498b95 100644 --- a/packages/server/admin/trials/manage/get-trial-event.ts +++ b/packages/server/admin/trials/manage/get-trial-event.ts @@ -74,6 +74,7 @@ export async function getAdminTrialEvent( data: { event: { trialEventId: result.trialEventId, + trialRuleWindowId: result.trialRuleWindowId, eventDate: formatTrialDateOnly(result.eventDate), eventPlace: result.eventPlace, eventName: result.eventName, diff --git a/packages/server/trials/pdf/__tests__/get-trial-dog-pdf-data.test.ts b/packages/server/trials/pdf/__tests__/get-trial-dog-pdf-data.test.ts index 6cbe5d4b..6424e64d 100644 --- a/packages/server/trials/pdf/__tests__/get-trial-dog-pdf-data.test.ts +++ b/packages/server/trials/pdf/__tests__/get-trial-dog-pdf-data.test.ts @@ -131,6 +131,41 @@ describe("getTrialDogPdfDataService", () => { expect(result.body.data.trialRuleWindowId).toBe("trw_range_2005_2011"); }); + it("pivots only PDF-consumed part a for lisatieto codes 25 and 27", async () => { + const base = dbRow(); + getTrialDogPdfDataDbMock.mockResolvedValue( + dbRow({ + eras: [ + { + ...base.eras[0], + lisatiedot: [ + { koodi: "25", osa: "a", arvo: "2.5" }, + { koodi: "25", osa: "b", arvo: "99" }, + { koodi: "27", osa: "a", arvo: "18" }, + { koodi: "27", osa: "c", arvo: "88" }, + ], + }, + { + ...base.eras[1], + lisatiedot: [ + { koodi: "25", osa: "a", arvo: "3.5" }, + { koodi: "27", osa: "a", arvo: "20" }, + ], + }, + ], + }), + ); + + const result = await getTrialDogPdfDataService("entry-1"); + + expect(result.status).toBe(200); + if (!result.body.ok) throw new Error("Expected ok=true"); + expect(result.body.data.lisatiedotRows).toEqual([ + { koodi: "25", era1: "2.5", era2: "3.5" }, + { koodi: "27", era1: "18", era2: "20" }, + ]); + }); + it("derives accepted driving minutes and driving time points for legacy rows", async () => { getTrialDogPdfDataDbMock.mockResolvedValue(dbRow()); From e343831955439a952cf408382a41ff2ab76797b3 Mon Sep 17 00:00:00 2001 From: Aki Kuivas <91662678+asku1990@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:21:04 +0300 Subject: [PATCH 5/8] fix(trials): preserve score subsections in result creation --- .../__tests__/entry-meta-section.test.tsx | 7 +- .../trials/internal/entry-meta-section.tsx | 137 ++++++------------ 2 files changed, 49 insertions(+), 95 deletions(-) diff --git a/apps/web/components/admin/trials/internal/__tests__/entry-meta-section.test.tsx b/apps/web/components/admin/trials/internal/__tests__/entry-meta-section.test.tsx index 89698d53..6c99436b 100644 --- a/apps/web/components/admin/trials/internal/__tests__/entry-meta-section.test.tsx +++ b/apps/web/components/admin/trials/internal/__tests__/entry-meta-section.test.tsx @@ -63,6 +63,7 @@ describe("EntryMetaSection", () => { expect(html).toContain("Haukku"); expect(html).toContain("Ajotaito / yleisvaikutelma"); expect(html).toContain("Muut"); + expect(html.match(/
{ expect(html).not.toContain("Tie ja estetyöskentely"); expect(html).not.toContain("Metsästysinto"); expect(html).not.toContain("Ajotaito / yleisvaikutelma"); - expect(html).not.toContain("Ajo
"); + expect(html).toContain(">Haku"); + expect(html).toContain(">Haukku"); + expect(html).toContain(">Muut"); expect(html).toContain("Ajotaito"); expect(html).toContain("Ansiopisteet yhteensä"); }); diff --git a/apps/web/components/admin/trials/internal/entry-meta-section.tsx b/apps/web/components/admin/trials/internal/entry-meta-section.tsx index 711725cb..b30696b0 100644 --- a/apps/web/components/admin/trials/internal/entry-meta-section.tsx +++ b/apps/web/components/admin/trials/internal/entry-meta-section.tsx @@ -166,72 +166,9 @@ export function EntryMetaSection({

Ansiopisteet

- {visibleFields ? ( -
- updateField("hyvaksytytAjominuutit", value)} - /> - updateField("ajoajanPisteet", value)} - /> - updateField("haku", value)} - /> - updateField("hauk", value)} - /> - updateField("yva", value)} - /> - {visible("tja") ? ( - updateField("tja", value)} - /> - ) : null} - {visible("pin") ? ( - updateField("pin", value)} - /> - ) : null} - updateField("ansiopisteetYhteensa", value)} - /> -
- ) : ( -
- +
+ + {visible("hyvaksytytAjominuutit") ? ( + ) : null} + {visible("ajoajanPisteet") ? ( updateField("ajoajanPisteet", value)} /> + ) : null} + {visible("yva") ? ( updateField("yva", value)} /> - - + ) : null} + + + {visible("haku") ? ( updateField("haku", value)} /> - - + ) : null} + + + {visible("hauk") ? ( updateField("hauk", value)} /> - - + ) : null} + + + {visible("ansiopisteetYhteensa") ? ( updateField("ansiopisteetYhteensa", value)} /> - {visible("tja") ? ( - updateField("tja", value)} - /> - ) : null} - {visible("pin") ? ( - updateField("pin", value)} - /> - ) : null} - -
- )} + ) : null} + {visible("tja") ? ( + updateField("tja", value)} + /> + ) : null} + {visible("pin") ? ( + updateField("pin", value)} + /> + ) : null} +
+
From a9b5e721022644b4da4ea604a8afc3cdd489fbd0 Mon Sep 17 00:00:00 2001 From: Aki Kuivas <91662678+asku1990@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:41:37 +0300 Subject: [PATCH 6/8] fix(trials): preserve compatibility fallback behavior --- .../admin-trial-entry-create-page-client.tsx | 2 + .../__tests__/entry-meta-section.test.tsx | 19 ++++++ .../internal/__tests__/era-section.test.tsx | 18 +++++- .../__tests__/lisatiedot-matrix.test.tsx | 58 +++++++++++++++++++ .../trials/internal/entry-meta-section.tsx | 6 +- .../admin/trials/internal/era-section.tsx | 6 +- .../trials/internal/lisatiedot-matrix.tsx | 22 ++++--- .../__tests__/entry-create-model.test.ts | 22 +++++++ .../result-create-field-registry.test.ts | 22 ++++++- .../admin/trials/entry-edit-dialog-model.ts | 8 +-- .../trials/result-create-field-registry.ts | 16 +++-- 11 files changed, 171 insertions(+), 28 deletions(-) diff --git a/apps/web/components/admin/trials/admin-trial-entry-create-page-client.tsx b/apps/web/components/admin/trials/admin-trial-entry-create-page-client.tsx index 77af1c1d..e9876c1a 100644 --- a/apps/web/components/admin/trials/admin-trial-entry-create-page-client.tsx +++ b/apps/web/components/admin/trials/admin-trial-entry-create-page-client.tsx @@ -259,6 +259,7 @@ function ResultCreateForm({ })) } visibleFields={fieldSet.entryFields} + yvaLabel={fieldSet.yvaLabels?.entry} /> { onChange: vi.fn(), visibleFields: resolveResultCreateFieldSet("trw_post_20230801").entryFields, + yvaLabel: + resolveResultCreateFieldSet("trw_post_20230801").yvaLabels?.entry, }), ); @@ -97,4 +99,21 @@ describe("EntryMetaSection", () => { expect(html).toContain("Ajotaito"); expect(html).toContain("Ansiopisteet yhteensä"); }); + + it("keeps the generic label and all fields in compatibility fallback", () => { + const fieldSet = resolveResultCreateFieldSet(null); + const html = renderToStaticMarkup( + React.createElement(EntryMetaSection, { + entryDraft, + isPending: false, + onChange: vi.fn(), + visibleFields: fieldSet.entryFields, + yvaLabel: fieldSet.yvaLabels?.entry, + }), + ); + + expect(html).toContain("Ajotaito / yleisvaikutelma"); + expect(html).toContain("Tie ja estetyöskentely"); + expect(html).toContain("Metsästysinto"); + }); }); diff --git a/apps/web/components/admin/trials/internal/__tests__/era-section.test.tsx b/apps/web/components/admin/trials/internal/__tests__/era-section.test.tsx index 30c182a0..d2367c5d 100644 --- a/apps/web/components/admin/trials/internal/__tests__/era-section.test.tsx +++ b/apps/web/components/admin/trials/internal/__tests__/era-section.test.tsx @@ -6,7 +6,7 @@ import { resolveResultCreateFieldSet } from "@/lib/admin/trials/result-create-fi import { EraSection } from "../era-section"; describe("EraSection", () => { - it("uses the current-era Ajotaito label only in configured creation", () => { + it("uses the current-era Ajotaito label only in verified creation", () => { const props = { eras: [createEmptyEraDraft(1)], isPending: false, @@ -14,20 +14,32 @@ describe("EraSection", () => { onRemoveEra: vi.fn(), onChangeEraField: vi.fn(), }; + const verifiedFieldSet = resolveResultCreateFieldSet("trw_post_20230801"); const createHtml = renderToStaticMarkup( React.createElement(EraSection, { ...props, - visibleFields: - resolveResultCreateFieldSet("trw_post_20230801").eraFields, + visibleFields: verifiedFieldSet.eraFields, + yvaLabel: verifiedFieldSet.yvaLabels?.era, }), ); const editHtml = renderToStaticMarkup( React.createElement(EraSection, props), ); + const fallbackFieldSet = resolveResultCreateFieldSet(null); + const fallbackHtml = renderToStaticMarkup( + React.createElement(EraSection, { + ...props, + visibleFields: fallbackFieldSet.eraFields, + yvaLabel: fallbackFieldSet.yvaLabels?.era, + }), + ); expect(createHtml).toContain("ajotaito"); expect(createHtml).not.toContain("ajotaito / yleisvaikutelma"); expect(editHtml).toContain("ajotaito / yleisvaikutelma"); + expect(fallbackHtml).toContain("ajotaito / yleisvaikutelma"); + expect(fallbackHtml).toContain("tie ja estetyöskentely"); + expect(fallbackHtml).toContain("metsästysinto"); }); }); diff --git a/apps/web/components/admin/trials/internal/__tests__/lisatiedot-matrix.test.tsx b/apps/web/components/admin/trials/internal/__tests__/lisatiedot-matrix.test.tsx index 7ddff2ff..47ecc692 100644 --- a/apps/web/components/admin/trials/internal/__tests__/lisatiedot-matrix.test.tsx +++ b/apps/web/components/admin/trials/internal/__tests__/lisatiedot-matrix.test.tsx @@ -21,6 +21,17 @@ describe("LisatiedotMatrix", () => { sortOrder: 10, eraValues: { 1: "1", 2: "" }, }, + { + koodi: "17", + osa: "", + nimi: "Lämpötila °C", + jarjestys: 17, + group: "olosuhteet", + label: "Lämpötila °C", + inputKind: "integer", + sortOrder: 17, + eraValues: { 1: "5", 2: "" }, + }, { koodi: "19", osa: "", @@ -73,5 +84,52 @@ describe("LisatiedotMatrix", () => { expect(html).toContain("90 legacy"); expect(html).toContain("Legacy field"); expect(html).toContain('inputMode="text"'); + expect(html).toContain(" { + const rows: LisatietoRowDraft[] = [ + { + koodi: "10", + osa: "", + nimi: "Vaativat olosuhteet", + jarjestys: 10, + group: "olosuhteet", + label: "Vaativat olosuhteet", + inputKind: "marker", + valueHint: "marker", + useSemanticControl: true, + sortOrder: 10, + eraValues: { 1: "" }, + }, + { + koodi: "17", + osa: "", + nimi: "Lämpötila °C", + jarjestys: 17, + group: "olosuhteet", + label: "Lämpötila °C", + inputKind: "integer", + valueHint: "integer", + useSemanticControl: true, + sortOrder: 17, + eraValues: { 1: "" }, + }, + ]; + + const html = renderToStaticMarkup( + React.createElement(LisatiedotMatrix, { + eras: [createEmptyEraDraft(1)], + rows, + isPending: false, + onChangeCell: vi.fn(), + }), + ); + + expect(html).toContain('type="checkbox"'); + expect(html).not.toContain(" EntryDraft) => void; visibleFields?: ReadonlySet; + yvaLabel?: string; }; export function EntryMetaSection({ @@ -14,6 +15,7 @@ export function EntryMetaSection({ isPending, onChange, visibleFields, + yvaLabel, }: Props) { function updateField(field: keyof EntryDraft, value: string) { onChange((current) => ({ @@ -190,9 +192,7 @@ export function EntryMetaSection({ ) : null} {visible("yva") ? ( void; visibleFields?: ReadonlySet>; + yvaLabel?: string; }; export function EraSection({ @@ -24,6 +25,7 @@ export function EraSection({ onRemoveEra, onChangeEraField, visibleFields, + yvaLabel, }: Props) { return ( <> @@ -70,8 +72,8 @@ export function EraSection({ visibleFields && !visibleFields.has(field) ? null : (