);
}
diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/feeding-edit-dialog.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/feeding-edit-dialog.tsx
index 0b7b069..8824165 100644
--- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/feeding-edit-dialog.tsx
+++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/feeding-edit-dialog.tsx
@@ -25,7 +25,7 @@ import {
DialogSubmitBar,
EditTriggerButton,
hasAnyValue,
-} from "./edit-dialog-parts";
+} from "@/components/form/edit-dialog-parts";
import { toApiDate } from "@/lib/dates";
interface FieldErrors {
diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/inspection-edit-dialog.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/inspection-edit-dialog.tsx
index 89987ad..2e1a481 100644
--- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/inspection-edit-dialog.tsx
+++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/inspection-edit-dialog.tsx
@@ -38,7 +38,7 @@ import {
DialogSubmitBar,
EditTriggerButton,
hasAnyValue,
-} from "./edit-dialog-parts";
+} from "@/components/form/edit-dialog-parts";
function valuesFromInspection(inspection: Inspection): InspectionFormValues {
return {
diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/swarm-control-edit-dialog.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/swarm-control-edit-dialog.tsx
index 8aee524..7e5ccc5 100644
--- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/swarm-control-edit-dialog.tsx
+++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/swarm-control-edit-dialog.tsx
@@ -26,7 +26,7 @@ import {
DialogSubmitBar,
EditTriggerButton,
hasAnyValue,
-} from "./edit-dialog-parts";
+} from "@/components/form/edit-dialog-parts";
import { toApiDate } from "@/lib/dates";
export function SwarmControlEditDialog({ check }: { check: SwarmControlCheck }) {
diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/treatment-edit-dialog.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/treatment-edit-dialog.tsx
index cb77b8a..131f189 100644
--- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/treatment-edit-dialog.tsx
+++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/treatment-edit-dialog.tsx
@@ -39,7 +39,7 @@ import {
DialogSubmitBar,
EditTriggerButton,
hasAnyValue,
-} from "./edit-dialog-parts";
+} from "@/components/form/edit-dialog-parts";
import { toApiDate } from "@/lib/dates";
interface CatalogueState {
diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/varroa-edit-dialog.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/varroa-edit-dialog.tsx
index fdc1801..1b78920 100644
--- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/varroa-edit-dialog.tsx
+++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/varroa-edit-dialog.tsx
@@ -25,7 +25,7 @@ import {
DialogSubmitBar,
EditTriggerButton,
hasAnyValue,
-} from "./edit-dialog-parts";
+} from "@/components/form/edit-dialog-parts";
import { toApiDate } from "@/lib/dates";
import { VarroaPeriodField } from "@/components/varroa/varroa-period-field";
import { SampleBeesField } from "@/components/varroa/sample-bees-field";
diff --git a/ApiCore-Frontend/src/app/(app)/harvests/[id]/harvest-edit-dialog.spec.tsx b/ApiCore-Frontend/src/app/(app)/harvests/[id]/harvest-edit-dialog.spec.tsx
index 5b20260..7d7bc5a 100644
--- a/ApiCore-Frontend/src/app/(app)/harvests/[id]/harvest-edit-dialog.spec.tsx
+++ b/ApiCore-Frontend/src/app/(app)/harvests/[id]/harvest-edit-dialog.spec.tsx
@@ -5,6 +5,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fetchApiaries } from "@/lib/apiaries/api";
import { fetchColonies } from "@/lib/colonies/api";
import { updateHarvest } from "@/lib/harvests/api";
+import { ApiError } from "@/lib/auth/api-client";
+import { toast } from "sonner";
import { fetchActiveRestrictions } from "@/lib/compliance/restrictions-api";
import type { Harvest } from "@/lib/harvests/types";
import { HarvestEditDialog } from "./harvest-edit-dialog";
@@ -85,3 +87,96 @@ describe("HarvestEditDialog – Sperrbezirk", () => {
expect(within(dialog).queryByText(/Im Sperrbezirk gelten Auflagen/)).not.toBeInTheDocument();
});
});
+
+describe("HarvestEditDialog – Formular", () => {
+ beforeEach(() => {
+ vi.mocked(fetchApiaries).mockResolvedValue([
+ { id: "apiary-1", name: "Garten" },
+ ] as never);
+ vi.mocked(fetchColonies).mockResolvedValue([
+ { id: "colony-1", code: "V-01", status: "ACTIVE", apiaryId: "apiary-1" },
+ { id: "colony-2", code: "V-02", status: "ACTIVE", apiaryId: "apiary-1" },
+ ] as never);
+ vi.mocked(fetchActiveRestrictions).mockResolvedValue([]);
+ vi.mocked(updateHarvest).mockReset();
+ vi.mocked(updateHarvest).mockResolvedValue(harvest);
+ });
+
+ async function open() {
+ renderDialog();
+ await userEvent.click(screen.getByRole("button", { name: /Bearbeiten/ }));
+ return screen.findByRole("dialog");
+ }
+ const save = () => userEvent.click(screen.getByRole("button", { name: "Speichern" }));
+
+ it("marks what is required and what is optional, in 44 px fields", async () => {
+ const dialog = await open();
+
+ expect(within(dialog).getByLabelText(/^Datum/)).toHaveClass("h-11");
+ expect(within(dialog).getByLabelText(/^Datum/).closest("div")).toHaveTextContent("Pflicht");
+ expect(within(dialog).getByLabelText(/^Honigsorte/).closest("div")).toHaveTextContent("optional");
+ expect(within(dialog).getByLabelText(/^Nettogewicht/)).toHaveClass("h-11");
+ });
+
+ it("explains a missing colony at the colony list, focuses it and sends nothing", async () => {
+ const dialog = await open();
+ await userEvent.click(await within(dialog).findByRole("checkbox", { name: "V-01" }));
+ await save();
+
+ expect(within(dialog).getByText("Bitte mindestens ein Volk auswählen.")).toBeInTheDocument();
+ expect(toast.error).not.toHaveBeenCalled();
+ expect(within(dialog).getByRole("checkbox", { name: "V-01" })).toHaveFocus();
+ expect(updateHarvest).not.toHaveBeenCalled();
+ });
+
+ it("an emptied date is explained at the field and nothing is sent", async () => {
+ const dialog = await open();
+ const date = within(dialog).getByLabelText(/^Datum/);
+ await userEvent.clear(date);
+ await save();
+
+ expect(within(dialog).getByText("Bitte ein Datum angeben.")).toBeInTheDocument();
+ expect(date).toHaveFocus();
+ expect(updateHarvest).not.toHaveBeenCalled();
+ });
+
+ it("sends the same payload as before: emptied optional fields as null, zero stays zero", async () => {
+ const dialog = await open();
+ const net = within(dialog).getByLabelText(/^Nettogewicht/);
+ await userEvent.type(net, "0");
+ await save();
+
+ await vi.waitFor(() => expect(updateHarvest).toHaveBeenCalledTimes(1));
+ expect(vi.mocked(updateHarvest).mock.calls[0]).toEqual([
+ "h-1",
+ {
+ apiaryId: "apiary-1",
+ colonyIds: ["colony-1"],
+ date: expect.stringContaining("2026-08-01"),
+ honeyType: null,
+ grossWeight: null,
+ netWeight: 0,
+ honeySuperCount: null,
+ notes: null,
+ },
+ ]);
+ });
+
+ it("shows the server's explanation above the save button and keeps the dialog open", async () => {
+ vi.mocked(updateHarvest).mockRejectedValue(new ApiError(400, "Netto darf nicht über Brutto liegen."));
+ const dialog = await open();
+ await save();
+
+ expect(await within(dialog).findByRole("alert")).toHaveTextContent("Netto darf nicht über Brutto liegen.");
+ expect(toast.error).not.toHaveBeenCalled();
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
+ });
+
+ it("closes after saving and puts the focus back on the Bearbeiten button", async () => {
+ await open();
+ await save();
+
+ await vi.waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
+ expect(screen.getByRole("button", { name: /Bearbeiten/ })).toHaveFocus();
+ });
+});
diff --git a/ApiCore-Frontend/src/app/(app)/harvests/[id]/harvest-edit-dialog.tsx b/ApiCore-Frontend/src/app/(app)/harvests/[id]/harvest-edit-dialog.tsx
index 541208a..dd75e59 100644
--- a/ApiCore-Frontend/src/app/(app)/harvests/[id]/harvest-edit-dialog.tsx
+++ b/ApiCore-Frontend/src/app/(app)/harvests/[id]/harvest-edit-dialog.tsx
@@ -1,19 +1,25 @@
"use client";
-import { FormEvent, useState } from "react";
-import { Pencil } from "lucide-react";
+import { FormEvent, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { toast } from "sonner";
import { updateHarvest } from "@/lib/harvests/api";
import { emptyToNull } from "@/lib/forms/empty-to-null";
+import { errorMessage } from "@/lib/forms/error-message";
import { Harvest } from "@/lib/harvests/types";
import { fetchApiaries } from "@/lib/apiaries/api";
import { fetchColonies } from "@/lib/colonies/api";
-import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
+import { Field } from "@/components/form/field";
+import { FormSection } from "@/components/form/form-section";
import { RecordSelect } from "@/components/form/record-select";
+import { ToggleChip } from "@/components/form/toggle-chip";
+import {
+ DATE_REQUIRED_MESSAGE,
+ DialogSubmitBar,
+ EditTriggerButton,
+ hasAnyValue,
+} from "@/components/form/edit-dialog-parts";
import {
Dialog,
DialogContent,
@@ -24,6 +30,9 @@ import {
import { toApiDate } from "@/lib/dates";
import { HarvestRestrictionWarning } from "@/components/compliance/restriction-notice";
+const DATE_ID = "harvest-edit-date";
+const NO_COLONY_MESSAGE = "Bitte mindestens ein Volk auswählen.";
+
export function HarvestEditDialog({ harvest }: { harvest: Harvest }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
@@ -39,6 +48,10 @@ export function HarvestEditDialog({ harvest }: { harvest: Harvest }) {
harvest.honeySuperCount?.toString() ?? "",
);
const [notes, setNotes] = useState(harvest.notes ?? "");
+ const [dateError, setDateError] = useState