From ed484380ac8ba0cb6d20dc93719fb7de5f226dd6 Mon Sep 17 00:00:00 2001 From: Aki Kuivas <91662678+asku1990@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:54:50 +0300 Subject: [PATCH 1/3] feat(trials): redesign result creation workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Organize manual result creation into collapsible cards following the official trial PDF domain structure without copying its matrix layout. Add a searchable, grouped Lisätiedot workspace with responsive desktop and mobile editors, accessible focus handling, localized controls, and stable reset behavior. Keep R3A field visibility, draft ownership, serialization, validation, mutations, navigation protection, and editing behavior unchanged. Files: - CHANGELOG.md - apps/web/components/admin/trials/admin-trial-entry-create-page-client.tsx - apps/web/components/admin/trials/internal/__tests__/entry-meta-section.test.tsx - apps/web/components/admin/trials/internal/__tests__/era-section.test.tsx - apps/web/components/admin/trials/internal/__tests__/result-create-card.test.tsx - apps/web/components/admin/trials/internal/entry-meta-basic-fields.tsx - apps/web/components/admin/trials/internal/entry-meta-field.tsx - apps/web/components/admin/trials/internal/entry-meta-judge-fields.tsx - apps/web/components/admin/trials/internal/entry-meta-loss-fields.tsx - apps/web/components/admin/trials/internal/entry-meta-result-fields.tsx - apps/web/components/admin/trials/internal/entry-meta-score-fields.tsx - apps/web/components/admin/trials/internal/entry-meta-section.tsx - apps/web/components/admin/trials/internal/era-section.tsx - apps/web/components/admin/trials/internal/lisatiedot-workspace.tsx - apps/web/components/admin/trials/internal/result-create-card.tsx - apps/web/components/ui/sheet.tsx - apps/web/hooks/admin/trials/manage/__tests__/use-result-create-workspace-mobile.test.ts - apps/web/hooks/admin/trials/manage/use-result-create-workspace-mobile.ts - apps/web/lib/admin/trials/__tests__/lisatiedot-workspace-state.test.ts - apps/web/lib/admin/trials/__tests__/result-create-field-registry.test.ts - apps/web/lib/admin/trials/entry-edit-config.ts - apps/web/lib/admin/trials/lisatiedot-workspace-state.ts - apps/web/lib/admin/trials/result-create-field-registry.ts - apps/web/lib/i18n/messages/admin/trials/manage.ts - docs/features/admin-trial-management.md Ref BEJ-103 --- CHANGELOG.md | 1 + .../admin-trial-entry-create-page-client.tsx | 331 ++++++++++++-- .../__tests__/entry-meta-section.test.tsx | 51 +++ .../internal/__tests__/era-section.test.tsx | 41 ++ .../__tests__/result-create-card.test.tsx | 37 ++ .../internal/entry-meta-basic-fields.tsx | 104 +++++ .../trials/internal/entry-meta-field.tsx | 39 ++ .../internal/entry-meta-judge-fields.tsx | 58 +++ .../internal/entry-meta-loss-fields.tsx | 53 +++ .../internal/entry-meta-result-fields.tsx | 114 +++++ .../internal/entry-meta-score-fields.tsx | 147 +++++++ .../trials/internal/entry-meta-section.tsx | 391 +++-------------- .../admin/trials/internal/era-section.tsx | 27 +- .../trials/internal/lisatiedot-workspace.tsx | 407 ++++++++++++++++++ .../trials/internal/result-create-card.tsx | 53 +++ apps/web/components/ui/sheet.tsx | 4 +- ...use-result-create-workspace-mobile.test.ts | 108 +++++ .../use-result-create-workspace-mobile.ts | 20 + .../lisatiedot-workspace-state.test.ts | 127 ++++++ .../result-create-field-registry.test.ts | 104 +++++ .../web/lib/admin/trials/entry-edit-config.ts | 10 + .../trials/lisatiedot-workspace-state.ts | 116 +++++ .../trials/result-create-field-registry.ts | 89 ++++ .../lib/i18n/messages/admin/trials/manage.ts | 52 +++ docs/features/admin-trial-management.md | 15 + 25 files changed, 2118 insertions(+), 381 deletions(-) create mode 100644 apps/web/components/admin/trials/internal/__tests__/result-create-card.test.tsx create mode 100644 apps/web/components/admin/trials/internal/entry-meta-basic-fields.tsx create mode 100644 apps/web/components/admin/trials/internal/entry-meta-field.tsx create mode 100644 apps/web/components/admin/trials/internal/entry-meta-judge-fields.tsx create mode 100644 apps/web/components/admin/trials/internal/entry-meta-loss-fields.tsx create mode 100644 apps/web/components/admin/trials/internal/entry-meta-result-fields.tsx create mode 100644 apps/web/components/admin/trials/internal/entry-meta-score-fields.tsx create mode 100644 apps/web/components/admin/trials/internal/lisatiedot-workspace.tsx create mode 100644 apps/web/components/admin/trials/internal/result-create-card.tsx create mode 100644 apps/web/hooks/admin/trials/manage/__tests__/use-result-create-workspace-mobile.test.ts create mode 100644 apps/web/hooks/admin/trials/manage/use-result-create-workspace-mobile.ts create mode 100644 apps/web/lib/admin/trials/__tests__/lisatiedot-workspace-state.test.ts create mode 100644 apps/web/lib/admin/trials/lisatiedot-workspace-state.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 191007c0..56fc2658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ This project uses a user-facing changelog format. ### Changed +- Ajokoetuloksen lisäyslomakkeen avattavat kortit noudattavat virallisen pöytäkirjan aihealueita ilman pöytäkirjan taulukkoasettelun kopiointia. Lisätietoja voi hakea ja muokata pöytäkirjan mukaisessa ryhmäjärjestyksessä myös mobiilissa. - 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. 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 e9876c1a..8c46ddbb 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 @@ -28,9 +28,18 @@ import { } from "@/queries/admin/trials"; import { EntryMetaSection } from "./internal/entry-meta-section"; import { EraSection } from "./internal/era-section"; -import { LisatiedotMatrix } from "./internal/lisatiedot-matrix"; +import { LisatiedotWorkspace } from "./internal/lisatiedot-workspace"; +import { ResultCreateCard } from "./internal/result-create-card"; type SaveIntent = "another" | "finish"; +type CardId = + | "basic" + | "time" + | "merit" + | "loss" + | "result" + | "judges" + | "additional"; export function AdminTrialEntryCreatePageClient({ trialEventId, @@ -123,6 +132,10 @@ function ResultCreateForm({ [event, fieldSet], ); const [draft, setDraft] = React.useState(initial); + const [openCards, setOpenCards] = React.useState>( + new Set(["basic"]), + ); + const [workspaceResetKey, setWorkspaceResetKey] = React.useState(0); const [errorText, setErrorText] = React.useState(null); const submittingRef = React.useRef(false); const dirty = !areAdminTrialEntryCreateDraftsEqual(initial, draft); @@ -157,6 +170,17 @@ function ResultCreateForm({ setErrorText(null); const parsed = toCreateAdminTrialEntryRequest(event.trialEventId, draft); if (!parsed.ok) { + setOpenCards( + (current) => + new Set([ + ...current, + parsed.section === "registration" + ? "basic" + : parsed.section === "eras" + ? "time" + : "result", + ]), + ); setErrorText( t( `admin.trials.manage.resultCreate.validation.${parsed.section}` as never, @@ -168,9 +192,11 @@ function ResultCreateForm({ try { await mutation.mutateAsync(parsed.request); toast.success(t("admin.trials.manage.resultCreate.success")); - if (intent === "another") + if (intent === "another") { setDraft(createAdminTrialEntryCreateDraft(event, fieldSet)); - else router.replace(workspaceHref); + setOpenCards(new Set(["basic"])); + setWorkspaceResetKey((current) => current + 1); + } else router.replace(workspaceHref); } catch (error) { const code = error instanceof AdminMutationError ? error.errorCode : undefined; @@ -192,6 +218,7 @@ function ResultCreateForm({ ? (error.details as AdminTrialEntryValidationIssue | undefined) : undefined; if (issue?.area === "additional_info" && issue.koodi) { + setOpenCards((current) => new Set([...current, "additional"])); const row = `${issue.koodi}${issue.osa ? issue.osa : ""}`; const issueKey = issue.reason === "invalid_lisatieto_order" @@ -199,6 +226,18 @@ function ResultCreateForm({ : "admin.trials.manage.resultCreate.error.additionalInfoRow"; setErrorText(`${t(issueKey)}: ${row}.`); } else { + const errorCard: Partial> = { + INVALID_REGISTRATION_NUMBER: "basic", + TRIAL_ENTRY_REGISTRATION_CONFLICT: "basic", + INVALID_TRIAL_ENTRY: "result", + INVALID_TRIAL_ERAS: "time", + INVALID_TRIAL_ADDITIONAL_INFO: "additional", + }; + if (code && errorCard[code]) { + setOpenCards( + (current) => new Set([...current, errorCard[code] as CardId]), + ); + } setErrorText( t( `admin.trials.manage.resultCreate.error.${map[code ?? ""] ?? "generic"}` as never, @@ -210,6 +249,50 @@ function ResultCreateForm({ } } + function toggleCard(id: CardId) { + setOpenCards((current) => { + const next = new Set(current); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + } + const displaySummary = (...values: string[]) => + values + .filter((value) => value.trim()) + .slice(0, 3) + .join(" • ") || t("admin.trials.manage.resultCreate.summary.empty"); + const entryGroups = Object.fromEntries( + fieldSet.presentationGroups.map((group) => [ + group.id, + new Set(group.entryFields), + ]), + ); + const eraGroups = Object.fromEntries( + fieldSet.presentationGroups.map((group) => [ + group.id, + new Set(group.eraFields), + ]), + ); + const changeEntry = ( + update: (current: typeof draft.entry) => typeof draft.entry, + ) => + setDraft((current) => ({ + ...current, + entry: update(current.entry), + })); + const changeEra = ( + era: number, + field: Exclude, + value: string, + ) => + setDraft((current) => ({ + ...current, + eras: current.eras.map((item) => + item.era === era ? { ...item, [field]: value } : item, + ), + })); + return (
@@ -229,13 +312,27 @@ function ResultCreateForm({

) : null}
- - - {errorText ? ( -

- {errorText} -

- ) : null} + {errorText ? ( +

+ {errorText} +

+ ) : null} + toggleCard("basic")} + > +
+
+ toggleCard("time")} + > +
- setDraft((current) => ({ - ...current, - eras: current.eras.map((item) => - item.era === era ? { ...item, [field]: value } : item, - ), - })) - } - visibleFields={fieldSet.eraFields} + onChangeEraField={changeEra} + visibleFields={eraGroups.time} yvaLabel={fieldSet.yvaLabels?.era} /> - +
+
+ toggleCard("merit")} + > +
+ - setDraft((current) => ({ - ...current, - lisatiedotRows: current.lisatiedotRows.map((row) => - row.koodi === koodi && row.osa === osa - ? { ...row, eraValues: { ...row.eraValues, [era]: value } } - : row, - ), - })) - } + onAddEra={addEra} + onRemoveEra={removeEra} + onChangeEraField={changeEra} + visibleFields={eraGroups.merit} + yvaLabel={fieldSet.yvaLabels?.era} + showControls={false} + /> + +
+
+ toggleCard("loss")} + > +
+ + +
+
+ toggleCard("result")} + > +
+ + {eraGroups.result.size ? ( + + ) : null} +
+
+ toggleCard("judges")} + > + + + Object.values(row.eraValues).some(Boolean)).length} ${t("admin.trials.manage.resultCreate.summary.selected")}`} + open={openCards.has("additional")} + onToggle={() => toggleCard("additional")} + > + + setDraft((current) => ({ + ...current, + lisatiedotRows: current.lisatiedotRows.map((row) => + row.koodi === koodi && row.osa === osa + ? { ...row, eraValues: { ...row.eraValues, [era]: value } } + : row, + ), + })) + } + onRemoveRow={(koodi, osa) => + setDraft((current) => ({ + ...current, + lisatiedotRows: current.lisatiedotRows.map((row) => + row.koodi === koodi && row.osa === osa + ? { + ...row, + eraValues: Object.fromEntries( + Object.keys(row.eraValues).map((era) => [era, ""]), + ), + } + : row, + ), + })) + } + /> + + +