diff --git a/.changeset/eleven-parrots-think.md b/.changeset/eleven-parrots-think.md new file mode 100644 index 000000000..b14614303 --- /dev/null +++ b/.changeset/eleven-parrots-think.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Display the application method for each fertilizer application in the list diff --git a/.changeset/fluffy-steaks-follow.md b/.changeset/fluffy-steaks-follow.md new file mode 100644 index 000000000..3f731f8f3 --- /dev/null +++ b/.changeset/fluffy-steaks-follow.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-calculator": patch +--- + +Fix unit conversion at calculation of N supply by other fertilizers diff --git a/.changeset/fresh-buses-shop.md b/.changeset/fresh-buses-shop.md new file mode 100644 index 000000000..872a8e22b --- /dev/null +++ b/.changeset/fresh-buses-shop.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": patch +--- + +Replace the `p_type_*` boolean flags with the unified `p_type` field across all fertilizer functions diff --git a/.changeset/little-carrots-share.md b/.changeset/little-carrots-share.md new file mode 100644 index 000000000..7cd375418 --- /dev/null +++ b/.changeset/little-carrots-share.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-data": minor +--- + +Add `p_app_method_options` as parameter of Fertilizer Catalogue diff --git a/.changeset/new-knives-bet.md b/.changeset/new-knives-bet.md new file mode 100644 index 000000000..e87cf3a55 --- /dev/null +++ b/.changeset/new-knives-bet.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-core": patch +--- + +Fixes a bug in `syncCatalogue` where, when an item already exists but its hash has changed, the other properties were not updated diff --git a/.changeset/poor-towns-appear.md b/.changeset/poor-towns-appear.md new file mode 100644 index 000000000..4d20b0a35 --- /dev/null +++ b/.changeset/poor-towns-appear.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Add a feature to select an existing fertilizer as a template for new fertilizers diff --git a/.changeset/quick-states-melt.md b/.changeset/quick-states-melt.md new file mode 100644 index 000000000..c2ec5956f --- /dev/null +++ b/.changeset/quick-states-melt.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Improve the design of the fertilizer form page by making it more intuitive and clear. diff --git a/.changeset/rich-donkeys-stay.md b/.changeset/rich-donkeys-stay.md new file mode 100644 index 000000000..c6edcf524 --- /dev/null +++ b/.changeset/rich-donkeys-stay.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-data": minor +--- + +Expose the `ApplicationMethods` union type in the `@svenvw/fdm-data` package diff --git a/.changeset/shaggy-dancers-work.md b/.changeset/shaggy-dancers-work.md new file mode 100644 index 000000000..e25654661 --- /dev/null +++ b/.changeset/shaggy-dancers-work.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-core": minor +--- + +Add the new function `getSoilParameterDescription` to obtain details about fertilizer parameters diff --git a/.changeset/silver-mice-train.md b/.changeset/silver-mice-train.md new file mode 100644 index 000000000..2ac4827dc --- /dev/null +++ b/.changeset/silver-mice-train.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Add an application-method field to the fertilizer-application form diff --git a/.changeset/tidy-apes-watch.md b/.changeset/tidy-apes-watch.md new file mode 100644 index 000000000..9d9393091 --- /dev/null +++ b/.changeset/tidy-apes-watch.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-core": minor +--- + +Add the parameter `p_app_method_options` to `fertilizer_catalogue` to represent the possible methods that can be used to apply the fertilizer diff --git a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx index 6cf89aa3f..e36e2b509 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx @@ -22,9 +22,16 @@ import { PopoverContent, PopoverTrigger, } from "~/components/ui/popover" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select" import { cn } from "~/lib/utils" -import { Combobox } from "../../custom/combobox" -import { LoadingSpinner } from "../../custom/loadingspinner" +import { Combobox } from "~/components/custom/combobox" +import { LoadingSpinner } from "~/components/custom/loadingspinner" import { FormSchema } from "./formschema" import type { FertilizerOption } from "./types.d" @@ -40,10 +47,19 @@ export function FertilizerApplicationForm({ resolver: zodResolver(FormSchema), defaultValues: { p_id: undefined, + p_app_method: undefined, p_app_amount: undefined, p_app_date: new Date(), }, }) + const p_id = form.watch("p_id") + const selectedFertilizer = options.find((option) => option.value === p_id) + + useEffect(() => { + if (p_id) { + form.setValue("p_app_method", "") + } + }, [p_id, form.setValue]) useEffect(() => { if (form.formState.isSubmitSuccessful) { @@ -75,6 +91,43 @@ export function FertilizerApplicationForm({ } /> + ( + + + Toedingsmethode + * + + + + + + )} + />
-
+
- - )} - - - - Samenstelling - - De gehalten van deze meststof - - - -
- {/* Stikstof Row */} -
Stikstof
-
- {editable ? ( - ( - - - - - - - - )} - /> - ) : ( - {fertilizer.p_n_rt} - )} -
-
- g N / kg -
+export type FertilizerParameterDescription = + FertilizerParameterDescriptionItem[] - {/* Stikstof, werkingscoëfficiënt Row */} -
- Stikstof, werkingscoëfficiënt -
-
- {editable ? ( - ( - - - - - - - - )} - /> - ) : ( - {fertilizer.p_n_wc} - )} -
-
- - -
- {/* Fosfaat Row */} -
Fosfaat
-
- {editable ? ( - ( - - - - - - - - )} - /> - ) : ( - {fertilizer.p_p_rt} - )} -
-
- g P2O5 / kg -
+type FormSchemaKeys = keyof z.infer - {/* Kalium Row */} -
Kalium
-
- {editable ? ( - ( - - - - - - - - )} - /> - ) : ( - {fertilizer.p_k_rt} - )} -
-
- g K2O / kg -
+type FertilizerFormNewProps = { + fertilizerParameters: FertilizerParameterDescription + form: ReturnType>> + editable?: boolean +} - {/* Organische stof Row */} -
- Organische stof -
-
- {editable ? ( - ( - - - - - - - - )} - /> - ) : ( - {fertilizer.p_om} - )} -
-
- g OS / kg -
+export function FertilizerForm({ + fertilizerParameters, + form, + editable = true, +}: FertilizerFormNewProps) { + const categories = [ + { + name: "general", + title: "Algemeen", + }, - {/* Koolstof, effectief Row */} -
- Koolstof, effectief -
-
- {editable ? ( - ( - - - - - - - - )} - /> - ) : ( - {fertilizer.p_eoc} - )} -
-
- g EOC / kg -
+ { + name: "primary", + title: "Primaire nutriënten", + }, + { + name: "secondary", + title: "OS & Secundaire nutriënten", + }, + { + name: "physical", + title: "Fysische eigenschappen", + }, + { + name: "trace", + title: "Sporenelementen", + }, + ] - {/* Zwavel Row */} -
Zwavel
-
- {editable ? ( - ( - - - - - - - - )} - /> - ) : ( - {fertilizer.p_s_rt} - )} -
-
- g SO3 / kg -
+ const getParameterInput = (param: FertilizerParameterDescriptionItem) => { + if ( + param.parameter === "p_source" || + param.parameter === "p_id_catalogue" + ) { + return null + } - {/* Calcium (Ca) Row */} -
Calcium
-
- {editable ? ( - ( - + return ( + ( + + + {param.name} {param.unit && `(${param.unit})`} + + + {param.type === "numeric" ? ( + + ) : param.type === "text" ? ( + + ) : param.type === "enum" ? ( + + ) : param.type === "enum_multi" ? ( +
+ {param.options?.map((option) => ( + { + return ( + - { + const currentValues = + Array.isArray( + field.value, + ) + ? field.value + : [] + if ( + checked + ) { + field.onChange( + [ + ...currentValues, + option.value, + ], + ) + } else { + field.onChange( + currentValues.filter( + ( + value, + ) => + value !== + option.value, + ), + ) + } + }} /> - - + + {option.label} + - )} - /> - ) : ( - {fertilizer.p_ca_rt} - )} -
-
- g CaO / kg -
+ ) + }} + /> + ))} +
+ ) : null} + + {param.description && ( + + {param.description} + + )} + + + )} + /> + ) + } - {/* Magnesium (Mg) Row */} -
Magnesium
-
- {editable ? ( - ( - - - - - - - - )} - /> - ) : ( - {fertilizer.p_mg_rt} + const groupedParameters = fertilizerParameters.reduce( + ( + acc: Record, + param: FertilizerParameterDescription[number], + ) => { + if (!acc[param.category]) { + acc[param.category] = [] + } + acc[param.category].push(param) + return acc + }, + {} as Record, + ) + + return ( + + +
+
+ {categories.map((category) => ( + + + {category.title} + + +
+ {groupedParameters[category.name]?.map( + (param) => ( +
+ {getParameterInput(param)} +
+ ), )}
-
- g MgO / kg -
-
- - {editable && ( - - - - )} - + + + ))}
+ {editable && ( +
+ +
+ )} diff --git a/fdm-app/app/components/blocks/fertilizer/formschema.tsx b/fdm-app/app/components/blocks/fertilizer/formschema.tsx index 286dc7a7e..d92ba4a7b 100644 --- a/fdm-app/app/components/blocks/fertilizer/formschema.tsx +++ b/fdm-app/app/components/blocks/fertilizer/formschema.tsx @@ -1,111 +1,377 @@ import { z } from "zod" -export const FormSchema = z.object({ - p_name_nl: z.string({ - required_error: "Naam is verplicht", - invalid_type_error: "Ongeldige waarde", - }), - p_type: z.string({ - required_error: "Type is verplicht", - invalid_type_error: "Ongeldige waarde", - }), - p_n_rt: z.coerce - .number({ - invalid_type_error: "Ongeldige waarde", - }) - .min(0, { - message: "Waarde mag niet negatief zijn", - }) - .max(1000, { - message: "Waarde mag niet groter zijn dan 1000", - }) - .optional(), - p_n_wc: z.coerce - .number({ - invalid_type_error: "Ongeldige waarde", - }) - .min(0, { - message: "Waarde mag niet negatief zijn", - }) - .max(1, { - message: "Waarde mag niet groter zijn dan 1", - }) - .optional(), - p_p_rt: z.coerce - .number({ - invalid_type_error: "Ongeldige waarde", - }) - .min(0, { - message: "Waarde mag niet negatief zijn", - }) - .max(4583, { - message: "Waarde mag niet groter zijn dan 4583", - }) - .optional(), - p_k_rt: z.coerce - .number({ - invalid_type_error: "Ongeldige waarde", - }) - .min(0, { - message: "Waarde mag niet negatief zijn", - }) - .max(2409.2, { - message: "Waarde mag niet groter zijn dan 2409.2", - }) - .optional(), - p_om: z.coerce - .number({ - invalid_type_error: "Ongeldige waarde", - }) - .min(0, { - message: "Waarde mag niet negatief zijn", - }) - .max(1000, { - message: "Waarde mag niet groter zijn dan 1000", - }) - .optional(), - p_eoc: z.coerce - .number({ - invalid_type_error: "Ongeldige waarde", - }) - .min(0, { - message: "Waarde mag niet negatief zijn", - }) - .max(1000, { - message: "Waarde mag niet groter zijn dan 1000", - }) - .optional(), - p_s_rt: z.coerce - .number({ - invalid_type_error: "Ongeldige waarde", - }) - .min(0, { - message: "Waarde mag niet negatief zijn", - }) - .max(2497.2, { - message: "Waarde mag niet groter zijn dan 2497.2", - }) - .optional(), - p_ca_rt: z.coerce - .number({ - invalid_type_error: "Ongeldige waarde", - }) - .min(0, { - message: "Waarde mag niet negatief zijn", - }) - .max(1399.2, { - message: "Waarde mag niet groter zijn dan 1399.2", - }) - .optional(), - p_mg_rt: z.coerce - .number({ - invalid_type_error: "Ongeldige waarde", - }) - .min(0, { - message: "Waarde mag niet negatief zijn", - }) - .max(1659, { - message: "Waarde mag niet groter zijn dan 1659", - }) - .optional(), -}) +export const FormSchema = z + .object({ + p_name_nl: z + .string({ + required_error: "Naam is verplicht", + invalid_type_error: "Ongeldige waarde", + }) + .min(1, { message: "Geef een naam op voor deze meststof" }), + p_name_en: z.string().optional(), + p_description: z.string().optional(), + p_type: z.enum(["manure", "mineral", "compost"], { + required_error: "Kies het type meststof", + }), + p_dm: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000, { + message: "Waarde mag niet groter zijn dan 1000", + }) + .optional(), + ), + p_density: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0.00016, { + message: "Waarde mag niet kleiner dan 0.00016 zijn", + }) + .max(17.31, { + message: "Waarde mag niet groter zijn dan 17.31", + }) + .optional(), + ), + p_om: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000, { + message: "Waarde mag niet groter zijn dan 1000", + }) + .optional(), + ), + p_a: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_hc: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_eom: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_eoc: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000, { + message: "Waarde mag niet groter zijn dan 1000", + }) + .optional(), + ), + p_c_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_c_of: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_c_if: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_c_fr: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_cn_of: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_n_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000, { + message: "Waarde mag niet groter zijn dan 1000", + }) + .optional(), + ), + p_n_if: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_n_of: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_n_wc: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1, { + message: "Waarde mag niet groter zijn dan 1", + }) + .optional(), + ), + p_p_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(4583, { + message: "Waarde mag niet groter zijn dan 4583", + }) + .optional(), + ), + p_k_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(2409.2, { + message: "Waarde mag niet groter zijn dan 2409.2", + }) + .optional(), + ), + p_mg_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1659, { + message: "Waarde mag niet groter zijn dan 1659", + }) + .optional(), + ), + p_ca_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1399.2, { + message: "Waarde mag niet groter zijn dan 1399.2", + }) + .optional(), + ), + p_ne: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_s_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(2497.2, { + message: "Waarde mag niet groter zijn dan 2497.2", + }) + .optional(), + ), + p_s_wc: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_cu_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000000, { + message: "Waarde mag niet groter zijn dan 1000000", + }) + .optional(), + ), + p_zn_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000000, { + message: "Waarde mag niet groter zijn dan 1000000", + }) + .optional(), + ), + p_na_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(2695900, { + message: "Waarde mag niet groter zijn dan 2695900", + }) + .optional(), + ), + p_si_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_b_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000000, { + message: "Waarde mag niet groter zijn dan 1000000", + }) + .optional(), + ), + p_mn_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000000, { + message: "Waarde mag niet groter zijn dan 1000000", + }) + .optional(), + ), + p_ni_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_fe_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_mo_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000000, { + message: "Waarde mag niet groter zijn dan 1000000", + }) + .optional(), + ), + p_co_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000000, { + message: "Waarde mag niet groter zijn dan 1000000", + }) + .optional(), + ), + p_as_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_cd_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_cr_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_cr_vi: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_pb_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_hg_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_cl_rt: z.preprocess( + (val) => (val === "" || val === null ? undefined : val), + z.coerce.number().optional(), + ), + p_app_method_options: z + .array(z.string(), { + required_error: "Selecteer minimaal 1 methode", + }) + .default([]) + .refine((value) => value.some((item) => item), { + message: "Selecteer minimaal 1 methode", + }), + }) + .refine( + (data) => { + if (data.p_n_rt && data.p_n_wc === undefined) { + return false + } + return true + }, + { + message: + "N-werkingscoëfficiënt is verplicht als meststof stikstofbevat", + path: ["p_n_wc"], + }, + ) diff --git a/fdm-app/app/components/ui/radio-group.tsx b/fdm-app/app/components/ui/radio-group.tsx new file mode 100644 index 000000000..385fc4cf8 --- /dev/null +++ b/fdm-app/app/components/ui/radio-group.tsx @@ -0,0 +1,43 @@ +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { CircleIcon } from "lucide-react" + +import { cn } from "~/lib/utils" + +function RadioGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { RadioGroup, RadioGroupItem } diff --git a/fdm-app/app/lib/form.ts b/fdm-app/app/lib/form.ts index ed067eb22..76a4910de 100644 --- a/fdm-app/app/lib/form.ts +++ b/fdm-app/app/lib/form.ts @@ -20,22 +20,38 @@ export async function extractFormValuesFromRequest( ) { try { const formData = await request.formData() + const formObject = Object.fromEntries(formData) as Record< + string, + FormDataEntryValue | unknown[] + > // Trim all values and remove quotation marks // Note: Somewhere additional quotation marks are added, preferably that is not the case, but this workaround removes them - for (const key of formData.keys()) { - const value = formData.get(key) - if (typeof value === "string" && key !== "b_geometry") { - formData.set(key, value.replace(/['"]+/g, "").trim()) + for (const key in formObject) { + const value = formObject[key] + + if (typeof value === "string") { + // Check if the value is a JSON array string + if ( + value.startsWith("[") && + value.endsWith("]") && + key !== "b_geometry" + ) { + try { + formObject[key] = JSON.parse(value) + } catch (e) { + // Not a valid JSON, so leave it as a string + } + } else if (key !== "b_geometry") { + formObject[key] = value.replace(/['"]+/g, "").trim() + } // Daypicker returns 01 Jan 1970 if no date is selected. This workaround removes the date if it is 01 Jan 1970 if (value === "1970-01-01T00:00:00.000Z") { - formData.delete(key) + delete formObject[key] } } } - - const formObject = Object.fromEntries(formData) const parsedData = schema.safeParse(formObject) if (!parsedData.success) { diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer.tsx index b1ee2df28..0b50c2b5e 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer.tsx @@ -2,6 +2,7 @@ import { calculateDose } from "@svenvw/fdm-calculator" import { addFertilizerApplication, getFertilizerApplications, + getFertilizerParametersDescription, getFertilizers, getField, removeFertilizerApplication, @@ -93,11 +94,26 @@ export async function loader({ request, params }: LoaderFunctionArgs) { session.principal_id, b_id_farm, ) + const fertilizerParameterDescription = + getFertilizerParametersDescription() + const applicationMethods = fertilizerParameterDescription.find( + (x) => x.parameter === "p_app_method_options", + ) + if (!applicationMethods) throw new Error("Parameter metadata missing") // Map fertilizers to options for the combobox const fertilizerOptions = fertilizers.map((fertilizer) => { + const applicationMethodOptions = fertilizer.p_app_method_options + .map((opt) => { + const meta = applicationMethods.options.find( + (x) => x.value === opt, + ) + return meta ? { value: opt, label: meta.label } : undefined + }) + .filter(Boolean) return { value: fertilizer.p_id, label: fertilizer.p_name_nl, + applicationMethodOptions: applicationMethodOptions, } }) @@ -120,6 +136,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { fertilizerOptions: fertilizerOptions, fertilizerApplications: fertilizerApplications, dose: dose.dose, + applicationMethodOptions: applicationMethods.options, } } catch (error) { throw handleLoaderError(error) @@ -154,13 +171,15 @@ export default function FarmFieldsOverviewBlock() {
@@ -201,7 +220,7 @@ export async function action({ request, params }: ActionFunctionArgs) { request, FormSchema, ) - const { p_id, p_app_amount, p_app_date } = formValues + const { p_id, p_app_amount, p_app_date, p_app_method } = formValues await addFertilizerApplication( fdm, @@ -209,7 +228,7 @@ export async function action({ request, params }: ActionFunctionArgs) { b_id, p_id, p_app_amount, - undefined, + p_app_method, p_app_date, ) diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx index 68886b1da..c6373b485 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx @@ -4,9 +4,11 @@ import { getFarms, getFertilizer, getFertilizers, + getFertilizerParametersDescription, +} from "@svenvw/fdm-core" +import { + updateFertilizerFromCatalogue, } from "@svenvw/fdm-core" -import { updateFertilizerFromCatalogue } from "@svenvw/fdm-core" -import { useEffect } from "react" import { type ActionFunctionArgs, type LoaderFunctionArgs, @@ -18,7 +20,7 @@ import { useRemixForm } from "remix-hook-form" import { dataWithSuccess } from "remix-toast" import type { z } from "zod" import { FarmTitle } from "~/components/blocks/farm/farm-title" -import { FertilizerForm } from "~/components/blocks/fertilizer/form" +import { FertilizerForm } from "@/app/components/blocks/fertilizer/form" import { FormSchema } from "~/components/blocks/fertilizer/formschema" import { SidebarInset } from "~/components/ui/sidebar" import { getSession } from "~/lib/auth.server" @@ -90,6 +92,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Get selected fertilizer const fertilizer = await getFertilizer(fdm, p_id) + const fertilizerParameters = getFertilizerParametersDescription() // Get the available fertilizers const fertilizers = await getFertilizers( @@ -100,7 +103,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const fertilizerOptions = fertilizers.map((fertilizer) => { return { p_id: fertilizer.p_id, - p_name_nl: fertilizer.p_name_nl, + p_name_nl: fertilizer.p_name_nl || "", } }) @@ -119,6 +122,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { fertilizerOptions: fertilizerOptions, fertilizer: fertilizer, editable: editable, + fertilizerParameters: fertilizerParameters, } } catch (error) { throw handleLoaderError(error) @@ -133,16 +137,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { */ export default function FarmFertilizerBlock() { const loaderData = useLoaderData() - const fertilizer = loaderData.fertilizer - - fertilizer.p_type = "" - if (fertilizer.p_type_manure) { - fertilizer.p_type = "manure" - } else if (fertilizer.p_type_compost) { - fertilizer.p_type = "compost" - } else if (fertilizer.p_type_mineral) { - fertilizer.p_type = "mineral" - } + const { fertilizer, fertilizerParameters, editable } = loaderData const form = useRemixForm>({ mode: "onTouched", @@ -150,33 +145,50 @@ export default function FarmFertilizerBlock() { defaultValues: { p_name_nl: fertilizer.p_name_nl, p_type: fertilizer.p_type, + p_dm: fertilizer.p_dm, + p_density: fertilizer.p_density, + p_om: fertilizer.p_om, + p_a: fertilizer.p_a, + p_hc: fertilizer.p_hc, + p_eom: fertilizer.p_eom, + p_eoc: fertilizer.p_eoc, + p_c_rt: fertilizer.p_c_rt, + p_c_of: fertilizer.p_c_of, + p_c_if: fertilizer.p_c_if, + p_c_fr: fertilizer.p_c_fr, + p_cn_of: fertilizer.p_cn_of, p_n_rt: fertilizer.p_n_rt, + p_n_if: fertilizer.p_n_if, + p_n_of: fertilizer.p_n_of, p_n_wc: fertilizer.p_n_wc, p_p_rt: fertilizer.p_p_rt, p_k_rt: fertilizer.p_k_rt, - p_om: fertilizer.p_om, - p_eoc: fertilizer.p_eoc, - p_s_rt: fertilizer.p_s_rt, - p_ca_rt: fertilizer.p_ca_rt, p_mg_rt: fertilizer.p_mg_rt, + p_ca_rt: fertilizer.p_ca_rt, + p_ne: fertilizer.p_ne, + p_s_rt: fertilizer.p_s_rt, + p_s_wc: fertilizer.p_s_wc, + p_cu_rt: fertilizer.p_cu_rt, + p_zn_rt: fertilizer.p_zn_rt, + p_na_rt: fertilizer.p_na_rt, + p_si_rt: fertilizer.p_si_rt, + p_b_rt: fertilizer.p_b_rt, + p_mn_rt: fertilizer.p_mn_rt, + p_ni_rt: fertilizer.p_ni_rt, + p_fe_rt: fertilizer.p_fe_rt, + p_mo_rt: fertilizer.p_mo_rt, + p_co_rt: fertilizer.p_co_rt, + p_as_rt: fertilizer.p_as_rt, + p_cd_rt: fertilizer.p_cd_rt, + p_cr_rt: fertilizer.p_cr_rt, + p_cr_vi: fertilizer.p_cr_vi, + p_pb_rt: fertilizer.p_pb_rt, + p_hg_rt: fertilizer.p_hg_rt, + p_cl_rt: fertilizer.p_cl_rt, + p_app_method_options: fertilizer.p_app_method_options || [], }, }) - useEffect(() => { - form.reset({ - p_name_nl: fertilizer.p_name_nl, - p_type: fertilizer.p_type, - p_n_rt: fertilizer.p_n_rt, - p_n_wc: fertilizer.p_n_wc, - p_p_rt: fertilizer.p_p_rt, - p_k_rt: fertilizer.p_k_rt, - p_om: fertilizer.p_om, - p_eoc: fertilizer.p_eoc, - p_s_rt: fertilizer.p_s_rt, - p_ca_rt: fertilizer.p_ca_rt, - p_mg_rt: fertilizer.p_mg_rt, - }) - }, [fertilizer, form.reset]) return ( @@ -204,10 +216,9 @@ export default function FarmFertilizerBlock() { />
@@ -233,34 +244,10 @@ export async function action({ request, params }: ActionFunctionArgs) { FormSchema, ) - const { - p_name_nl, - p_type, - p_n_rt, - p_n_wc, - p_p_rt, - p_k_rt, - p_om, - p_eoc, - p_s_rt, - p_ca_rt, - p_mg_rt, - } = formValues - - let p_type_manure = false - let p_type_compost = false - let p_type_mineral = false - if (p_type === "manure") { - p_type_manure = true - } - if (p_type === "compost") { - p_type_compost = true - } - if (p_type === "mineral") { - p_type_mineral = true - } - const fertilizer = await getFertilizer(fdm, p_id) + if (fertilizer.p_source !== b_id_farm) { + throw new Error("Forbidden") + } const p_id_catalogue = fertilizer.p_id_catalogue await updateFertilizerFromCatalogue( @@ -268,21 +255,7 @@ export async function action({ request, params }: ActionFunctionArgs) { session.principal_id, b_id_farm, p_id_catalogue, - { - p_name_nl, - p_type_manure, - p_type_mineral, - p_type_compost, - p_n_rt, - p_n_wc, - p_p_rt, - p_k_rt, - p_om, - p_eoc, - p_s_rt, - p_ca_rt, - p_mg_rt, - }, + formValues, ) return dataWithSuccess( diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.$p_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.$p_id.tsx new file mode 100644 index 000000000..614683792 --- /dev/null +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.$p_id.tsx @@ -0,0 +1,282 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { + getFarm, + getFarms, + getFertilizer, + getFertilizers, + getFertilizerParametersDescription, + addFertilizer, + addFertilizerToCatalogue, +} from "@svenvw/fdm-core" +import { updateFertilizerFromCatalogue } from "@svenvw/fdm-core" +import { useEffect } from "react" +import { + type ActionFunctionArgs, + type LoaderFunctionArgs, + type MetaFunction, + data, + useLoaderData, +} from "react-router" +import { useRemixForm } from "remix-hook-form" +import { dataWithSuccess, redirectWithSuccess } from "remix-toast" +import type { z } from "zod" +import { FarmTitle } from "~/components/blocks/farm/farm-title" +import { FertilizerForm } from "@/app/components/blocks/fertilizer/form" +import { FormSchema } from "~/components/blocks/fertilizer/formschema" +import { SidebarInset } from "~/components/ui/sidebar" +import { getSession } from "~/lib/auth.server" +import { clientConfig } from "~/lib/config" +import { handleActionError, handleLoaderError } from "~/lib/error" +import { fdm } from "~/lib/fdm.server" +import { extractFormValuesFromRequest } from "~/lib/form" +import { Header } from "~/components/blocks/header/base" +import { HeaderFarm } from "~/components/blocks/header/farm" +import { HeaderFertilizer } from "~/components/blocks/header/fertilizer" + +export const meta: MetaFunction = () => { + return [ + { title: `Meststof | ${clientConfig.name}` }, + { + name: "description", + content: "Bekij de details van deze meststof", + }, + ] +} + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + // Get the farm id + const b_id_farm = params.b_id_farm + if (!b_id_farm) { + throw data("invalid: b_id_farm", { + status: 400, + statusText: "invalid: b_id_farm", + }) + } + + // Get the fertilizer id + const p_id = params.p_id + if (!p_id) { + throw data("invalid: p_id", { + status: 400, + statusText: "invalid: p_id", + }) + } + + // Get the session + const session = await getSession(request) + + // Get details of farm + const farm = await getFarm(fdm, session.principal_id, b_id_farm) + if (!farm) { + throw data("not found: b_id_farm", { + status: 404, + statusText: "not found: b_id_farm", + }) + } + + // Get a list of possible farms of the user + const farms = await getFarms(fdm, session.principal_id) + if (!farms || farms.length === 0) { + throw data("not found: farms", { + status: 404, + statusText: "not found: farms", + }) + } + + const farmOptions = farms.map((farm) => { + return { + b_id_farm: farm.b_id_farm, + b_name_farm: farm.b_name_farm, + } + }) + + // Get selected fertilizer + const fertilizer = await getFertilizer(fdm, p_id) + const fertilizerParameters = getFertilizerParametersDescription() + + // Get the available fertilizers + const fertilizers = await getFertilizers( + fdm, + session.principal_id, + b_id_farm, + ) + const fertilizerOptions = fertilizers.map((fertilizer) => { + return { + p_id: fertilizer.p_id, + p_name_nl: fertilizer.p_name_nl, + } + }) + + // Return user information from loader + return { + farm: farm, + p_id: p_id, + b_id_farm: b_id_farm, + farmOptions: farmOptions, + fertilizerOptions: fertilizerOptions, + fertilizer: fertilizer, + editable: true, + fertilizerParameters: fertilizerParameters, + } + } catch (error) { + throw handleLoaderError(error) + } +} + +/** + * Renders the layout for managing farm settings. + * + * This component displays a sidebar that includes the farm header, navigation options, and a link to farm fields. + * It also renders a main section containing the farm title, description, nested routes via an Outlet, and a notification toaster. + */ +export default function FarmFertilizerBlock() { + const loaderData = useLoaderData() + const { fertilizer, fertilizerParameters, editable } = loaderData + + const form = useRemixForm>({ + mode: "onTouched", + resolver: zodResolver(FormSchema), + defaultValues: { + p_name_nl: "", + p_type: fertilizer.p_type, + p_dm: fertilizer.p_dm, + p_density: fertilizer.p_density, + p_om: fertilizer.p_om, + p_a: fertilizer.p_a, + p_hc: fertilizer.p_hc, + p_eom: fertilizer.p_eom, + p_eoc: fertilizer.p_eoc, + p_c_rt: fertilizer.p_c_rt, + p_c_of: fertilizer.p_c_of, + p_c_if: fertilizer.p_c_if, + p_c_fr: fertilizer.p_c_fr, + p_cn_of: fertilizer.p_cn_of, + p_n_rt: fertilizer.p_n_rt, + p_n_if: fertilizer.p_n_if, + p_n_of: fertilizer.p_n_of, + p_n_wc: fertilizer.p_n_wc, + p_p_rt: fertilizer.p_p_rt, + p_k_rt: fertilizer.p_k_rt, + p_mg_rt: fertilizer.p_mg_rt, + p_ca_rt: fertilizer.p_ca_rt, + p_ne: fertilizer.p_ne, + p_s_rt: fertilizer.p_s_rt, + p_s_wc: fertilizer.p_s_wc, + p_cu_rt: fertilizer.p_cu_rt, + p_zn_rt: fertilizer.p_zn_rt, + p_na_rt: fertilizer.p_na_rt, + p_si_rt: fertilizer.p_si_rt, + p_b_rt: fertilizer.p_b_rt, + p_mn_rt: fertilizer.p_mn_rt, + p_ni_rt: fertilizer.p_ni_rt, + p_fe_rt: fertilizer.p_fe_rt, + p_mo_rt: fertilizer.p_mo_rt, + p_co_rt: fertilizer.p_co_rt, + p_as_rt: fertilizer.p_as_rt, + p_cd_rt: fertilizer.p_cd_rt, + p_cr_rt: fertilizer.p_cr_rt, + p_cr_vi: fertilizer.p_cr_vi, + p_pb_rt: fertilizer.p_pb_rt, + p_hg_rt: fertilizer.p_hg_rt, + p_cl_rt: fertilizer.p_cl_rt, + p_app_method_options: fertilizer.p_app_method_options, + }, + }) + + return ( + + ) +} + +export async function action({ request, params }: ActionFunctionArgs) { + try { + const b_id_farm = params.b_id_farm + const p_id = params.p_id + + if (!b_id_farm) { + throw new Error("missing: b_id_farm") + } + if (!p_id) { + throw new Error("missing: p_id") + } + + const session = await getSession(request) + const formValues = await extractFormValuesFromRequest( + request, + FormSchema, + ) + + const p_id_catalogue = await addFertilizerToCatalogue( + fdm, + session.principal_id, + b_id_farm, + { + p_name_nl: formValues.p_name_nl, + p_name_en: formValues.p_name_en, + p_description: formValues.p_description, + p_type: formValues.p_type, + p_dm: formValues.p_dm, + p_density: formValues.p_density, + p_om: formValues.p_om, + p_a: formValues.p_a, + p_hc: formValues.p_hc, + p_eom: formValues.p_eom, + p_eoc: formValues.p_eoc, + p_c_rt: formValues.p_c_rt, + p_c_of: formValues.p_c_of, + p_c_if: formValues.p_c_if, + p_c_fr: formValues.p_c_fr, + p_cn_of: formValues.p_cn_of, + p_n_rt: formValues.p_n_rt, + p_n_if: formValues.p_n_if, + p_n_of: formValues.p_n_of, + p_n_wc: formValues.p_n_wc, + p_p_rt: formValues.p_p_rt, + p_k_rt: formValues.p_k_rt, + p_mg_rt: formValues.p_mg_rt, + p_ca_rt: formValues.p_ca_rt, + p_ne: formValues.p_ne, + p_s_rt: formValues.p_s_rt, + p_s_wc: formValues.p_s_wc, + p_cu_rt: formValues.p_cu_rt, + p_zn_rt: formValues.p_zn_rt, + p_na_rt: formValues.p_na_rt, + p_si_rt: formValues.p_si_rt, + p_b_rt: formValues.p_b_rt, + p_mn_rt: formValues.p_mn_rt, + p_ni_rt: formValues.p_ni_rt, + p_fe_rt: formValues.p_fe_rt, + p_mo_rt: formValues.p_mo_rt, + p_co_rt: formValues.p_co_rt, + p_as_rt: formValues.p_as_rt, + p_cd_rt: formValues.p_cd_rt, + p_cr_rt: formValues.p_cr_rt, + p_cr_vi: formValues.p_cr_vi, + p_pb_rt: formValues.p_pb_rt, + p_hg_rt: formValues.p_hg_rt, + p_cl_rt: formValues.p_cl_rt, + p_app_method_options: formValues.p_app_method_options, + }, + ) + + await addFertilizer( + fdm, + session.principal_id, + p_id_catalogue, + b_id_farm, + undefined, + undefined, + ) + + return redirectWithSuccess(`/farm/${b_id_farm}/fertilizers`, { + message: `${formValues.p_name_nl} is toegevoegd! 🎉`, + }) + } catch (error) { + throw handleActionError(error) + } +} diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new._index.tsx new file mode 100644 index 000000000..5765c2d82 --- /dev/null +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new._index.tsx @@ -0,0 +1,68 @@ +import type { LoaderFunctionArgs } from "react-router" +import { Link, NavLink, useLoaderData } from "react-router" +import { getFertilizers } from "@svenvw/fdm-core" +import type { Fertilizer } from "@svenvw/fdm-core" +import { getSession } from "~/lib/auth.server" +import { fdm } from "~/lib/fdm.server" +import { Card, CardContent } from "~/components/ui/card" + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { b_id_farm } = params + if (!b_id_farm) { + throw new Error("Farm ID is required") + } + + const session = await getSession(request) + + const fertilizers = await getFertilizers( + fdm, + session.principal_id, + b_id_farm, + ) + + return { b_id_farm: b_id_farm, fertilizers: fertilizers } +} + +export default function NewFertilizerIndex() { + const { fertilizers } = useLoaderData() + + return ( +
+ + + +

+ Maak een nieuwe meststof aan +

+

+ Begin met een leeg formulier +

+
+
+ + +

Of baseer op een meststof

+
+ {fertilizers.map((fertilizer: Fertilizer) => ( + + + +

+ {fertilizer.p_name_nl || "Onbekend"} +

+ {/*

+ {fertilizer.p_description || + "No description available."} +

*/} +
+
+
+ ))} +
+
+ ) +} diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.custom.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.custom.tsx new file mode 100644 index 000000000..c9bb1ee93 --- /dev/null +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.custom.tsx @@ -0,0 +1,325 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { + addFertilizer, + addFertilizerToCatalogue, + getFertilizers, + getFertilizerParametersDescription, +} from "@svenvw/fdm-core" +import { useEffect } from "react" +import { + type ActionFunctionArgs, + type LoaderFunctionArgs, + type MetaFunction, + data, + useLoaderData, +} from "react-router" +import { useRemixForm } from "remix-hook-form" +import { redirectWithSuccess } from "remix-toast" +import type { z } from "zod" +import { FormSchema } from "~/components/blocks/fertilizer/formschema" +import { getSession } from "~/lib/auth.server" +import { clientConfig } from "~/lib/config" +import { handleActionError, handleLoaderError } from "~/lib/error" +import { fdm } from "~/lib/fdm.server" +import { extractFormValuesFromRequest } from "~/lib/form" +import { FertilizerForm } from "@/app/components/blocks/fertilizer/form" + +export const meta: MetaFunction = () => { + return [ + { title: `Meststof toevoegen | ${clientConfig.name}` }, + { + name: "description", + content: + "Voeg een meststof toe om deze te gebruiken op dit bedrijf.", + }, + ] +} + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + // Get the farm id + const b_id_farm = params.b_id_farm + if (!b_id_farm) { + throw data("invalid: b_id_farm", { + status: 400, + statusText: "invalid: b_id_farm", + }) + } + + // Get the session + const session = await getSession(request) + + // Get selected fertilizer + const fertilizerParameters = getFertilizerParametersDescription() + + const fertilizer = { + p_id: undefined, // Added p_id + p_source: b_id_farm, + p_name_nl: "", + p_type: undefined, + p_dm: undefined, + p_density: undefined, + p_om: undefined, + p_a: undefined, + p_hc: undefined, + p_eom: undefined, + p_eoc: undefined, + p_c_rt: undefined, + p_c_of: undefined, + p_c_if: undefined, + p_c_fr: undefined, + p_cn_of: undefined, + p_n_rt: undefined, + p_n_if: undefined, + p_n_of: undefined, + p_n_wc: undefined, + p_p_rt: undefined, + p_k_rt: undefined, + p_mg_rt: undefined, + p_ca_rt: undefined, + p_ne: undefined, + p_s_rt: undefined, + p_s_wc: undefined, + p_cu_rt: undefined, + p_zn_rt: undefined, + p_na_rt: undefined, + p_si_rt: undefined, + p_b_rt: undefined, + p_mn_rt: undefined, + p_ni_rt: undefined, + p_fe_rt: undefined, + p_mo_rt: undefined, + p_co_rt: undefined, + p_as_rt: undefined, + p_cd_rt: undefined, + p_cr_rt: undefined, + p_cr_vi: undefined, + p_pb_rt: undefined, + p_hg_rt: undefined, + p_cl_rt: undefined, + p_app_method_options: [], + } + + // Get the available fertilizers + const fertilizers = await getFertilizers( + fdm, + session.principal_id, + b_id_farm, + ) + const fertilizerOptions = fertilizers.map((fertilizer) => { + return { + p_id: fertilizer.p_id, + p_name_nl: fertilizer.p_name_nl, + } + }) + + // Return user information from loader + return { + fertilizerOptions: fertilizerOptions, + fertilizer: fertilizer, + fertilizerParameters: fertilizerParameters, + } + } catch (error) { + throw handleLoaderError(error) + } +} + +/** + * Renders the layout for managing farm settings. + * + * This component displays a sidebar that includes the farm header, navigation options, and a link to farm fields. + * It also renders a main section containing the farm title, description, nested routes via an Outlet, and a notification toaster. + */ +export default function FarmFertilizerBlock() { + const loaderData = useLoaderData() + const { fertilizer, fertilizerParameters } = loaderData + + const form = useRemixForm>({ + mode: "onTouched", + resolver: zodResolver(FormSchema), + defaultValues: { + p_name_nl: fertilizer.p_name_nl, + p_type: fertilizer.p_type, + p_dm: fertilizer.p_dm, + p_density: fertilizer.p_density, + p_om: fertilizer.p_om, + p_a: fertilizer.p_a, + p_hc: fertilizer.p_hc, + p_eom: fertilizer.p_eom, + p_eoc: fertilizer.p_eoc, + p_c_rt: fertilizer.p_c_rt, + p_c_of: fertilizer.p_c_of, + p_c_if: fertilizer.p_c_if, + p_c_fr: fertilizer.p_c_fr, + p_cn_of: fertilizer.p_cn_of, + p_n_rt: fertilizer.p_n_rt, + p_n_if: fertilizer.p_n_if, + p_n_of: fertilizer.p_n_of, + p_n_wc: fertilizer.p_n_wc, + p_p_rt: fertilizer.p_p_rt, + p_k_rt: fertilizer.p_k_rt, + p_mg_rt: fertilizer.p_mg_rt, + p_ca_rt: fertilizer.p_ca_rt, + p_ne: fertilizer.p_ne, + p_s_rt: fertilizer.p_s_rt, + p_s_wc: fertilizer.p_s_wc, + p_cu_rt: fertilizer.p_cu_rt, + p_zn_rt: fertilizer.p_zn_rt, + p_na_rt: fertilizer.p_na_rt, + p_si_rt: fertilizer.p_si_rt, + p_b_rt: fertilizer.p_b_rt, + p_mn_rt: fertilizer.p_mn_rt, + p_ni_rt: fertilizer.p_ni_rt, + p_fe_rt: fertilizer.p_fe_rt, + p_mo_rt: fertilizer.p_mo_rt, + p_co_rt: fertilizer.p_co_rt, + p_as_rt: fertilizer.p_as_rt, + p_cd_rt: fertilizer.p_cd_rt, + p_cr_rt: fertilizer.p_cr_rt, + p_cr_vi: fertilizer.p_cr_vi, + p_pb_rt: fertilizer.p_pb_rt, + p_hg_rt: fertilizer.p_hg_rt, + p_cl_rt: fertilizer.p_cl_rt, + p_app_method_options: fertilizer.p_app_method_options, + }, + }) + + useEffect(() => { + form.reset({ + p_name_nl: fertilizer.p_name_nl, + p_type: fertilizer.p_type, + p_dm: fertilizer.p_dm, + p_density: fertilizer.p_density, + p_om: fertilizer.p_om, + p_a: fertilizer.p_a, + p_hc: fertilizer.p_hc, + p_eom: fertilizer.p_eom, + p_eoc: fertilizer.p_eoc, + p_c_rt: fertilizer.p_c_rt, + p_c_of: fertilizer.p_c_of, + p_c_if: fertilizer.p_c_if, + p_c_fr: fertilizer.p_c_fr, + p_cn_of: fertilizer.p_cn_of, + p_n_rt: fertilizer.p_n_rt, + p_n_if: fertilizer.p_n_if, + p_n_of: fertilizer.p_n_of, + p_n_wc: fertilizer.p_n_wc, + p_p_rt: fertilizer.p_p_rt, + p_k_rt: fertilizer.p_k_rt, + p_mg_rt: fertilizer.p_mg_rt, + p_ca_rt: fertilizer.p_ca_rt, + p_ne: fertilizer.p_ne, + p_s_rt: fertilizer.p_s_rt, + p_s_wc: fertilizer.p_s_wc, + p_cu_rt: fertilizer.p_cu_rt, + p_zn_rt: fertilizer.p_zn_rt, + p_na_rt: fertilizer.p_na_rt, + p_si_rt: fertilizer.p_si_rt, + p_b_rt: fertilizer.p_b_rt, + p_mn_rt: fertilizer.p_mn_rt, + p_ni_rt: fertilizer.p_ni_rt, + p_fe_rt: fertilizer.p_fe_rt, + p_mo_rt: fertilizer.p_mo_rt, + p_co_rt: fertilizer.p_co_rt, + p_as_rt: fertilizer.p_as_rt, + p_cd_rt: fertilizer.p_cd_rt, + p_cr_rt: fertilizer.p_cr_rt, + p_cr_vi: fertilizer.p_cr_vi, + p_pb_rt: fertilizer.p_pb_rt, + p_hg_rt: fertilizer.p_hg_rt, + p_cl_rt: fertilizer.p_cl_rt, + p_app_method_options: fertilizer.p_app_method_options, + }) + }, [fertilizer, form.reset]) + + return ( + + ) +} + +export async function action({ request, params }: ActionFunctionArgs) { + try { + const b_id_farm = params.b_id_farm + + if (!b_id_farm) { + throw new Error("missing: b_id_farm") + } + + const session = await getSession(request) + const formValues = await extractFormValuesFromRequest( + request, + FormSchema, + ) + + const p_id_catalogue = await addFertilizerToCatalogue( + fdm, + session.principal_id, + b_id_farm, + { + p_name_nl: formValues.p_name_nl, + p_name_en: formValues.p_name_en, + p_description: formValues.p_description, + p_type: formValues.p_type, + p_dm: formValues.p_dm, + p_density: formValues.p_density, + p_om: formValues.p_om, + p_a: formValues.p_a, + p_hc: formValues.p_hc, + p_eom: formValues.p_eom, + p_eoc: formValues.p_eoc, + p_c_rt: formValues.p_c_rt, + p_c_of: formValues.p_c_of, + p_c_if: formValues.p_c_if, + p_c_fr: formValues.p_c_fr, + p_cn_of: formValues.p_cn_of, + p_n_rt: formValues.p_n_rt, + p_n_if: formValues.p_n_if, + p_n_of: formValues.p_n_of, + p_n_wc: formValues.p_n_wc, + p_p_rt: formValues.p_p_rt, + p_k_rt: formValues.p_k_rt, + p_mg_rt: formValues.p_mg_rt, + p_ca_rt: formValues.p_ca_rt, + p_ne: formValues.p_ne, + p_s_rt: formValues.p_s_rt, + p_s_wc: formValues.p_s_wc, + p_cu_rt: formValues.p_cu_rt, + p_zn_rt: formValues.p_zn_rt, + p_na_rt: formValues.p_na_rt, + p_si_rt: formValues.p_si_rt, + p_b_rt: formValues.p_b_rt, + p_mn_rt: formValues.p_mn_rt, + p_ni_rt: formValues.p_ni_rt, + p_fe_rt: formValues.p_fe_rt, + p_mo_rt: formValues.p_mo_rt, + p_co_rt: formValues.p_co_rt, + p_as_rt: formValues.p_as_rt, + p_cd_rt: formValues.p_cd_rt, + p_cr_rt: formValues.p_cr_rt, + p_cr_vi: formValues.p_cr_vi, + p_pb_rt: formValues.p_pb_rt, + p_hg_rt: formValues.p_hg_rt, + p_cl_rt: formValues.p_cl_rt, + p_app_method_options: formValues.p_app_method_options, + }, + ) + + await addFertilizer( + fdm, + session.principal_id, + p_id_catalogue, + b_id_farm, + undefined, + undefined, + ) + + return redirectWithSuccess(`/farm/${b_id_farm}/fertilizers`, { + message: `${formValues.p_name_nl} is toegevoegd! 🎉`, + }) + } catch (error) { + throw handleActionError(error) + } +} diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx index d895a79b5..17ac96892 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx @@ -1,31 +1,17 @@ -import { zodResolver } from "@hookform/resolvers/zod" +import { getFarm, getFarms } from "@svenvw/fdm-core" import { - addFertilizer, - addFertilizerToCatalogue, - getFarm, - getFarms, - getFertilizers, -} from "@svenvw/fdm-core" -import { useEffect } from "react" -import { - type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction, + Outlet, data, useLoaderData, } from "react-router" -import { useRemixForm } from "remix-hook-form" -import { redirectWithSuccess } from "remix-toast" -import type { z } from "zod" import { FarmTitle } from "~/components/blocks/farm/farm-title" -import { FertilizerForm } from "~/components/blocks/fertilizer/form" -import { FormSchema } from "~/components/blocks/fertilizer/formschema" import { SidebarInset } from "~/components/ui/sidebar" import { getSession } from "~/lib/auth.server" import { clientConfig } from "~/lib/config" -import { handleActionError, handleLoaderError } from "~/lib/error" +import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" -import { extractFormValuesFromRequest } from "~/lib/form" import { Header } from "~/components/blocks/header/base" import { HeaderFarm } from "~/components/blocks/header/farm" import { HeaderFertilizer } from "~/components/blocks/header/fertilizer" @@ -76,36 +62,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const farmOptions = farms.map((farm) => { return { b_id_farm: farm.b_id_farm, - b_name_farm: farm.b_name_farm, - } - }) - - // Get selected fertilizer - const fertilizer = { - p_source: b_id_farm, - p_name_nl: undefined, - p_type: undefined, - p_n_rt: undefined, - p_n_wc: undefined, - p_p_rt: undefined, - p_k_rt: undefined, - p_om: undefined, - p_eoc: undefined, - p_s_rt: undefined, - p_ca_rt: undefined, - p_mg_rt: undefined, - } - - // Get the available fertilizers - const fertilizers = await getFertilizers( - fdm, - session.principal_id, - b_id_farm, - ) - const fertilizerOptions = fertilizers.map((fertilizer) => { - return { - p_id: fertilizer.p_id, - p_name_nl: fertilizer.p_name_nl, + b_name_farm: farm.b_name_farm || "", } }) @@ -114,8 +71,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { farm: farm, b_id_farm: b_id_farm, farmOptions: farmOptions, - fertilizerOptions: fertilizerOptions, - fertilizer: fertilizer, } } catch (error) { throw handleLoaderError(error) @@ -130,41 +85,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { */ export default function FarmFertilizerBlock() { const loaderData = useLoaderData() - const fertilizer = loaderData.fertilizer - - const form = useRemixForm>({ - mode: "onTouched", - resolver: zodResolver(FormSchema), - defaultValues: { - p_name_nl: fertilizer.p_name_nl, - p_type: fertilizer.p_type, - p_n_rt: fertilizer.p_n_rt, - p_n_wc: fertilizer.p_n_wc, - p_p_rt: fertilizer.p_p_rt, - p_k_rt: fertilizer.p_k_rt, - p_om: fertilizer.p_om, - p_eoc: fertilizer.p_eoc, - p_s_rt: fertilizer.p_s_rt, - p_ca_rt: fertilizer.p_ca_rt, - p_mg_rt: fertilizer.p_mg_rt, - }, - }) - - useEffect(() => { - form.reset({ - p_name_nl: fertilizer.p_name_nl, - p_type: fertilizer.p_type, - p_n_rt: fertilizer.p_n_rt, - p_n_wc: fertilizer.p_n_wc, - p_p_rt: fertilizer.p_p_rt, - p_k_rt: fertilizer.p_k_rt, - p_om: fertilizer.p_om, - p_eoc: fertilizer.p_eoc, - p_s_rt: fertilizer.p_s_rt, - p_ca_rt: fertilizer.p_ca_rt, - p_mg_rt: fertilizer.p_mg_rt, - }) - }, [fertilizer, form.reset]) return ( @@ -185,7 +105,7 @@ export default function FarmFertilizerBlock() { fertilizerOptions={[]} /> -
+
- +
) } - -export async function action({ request, params }: ActionFunctionArgs) { - try { - const b_id_farm = params.b_id_farm - - if (!b_id_farm) { - throw new Error("missing: b_id_farm") - } - - const session = await getSession(request) - const formValues = await extractFormValuesFromRequest( - request, - FormSchema, - ) - - const { - p_name_nl, - p_type, - p_n_rt, - p_n_wc, - p_p_rt, - p_k_rt, - p_om, - p_eoc, - p_s_rt, - p_ca_rt, - p_mg_rt, - } = formValues - - let p_type_manure = false - let p_type_compost = false - let p_type_mineral = false - if (p_type === "manure") { - p_type_manure = true - } - if (p_type === "compost") { - p_type_compost = true - } - if (p_type === "mineral") { - p_type_mineral = true - } - - const p_id_catalogue = await addFertilizerToCatalogue( - fdm, - session.principal_id, - b_id_farm, - { - p_name_nl, - p_type_manure, - p_type_mineral, - p_type_compost, - p_n_rt, - p_n_wc, - p_p_rt, - p_k_rt, - p_om, - p_eoc, - p_s_rt, - p_ca_rt, - p_mg_rt, - p_name_en: undefined, - p_description: undefined, - p_dm: undefined, - p_density: undefined, - p_a: undefined, - p_hc: undefined, - p_eom: undefined, - p_c_rt: undefined, - p_c_of: undefined, - p_c_if: undefined, - p_c_fr: undefined, - p_cn_of: undefined, - p_n_if: undefined, - p_n_of: undefined, - p_ne: undefined, - p_s_wc: undefined, - p_cu_rt: undefined, - p_zn_rt: undefined, - p_na_rt: undefined, - p_si_rt: undefined, - p_b_rt: undefined, - p_mn_rt: undefined, - p_ni_rt: undefined, - p_fe_rt: undefined, - p_mo_rt: undefined, - p_co_rt: undefined, - p_as_rt: undefined, - p_cd_rt: undefined, - pr_cr_rt: undefined, - p_cr_vi: undefined, - p_pb_rt: undefined, - p_hg_rt: undefined, - p_cl_rt: undefined, - }, - ) - - await addFertilizer( - fdm, - session.principal_id, - p_id_catalogue, - b_id_farm, - undefined, - undefined, - ) - - return redirectWithSuccess("../fertilizers", { - message: "Meststof is toegevoegd! 🎉", - }) - } catch (error) { - throw handleActionError(error) - } -} diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.fertilizers.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.fertilizers.tsx index 381ca8382..0268b39aa 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.fertilizers.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.fertilizers.tsx @@ -2,6 +2,7 @@ import { calculateDose } from "@svenvw/fdm-calculator" import { addFertilizerApplication, getCultivationPlan, + getFertilizerParametersDescription, getFertilizers, removeFertilizerApplication, } from "@svenvw/fdm-core" @@ -79,11 +80,26 @@ export async function loader({ request, params }: LoaderFunctionArgs) { session.principal_id, b_id_farm, ) + const fertilizerParameterDescription = + getFertilizerParametersDescription() + const applicationMethods = fertilizerParameterDescription.find( + (x) => x.parameter === "p_app_method_options", + ) + if (!applicationMethods) throw new Error("Parameter metadata missing") // Map fertilizers to options for the combobox const fertilizerOptions = fertilizers.map((fertilizer) => { + const applicationMethodOptions = fertilizer.p_app_method_options + .map((opt) => { + const meta = applicationMethods.options.find( + (x) => x.value === opt, + ) + return meta ? { value: opt, label: meta.label } : undefined + }) + .filter(Boolean) return { value: fertilizer.p_id, label: fertilizer.p_name_nl, + applicationMethodOptions: applicationMethodOptions, } }) @@ -143,6 +159,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { fertilizerOptions: fertilizerOptions, fertilizerApplications: fertilizerApplications, dose: dose.dose, + applicationMethodOptions: applicationMethods.options, } } catch (error) { return handleLoaderError(error) @@ -175,13 +192,15 @@ export default function Index() {
@@ -228,7 +247,7 @@ export async function action({ request, params }: ActionFunctionArgs) { request, FormSchema, ) - const { p_id, p_app_amount, p_app_date } = formValues + const { p_id, p_app_amount, p_app_date, p_app_method } = formValues // Get the cultivation details for this cultivation const cultivationPlan = await getCultivationPlan( @@ -252,7 +271,7 @@ export async function action({ request, params }: ActionFunctionArgs) { b_id, p_id, p_app_amount, - undefined, + p_app_method, p_app_date, ) }), diff --git a/fdm-app/package.json b/fdm-app/package.json index 8eedff1f9..9af6117a3 100644 --- a/fdm-app/package.json +++ b/fdm-app/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", @@ -42,6 +43,7 @@ "@sentry/react": "^9.22.0", "@svenvw/fdm-calculator": "workspace:^", "@svenvw/fdm-core": "workspace:^", + "@svenvw/fdm-data": "workspace:*", "@tailwindcss/vite": "^4.1.8", "@tanstack/react-table": "^8.21.3", "@turf/centroid": "^7.2.0", @@ -88,6 +90,7 @@ "@react-router/fs-routes": "^7.6.1", "@svenvw/fdm-calculator": "workspace:*", "@svenvw/fdm-core": "workspace:*", + "@svenvw/fdm-data": "workspace:*", "@tailwindcss/postcss": "^4.1.8", "@types/geojson": "^7946.0.16", "@types/lodash.throttle": "^4.1.9", diff --git a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/compost.test.ts b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/compost.test.ts index ce7f065c4..93e86baca 100644 --- a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/compost.test.ts +++ b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/compost.test.ts @@ -32,20 +32,16 @@ describe("calculateNitrogenSupplyByCompost", () => { "app1", { p_id_catalogue: "app1", - p_type_compost: true, p_n_rt: 20, - p_type_manure: false, - p_type_mineral: false, + p_type: "compost", }, ], [ "app2", { p_id_catalogue: "app2", - p_type_compost: true, p_n_rt: 15, - p_type_manure: false, - p_type_mineral: false, + p_type: "compost", }, ], ]) diff --git a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/compost.ts b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/compost.ts index b50f28c45..ec4b9d465 100644 --- a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/compost.ts +++ b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/compost.ts @@ -37,11 +37,10 @@ export function calculateNitrogenSupplyByCompost( ) } - const p_type_compost = fertilizerDetail.p_type_compost const p_n_rt = new Decimal(fertilizerDetail.p_n_rt ?? 0).dividedBy(1000) // Convert from g N / kg to kg N / kg // If the fertilizer used is not of the type compost - if (p_type_compost === false) { + if (fertilizerDetail.p_type !== "compost") { return { id: application.p_app_id, value: new Decimal(0), diff --git a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/index.test.ts b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/index.test.ts index e97231747..b0c33374e 100644 --- a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/index.test.ts +++ b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/index.test.ts @@ -42,40 +42,32 @@ describe("calculateNitrogenSupplyByFertilizers", () => { "mineral1", { p_id_catalogue: "mineral1", - p_type_compost: false, p_n_rt: 20, - p_type_manure: false, - p_type_mineral: true, + p_type: "mineral", }, ], [ "manure1", { p_id_catalogue: "manure1", - p_type_compost: false, p_n_rt: 15, - p_type_manure: true, - p_type_mineral: false, + p_type: "manure", }, ], [ "compost1", { p_id_catalogue: "compost1", - p_type_compost: true, p_n_rt: 10, - p_type_manure: false, - p_type_mineral: false, + p_type: "compost", }, ], [ "other1", { p_id_catalogue: "other1", - p_type_compost: false, p_n_rt: 10, - p_type_manure: false, - p_type_mineral: false, + p_type: "other", }, ], ]) @@ -107,10 +99,8 @@ describe("calculateNitrogenSupplyByFertilizers", () => { "mineral1", { p_id_catalogue: "mineral1", - p_type_compost: false, p_n_rt: 20, - p_type_manure: false, - p_type_mineral: true, + p_type: "mineral", }, ], ]) diff --git a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/manure.test.ts b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/manure.test.ts index 68c4a91a4..bae211e83 100644 --- a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/manure.test.ts +++ b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/manure.test.ts @@ -36,20 +36,16 @@ describe("calculateNitrogenSupplyByManure", () => { "app1", { p_id_catalogue: "app1", - p_type_compost: false, p_n_rt: 20, - p_type_manure: true, - p_type_mineral: false, + p_type: "manure", }, ], [ "app2", { p_id_catalogue: "app2", - p_type_compost: false, p_n_rt: 15, - p_type_manure: true, - p_type_mineral: false, + p_type: "manure", }, ], ]) diff --git a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/manure.ts b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/manure.ts index c0c760af2..ff17963ca 100644 --- a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/manure.ts +++ b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/manure.ts @@ -36,11 +36,10 @@ export function calculateNitrogenSupplyByManure( `Fertilizer application ${application.p_app_id} has no fertilizerDetails`, ) } - const p_type_manure = fertilizerDetail.p_type_manure const p_n_rt = new Decimal(fertilizerDetail.p_n_rt ?? 0) // If the fertilizer used is not of the type manure - if (p_type_manure === false) { + if (fertilizerDetail.p_type !== "manure") { return { id: application.p_app_id, value: new Decimal(0), diff --git a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/mineral.test.ts b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/mineral.test.ts index d5ccf7704..3af381ee4 100644 --- a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/mineral.test.ts +++ b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/mineral.test.ts @@ -36,20 +36,16 @@ describe("calculateNitrogenSupplyByMineralFertilizers", () => { "app1", { p_id_catalogue: "app1", - p_type_compost: false, p_n_rt: 20, - p_type_manure: false, - p_type_mineral: true, + p_type: "mineral", }, ], [ "app2", { p_id_catalogue: "app2", - p_type_compost: false, p_n_rt: 15, - p_type_manure: false, - p_type_mineral: true, + p_type: "mineral", }, ], ]) diff --git a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/mineral.ts b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/mineral.ts index 0d69d1aed..d24a7538c 100644 --- a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/mineral.ts +++ b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/mineral.ts @@ -35,11 +35,10 @@ export function calculateNitrogenSupplyByMineralFertilizers( `Fertilizer application ${application.p_app_id} has no fertilizerDetails`, ) } - const p_type_mineral = fertilizerDetail.p_type_mineral const p_n_rt = new Decimal(fertilizerDetail.p_n_rt ?? 0).dividedBy(1000) // Convert from g N / kg to kg N / kg // If the fertilizer used is not of the type mineral - if (p_type_mineral === false) { + if (fertilizerDetail.p_type !== "mineral") { return { id: application.p_app_id, value: new Decimal(0), diff --git a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/other.test.ts b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/other.test.ts index 0005d1bfd..0c6309973 100644 --- a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/other.test.ts +++ b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/other.test.ts @@ -36,20 +36,16 @@ describe("calculateNitrogenSupplyByOtherFertilizers", () => { "app1", { p_id_catalogue: "app1", - p_type_compost: false, p_n_rt: 20, - p_type_manure: false, - p_type_mineral: false, + p_type: "other", }, ], [ "app2", { p_id_catalogue: "app2", - p_type_compost: false, p_n_rt: 15, - p_type_manure: false, - p_type_mineral: false, + p_type: "other", }, ], ]) diff --git a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/other.ts b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/other.ts index 1e7164ee1..e6bfc14b4 100644 --- a/fdm-calculator/src/balance/nitrogen/supply/fertilizers/other.ts +++ b/fdm-calculator/src/balance/nitrogen/supply/fertilizers/other.ts @@ -30,16 +30,14 @@ export function calculateNitrogenSupplyByOtherFertilizers( `Fertilizer application ${application.p_app_id} has no fertilizerDetails`, ) } - const p_type_manure = fertilizerDetail.p_type_manure - const p_type_mineral = fertilizerDetail.p_type_mineral - const p_type_compost = fertilizerDetail.p_type_compost - const p_n_rt = new Decimal(fertilizerDetail.p_n_rt ?? 0) + + const p_n_rt = new Decimal(fertilizerDetail.p_n_rt ?? 0).dividedBy(1000) // Convert from g N / kg to kg N / kg // If the fertilizer used is not of the type other fertilizers if ( - p_type_manure === true || - p_type_mineral === true || - p_type_compost === true + fertilizerDetail.p_type === "manure" || + fertilizerDetail.p_type === "mineral" || + fertilizerDetail.p_type === "compost" ) { return { id: application.p_app_id, @@ -49,7 +47,7 @@ export function calculateNitrogenSupplyByOtherFertilizers( // Calculate for this application the amount of Nitrogen supplied by other fertilizers const p_app_amount = new Decimal(application.p_app_amount ?? 0) - const applicationValue = p_app_amount.times(p_n_rt).dividedBy(1000) // convert from g N to kg N + const applicationValue = p_app_amount.times(p_n_rt) return { id: application.p_app_id, diff --git a/fdm-calculator/src/balance/nitrogen/types.d.ts b/fdm-calculator/src/balance/nitrogen/types.d.ts index 7b66ab8d2..99d48e6f3 100644 --- a/fdm-calculator/src/balance/nitrogen/types.d.ts +++ b/fdm-calculator/src/balance/nitrogen/types.d.ts @@ -389,9 +389,7 @@ export type FertilizerDetail = Pick< Fertilizer, | "p_id_catalogue" | "p_n_rt" // Total nitrogen content (g N / kg fertilizer) - | "p_type_manure" - | "p_type_mineral" - | "p_type_compost" + | "p_type" > /** diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index 4451571f9..c0d4adc0f 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -527,12 +527,17 @@ describe("Catalogues", () => { // Should not reach here expect(true).toBe(false) } catch (error) { - expect(error.message).toContain( + const err = error as Error & { context: unknown } + expect(err.message).toContain( "Exception for getEnabledCultivationCatalogues", ) - expect(error.context).toBeDefined() - expect(error.context.principal_id).toBe(principal_id) - expect(error.context.b_id_farm).toBe(b_id_farm) + expect(err.context).toBeDefined() + expect((err.context as { principal_id: string }).principal_id).toBe( + principal_id, + ) + expect((err.context as { b_id_farm: string }).b_id_farm).toBe( + b_id_farm, + ) } }) @@ -662,6 +667,48 @@ describe("Catalogues syncing", () => { expect(itemSynced[0].hash).toBe(item[0].hash) }) + it("should update all columns of a fertilizer when the hash changes", async () => { + await syncCatalogues(fdm) + + const item = await fdm + .select() + .from(schema.fertilizersCatalogue) + .where(isNotNull(schema.fertilizersCatalogue.hash)) + .orderBy(schema.fertilizersCatalogue.p_id_catalogue) + .limit(1) + + const updatedItem = { + ...item[0], + p_name_nl: "Updated Name", + hash: "new_hash", + } + + await fdm + .update(schema.fertilizersCatalogue) + .set(updatedItem) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) + + await syncCatalogues(fdm) + + const syncedItem = await fdm + .select() + .from(schema.fertilizersCatalogue) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) + .limit(1) + + expect(syncedItem[0].p_name_nl).not.toBe("Updated Name") + }) + it("should update cultivation catalogue", async () => { await syncCatalogues(fdm) @@ -719,4 +766,46 @@ describe("Catalogues syncing", () => { expect(itemSynced[0].b_lu_catalogue).toBeDefined() expect(itemSynced[0].hash).toBe(item[0].hash) }) + + it("should update all columns of a cultivation when the hash changes", async () => { + await syncCatalogues(fdm) + + const item = await fdm + .select() + .from(schema.cultivationsCatalogue) + .where(isNotNull(schema.cultivationsCatalogue.hash)) + .orderBy(schema.cultivationsCatalogue.b_lu_catalogue) + .limit(1) + + const updatedItem = { + ...item[0], + b_lu_name: "Updated Name", + hash: "new_hash", + } + + await fdm + .update(schema.cultivationsCatalogue) + .set(updatedItem) + .where( + eq( + schema.cultivationsCatalogue.b_lu_catalogue, + item[0].b_lu_catalogue, + ), + ) + + await syncCatalogues(fdm) + + const syncedItem = await fdm + .select() + .from(schema.cultivationsCatalogue) + .where( + eq( + schema.cultivationsCatalogue.b_lu_catalogue, + item[0].b_lu_catalogue, + ), + ) + .limit(1) + + expect(syncedItem[0].b_lu_name).not.toBe("Updated Name") + }) }) diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index d3f2e9454..793702a35 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -10,6 +10,7 @@ import type { PrincipalId } from "./authorization.d" import * as schema from "./db/schema" import { handleError } from "./error" import type { FdmType } from "./fdm" +import type { FdmServerType } from "./fdm-server.d" /** * Gets all enabled fertilizer catalogues for a farm. @@ -376,7 +377,7 @@ export async function syncCatalogues(fdm: FdmType): Promise { async function syncFertilizerCatalogue(fdm: FdmType) { const srmCatalogue = await getFertilizersCatalogue("srm") - await fdm.transaction(async (tx) => { + await fdm.transaction(async (tx: FdmServerType) => { try { for (const item of srmCatalogue) { const hash = await hashFertilizer(item) @@ -405,7 +406,7 @@ async function syncFertilizerCatalogue(fdm: FdmType) { ) { await tx .update(schema.fertilizersCatalogue) - .set({ hash: hash }) + .set({ ...item, hash: hash, updated: new Date() }) .where( eq( schema.fertilizersCatalogue.p_id_catalogue, @@ -424,7 +425,7 @@ async function syncFertilizerCatalogue(fdm: FdmType) { async function syncCultivationCatalogue(fdm: FdmType) { const brpCatalogue = await getCultivationCatalogue("brp") - await fdm.transaction(async (tx) => { + await fdm.transaction(async (tx: FdmServerType) => { try { for (const item of brpCatalogue) { const hash = await hashCultivation(item) @@ -453,7 +454,7 @@ async function syncCultivationCatalogue(fdm: FdmType) { ) { await tx .update(schema.cultivationsCatalogue) - .set({ hash: hash }) + .set({ ...item, hash: hash, updated: new Date() }) .where( eq( schema.cultivationsCatalogue.b_lu_catalogue, diff --git a/fdm-core/src/cultivation.test.ts b/fdm-core/src/cultivation.test.ts index f42d6d846..0819607f1 100644 --- a/fdm-core/src/cultivation.test.ts +++ b/fdm-core/src/cultivation.test.ts @@ -671,6 +671,7 @@ describe("Cultivation Data Model", () => { principal_id, b_id_farm, { + p_app_method_options: null, p_name_nl, p_name_en, p_description, @@ -714,9 +715,7 @@ describe("Cultivation Data Model", () => { p_pb_rt: 370, p_hg_rt: 380, p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, + p_type: "manure", }, ) @@ -1140,13 +1139,17 @@ describe("getCultivationsFromCatalogue error handling", () => { ) // Should not reach here expect.fail("Expected an error to be thrown") - } catch (err: any) { + } catch (err) { + type ErrorWithContext = Error & { + context: { principal_id: string; b_id_farm: string } + } + const e = err as ErrorWithContext // Check that error was handled correctly - expect(err).toBeDefined() - expect(err.message).toContain( + expect(e).toBeDefined() + expect(e.message).toContain( "Exception for getCultivationsFromCatalogue", ) - expect(err.context).toEqual({ + expect(e.context).toEqual({ principal_id, b_id_farm, }) diff --git a/fdm-core/src/db/migrations/0007_flippant_doctor_octopus.sql b/fdm-core/src/db/migrations/0007_flippant_doctor_octopus.sql new file mode 100644 index 000000000..2ab801bed --- /dev/null +++ b/fdm-core/src/db/migrations/0007_flippant_doctor_octopus.sql @@ -0,0 +1 @@ +ALTER TABLE "fdm"."fertilizers_catalogue" ADD COLUMN "p_app_method_options" "fdm"."p_app_method"[]; \ No newline at end of file diff --git a/fdm-core/src/db/migrations/meta/0007_snapshot.json b/fdm-core/src/db/migrations/meta/0007_snapshot.json new file mode 100644 index 000000000..5d2e03313 --- /dev/null +++ b/fdm-core/src/db/migrations/meta/0007_snapshot.json @@ -0,0 +1,2951 @@ +{ + "id": "c4d35681-e2ca-4cb7-b632-7c00b025dc3d", + "prevId": "c58fdae0-902d-4a8e-b24b-c96cda17c470", + "version": "7", + "dialect": "postgresql", + "tables": { + "fdm.cultivation_catalogue_selecting": { + "name": "cultivation_catalogue_selecting", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_source": { + "name": "b_lu_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_catalogue_selecting_b_id_farm_farms_b_id_farm_fk": { + "name": "cultivation_catalogue_selecting_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "cultivation_catalogue_selecting", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_ending": { + "name": "cultivation_ending", + "schema": "fdm", + "columns": { + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_end": { + "name": "b_lu_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "m_cropresidue": { + "name": "m_cropresidue", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_ending_b_lu_cultivations_b_lu_fk": { + "name": "cultivation_ending_b_lu_cultivations_b_lu_fk", + "tableFrom": "cultivation_ending", + "tableTo": "cultivations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu" + ], + "columnsTo": [ + "b_lu" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_harvesting": { + "name": "cultivation_harvesting", + "schema": "fdm", + "columns": { + "b_id_harvesting": { + "name": "b_id_harvesting", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_id_harvestable": { + "name": "b_id_harvestable", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_harvest_date": { + "name": "b_lu_harvest_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_harvesting_b_id_harvestable_harvestables_b_id_harvestable_fk": { + "name": "cultivation_harvesting_b_id_harvestable_harvestables_b_id_harvestable_fk", + "tableFrom": "cultivation_harvesting", + "tableTo": "harvestables", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_harvestable" + ], + "columnsTo": [ + "b_id_harvestable" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cultivation_harvesting_b_lu_cultivations_b_lu_fk": { + "name": "cultivation_harvesting_b_lu_cultivations_b_lu_fk", + "tableFrom": "cultivation_harvesting", + "tableTo": "cultivations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu" + ], + "columnsTo": [ + "b_lu" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_starting": { + "name": "cultivation_starting", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_start": { + "name": "b_lu_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_sowing_amount": { + "name": "b_sowing_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_sowing_method": { + "name": "b_sowing_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_starting_b_id_fields_b_id_fk": { + "name": "cultivation_starting_b_id_fields_b_id_fk", + "tableFrom": "cultivation_starting", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cultivation_starting_b_lu_cultivations_b_lu_fk": { + "name": "cultivation_starting_b_lu_cultivations_b_lu_fk", + "tableFrom": "cultivation_starting", + "tableTo": "cultivations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu" + ], + "columnsTo": [ + "b_lu" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivations": { + "name": "cultivations", + "schema": "fdm", + "columns": { + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_catalogue": { + "name": "b_lu_catalogue", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_lu_idx": { + "name": "b_lu_idx", + "columns": [ + { + "expression": "b_lu", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cultivations_b_lu_catalogue_cultivations_catalogue_b_lu_catalogue_fk": { + "name": "cultivations_b_lu_catalogue_cultivations_catalogue_b_lu_catalogue_fk", + "tableFrom": "cultivations", + "tableTo": "cultivations_catalogue", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu_catalogue" + ], + "columnsTo": [ + "b_lu_catalogue" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivations_catalogue": { + "name": "cultivations_catalogue", + "schema": "fdm", + "columns": { + "b_lu_catalogue": { + "name": "b_lu_catalogue", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_source": { + "name": "b_lu_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_name": { + "name": "b_lu_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_name_en": { + "name": "b_lu_name_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_lu_harvestable": { + "name": "b_lu_harvestable", + "type": "b_lu_harvestable", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": true + }, + "b_lu_hcat3": { + "name": "b_lu_hcat3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_lu_hcat3_name": { + "name": "b_lu_hcat3_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_lu_croprotation": { + "name": "b_lu_croprotation", + "type": "b_lu_croprotation", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "b_lu_yield": { + "name": "b_lu_yield", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_hi": { + "name": "b_lu_hi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_harvestable": { + "name": "b_lu_n_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_residue": { + "name": "b_lu_n_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_n_fixation": { + "name": "b_n_fixation", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_lu_catalogue_idx": { + "name": "b_lu_catalogue_idx", + "columns": [ + { + "expression": "b_lu_catalogue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.farms": { + "name": "farms", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_name_farm": { + "name": "b_name_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_businessid_farm": { + "name": "b_businessid_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_address_farm": { + "name": "b_address_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_postalcode_farm": { + "name": "b_postalcode_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_farm_idx": { + "name": "b_id_farm_idx", + "columns": [ + { + "expression": "b_id_farm", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_acquiring": { + "name": "fertilizer_acquiring", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_acquiring_amount": { + "name": "p_acquiring_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_acquiring_date": { + "name": "p_acquiring_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fertilizer_acquiring_b_id_farm_farms_b_id_farm_fk": { + "name": "fertilizer_acquiring_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "fertilizer_acquiring", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fertilizer_acquiring_p_id_fertilizers_p_id_fk": { + "name": "fertilizer_acquiring_p_id_fertilizers_p_id_fk", + "tableFrom": "fertilizer_acquiring", + "tableTo": "fertilizers", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id" + ], + "columnsTo": [ + "p_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_applying": { + "name": "fertilizer_applying", + "schema": "fdm", + "columns": { + "p_app_id": { + "name": "p_app_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_app_amount": { + "name": "p_app_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_app_method": { + "name": "p_app_method", + "type": "p_app_method", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "p_app_date": { + "name": "p_app_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "p_app_idx": { + "name": "p_app_idx", + "columns": [ + { + "expression": "p_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fertilizer_applying_b_id_fields_b_id_fk": { + "name": "fertilizer_applying_b_id_fields_b_id_fk", + "tableFrom": "fertilizer_applying", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fertilizer_applying_p_id_fertilizers_p_id_fk": { + "name": "fertilizer_applying_p_id_fertilizers_p_id_fk", + "tableFrom": "fertilizer_applying", + "tableTo": "fertilizers", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id" + ], + "columnsTo": [ + "p_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_catalogue_enabling": { + "name": "fertilizer_catalogue_enabling", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_source": { + "name": "p_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fertilizer_catalogue_enabling_b_id_farm_farms_b_id_farm_fk": { + "name": "fertilizer_catalogue_enabling_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "fertilizer_catalogue_enabling", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_picking": { + "name": "fertilizer_picking", + "schema": "fdm", + "columns": { + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_id_catalogue": { + "name": "p_id_catalogue", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_picking_date": { + "name": "p_picking_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fertilizer_picking_p_id_fertilizers_p_id_fk": { + "name": "fertilizer_picking_p_id_fertilizers_p_id_fk", + "tableFrom": "fertilizer_picking", + "tableTo": "fertilizers", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id" + ], + "columnsTo": [ + "p_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fertilizer_picking_p_id_catalogue_fertilizers_catalogue_p_id_catalogue_fk": { + "name": "fertilizer_picking_p_id_catalogue_fertilizers_catalogue_p_id_catalogue_fk", + "tableFrom": "fertilizer_picking", + "tableTo": "fertilizers_catalogue", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id_catalogue" + ], + "columnsTo": [ + "p_id_catalogue" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizers": { + "name": "fertilizers", + "schema": "fdm", + "columns": { + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "p_id_idx": { + "name": "p_id_idx", + "columns": [ + { + "expression": "p_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizers_catalogue": { + "name": "fertilizers_catalogue", + "schema": "fdm", + "columns": { + "p_id_catalogue": { + "name": "p_id_catalogue", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "p_source": { + "name": "p_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_name_nl": { + "name": "p_name_nl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_name_en": { + "name": "p_name_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "p_description": { + "name": "p_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "p_app_method_options": { + "name": "p_app_method_options", + "type": "p_app_method[]", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "p_dm": { + "name": "p_dm", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_density": { + "name": "p_density", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_om": { + "name": "p_om", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_a": { + "name": "p_a", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_hc": { + "name": "p_hc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_eom": { + "name": "p_eom", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_eoc": { + "name": "p_eoc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_rt": { + "name": "p_c_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_of": { + "name": "p_c_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_if": { + "name": "p_c_if", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_fr": { + "name": "p_c_fr", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cn_of": { + "name": "p_cn_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_rt": { + "name": "p_n_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_if": { + "name": "p_n_if", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_of": { + "name": "p_n_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_wc": { + "name": "p_n_wc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_p_rt": { + "name": "p_p_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_k_rt": { + "name": "p_k_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_mg_rt": { + "name": "p_mg_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ca_rt": { + "name": "p_ca_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ne": { + "name": "p_ne", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_s_rt": { + "name": "p_s_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_s_wc": { + "name": "p_s_wc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cu_rt": { + "name": "p_cu_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_zn_rt": { + "name": "p_zn_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_na_rt": { + "name": "p_na_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_si_rt": { + "name": "p_si_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_b_rt": { + "name": "p_b_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_mn_rt": { + "name": "p_mn_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ni_rt": { + "name": "p_ni_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_fe_rt": { + "name": "p_fe_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_mo_rt": { + "name": "p_mo_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_co_rt": { + "name": "p_co_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_as_rt": { + "name": "p_as_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cd_rt": { + "name": "p_cd_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cr_rt": { + "name": "p_cr_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cr_vi": { + "name": "p_cr_vi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_pb_rt": { + "name": "p_pb_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_hg_rt": { + "name": "p_hg_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cl_rt": { + "name": "p_cl_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_type_manure": { + "name": "p_type_manure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "p_type_mineral": { + "name": "p_type_mineral", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "p_type_compost": { + "name": "p_type_compost", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "p_id_catalogue_idx": { + "name": "p_id_catalogue_idx", + "columns": [ + { + "expression": "p_id_catalogue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.field_acquiring": { + "name": "field_acquiring", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_start": { + "name": "b_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_acquiring_method": { + "name": "b_acquiring_method", + "type": "b_acquiring_method", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "field_acquiring_b_id_fields_b_id_fk": { + "name": "field_acquiring_b_id_fields_b_id_fk", + "tableFrom": "field_acquiring", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "field_acquiring_b_id_farm_farms_b_id_farm_fk": { + "name": "field_acquiring_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "field_acquiring", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.field_discarding": { + "name": "field_discarding", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_end": { + "name": "b_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "field_discarding_b_id_fields_b_id_fk": { + "name": "field_discarding_b_id_fields_b_id_fk", + "tableFrom": "field_discarding", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fields": { + "name": "fields", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_name": { + "name": "b_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_geometry": { + "name": "b_geometry", + "type": "geometry(Polygon,4326)", + "primaryKey": false, + "notNull": false + }, + "b_id_source": { + "name": "b_id_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_idx": { + "name": "b_id_idx", + "columns": [ + { + "expression": "b_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "b_geom_idx": { + "name": "b_geom_idx", + "columns": [ + { + "expression": "b_geometry", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.harvestable_analyses": { + "name": "harvestable_analyses", + "schema": "fdm", + "columns": { + "b_id_harvestable_analysis": { + "name": "b_id_harvestable_analysis", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_yield": { + "name": "b_lu_yield", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_harvestable": { + "name": "b_lu_n_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_residue": { + "name": "b_lu_n_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_p_harvestable": { + "name": "b_lu_p_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_p_residue": { + "name": "b_lu_p_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_k_harvestable": { + "name": "b_lu_k_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_k_residue": { + "name": "b_lu_k_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_harvestable_analyses_idx": { + "name": "b_id_harvestable_analyses_idx", + "columns": [ + { + "expression": "b_id_harvestable_analysis", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.harvestable_sampling": { + "name": "harvestable_sampling", + "schema": "fdm", + "columns": { + "b_id_harvestable": { + "name": "b_id_harvestable", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_harvestable_analysis": { + "name": "b_id_harvestable_analysis", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_sampling_date": { + "name": "b_sampling_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "harvestable_sampling_b_id_harvestable_harvestables_b_id_harvestable_fk": { + "name": "harvestable_sampling_b_id_harvestable_harvestables_b_id_harvestable_fk", + "tableFrom": "harvestable_sampling", + "tableTo": "harvestables", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_harvestable" + ], + "columnsTo": [ + "b_id_harvestable" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "harvestable_sampling_b_id_harvestable_analysis_harvestable_analyses_b_id_harvestable_analysis_fk": { + "name": "harvestable_sampling_b_id_harvestable_analysis_harvestable_analyses_b_id_harvestable_analysis_fk", + "tableFrom": "harvestable_sampling", + "tableTo": "harvestable_analyses", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_harvestable_analysis" + ], + "columnsTo": [ + "b_id_harvestable_analysis" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.harvestables": { + "name": "harvestables", + "schema": "fdm", + "columns": { + "b_id_harvestable": { + "name": "b_id_harvestable", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_harvestable_idx": { + "name": "b_id_harvestable_idx", + "columns": [ + { + "expression": "b_id_harvestable", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.soil_analysis": { + "name": "soil_analysis", + "schema": "fdm", + "columns": { + "a_id": { + "name": "a_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "a_date": { + "name": "a_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "a_source": { + "name": "a_source", + "type": "a_source", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false, + "default": "'other'" + }, + "a_al_ox": { + "name": "a_al_ox", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_c_of": { + "name": "a_c_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_ca_co": { + "name": "a_ca_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_ca_co_po": { + "name": "a_ca_co_po", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_caco3_if": { + "name": "a_caco3_if", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_cec_co": { + "name": "a_cec_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_clay_mi": { + "name": "a_clay_mi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_cn_fr": { + "name": "a_cn_fr", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_com_fr": { + "name": "a_com_fr", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_cu_cc": { + "name": "a_cu_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_density_sa": { + "name": "a_density_sa", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_fe_ox": { + "name": "a_fe_ox", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_k_cc": { + "name": "a_k_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_k_co": { + "name": "a_k_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_k_co_po": { + "name": "a_k_co_po", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_mg_cc": { + "name": "a_mg_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_mg_co": { + "name": "a_mg_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_mg_co_po": { + "name": "a_mg_co_po", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_n_pmn": { + "name": "a_n_pmn", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_n_rt": { + "name": "a_n_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_nh4_cc": { + "name": "a_nh4_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_nmin_cc": { + "name": "a_nmin_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_no3_cc": { + "name": "a_no3_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_al": { + "name": "a_p_al", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_cc": { + "name": "a_p_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_ox": { + "name": "a_p_ox", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_rt": { + "name": "a_p_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_sg": { + "name": "a_p_sg", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_wa": { + "name": "a_p_wa", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_ph_cc": { + "name": "a_ph_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_s_rt": { + "name": "a_s_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_sand_mi": { + "name": "a_sand_mi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_silt_mi": { + "name": "a_silt_mi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_som_loi": { + "name": "a_som_loi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_zn_cc": { + "name": "a_zn_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_gwl_class": { + "name": "b_gwl_class", + "type": "b_gwl_class", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "b_soiltype_agr": { + "name": "b_soiltype_agr", + "type": "b_soiltype_agr", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.soil_sampling": { + "name": "soil_sampling", + "schema": "fdm", + "columns": { + "b_id_sampling": { + "name": "b_id_sampling", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "a_id": { + "name": "a_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "a_depth_upper": { + "name": "a_depth_upper", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "a_depth_lower": { + "name": "a_depth_lower", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_sampling_date": { + "name": "b_sampling_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_sampling_geometry": { + "name": "b_sampling_geometry", + "type": "geometry(MultiPoint,4326)", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "soil_sampling_b_id_fields_b_id_fk": { + "name": "soil_sampling_b_id_fields_b_id_fk", + "tableFrom": "soil_sampling", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "soil_sampling_a_id_soil_analysis_a_id_fk": { + "name": "soil_sampling_a_id_soil_analysis_a_id_fk", + "tableFrom": "soil_sampling", + "tableTo": "soil_analysis", + "schemaTo": "fdm", + "columnsFrom": [ + "a_id" + ], + "columnsTo": [ + "a_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.account": { + "name": "account", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.invitation": { + "name": "invitation", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.member": { + "name": "member", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.organization": { + "name": "organization", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.rate_limit": { + "name": "rate_limit", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.session": { + "name": "session", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.user": { + "name": "user", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "firstname": { + "name": "firstname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surname": { + "name": "surname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lang": { + "name": "lang", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "farm_active": { + "name": "farm_active", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.verification": { + "name": "verification", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authz.audit": { + "name": "audit", + "schema": "fdm-authz", + "columns": { + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "audit_timestamp": { + "name": "audit_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "audit_origin": { + "name": "audit_origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_resource": { + "name": "target_resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_resource_id": { + "name": "target_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granting_resource": { + "name": "granting_resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granting_resource_id": { + "name": "granting_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed": { + "name": "allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authz.role": { + "name": "role", + "schema": "fdm-authz", + "columns": { + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted": { + "name": "deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "role_idx": { + "name": "role_idx", + "columns": [ + { + "expression": "resource", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "fdm.b_acquiring_method": { + "name": "b_acquiring_method", + "schema": "fdm", + "values": [ + "owner", + "lease", + "unknown" + ] + }, + "fdm.p_app_method": { + "name": "p_app_method", + "schema": "fdm", + "values": [ + "slotted coulter", + "incorporation", + "injection", + "spraying", + "broadcasting", + "spoke wheel", + "pocket placement" + ] + }, + "fdm.b_gwl_class": { + "name": "b_gwl_class", + "schema": "fdm", + "values": [ + "II", + "IV", + "IIIb", + "V", + "VI", + "VII", + "Vb", + "-", + "Va", + "III", + "VIII", + "sVI", + "I", + "IIb", + "sVII", + "IVu", + "bVII", + "sV", + "sVb", + "bVI", + "IIIa" + ] + }, + "fdm.b_lu_harvestable": { + "name": "b_lu_harvestable", + "schema": "fdm", + "values": [ + "none", + "once", + "multiple" + ] + }, + "fdm.b_lu_croprotation": { + "name": "b_lu_croprotation", + "schema": "fdm", + "values": [ + "other", + "clover", + "nature", + "potato", + "grass", + "rapeseed", + "starch", + "maize", + "cereal", + "sugarbeet", + "alfalfa", + "catchcrop" + ] + }, + "fdm.a_source": { + "name": "a_source", + "schema": "fdm", + "values": [ + "nl-rva-l122", + "nl-rva-l136", + "nl-rva-l264", + "nl-rva-l320", + "nl-rva-l335", + "nl-rva-l610", + "nl-rva-l648", + "nl-rva-l697", + "nl-other-nmi", + "other" + ] + }, + "fdm.b_soiltype_agr": { + "name": "b_soiltype_agr", + "schema": "fdm", + "values": [ + "moerige_klei", + "rivierklei", + "dekzand", + "zeeklei", + "dalgrond", + "veen", + "loess", + "duinzand", + "maasklei" + ] + } + }, + "schemas": { + "fdm": "fdm", + "fdm-authn": "fdm-authn", + "fdm-authz": "fdm-authz" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/fdm-core/src/db/migrations/meta/_journal.json b/fdm-core/src/db/migrations/meta/_journal.json index 4af792f25..4c7d6f667 100644 --- a/fdm-core/src/db/migrations/meta/_journal.json +++ b/fdm-core/src/db/migrations/meta/_journal.json @@ -1,55 +1,62 @@ { - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1731414293847, - "tag": "0000_v0", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1741267610502, - "tag": "0001_v0-15-0", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1743420907290, - "tag": "0002_v0-18-0", - "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1744205441260, - "tag": "0003_v0-20-0-1", - "breakpoints": true - }, - { - "idx": 4, - "version": "7", - "when": 1745410821339, - "tag": "0004_v0-20-0-2", - "breakpoints": true - }, - { - "idx": 5, - "version": "7", - "when": 1748353081475, - "tag": "0005_v0-20-0-3", - "breakpoints": true - }, - { - "idx": 6, - "version": "7", - "when": 1748353926519, - "tag": "0006_v0-20-0-4", - "breakpoints": true - } - ] -} + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1731414293847, + "tag": "0000_v0", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1741267610502, + "tag": "0001_v0-15-0", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1743420907290, + "tag": "0002_v0-18-0", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1744205441260, + "tag": "0003_v0-20-0-1", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1745410821339, + "tag": "0004_v0-20-0-2", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1748353081475, + "tag": "0005_v0-20-0-3", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1748353926519, + "tag": "0006_v0-20-0-4", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1750146397071, + "tag": "0007_flippant_doctor_octopus", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/fdm-core/src/db/schema.ts b/fdm-core/src/db/schema.ts index 69b45a230..560b94376 100644 --- a/fdm-core/src/db/schema.ts +++ b/fdm-core/src/db/schema.ts @@ -8,6 +8,7 @@ import { uniqueIndex, } from "drizzle-orm/pg-core" import { geometry, numericCasted } from "./schema-custom-types" +import type { ApplicationMethods } from "@svenvw/fdm-data" // Define postgres schema export const fdmSchema = pgSchema("fdm") @@ -142,15 +143,19 @@ export type fertilizerAcquiringTypeInsert = typeof fertilizerAcquiring.$inferInsert // Define fertilizers application table -export const applicationMethodEnum = fdmSchema.enum("p_app_method", [ - "slotted coulter", - "incorporation", - "injection", - "spraying", - "broadcasting", - "spoke wheel", - "pocket placement", -]) +export const applicationMethodOptions = [ + { value: "slotted coulter", label: "Zodenbemester / Sleepvoet" }, + { value: "incorporation", label: "Inwerken" }, + { value: "injection", label: "Injecteren" }, + { value: "spraying", label: "Spuiten" }, + { value: "broadcasting", label: "Breedwerpig uitstrooien" }, + { value: "spoke wheel", label: "Spaakwiel" }, + { value: "pocket placement", label: "Plantgat" }, +] satisfies { value: ApplicationMethods; label: string }[] +export const applicationMethodEnum = fdmSchema.enum( + "p_app_method", + applicationMethodOptions.map((x) => x.value) as [string, ...string[]], +) export const fertilizerApplication = fdmSchema.table( "fertilizer_applying", { @@ -184,6 +189,7 @@ export const fertilizersCatalogue = fdmSchema.table( p_name_nl: text().notNull(), p_name_en: text(), p_description: text(), + p_app_method_options: applicationMethodEnum().array(), p_dm: numericCasted(), p_density: numericCasted(), p_om: numericCasted(), diff --git a/fdm-core/src/fertilizer.d.ts b/fdm-core/src/fertilizer.d.ts index 7c6883c19..c725b3ec7 100644 --- a/fdm-core/src/fertilizer.d.ts +++ b/fdm-core/src/fertilizer.d.ts @@ -1,8 +1,11 @@ +import type { ApplicationMethods } from "@svenvw/fdm-data" + export interface Fertilizer { p_id: string p_name_nl: string | null p_name_en: string | null p_description: string | null + p_app_method_options: ApplicationMethods[] | null p_app_amount: number | null p_date_acquiring: Date | null p_picking_date: Date | null @@ -34,14 +37,86 @@ export interface Fertilizer { p_pb_rt: number | null p_hg_rt: number | null p_cl_rt: number | null + p_type: FertilizerType | null } +type FertilizerType = "manure" | "mineral" | "compost" export interface FertilizerApplication { p_id: string p_id_catalogue: string p_name_nl: string | null p_app_amount: number | null - p_app_method: string | null + p_app_method: ApplicationMethods | null p_app_date: Date | null p_app_id: string } + +export type FertilizerParameters = + | "p_id_catalogue" + | "p_source" + | "p_name_nl" + | "p_name_en" + | "p_description" + | "p_app_method_options" + | "p_dm" + | "p_density" + | "p_om" + | "p_a" + | "p_hc" + | "p_eom" + | "p_eoc" + | "p_c_rt" + | "p_c_of" + | "p_c_if" + | "p_c_fr" + | "p_cn_of" + | "p_n_rt" + | "p_n_if" + | "p_n_of" + | "p_n_wc" + | "p_p_rt" + | "p_k_rt" + | "p_mg_rt" + | "p_ca_rt" + | "p_ne" + | "p_s_rt" + | "p_s_wc" + | "p_cu_rt" + | "p_zn_rt" + | "p_na_rt" + | "p_si_rt" + | "p_b_rt" + | "p_mn_rt" + | "p_ni_rt" + | "p_fe_rt" + | "p_mo_rt" + | "p_co_rt" + | "p_as_rt" + | "p_cd_rt" + | "p_cr_rt" + | "p_cr_vi" + | "p_pb_rt" + | "p_hg_rt" + | "p_cl_rt" + | "p_type" + +export interface FertilizerParameterDescriptionItem { + parameter: FertilizerParameters + unit: string + type: "numeric" | "enum" | "enum_multi" | "date" | "text" + name: string + description: string + category: + | "general" + | "primary" + | "secondary" + | "trace" + | "heavy_metals" + | "physical" + min?: number + max?: number + options?: { value: FertilizerType | ApplicationMethods; label: string }[] +} + +export type FertilizerParameterDescription = + FertilizerParameterDescriptionItem[] diff --git a/fdm-core/src/fertilizer.test.ts b/fdm-core/src/fertilizer.test.ts index 6796c5e91..be4007ab6 100644 --- a/fdm-core/src/fertilizer.test.ts +++ b/fdm-core/src/fertilizer.test.ts @@ -21,6 +21,7 @@ import { getFertilizer, getFertilizerApplication, getFertilizerApplications, + getFertilizerParametersDescription, getFertilizers, getFertilizersFromCatalogue, removeFertilizer, @@ -85,6 +86,7 @@ describe("Fertilizer Data Model", () => { p_name_nl, p_name_en, p_description, + p_app_method_options: ["injection", "incorporation"], p_dm: 37, p_density: 20, p_om: 20, @@ -125,9 +127,7 @@ describe("Fertilizer Data Model", () => { p_pb_rt: 370, p_hg_rt: 380, p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, + p_type: "manure", }, ) @@ -160,6 +160,7 @@ describe("Fertilizer Data Model", () => { p_name_nl, p_name_en, p_description, + p_app_method_options: [], p_dm: 37, p_density: 20, p_om: 20, @@ -200,9 +201,7 @@ describe("Fertilizer Data Model", () => { p_pb_rt: 370, p_hg_rt: 380, p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, + p_type: "manure", }, ) @@ -235,6 +234,7 @@ describe("Fertilizer Data Model", () => { p_name_nl, p_name_en, p_description, + p_app_method_options: [], p_dm: 37, p_density: 20, p_om: 20, @@ -275,9 +275,7 @@ describe("Fertilizer Data Model", () => { p_pb_rt: 370, p_hg_rt: 380, p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, + p_type: "manure", }, ) @@ -323,6 +321,7 @@ describe("Fertilizer Data Model", () => { p_name_nl, p_name_en, p_description, + p_app_method_options: [], p_dm: 37, p_density: 20, p_om: 20, @@ -363,9 +362,7 @@ describe("Fertilizer Data Model", () => { p_pb_rt: 370, p_hg_rt: 380, p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, + p_type: "manure", }, ) @@ -383,8 +380,9 @@ describe("Fertilizer Data Model", () => { await removeFertilizer(fdm, p_id) - const fertilizer = await getFertilizer(fdm, p_id) - expect(fertilizer).toBeUndefined() + await expect(getFertilizer(fdm, p_id)).rejects.toThrow( + "Exception for getFertilizer", + ) }) it("should return empty array when no catalogues are enabled", async () => { @@ -419,6 +417,7 @@ describe("Fertilizer Data Model", () => { p_name_nl: "Test Fertilizer", p_name_en: "Test Fertilizer (EN)", p_description: "This is a test fertilizer", + p_app_method_options: [], p_dm: 37, p_density: 20, p_om: 20, @@ -459,9 +458,7 @@ describe("Fertilizer Data Model", () => { p_pb_rt: 370, p_hg_rt: 380, p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, + p_type: "manure", }, ) }) @@ -633,6 +630,7 @@ describe("Fertilizer Data Model", () => { p_name_nl: "Test Fertilizer 2", p_name_en: "Test Fertilizer (EN) 2", p_description: "This is a test fertilizer 2", + p_app_method_options: [], p_dm: 37, p_density: 20, p_om: 20, @@ -673,9 +671,7 @@ describe("Fertilizer Data Model", () => { p_pb_rt: 370, p_hg_rt: 380, p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, + p_type: "manure", }, ) const updatedProperties = { @@ -696,7 +692,6 @@ describe("Fertilizer Data Model", () => { describe("Fertilizer Application", () => { let b_id: string let p_id: string - let p_id_catalogue: string beforeAll(async () => { const farmName = "Test Farm" @@ -747,6 +742,7 @@ describe("Fertilizer Data Model", () => { p_name_nl, p_name_en, p_description, + p_app_method_options: [], p_dm: 37, p_density: 20, p_om: 20, @@ -787,9 +783,7 @@ describe("Fertilizer Data Model", () => { p_pb_rt: 370, p_hg_rt: 380, p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, + p_type: "manure", }, ) @@ -1038,3 +1032,46 @@ describe("Fertilizer Data Model", () => { }) }) }) + +describe("getFertilizerParametersDescription", () => { + it("should return the correct fertilizer parameter descriptions for NL-nl locale", () => { + const descriptions = getFertilizerParametersDescription("NL-nl") + expect(descriptions).toHaveLength(22) + for (const description of descriptions) { + expect(description).toHaveProperty("parameter") + expect(description).toHaveProperty("unit") + expect(description).toHaveProperty("name") + expect(description).toHaveProperty("type") + expect(description).toHaveProperty("description") + expect(description).toHaveProperty("category") + if (description.type === "enum") { + expect(description).toHaveProperty("options") + } + } + }) + + it("should throw an error for unsupported locales", () => { + expect(() => getFertilizerParametersDescription("en-US")).toThrowError( + "Unsupported locale", + ) + expect(() => getFertilizerParametersDescription("de-DE")).toThrowError( + "Unsupported locale", + ) + }) + + it("should return the correct fertilizer parameter descriptions for default locale", () => { + const descriptions = getFertilizerParametersDescription() + expect(descriptions).toHaveLength(22) + for (const description of descriptions) { + expect(description).toHaveProperty("parameter") + expect(description).toHaveProperty("unit") + expect(description).toHaveProperty("name") + expect(description).toHaveProperty("type") + expect(description).toHaveProperty("description") + expect(description).toHaveProperty("category") + if (description.type === "enum") { + expect(description).toHaveProperty("options") + } + } + }) +}) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 98589189a..175493715 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -1,13 +1,17 @@ import { and, asc, desc, eq, gte, inArray, lte } from "drizzle-orm" import { createId } from "./id" -import { hashFertilizer } from "@svenvw/fdm-data" +import { type CatalogueFertilizerItem, hashFertilizer } from "@svenvw/fdm-data" import { checkPermission } from "./authorization" import type { PrincipalId } from "./authorization.d" import * as schema from "./db/schema" import { handleError } from "./error" import type { FdmType } from "./fdm" -import type { Fertilizer, FertilizerApplication } from "./fertilizer.d" +import type { + Fertilizer, + FertilizerApplication, + FertilizerParameterDescription, +} from "./fertilizer.d" import type { Timeframe } from "./timeframe" /** @@ -89,6 +93,7 @@ export async function addFertilizerToCatalogue( p_name_nl: schema.fertilizersCatalogueTypeInsert["p_name_nl"] p_name_en: schema.fertilizersCatalogueTypeInsert["p_name_en"] p_description: schema.fertilizersCatalogueTypeInsert["p_description"] + p_app_method_options: schema.fertilizersCatalogueTypeInsert["p_app_method_options"] p_dm: schema.fertilizersCatalogueTypeInsert["p_dm"] p_density: schema.fertilizersCatalogueTypeInsert["p_density"] p_om: schema.fertilizersCatalogueTypeInsert["p_om"] @@ -129,9 +134,7 @@ export async function addFertilizerToCatalogue( p_pb_rt: schema.fertilizersCatalogueTypeInsert["p_pb_rt"] p_hg_rt: schema.fertilizersCatalogueTypeInsert["p_hg_rt"] p_cl_rt: schema.fertilizersCatalogueTypeInsert["p_cl_rt"] - p_type_manure: schema.fertilizersCatalogueTypeInsert["p_type_manure"] - p_type_mineral: schema.fertilizersCatalogueTypeInsert["p_type_mineral"] - p_type_compost: schema.fertilizersCatalogueTypeInsert["p_type_compost"] + p_type: "manure" | "mineral" | "compost" | null }, ): Promise { try { @@ -150,8 +153,13 @@ export async function addFertilizerToCatalogue( p_id_catalogue: p_id_catalogue, p_source: b_id_farm, hash: null, + p_type_manure: properties.p_type === "manure", + p_type_mineral: properties.p_type === "mineral", + p_type_compost: properties.p_type === "compost", } - input.hash = await hashFertilizer(input) + input.hash = await hashFertilizer( + input as unknown as CatalogueFertilizerItem, + ) // Insert the farm in the db await fdm.insert(schema.fertilizersCatalogue).values(input) @@ -264,6 +272,8 @@ export async function getFertilizer( p_name_nl: schema.fertilizersCatalogue.p_name_nl, p_name_en: schema.fertilizersCatalogue.p_name_en, p_description: schema.fertilizersCatalogue.p_description, + p_app_method_options: + schema.fertilizersCatalogue.p_app_method_options, p_acquiring_amount: schema.fertilizerAcquiring.p_acquiring_amount, p_acquiring_date: schema.fertilizerAcquiring.p_acquiring_date, @@ -331,7 +341,24 @@ export async function getFertilizer( .where(eq(schema.fertilizers.p_id, p_id)) .limit(1) - return fertilizer[0] + const result = fertilizer[0] + if (!result) { + throw new Error("Fertilizer not found") + } + + let p_type: "manure" | "mineral" | "compost" | null = null + if (result.p_type_manure) { + p_type = "manure" + } else if (result.p_type_mineral) { + p_type = "mineral" + } else if (result.p_type_compost) { + p_type = "compost" + } + + return { + ...result, + p_type, + } } catch (err) { throw handleError(err, "Exception for getFertilizer", { p_id, @@ -360,6 +387,7 @@ export async function updateFertilizerFromCatalogue( p_name_nl: schema.fertilizersCatalogueTypeInsert["p_name_nl"] p_name_en: schema.fertilizersCatalogueTypeInsert["p_name_en"] p_description: schema.fertilizersCatalogueTypeInsert["p_description"] + p_app_method_options: schema.fertilizersCatalogueTypeInsert["p_app_method_options"] p_dm: schema.fertilizersCatalogueTypeInsert["p_dm"] p_density: schema.fertilizersCatalogueTypeInsert["p_density"] p_om: schema.fertilizersCatalogueTypeInsert["p_om"] @@ -400,9 +428,7 @@ export async function updateFertilizerFromCatalogue( p_pb_rt: schema.fertilizersCatalogueTypeInsert["p_pb_rt"] p_hg_rt: schema.fertilizersCatalogueTypeInsert["p_hg_rt"] p_cl_rt: schema.fertilizersCatalogueTypeInsert["p_cl_rt"] - p_type_manure: schema.fertilizersCatalogueTypeInsert["p_type_manure"] - p_type_mineral: schema.fertilizersCatalogueTypeInsert["p_type_mineral"] - p_type_compost: schema.fertilizersCatalogueTypeInsert["p_type_compost"] + p_type: "manure" | "mineral" | "compost" | null }>, ): Promise { try { @@ -430,12 +456,29 @@ export async function updateFertilizerFromCatalogue( if (existingFertilizer.length === 0) { throw new Error("Fertilizer does not exist in catalogue") } - const updatedProperties = { + + const { p_type, ...rest } = properties + const updatedProperties: schema.fertilizersCatalogueTypeInsert = { ...existingFertilizer[0], - ...properties, + ...rest, hash: null, + // Preserve current flags when p_type is not provided + p_type_manure: + p_type !== undefined + ? p_type === "manure" + : existingFertilizer[0].p_type_manure, + p_type_mineral: + p_type !== undefined + ? p_type === "mineral" + : existingFertilizer[0].p_type_mineral, + p_type_compost: + p_type !== undefined + ? p_type === "compost" + : existingFertilizer[0].p_type_compost, } - updatedProperties.hash = await hashFertilizer(updatedProperties) + updatedProperties.hash = await hashFertilizer( + updatedProperties as unknown as CatalogueFertilizerItem, + ) await fdm .update(schema.fertilizersCatalogue) @@ -494,6 +537,8 @@ export async function getFertilizers( p_name_nl: schema.fertilizersCatalogue.p_name_nl, p_name_en: schema.fertilizersCatalogue.p_name_en, p_description: schema.fertilizersCatalogue.p_description, + p_app_method_options: + schema.fertilizersCatalogue.p_app_method_options, p_acquiring_amount: schema.fertilizerAcquiring.p_acquiring_amount, p_acquiring_date: schema.fertilizerAcquiring.p_acquiring_date, @@ -561,7 +606,20 @@ export async function getFertilizers( .where(eq(schema.fertilizerAcquiring.b_id_farm, b_id_farm)) .orderBy(asc(schema.fertilizersCatalogue.p_name_nl)) - return fertilizers + return fertilizers.map((f: (typeof fertilizers)[number]) => { + let p_type: "manure" | "mineral" | "compost" | null = null + if (f.p_type_manure) { + p_type = "manure" + } else if (f.p_type_mineral) { + p_type = "mineral" + } else if (f.p_type_compost) { + p_type = "compost" + } + return { + ...f, + p_type, + } + }) } catch (err) { throw handleError(err, "Exception for getFertilizers", { b_id_farm, @@ -907,3 +965,257 @@ export async function getFertilizerApplications( }) } } + +/** + * Retrieves a description of the available fertilizer parameters. + * + * This function returns an array of objects, each describing a fertilizer parameter. + * Each description includes the parameter's name, unit, type (numeric or enum), + * a human-readable name, a detailed description, and optional constraints like + * minimum and maximum values or a list of valid options for enum types. + * + * @param locale - The locale for which to retrieve the descriptions. Currently only 'NL-nl' is supported. + * @returns An array of fertilizerParameterDescriptionItem objects. + * @throws {Error} If an unsupported locale is provided. + */ +export function getFertilizerParametersDescription( + locale = "NL-nl", +): FertilizerParameterDescription { + if (locale !== "NL-nl") throw new Error("Unsupported locale") + const fertilizerParameterDescription: FertilizerParameterDescription = [ + { + parameter: "p_id_catalogue", + unit: "", + name: "ID", + type: "text", + category: "general", + description: "Catalogu ID van meststof", + }, + { + parameter: "p_source", + unit: "", + name: "Bron", + type: "text", + category: "general", + description: "Gegevensbron van meststof", + }, + { + parameter: "p_name_nl", + unit: "", + name: "Naam", + type: "text", + category: "general", + description: "Nederlandse naam van meststof", + }, + // { + // parameter: "p_name_en", + // unit: "", + // name: "Naam (Engels)", + // type: "text", + // category: "general", + // description: "Engelse naam van meststof", + // }, + // { + // parameter: "p_description", + // unit: "", + // name: "Beschrijving", + // type: "text", + // category: "general", + // description: "Beschrijvingen en/of opmerkingen over de meststof", + // }, + { + parameter: "p_type", + unit: "", + name: "Type", + type: "enum", + category: "general", + description: "Typering van de meststof", + options: [ + { value: "manure", label: "Dierlijke mest" }, + { value: "mineral", label: "Kunstmest" }, + { value: "compost", label: "Compost" }, + ], + }, + { + parameter: "p_app_method_options", + unit: "", + name: "Toedieningsmethodes", + type: "enum_multi", + category: "general", + description: "Toedieningsmethodes mogelijk voor deze meststof", + options: schema.applicationMethodOptions, + }, + { + parameter: "p_dm", + unit: "g/kg", + name: "Droge stofgehalte", + type: "numeric", + category: "physical", + description: "", + min: 0, + max: 1000, + }, + { + parameter: "p_density", + unit: "kg/l", + name: "Dichtheid", + type: "numeric", + category: "physical", + description: "", + min: 0.00016, + max: 17.31, + }, + { + parameter: "p_n_rt", + unit: "g N/kg", + name: "N", + type: "numeric", + category: "primary", + description: "Stikstof, totaal", + min: 0, + max: 1000, + }, + { + parameter: "p_n_wc", + unit: "-", + name: "N-werking", + type: "numeric", + category: "primary", + description: "Stikstof, werkingscoëfficient", + min: 0, + max: 1, + }, + { + parameter: "p_p_rt", + unit: "g P2O5/kg", + name: "P", + type: "numeric", + category: "primary", + description: "Fosfaat", + min: 0, + max: 4583, + }, + { + parameter: "p_k_rt", + unit: "g K2O/kg", + name: "K", + type: "numeric", + category: "primary", + description: "Kalium", + min: 0, + max: 2409.2, + }, + { + parameter: "p_eoc", + unit: "g EOC/kg", + name: "EOC", + type: "numeric", + category: "secondary", + description: "Koolstof, effectief", + min: 0, + max: 1000, + }, + { + parameter: "p_s_rt", + unit: "g SO3/kg", + name: "S", + type: "numeric", + category: "secondary", + description: "Zwavel", + min: 0, + max: 2497.2, + }, + { + parameter: "p_mg_rt", + unit: "g MgO/kg", + name: "Mg", + type: "numeric", + category: "secondary", + description: "Magnesium", + min: 0, + max: 1659, + }, + { + parameter: "p_ca_rt", + unit: "g CaO/kg", + name: "Ca", + type: "numeric", + category: "secondary", + description: "Calcium", + min: 0, + max: 1399.2, + }, + { + parameter: "p_na_rt", + unit: "g Na2O/kg", + name: "Na", + type: "numeric", + category: "secondary", + description: "Natrium", + min: 0, + max: 2695900, + }, + { + parameter: "p_cu_rt", + unit: "mg Cu/kg", + name: "Cu", + type: "numeric", + category: "trace", + description: "Koper", + min: 0, + max: 1000000, + }, + { + parameter: "p_zn_rt", + unit: "mg Zn/kg", + name: "Zn", + type: "numeric", + category: "trace", + description: "Zink", + min: 0, + max: 1000000, + }, + { + parameter: "p_co_rt", + unit: "mg Co/kg", + name: "Co", + type: "numeric", + category: "trace", + description: "Kobalt", + min: 0, + max: 1000000, + }, + { + parameter: "p_mn_rt", + unit: "mg Mn/kg", + name: "Mn", + type: "numeric", + category: "trace", + description: "Mangaan", + min: 0, + max: 1000000, + }, + { + parameter: "p_mo_rt", + unit: "mg Mo/kg", + name: "Mo", + type: "numeric", + category: "trace", + description: "Molybdeen", + min: 0, + max: 1000000, + }, + { + parameter: "p_b_rt", + unit: "mg B/kg", + name: "B", + type: "numeric", + category: "trace", + description: "Boor", + min: 0, + max: 1000000, + }, + ] + + return fertilizerParameterDescription +} diff --git a/fdm-core/src/index.ts b/fdm-core/src/index.ts index 89732a10f..14e032188 100644 --- a/fdm-core/src/index.ts +++ b/fdm-core/src/index.ts @@ -52,10 +52,14 @@ export { removeFertilizerApplication, getFertilizerApplication, getFertilizerApplications, + getFertilizerParametersDescription, } from "./fertilizer" export type { Fertilizer, FertilizerApplication, + FertilizerParameters, + FertilizerParameterDescription, + FertilizerParameterDescriptionItem, } from "./fertilizer.d" export { addCultivationToCatalogue, diff --git a/fdm-data/src/fertilizers/catalogues/srm.ts b/fdm-data/src/fertilizers/catalogues/srm.ts index 701bf6751..a39b901cf 100644 --- a/fdm-data/src/fertilizers/catalogues/srm.ts +++ b/fdm-data/src/fertilizers/catalogues/srm.ts @@ -1,4 +1,8 @@ -import type { CatalogueFertilizer, CatalogueFertilizerItem } from "../d" +import type { + ApplicationMethods, + CatalogueFertilizer, + CatalogueFertilizerItem, +} from "../d" import { hashFertilizer } from "../hash" import srm from "./srm.json" @@ -20,6 +24,12 @@ export async function getCatalogueSrm(): Promise { p_name_nl: fertilizer.p_name_nl, p_name_en: null, p_description: null, + p_app_method_options: + fertilizer.p_app_method_options === undefined + ? null + : (fertilizer.p_app_method_options.split( + "||", + ) as ApplicationMethods[]), p_dm: fertilizer.p_dm === undefined ? null : fertilizer.p_dm, p_density: fertilizer.p_density === undefined diff --git a/fdm-data/src/fertilizers/d.ts b/fdm-data/src/fertilizers/d.ts index 7646169d0..15e884f5e 100644 --- a/fdm-data/src/fertilizers/d.ts +++ b/fdm-data/src/fertilizers/d.ts @@ -1,11 +1,21 @@ export type CatalogueFertilizerName = "srm" +export type ApplicationMethods = + | "slotted coulter" + | "incorporation" + | "injection" + | "spraying" + | "broadcasting" + | "spoke wheel" + | "pocket placement" + export interface CatalogueFertilizerItem { p_source: CatalogueFertilizerName | string p_id_catalogue: string p_name_nl: string p_name_en?: string | null | undefined p_description?: string | null | undefined + p_app_method_options?: ApplicationMethods[] | null | undefined p_dm?: number | null p_density?: number | null p_om?: number | null diff --git a/fdm-data/src/fertilizers/index.test.ts b/fdm-data/src/fertilizers/index.test.ts index da36fe820..77ce95f95 100644 --- a/fdm-data/src/fertilizers/index.test.ts +++ b/fdm-data/src/fertilizers/index.test.ts @@ -30,7 +30,6 @@ describe("getFertilizersCatalogue", () => { }) describe("getCatalogueSrm", async () => { - const originalSrm = require("./catalogues/srm.json") it("should return an array of CatalogueFertilizerItem", async () => { const catalogue = await getCatalogueSrm() expect(Array.isArray(catalogue)).toBe(true) @@ -41,6 +40,7 @@ describe("getCatalogueSrm", async () => { expect(item).toHaveProperty("p_name_nl") expect(item).toHaveProperty("p_name_en") expect(item).toHaveProperty("p_description") + expect(item).toHaveProperty("p_app_method_options") expect(item).toHaveProperty("p_dm") expect(item).toHaveProperty("p_density") expect(item).toHaveProperty("p_om") diff --git a/fdm-data/src/index.ts b/fdm-data/src/index.ts index 3addf3e03..f29459103 100644 --- a/fdm-data/src/index.ts +++ b/fdm-data/src/index.ts @@ -12,6 +12,17 @@ */ export { getFertilizersCatalogue } from "./fertilizers" +export type { + CatalogueFertilizerName, + ApplicationMethods, + CatalogueFertilizer, + CatalogueFertilizerItem, +} from "./fertilizers/d" export { getCultivationCatalogue } from "./cultivations" +export type { + CatalogueCultivationName, + CatalogueCultivation, + CatalogueCultivationItem, +} from "./cultivations/d" export { hashFertilizer } from "./fertilizers/hash" export { hashCultivation } from "./cultivations/hash" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e73273d8c..05abb58b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,6 +118,9 @@ importers: '@radix-ui/react-progress': specifier: ^1.1.7 version: 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-radio-group': + specifier: ^1.3.7 + version: 1.3.7(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-select': specifier: ^2.2.5 version: 2.2.5(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -163,6 +166,9 @@ importers: '@svenvw/fdm-core': specifier: workspace:^ version: link:../fdm-core + '@svenvw/fdm-data': + specifier: workspace:* + version: link:../fdm-data '@tailwindcss/vite': specifier: ^4.1.8 version: 4.1.8(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(yaml@2.8.0)) @@ -2835,6 +2841,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-radio-group@1.3.7': + resolution: {integrity: sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.10': resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} peerDependencies: @@ -12732,6 +12751,24 @@ snapshots: '@types/react': 19.1.6 '@types/react-dom': 19.1.5(@types/react@19.1.6) + '@radix-ui/react-radio-group@1.3.7(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.6)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.6)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.6)(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.6)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.6)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.6)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.6 + '@types/react-dom': 19.1.5(@types/react@19.1.6) + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2