Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
70b28ad
feat/create-trials-planning
asku1990 Jul 21, 2026
29fd214
docs(trials): limit E2 rule-window resolution to creation
asku1990 Jul 21, 2026
c519d26
feat(admin-trials): add dedicated event workspace
asku1990 Jul 21, 2026
8f25a9e
chore(admin-trials): remove redundant workspace
asku1990 Jul 21, 2026
d7cc920
feat(admin-trials): add event creation and empty-event lifecycle
asku1990 Jul 21, 2026
77b93a7
Merge pull request #341 from asku1990/feature/bej-103-e2-trial-event-…
asku1990 Jul 22, 2026
63c0e3b
trial entry planning docs
asku1990 Jul 22, 2026
248cae8
fix r1 and r2 planning
asku1990 Jul 22, 2026
b4d8a63
feat(trials): add manual admin result creation
asku1990 Jul 22, 2026
ef0d0b0
fix(trials): harden manual entry validation and conflict detection
asku1990 Jul 22, 2026
eaef912
refactor(trials): remove obsolete registration helper
asku1990 Jul 22, 2026
8fa7c3f
Merge pull request #342 from asku1990/feature/bej-103-r1-manual-resul…
asku1990 Jul 22, 2026
598871d
feat(admin-trials): add event-scoped manual result entry workflow
asku1990 Jul 22, 2026
1242ca0
fix(admin-trials): simplify unsaved navigation guard
asku1990 Jul 23, 2026
ba92e86
fix(admin-trials): reject oversized integer input
asku1990 Jul 23, 2026
db0a291
Merge pull request #343 from asku1990/feat/bej-103-result-creation-r2
asku1990 Jul 23, 2026
d68b6c5
docs/bej-103-creation-planning
asku1990 Jul 23, 2026
873c294
docs(trials): refine rule-window creation UX plans
asku1990 Jul 23, 2026
6298f1f
docs(trials): clarify reusable creation UX scope
asku1990 Jul 23, 2026
ae771cc
feat(trials): make result creation rule-window aware
asku1990 Jul 23, 2026
e343831
fix(trials): preserve score subsections in result creation
asku1990 Jul 23, 2026
a9b5e72
fix(trials): preserve compatibility fallback behavior
asku1990 Jul 23, 2026
7d8c20b
docs(trials): document rule-window behavior
asku1990 Jul 23, 2026
ce713bc
fic docs
asku1990 Jul 23, 2026
2b8abf2
Merge pull request #344 from asku1990/feat/bej-103-rule-window-aware-…
asku1990 Jul 23, 2026
ed48438
feat(trials): redesign result creation workspace
asku1990 Jul 23, 2026
4bb2992
fix(trials): simplify Lisätiedot workspace
asku1990 Aug 2, 2026
b2529ce
test(trials): cover simplified Lisätiedot workspace
asku1990 Aug 2, 2026
d197f9a
Merge pull request #345 from asku1990/codex/bej-103-r3b-result-creati…
asku1990 Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,24 @@ This project uses a user-facing changelog format.

### Added

- Ylläpitäjä voi avata ajokoetapahtuman omalle sivulleen tarkastelua ja nykyisiä muokkaustoimintoja varten.
- Ylläpitäjä voi luoda ajokoetapahtuman perustietoineen ja avata sen omalle sivulleen ennen koetulosten lisäämistä.
- Ylläpitäjä voi poistaa tyhjän ajokoetapahtuman sen omalta sivulta.
- Ylläpitäjä voi lisätä ajokoetapahtumaan koirakohtaisia tuloksia yksi kerrallaan ja jatkaa seuraavaan tulokseen tai palata tapahtuman sivulle.

### Changed

- Ajokoetuloksen lisäyslomakkeen avattavat kortit noudattavat virallisen pöytäkirjan aihealueita ilman pöytäkirjan taulukkoasettelun kopiointia. Lisätietoja muokataan pöytäkirjan mukaisissa yksittäin avattavissa ryhmissä 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.

### Fixed

- Ajokoetuloksen lisätietorivit tallennetaan kelvollisessa kokonaislukujärjestyksessä, ja hylätty lisätietorivi näytetään aiempaa tarkemmin.
- Ajokoetuloksen lisäyslomakkeen selaimen Takaisin-toiminto ei enää yritä perua navigointia epäluotettavalla edestakaisella sivuhistorian käsittelyllä.
- Ylisuuria kokonaislukuarvoja ei enää tallenneta tyhjinä ajokoetuloksen luonnissa tai muokkauksessa.

### Removed

## [0.19.0] - 2026-07-20
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
import AdminTrialEventWorkspacePage from "../page";

vi.mock("@/components/admin/trials", () => ({
AdminTrialEventWorkspacePageClient: ({
trialEventId,
}: {
trialEventId: string;
}) => React.createElement("div", null, trialEventId),
}));

describe("AdminTrialEventWorkspacePage", () => {
it("forwards the route trial event id", async () => {
const page = await AdminTrialEventWorkspacePage({
params: Promise.resolve({ trialEventId: "event-1" }),
});

expect(renderToStaticMarkup(page)).toContain("event-1");
});
});
11 changes: 11 additions & 0 deletions apps/web/app/(admin)/admin/trials/[trialEventId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AdminTrialEventWorkspacePageClient } from "@/components/admin/trials";

export default async function AdminTrialEventWorkspacePage({
params,
}: {
params: Promise<{ trialEventId: string }>;
}) {
const { trialEventId } = await params;

return <AdminTrialEventWorkspacePageClient trialEventId={trialEventId} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
import Page from "../page";

vi.mock(
"@/components/admin/trials/admin-trial-entry-create-page-client",
() => ({
AdminTrialEntryCreatePageClient: ({
trialEventId,
}: {
trialEventId: string;
}) => React.createElement("div", null, trialEventId),
}),
);

describe("manual trial result create page", () => {
it("passes the route event id to the client workflow", async () => {
const html = renderToStaticMarkup(
await Page({ params: Promise.resolve({ trialEventId: "event-1" }) }),
);
expect(html).toContain("event-1");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { AdminTrialEntryCreatePageClient } from "@/components/admin/trials/admin-trial-entry-create-page-client";

export default async function AdminTrialEntryCreatePage({
params,
}: {
params: Promise<{ trialEventId: string }>;
}) {
const { trialEventId } = await params;
return <AdminTrialEntryCreatePageClient trialEventId={trialEventId} />;
}
32 changes: 32 additions & 0 deletions apps/web/app/(admin)/admin/trials/new/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
import AdminTrialEventCreatePage from "../page";

const { clientPropsRef, toBusinessDateInputValueMock } = vi.hoisted(() => ({
clientPropsRef: { current: null as { initialEventDate: string } | null },
toBusinessDateInputValueMock: vi.fn(() => "2026-07-21"),
}));

vi.mock("@/components/admin/trials", () => ({
AdminTrialEventCreatePageClient: (props: { initialEventDate: string }) => {
clientPropsRef.current = props;
return React.createElement("div", null, "create-trial-event");
},
}));

vi.mock("@/lib/admin/core/date", () => ({
toBusinessDateInputValue: toBusinessDateInputValueMock,
}));

describe("AdminTrialEventCreatePage", () => {
it("renders the event creation client", () => {
expect(renderToStaticMarkup(<AdminTrialEventCreatePage />)).toContain(
"create-trial-event",
);
expect(toBusinessDateInputValueMock).toHaveBeenCalledWith(expect.any(Date));
expect(clientPropsRef.current).toEqual({
initialEventDate: "2026-07-21",
});
});
});
10 changes: 10 additions & 0 deletions apps/web/app/(admin)/admin/trials/new/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { AdminTrialEventCreatePageClient } from "@/components/admin/trials";
import { toBusinessDateInputValue } from "@/lib/admin/core/date";

export default function AdminTrialEventCreatePage() {
return (
<AdminTrialEventCreatePageClient
initialEventDate={toBusinessDateInputValue(new Date())}
/>
);
}
1 change: 1 addition & 0 deletions apps/web/app/actions/admin/trials/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { deleteAdminTrialEntryAction } from "./manage";
export { updateAdminTrialEventAction } from "./manage";
export { updateAdminTrialEntryAction } from "./manage";
export { createAdminTrialEntryAction } from "./manage";
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createAdminTrialEntryAction } from "../create-admin-trial-entry";

const { guard, user, create } = vi.hoisted(() => ({
guard: vi.fn(),
user: vi.fn(),
create: vi.fn(),
}));
vi.mock("@/lib/server/admin-guard", () => ({
requireAdminLayoutAccess: guard,
}));
vi.mock("@/lib/server/current-user", () => ({ getSessionCurrentUser: user }));
vi.mock("@beagle/server", () => ({ createAdminTrialEntry: create }));

const input = {
trialEventId: "event-1",
registrationNo: "FI1/20",
entry: {} as never,
eras: [],
lisatiedotRows: [],
};

describe("createAdminTrialEntryAction", () => {
beforeEach(() => {
guard.mockReset();
user.mockReset();
create.mockReset();
});

it("rejects missing admin access", async () => {
guard.mockResolvedValue({ ok: false, status: 403 });
await expect(createAdminTrialEntryAction(input)).resolves.toMatchObject({
hasError: true,
errorCode: "FORBIDDEN",
});
});

it("passes through stable service errors", async () => {
guard.mockResolvedValue({ ok: true });
user.mockResolvedValue({ id: "u1", email: "a@example.com", role: "ADMIN" });
create.mockResolvedValue({
status: 409,
body: {
ok: false,
code: "TRIAL_ENTRY_REGISTRATION_CONFLICT",
error: "Conflict",
},
});
await expect(createAdminTrialEntryAction(input)).resolves.toEqual({
data: null,
hasError: true,
errorCode: "TRIAL_ENTRY_REGISTRATION_CONFLICT",
message: "Conflict",
});
});

it("passes through safe validation details", async () => {
guard.mockResolvedValue({ ok: true });
user.mockResolvedValue({ id: "u1", email: "a@example.com", role: "ADMIN" });
create.mockResolvedValue({
status: 400,
body: {
ok: false,
code: "INVALID_TRIAL_ADDITIONAL_INFO",
error: "Invalid",
details: {
area: "additional_info",
reason: "invalid_lisatieto_order",
koodi: "25",
osa: "b",
},
},
});
await expect(createAdminTrialEntryAction(input)).resolves.toMatchObject({
validationIssue: {
reason: "invalid_lisatieto_order",
koodi: "25",
osa: "b",
},
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createAdminTrialEventAction } from "../create-admin-trial-event";

const { guardMock, userMock, createMock } = vi.hoisted(() => ({
guardMock: vi.fn(),
userMock: vi.fn(),
createMock: vi.fn(),
}));

vi.mock("@/lib/server/admin-guard", () => ({
requireAdminLayoutAccess: guardMock,
}));
vi.mock("@/lib/server/current-user", () => ({
getSessionCurrentUser: userMock,
}));
vi.mock("@beagle/server", () => ({ createAdminTrialEvent: createMock }));

const input = {
eventDate: "2026-07-21",
eventPlace: "Helsinki",
jarjestaja: null,
ylituomari: null,
ylituomariNumero: null,
ytKertomus: null,
kennelpiiri: null,
kennelpiirinro: null,
sklKoeId: 123,
};

describe("createAdminTrialEventAction", () => {
beforeEach(() => {
guardMock.mockReset();
userMock.mockReset();
createMock.mockReset();
});

it("rejects missing admin access", async () => {
guardMock.mockResolvedValue({ ok: false, status: 403 });

await expect(createAdminTrialEventAction(input)).resolves.toMatchObject({
hasError: true,
errorCode: "FORBIDDEN",
});
expect(createMock).not.toHaveBeenCalled();
});

it("propagates service errors", async () => {
guardMock.mockResolvedValue({ ok: true });
userMock.mockResolvedValue({
id: "u_1",
email: "admin@example.com",
role: "ADMIN",
});
createMock.mockResolvedValue({
status: 409,
body: { ok: false, code: "SKL_KOE_ID_CONFLICT", error: "Conflict" },
});

await expect(createAdminTrialEventAction(input)).resolves.toEqual({
data: null,
hasError: true,
errorCode: "SKL_KOE_ID_CONFLICT",
message: "Conflict",
});
});

it("returns navigation data", async () => {
guardMock.mockResolvedValue({ ok: true });
userMock.mockResolvedValue({
id: "u_1",
email: "admin@example.com",
role: "ADMIN",
});
createMock.mockResolvedValue({
status: 201,
body: { ok: true, data: { trialEventId: "event-1" } },
});

await expect(createAdminTrialEventAction(input)).resolves.toEqual({
data: { trialEventId: "event-1" },
hasError: false,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { deleteAdminTrialEventAction } from "../delete-admin-trial-event";

const { guardMock, userMock, deleteMock } = vi.hoisted(() => ({
guardMock: vi.fn(),
userMock: vi.fn(),
deleteMock: vi.fn(),
}));

vi.mock("@/lib/server/admin-guard", () => ({
requireAdminLayoutAccess: guardMock,
}));
vi.mock("@/lib/server/current-user", () => ({
getSessionCurrentUser: userMock,
}));
vi.mock("@beagle/server", () => ({ deleteAdminTrialEvent: deleteMock }));

describe("deleteAdminTrialEventAction", () => {
beforeEach(() => {
guardMock.mockReset();
userMock.mockReset();
deleteMock.mockReset();
guardMock.mockResolvedValue({ ok: true });
userMock.mockResolvedValue({
id: "u_1",
email: "admin@example.com",
role: "ADMIN",
});
});

it("propagates the non-empty conflict", async () => {
deleteMock.mockResolvedValue({
status: 409,
body: { ok: false, code: "TRIAL_EVENT_NOT_EMPTY", error: "Not empty" },
});

await expect(
deleteAdminTrialEventAction({ trialEventId: "event-1" }),
).resolves.toEqual({
data: null,
hasError: true,
errorCode: "TRIAL_EVENT_NOT_EMPTY",
message: "Not empty",
});
});

it("returns the deleted event id", async () => {
deleteMock.mockResolvedValue({
status: 200,
body: { ok: true, data: { deletedTrialEventId: "event-1" } },
});

await expect(
deleteAdminTrialEventAction({ trialEventId: "event-1" }),
).resolves.toEqual({
data: { deletedTrialEventId: "event-1" },
hasError: false,
});
});
});
Loading
Loading