From b7e75c002321e309558a0033a049041cecc6c054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 12 Jan 2026 13:52:31 +0100 Subject: [PATCH 1/6] Add start and end date validation for cultivation harvest bulk action --- .../app/components/blocks/harvest/form.tsx | 88 ++++++++++++++----- ...farm.$calendar.rotation.harvest._index.tsx | 37 ++++++-- 2 files changed, 96 insertions(+), 29 deletions(-) diff --git a/fdm-app/app/components/blocks/harvest/form.tsx b/fdm-app/app/components/blocks/harvest/form.tsx index e0059a75c..7e1a2a00c 100644 --- a/fdm-app/app/components/blocks/harvest/form.tsx +++ b/fdm-app/app/components/blocks/harvest/form.tsx @@ -34,6 +34,8 @@ import { import { Input } from "~/components/ui/input" import { getHarvestParameterLabel } from "./parameters" import { FormSchema } from "./schema" +import { format } from "date-fns" +import { nl } from "date-fns/locale" type HarvestFormDialogProps = { harvestParameters: HarvestParameters @@ -50,6 +52,7 @@ type HarvestFormDialogProps = { b_lu_harvestable: "once" | "multiple" | "none" b_lu_start: Date | undefined | null b_lu_end: Date | undefined | null + mixedDates?: boolean action?: string handleConfirmation?: (data: z.infer) => Promise editable?: boolean @@ -140,6 +143,9 @@ function useHarvestRemixForm({ } function HarvestFields({ + b_lu_start, + b_lu_end, + mixedDates, b_lu_harvest_date, harvestParameters, form, @@ -148,30 +154,68 @@ function HarvestFields({ form: ReturnType className: React.ComponentProps["className"] }) { + const fmt_b_lu_start = + b_lu_start && format(b_lu_start, "PP", { locale: nl }) + const fmt_b_lu_end = b_lu_end && format(b_lu_end, "PP", { locale: nl }) + const fmt_mixed_dates_start = mixedDates + ? "Laatste zaaidatum: " + : "Zaaidatum: " + const fmt_mixed_dates_end = mixedDates + ? "Vroegste einddatum: " + : "Einddatum: " + + const startMessage = + fmt_b_lu_start && fmt_b_lu_end ? ( + <> + {fmt_mixed_dates_start} + {fmt_b_lu_start},{" "} + {fmt_mixed_dates_end} + {fmt_b_lu_end} + + ) : fmt_b_lu_start ? ( + <> + {fmt_mixed_dates_start} + {fmt_b_lu_start}. Nog + geen einddatum opgegeven. + + ) : fmt_b_lu_end ? ( + <> + {fmt_mixed_dates_end} + {fmt_b_lu_end}. Nog + geen zaaidatum opgegeven. + + ) : null return ( - ( - +
+ ( + + )} + /> + {startMessage && ( +

+ {startMessage} +

)} - /> +
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx index 8c659cd8f..0e39d7fd6 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx @@ -294,6 +294,30 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ) } + const b_lu_starts = selectedFields.map( + (field) => + field.cultivations.find((cultivation) => + cultivationIds.includes(cultivation.b_lu_catalogue), + )?.b_lu_start, + ) + const b_lu_ends = selectedFields.map( + (field) => + field.cultivations.find((cultivation) => + cultivationIds.includes(cultivation.b_lu_catalogue), + )?.b_lu_end, + ) + const b_lu_start = b_lu_starts.reduce( + (max, date) => + max && date ? (max > date ? max : date) : max || date, + undefined, + ) + const b_lu_end = b_lu_ends.reduce( + (min, date) => + min && date ? (min < date ? min : date) : min || date, + undefined, + ) + const mixedDates = b_lu_starts.length > 1 || b_lu_ends.length > 1 + const fieldOptions = allFieldsWithCultivations.map((field) => { if (!field?.b_id || !field?.b_name) { throw new Error("Invalid field data structure") @@ -333,6 +357,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { harvestApplication: harvestApplication, harvestableAnalysis: harvestableAnalysis, harvestParameters: harvestParameters, + b_lu_start: b_lu_start, + b_lu_end: b_lu_end, + mixedDates: mixedDates, } } catch (error) { throw handleLoaderError(error) @@ -737,13 +764,9 @@ export default function FarmRotationHarvestAddIndex() { loaderData.cultivation .b_lu_harvestable } - b_lu_start={ - loaderData.cultivation - .b_lu_start - } - b_lu_end={ - loaderData.cultivation.b_lu_end - } + b_lu_start={loaderData.b_lu_start} + b_lu_end={loaderData.b_lu_end} + mixedDates={loaderData.mixedDates} action={modifySearchParams( `${location.pathname}${location.search}`, (searchParams) => From 316d3c7c497fbe5fe31099b796b4e6efebe78e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 12 Jan 2026 14:16:07 +0100 Subject: [PATCH 2/6] Add changeset --- .changeset/long-pianos-admire.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/long-pianos-admire.md diff --git a/.changeset/long-pianos-admire.md b/.changeset/long-pianos-admire.md new file mode 100644 index 000000000..dc9679665 --- /dev/null +++ b/.changeset/long-pianos-admire.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": patch +--- + +While adding a harvest on the crop rotation table, the harvest date is now validated against the latest sowing date and the earliest cultivation ending date before submitting the form. From c26ca42c6988549a0e832a366a3409cba4a6342c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 19 Jan 2026 13:55:28 +0100 Subject: [PATCH 3/6] Move note of the cultivation start and end dates into the Zod error message --- .../app/components/blocks/harvest/form.tsx | 88 +++++-------------- .../app/components/blocks/harvest/schema.tsx | 7 +- ...farm.$calendar.rotation.harvest._index.tsx | 3 - 3 files changed, 26 insertions(+), 72 deletions(-) diff --git a/fdm-app/app/components/blocks/harvest/form.tsx b/fdm-app/app/components/blocks/harvest/form.tsx index 7e1a2a00c..e0059a75c 100644 --- a/fdm-app/app/components/blocks/harvest/form.tsx +++ b/fdm-app/app/components/blocks/harvest/form.tsx @@ -34,8 +34,6 @@ import { import { Input } from "~/components/ui/input" import { getHarvestParameterLabel } from "./parameters" import { FormSchema } from "./schema" -import { format } from "date-fns" -import { nl } from "date-fns/locale" type HarvestFormDialogProps = { harvestParameters: HarvestParameters @@ -52,7 +50,6 @@ type HarvestFormDialogProps = { b_lu_harvestable: "once" | "multiple" | "none" b_lu_start: Date | undefined | null b_lu_end: Date | undefined | null - mixedDates?: boolean action?: string handleConfirmation?: (data: z.infer) => Promise editable?: boolean @@ -143,9 +140,6 @@ function useHarvestRemixForm({ } function HarvestFields({ - b_lu_start, - b_lu_end, - mixedDates, b_lu_harvest_date, harvestParameters, form, @@ -154,68 +148,30 @@ function HarvestFields({ form: ReturnType className: React.ComponentProps["className"] }) { - const fmt_b_lu_start = - b_lu_start && format(b_lu_start, "PP", { locale: nl }) - const fmt_b_lu_end = b_lu_end && format(b_lu_end, "PP", { locale: nl }) - const fmt_mixed_dates_start = mixedDates - ? "Laatste zaaidatum: " - : "Zaaidatum: " - const fmt_mixed_dates_end = mixedDates - ? "Vroegste einddatum: " - : "Einddatum: " - - const startMessage = - fmt_b_lu_start && fmt_b_lu_end ? ( - <> - {fmt_mixed_dates_start} - {fmt_b_lu_start},{" "} - {fmt_mixed_dates_end} - {fmt_b_lu_end} - - ) : fmt_b_lu_start ? ( - <> - {fmt_mixed_dates_start} - {fmt_b_lu_start}. Nog - geen einddatum opgegeven. - - ) : fmt_b_lu_end ? ( - <> - {fmt_mixed_dates_end} - {fmt_b_lu_end}. Nog - geen zaaidatum opgegeven. - - ) : null return ( -
- ( - - )} - /> - {startMessage && ( -

- {startMessage} -

+ ( + )} -
+ />
diff --git a/fdm-app/app/components/blocks/harvest/schema.tsx b/fdm-app/app/components/blocks/harvest/schema.tsx index a6b89fe47..913f916cd 100644 --- a/fdm-app/app/components/blocks/harvest/schema.tsx +++ b/fdm-app/app/components/blocks/harvest/schema.tsx @@ -1,3 +1,5 @@ +import { format } from "date-fns" +import { nl } from "date-fns/locale" import { z } from "zod" export const FormSchema = z @@ -216,7 +218,7 @@ export const FormSchema = z ) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Oogstdatum moet na de zaaidatum van de teelt liggen", + message: `Oogstdatum moet na de zaaidatum van de teelt (${format(data.b_lu_start, "PP", { locale: nl })}) liggen`, path: ["b_lu_harvest_date"], }) } @@ -228,8 +230,7 @@ export const FormSchema = z ) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: - "Oogstdatum mag niet na de einddatum van de teelt liggen", + message: `Oogstdatum mag niet na de einddatum van de teelt (${format(data.b_lu_end, "PP", { locale: nl })}) liggen`, path: ["b_lu_harvest_date"], }) } diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx index 0e39d7fd6..b41dd35ff 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx @@ -316,7 +316,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { min && date ? (min < date ? min : date) : min || date, undefined, ) - const mixedDates = b_lu_starts.length > 1 || b_lu_ends.length > 1 const fieldOptions = allFieldsWithCultivations.map((field) => { if (!field?.b_id || !field?.b_name) { @@ -359,7 +358,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { harvestParameters: harvestParameters, b_lu_start: b_lu_start, b_lu_end: b_lu_end, - mixedDates: mixedDates, } } catch (error) { throw handleLoaderError(error) @@ -766,7 +764,6 @@ export default function FarmRotationHarvestAddIndex() { } b_lu_start={loaderData.b_lu_start} b_lu_end={loaderData.b_lu_end} - mixedDates={loaderData.mixedDates} action={modifySearchParams( `${location.pathname}${location.search}`, (searchParams) => From bd2da4eae6f9f69274f14f1274d289c05d530698 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:02:36 +0100 Subject: [PATCH 4/6] refactor: improve validation messages for harvest form --- .../app/components/blocks/harvest/schema.tsx | 103 +++++++++--------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/fdm-app/app/components/blocks/harvest/schema.tsx b/fdm-app/app/components/blocks/harvest/schema.tsx index 913f916cd..2c90bf437 100644 --- a/fdm-app/app/components/blocks/harvest/schema.tsx +++ b/fdm-app/app/components/blocks/harvest/schema.tsx @@ -6,18 +6,15 @@ export const FormSchema = z .object({ b_lu_harvest_date: z .string({ - required_error: - "Geef een datum op voor wanneer dit gewas is geoogst", - invalid_type_error: - "Geef een datum op voor wanneer dit gewas is geoogst", + required_error: "Selecteer een oogstdatum", + invalid_type_error: "Selecteer een geldige oogstdatum", }) .transform((val, ctx) => { const date = new Date(val) if (Number.isNaN(date.getTime())) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: - "Geef een datum op voor wanneer dit gewas is geoogst", + message: "Selecteer een geldige oogstdatum", }) return z.NEVER } @@ -27,20 +24,20 @@ export const FormSchema = z (val) => (val === "" ? undefined : val), z.coerce .number({ - invalid_type_error: "Hoeveelheid moet een getal zijn", + invalid_type_error: "De waarde moet een getal zijn", }) .positive({ - message: "Hoeveelheid moet groter zijn dan 0", + message: "De waarde moet groter zijn dan 0", }) .finite({ - message: "Hoeveelheid moet een geheel getal zijn", + message: "De waarde moet een geldig getal zijn", }) .max(250000, { message: - "Hoeveelheid mag niet groter zijn dan 250.000 kg DS / ha", + "Opbrengst mag niet groter zijn dan 250.000 kg DS / ha", }) .safe({ - message: "Hoeveelheid moet een safe getal zijn", + message: "De waarde is buiten het toegestane bereik", }) .optional(), ), @@ -48,20 +45,20 @@ export const FormSchema = z (val) => (val === "" ? undefined : val), z.coerce .number({ - invalid_type_error: "Hoeveelheid moet een getal zijn", + invalid_type_error: "De waarde moet een getal zijn", }) .positive({ - message: "Hoeveelheid moet groter zijn dan 0", + message: "De waarde moet groter zijn dan 0", }) .finite({ - message: "Hoeveelheid moet een geheel getal zijn", + message: "De waarde moet een geldig getal zijn", }) .max(250000, { message: - "Hoeveelheid mag niet groter zijn dan 250.000 kg versproduct / ha", + "Opbrengst mag niet groter zijn dan 250.000 kg versproduct / ha", }) .safe({ - message: "Hoeveelheid moet een safe getal zijn", + message: "De waarde is buiten het toegestane bereik", }) .optional(), ), @@ -69,20 +66,20 @@ export const FormSchema = z (val) => (val === "" ? undefined : val), z.coerce .number({ - invalid_type_error: "Hoeveelheid moet een getal zijn", + invalid_type_error: "De waarde moet een getal zijn", }) .positive({ - message: "Hoeveelheid moet groter zijn dan 0", + message: "De waarde moet groter zijn dan 0", }) .finite({ - message: "Hoeveelheid moet een geheel getal zijn", + message: "De waarde moet een geldig getal zijn", }) .max(250000, { message: - "Hoeveelheid mag niet groter zijn dan 250.000 kg versproduct (incl. tarra) / ha", + "Opbrengst mag niet groter zijn dan 250.000 kg versproduct (incl. tarra) / ha", }) .safe({ - message: "Hoeveelheid moet een safe getal zijn", + message: "De waarde is buiten het toegestane bereik", }) .optional(), ), @@ -90,20 +87,20 @@ export const FormSchema = z (val) => (val === "" ? undefined : val), z.coerce .number({ - invalid_type_error: "Hoeveelheid moet een getal zijn", + invalid_type_error: "De waarde moet een getal zijn", }) .positive({ - message: "Hoeveelheid moet groter zijn dan 0", + message: "De waarde moet groter zijn dan 0", }) .finite({ - message: "Hoeveelheid moet een geheel getal zijn", + message: "De waarde moet een geldig getal zijn", }) .max(1000, { message: - "Hoeveelheid mag niet groter zijn dan 1.000 g Ds / kg versproduct", + "Het droge stof gehalte mag niet groter zijn dan 1.000 g / kg", }) .safe({ - message: "Hoeveelheid moet een safe getal zijn", + message: "De waarde is buiten het toegestane bereik", }) .optional(), ), @@ -111,13 +108,14 @@ export const FormSchema = z (val) => (val === "" ? undefined : val), z.coerce .number({ - invalid_type_error: "Hoeveelheid moet een getal zijn", + invalid_type_error: "De waarde moet een getal zijn", }) .positive({ - message: "Hoeveelheid moet groter zijn dan 0", + message: "De waarde moet groter zijn dan 0", }) .max(1000, { - message: "Hoeveelheid mag niet groter zijn dan 1000", + message: + "De stikstofopbrengst mag niet groter zijn dan 1.000 kg N / ha", }) .optional(), ), @@ -125,19 +123,19 @@ export const FormSchema = z (val) => (val === "" ? undefined : val), z.coerce .number({ - invalid_type_error: "Hoeveelheid moet een getal zijn", + invalid_type_error: "De waarde moet een getal zijn", }) .positive({ - message: "Hoeveelheid moet groter zijn dan 0", + message: "De waarde moet groter zijn dan 0", }) .finite({ - message: "Hoeveelheid moet een geheel getal zijn", + message: "De waarde moet een geldig getal zijn", }) .max(25, { - message: "Hoeveelheid mag niet groter zijn dan 25 %", + message: "Het tarra-percentage mag niet hoger zijn dan 25%", }) .safe({ - message: "Hoeveelheid moet een safe getal zijn", + message: "De waarde is buiten het toegestane bereik", }) .optional(), ), @@ -145,23 +143,24 @@ export const FormSchema = z (val) => (val === "" ? undefined : val), z.coerce .number({ - invalid_type_error: "Hoeveelheid moet een getal zijn", + invalid_type_error: "De waarde moet een getal zijn", }) .positive({ - message: "Hoeveelheid moet groter zijn dan 0", + message: "De waarde moet groter zijn dan 0", }) .finite({ - message: "Hoeveelheid moet een geheel getal zijn", + message: "De waarde moet een geldig getal zijn", }) .min(100, { - message: "Hoeveelheid mag niet kleiner zijn dan 100", + message: + "Het onderwatergewicht mag niet kleiner zijn dan 100 g / 5 kg", }) .max(1000, { message: - "Hoeveelheid mag niet groter zijn dan 1.000 g / 5 kg", + "Het onderwatergewicht mag niet groter zijn dan 1.000 g / 5 kg", }) .safe({ - message: "Hoeveelheid moet een safe getal zijn", + message: "De waarde is buiten het toegestane bereik", }) .optional(), ), @@ -169,19 +168,19 @@ export const FormSchema = z (val) => (val === "" ? undefined : val), z.coerce .number({ - invalid_type_error: "Hoeveelheid moet een getal zijn", + invalid_type_error: "De waarde moet een getal zijn", }) .positive({ - message: "Hoeveelheid moet groter zijn dan 0", + message: "De waarde moet groter zijn dan 0", }) .finite({ - message: "Hoeveelheid moet een geheel getal zijn", + message: "De waarde moet een geldig getal zijn", }) .max(100, { - message: "Hoeveelheid mag niet groter zijn dan 100 %", + message: "Het vochtpercentage mag niet hoger zijn dan 100%", }) .safe({ - message: "Hoeveelheid moet een safe getal zijn", + message: "De waarde is buiten het toegestane bereik", }) .optional(), ), @@ -189,20 +188,20 @@ export const FormSchema = z (val) => (val === "" ? undefined : val), z.coerce .number({ - invalid_type_error: "Hoeveelheid moet een getal zijn", + invalid_type_error: "De waarde moet een getal zijn", }) .positive({ - message: "Hoeveelheid moet groter zijn dan 0", + message: "De waarde moet groter zijn dan 0", }) .finite({ - message: "Hoeveelheid moet een geheel getal zijn", + message: "De waarde moet een geldig getal zijn", }) .max(500, { message: - "Hoeveelheid mag niet groter zijn dan 500 g RE / kg DS", + "Het ruw eiwit gehalte mag niet groter zijn dan 500 g / kg DS", }) .safe({ - message: "Hoeveelheid moet een safe getal zijn", + message: "De waarde is buiten het toegestane bereik", }) .optional(), ), @@ -218,7 +217,7 @@ export const FormSchema = z ) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Oogstdatum moet na de zaaidatum van de teelt (${format(data.b_lu_start, "PP", { locale: nl })}) liggen`, + message: `De oogstdatum mag niet vóór de start van de teelt (${format(data.b_lu_start, "PP", { locale: nl })}) vallen`, path: ["b_lu_harvest_date"], }) } @@ -230,7 +229,7 @@ export const FormSchema = z ) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Oogstdatum mag niet na de einddatum van de teelt (${format(data.b_lu_end, "PP", { locale: nl })}) liggen`, + message: `De oogstdatum mag niet ná het einde van de teelt (${format(data.b_lu_end, "PP", { locale: nl })}) vallen`, path: ["b_lu_harvest_date"], }) } From 09421d303e1132ecfc19d5ee41549c499f47ba59 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:11:23 +0100 Subject: [PATCH 5/6] fix: reset form when visiting again the form --- .../farm.$b_id_farm.$calendar.rotation.harvest._index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx index 4d22c784d..fda1d0b65 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx @@ -711,6 +711,7 @@ export default function FarmRotationHarvestAddIndex() {
) : loaderData.fieldAmount > 0 ? ( From 200d7fe0d6d04d63b511f5b3d7c4333daebc0c64 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:14:49 +0100 Subject: [PATCH 6/6] fix: remove duplicate key --- .../routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx index fda1d0b65..9ba86e545 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation.harvest._index.tsx @@ -761,7 +761,6 @@ export default function FarmRotationHarvestAddIndex() { } b_lu_start={loaderData.b_lu_start} b_lu_end={loaderData.b_lu_end} - key={selectedFieldIds.join(",")} action={modifySearchParams( `${location.pathname}${location.search}`, (searchParams) =>