From f589317242a3b2d609ef81d6c4dcefd2b129be83 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:51:00 +0200 Subject: [PATCH 1/3] fix: storing additional soil parameters when adding a new field (not in the farm create wizard) --- .changeset/cold-icons-vanish.md | 5 + .../farm.$b_id_farm.$calendar.field.new.tsx | 807 +++++++++--------- 2 files changed, 397 insertions(+), 415 deletions(-) create mode 100644 .changeset/cold-icons-vanish.md diff --git a/.changeset/cold-icons-vanish.md b/.changeset/cold-icons-vanish.md new file mode 100644 index 000000000..4cb3336a0 --- /dev/null +++ b/.changeset/cold-icons-vanish.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": patch +--- + +Fix storing additional soil parameters when adding a new field (not in the farm create wizard) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new.tsx index bf220eb71..1285b5649 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new.tsx @@ -1,64 +1,64 @@ import { - addCultivation, - addField, - addSoilAnalysis, - getCultivationsFromCatalogue, - getFarm, - getFarms, - getFields, -} from "@svenvw/fdm-core" -import type { Feature, FeatureCollection, Polygon } from "geojson" -import { useState } from "react" + addCultivation, + addField, + addSoilAnalysis, + getCultivationsFromCatalogue, + getFarm, + getFarms, + getFields, +} from "@svenvw/fdm-core"; +import type { Feature, FeatureCollection, Polygon } from "geojson"; +import { useState } from "react"; import { - GeolocateControl, - Layer, - Map as MapGL, - NavigationControl, -} from "react-map-gl/mapbox" + GeolocateControl, + Layer, + Map as MapGL, + NavigationControl, +} from "react-map-gl/mapbox"; import { - type ActionFunctionArgs, - type LoaderFunctionArgs, - type MetaFunction, - data, - useLoaderData, -} from "react-router" -import { dataWithError, redirectWithSuccess } from "remix-toast" -import { ClientOnly } from "remix-utils/client-only" -import { ZOOM_LEVEL_FIELDS } from "~/components/blocks/atlas/atlas" + type ActionFunctionArgs, + type LoaderFunctionArgs, + type MetaFunction, + data, + useLoaderData, +} from "react-router"; +import { dataWithError, redirectWithSuccess } from "remix-toast"; +import { ClientOnly } from "remix-utils/client-only"; +import { ZOOM_LEVEL_FIELDS } from "~/components/blocks/atlas/atlas"; import { - FieldsPanelHover, - FieldsPanelZoom, -} from "~/components/blocks/atlas/atlas-panels" + FieldsPanelHover, + FieldsPanelZoom, +} from "~/components/blocks/atlas/atlas-panels"; import { - FieldsSourceAvailable, - FieldsSourceNotClickable, -} from "~/components/blocks/atlas/atlas-sources" -import { getFieldsStyle } from "~/components/blocks/atlas/atlas-styles" -import { getViewState } from "~/components/blocks/atlas/atlas-viewstate" -import FieldDetailsDialog from "~/components/blocks/field/form" -import { FormSchema } from "~/components/blocks/field/schema" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarm } from "~/components/blocks/header/farm" -import { HeaderField } from "~/components/blocks/header/field" -import { Separator } from "~/components/ui/separator" -import { SidebarInset } from "~/components/ui/sidebar" -import { Skeleton } from "~/components/ui/skeleton" -import { getMapboxStyle, getMapboxToken } from "~/integrations/mapbox" -import { getNmiApiKey, getSoilParameterEstimates } from "~/integrations/nmi" -import { getSession } from "~/lib/auth.server" -import { getCalendar, getTimeframe } from "~/lib/calendar" -import { handleActionError, handleLoaderError } from "~/lib/error" -import { fdm } from "~/lib/fdm.server" -import { extractFormValuesFromRequest } from "~/lib/form" -import { useCalendarStore } from "~/store/calendar" + FieldsSourceAvailable, + FieldsSourceNotClickable, +} from "~/components/blocks/atlas/atlas-sources"; +import { getFieldsStyle } from "~/components/blocks/atlas/atlas-styles"; +import { getViewState } from "~/components/blocks/atlas/atlas-viewstate"; +import FieldDetailsDialog from "~/components/blocks/field/form"; +import { FormSchema } from "~/components/blocks/field/schema"; +import { Header } from "~/components/blocks/header/base"; +import { HeaderFarm } from "~/components/blocks/header/farm"; +import { HeaderField } from "~/components/blocks/header/field"; +import { Separator } from "~/components/ui/separator"; +import { SidebarInset } from "~/components/ui/sidebar"; +import { Skeleton } from "~/components/ui/skeleton"; +import { getMapboxStyle, getMapboxToken } from "~/integrations/mapbox"; +import { getNmiApiKey, getSoilParameterEstimates } from "~/integrations/nmi"; +import { getSession } from "~/lib/auth.server"; +import { getCalendar, getTimeframe } from "~/lib/calendar"; +import { handleActionError, handleLoaderError } from "~/lib/error"; +import { fdm } from "~/lib/fdm.server"; +import { extractFormValuesFromRequest } from "~/lib/form"; +import { useCalendarStore } from "~/store/calendar"; // Meta export const meta: MetaFunction = () => { - return [ - { title: "FDM App" }, - { name: "description", content: "Welcome to FDM!" }, - ] -} + return [ + { title: "FDM App" }, + { name: "description", content: "Welcome to FDM!" }, + ]; +}; /** * Retrieves farm details and map configurations for rendering the farm map. @@ -70,163 +70,162 @@ export const meta: MetaFunction = () => { * @returns An object containing the farm name, Mapbox token, Mapbox style, and the URL for available fields. */ export async function loader({ request, params }: LoaderFunctionArgs) { - try { - // Get the Id and name of the farm - const b_id_farm = params.b_id_farm - if (!b_id_farm) { - throw data("Farm ID is required", { - status: 400, - statusText: "Farm ID is required", - }) - } - - // Get the session - const session = await getSession(request) - - // Get timeframe from calendar store - const timeframe = getTimeframe(params) - - // Get a list of possible farms of the user - const farms = await getFarms(fdm, session.principal_id) - const farmOptions = farms.map((farm) => { - if (!farm?.b_id_farm || !farm?.b_name_farm) { - throw new Error("Invalid farm data structure") - } - return { - b_id_farm: farm.b_id_farm, - b_name_farm: farm.b_name_farm, - } - }) - - const farm = await getFarm(fdm, session.principal_id, b_id_farm) - - if (!farm) { - throw data("Farm not found", { - status: 404, - statusText: "Farm not found", - }) - } - - // Get the fields of the farm - const fields = await getFields( - fdm, - session.principal_id, - b_id_farm, - timeframe, - ) - const features = fields.map((field) => { - const feature: Feature = { - type: "Feature" as const, - properties: { - b_id: field.b_id, - b_name: field.b_name, - b_area: Math.round(field.b_area * 10) / 10, - b_lu_name: field.b_lu_name, - b_id_source: field.b_id_source, - }, - geometry: field.b_geometry, - } - return feature - }) - - const featureCollection: FeatureCollection = { - type: "FeatureCollection", - features: features, - } - - // Get the available cultivations - let cultivationOptions = [] - const cultivationsCatalogue = await getCultivationsFromCatalogue( - fdm, - session.principal_id, - b_id_farm, - ) - cultivationOptions = cultivationsCatalogue - .filter( - (cultivation) => - cultivation?.b_lu_catalogue && cultivation?.b_lu_name, - ) - .map((cultivation) => ({ - value: cultivation.b_lu_catalogue, - label: `${cultivation.b_lu_name} (${cultivation.b_lu_catalogue.split("_")[1]})`, - })) - if (!cultivationOptions.length) { - throw dataWithError( - "No cultivations are available", - "Er zijn nog geen gewassen beschikbaar.", - ) - } - - // Create default field name - const fieldNameDefault = `Perceel ${fields.length + 1}` - - // Get the Mapbox token and style - const mapboxToken = getMapboxToken() - const mapboxStyle = getMapboxStyle() - - return { - farmOptions: farmOptions, - b_id_farm: b_id_farm, - b_name_farm: farm.b_name_farm, - featureCollection: featureCollection, - fieldNameDefault: fieldNameDefault, - cultivationOptions: cultivationOptions, - mapboxToken: mapboxToken, - mapboxStyle: mapboxStyle, - fieldsAvailableUrl: process.env.AVAILABLE_FIELDS_URL, - } - } catch (error) { - throw handleLoaderError(error) - } + try { + // Get the Id and name of the farm + const b_id_farm = params.b_id_farm; + if (!b_id_farm) { + throw data("Farm ID is required", { + status: 400, + statusText: "Farm ID is required", + }); + } + + // Get the session + const session = await getSession(request); + + // Get timeframe from calendar store + const timeframe = getTimeframe(params); + + // Get a list of possible farms of the user + const farms = await getFarms(fdm, session.principal_id); + const farmOptions = farms.map((farm) => { + if (!farm?.b_id_farm || !farm?.b_name_farm) { + throw new Error("Invalid farm data structure"); + } + return { + b_id_farm: farm.b_id_farm, + b_name_farm: farm.b_name_farm, + }; + }); + + const farm = await getFarm(fdm, session.principal_id, b_id_farm); + + if (!farm) { + throw data("Farm not found", { + status: 404, + statusText: "Farm not found", + }); + } + + // Get the fields of the farm + const fields = await getFields( + fdm, + session.principal_id, + b_id_farm, + timeframe, + ); + const features = fields.map((field) => { + const feature: Feature = { + type: "Feature" as const, + properties: { + b_id: field.b_id, + b_name: field.b_name, + b_area: Math.round(field.b_area * 10) / 10, + b_lu_name: field.b_lu_name, + b_id_source: field.b_id_source, + }, + geometry: field.b_geometry, + }; + return feature; + }); + + const featureCollection: FeatureCollection = { + type: "FeatureCollection", + features: features, + }; + + // Get the available cultivations + let cultivationOptions = []; + const cultivationsCatalogue = await getCultivationsFromCatalogue( + fdm, + session.principal_id, + b_id_farm, + ); + cultivationOptions = cultivationsCatalogue + .filter( + (cultivation) => cultivation?.b_lu_catalogue && cultivation?.b_lu_name, + ) + .map((cultivation) => ({ + value: cultivation.b_lu_catalogue, + label: `${cultivation.b_lu_name} (${cultivation.b_lu_catalogue.split("_")[1]})`, + })); + if (!cultivationOptions.length) { + throw dataWithError( + "No cultivations are available", + "Er zijn nog geen gewassen beschikbaar.", + ); + } + + // Create default field name + const fieldNameDefault = `Perceel ${fields.length + 1}`; + + // Get the Mapbox token and style + const mapboxToken = getMapboxToken(); + const mapboxStyle = getMapboxStyle(); + + return { + farmOptions: farmOptions, + b_id_farm: b_id_farm, + b_name_farm: farm.b_name_farm, + featureCollection: featureCollection, + fieldNameDefault: fieldNameDefault, + cultivationOptions: cultivationOptions, + mapboxToken: mapboxToken, + mapboxStyle: mapboxStyle, + fieldsAvailableUrl: process.env.AVAILABLE_FIELDS_URL, + }; + } catch (error) { + throw handleLoaderError(error); + } } // Main export default function Index() { - const loaderData = useLoaderData() - const calendar = useCalendarStore((state) => state.calendar) - - const fieldsSavedId = "fieldsSaved" - const fieldsSaved = loaderData.featureCollection - const fieldsSavedStyle = getFieldsStyle(fieldsSavedId) - let viewState = getViewState(null) - if (fieldsSaved.features.length > 0) { - viewState = getViewState(fieldsSaved) - } - - const fieldsAvailableId = "fieldsAvailable" - const fieldsAvailableStyle = getFieldsStyle(fieldsAvailableId) - - const [open, setOpen] = useState(false) - - const [selectedField, setSelectedField] = useState | null>( - null, - ) - - const handleSelectField = (feature: Feature) => { - setSelectedField(feature) - setOpen(true) - } - - return ( - -
- - -
- {/* (); + const calendar = useCalendarStore((state) => state.calendar); + + const fieldsSavedId = "fieldsSaved"; + const fieldsSaved = loaderData.featureCollection; + const fieldsSavedStyle = getFieldsStyle(fieldsSavedId); + let viewState = getViewState(null); + if (fieldsSaved.features.length > 0) { + viewState = getViewState(fieldsSaved); + } + + const fieldsAvailableId = "fieldsAvailable"; + const fieldsAvailableStyle = getFieldsStyle(fieldsAvailableId); + + const [open, setOpen] = useState(false); + + const [selectedField, setSelectedField] = useState | null>( + null, + ); + + const handleSelectField = (feature: Feature) => { + setSelectedField(feature); + setOpen(true); + }; + + return ( + +
+ + +
+ {/* */} -
-
-
-
-

- Nieuw perceel -

-

- Zoom in en voeg een nieuw perceel toe -

-
-
- -
-
- - } - > - {() => ( - { - if (!evt.features) return - const polygonFeature = evt.features.find( - (f) => - f.source === fieldsAvailableId && - f.geometry?.type === "Polygon", - ) - if (polygonFeature) { - handleSelectField( - polygonFeature as Feature, - ) - } - }} - > - - - - - - - - - - - -
- - -
-
- )} -
-
-
- {selectedField && ( - } - cultivationOptions={loaderData.cultivationOptions} - fieldNameDefault={loaderData.fieldNameDefault} - /> - )} -
- ) +
+
+
+
+

+ Nieuw perceel +

+

+ Zoom in en voeg een nieuw perceel toe +

+
+
+ +
+
+ } + > + {() => ( + { + if (!evt.features) return; + const polygonFeature = evt.features.find( + (f) => + f.source === fieldsAvailableId && + f.geometry?.type === "Polygon", + ); + if (polygonFeature) { + handleSelectField(polygonFeature as Feature); + } + }} + > + + + + + + + + + + + +
+ + +
+
+ )} +
+
+
+ {selectedField && ( + } + cultivationOptions={loaderData.cultivationOptions} + fieldNameDefault={loaderData.fieldNameDefault} + /> + )} +
+ ); } export async function action({ request, params }: ActionFunctionArgs) { - // Get the farm id - const b_id_farm = params.b_id_farm - if (!b_id_farm) { - throw data("Farm ID is required", { - status: 400, - statusText: "Farm ID is required", - }) - } - - try { - // Get the session - const session = await getSession(request) - - // Get the timeframe - const timeframe = getTimeframe(params) - const calendar = getCalendar(params) - - const nmiApiKey = getNmiApiKey() - - // Get form values - const formValues = await extractFormValuesFromRequest( - request, - FormSchema, - ) - - // Check if cultivation is available - let cultivationOptions = [] - const cultivationsCatalogue = await getCultivationsFromCatalogue( - fdm, - session.principal_id, - b_id_farm, - ) - cultivationOptions = cultivationsCatalogue - .filter( - (cultivation) => - cultivation?.b_lu_catalogue && cultivation?.b_lu_name, - ) - .map((cultivation) => { - return cultivation.b_lu_catalogue - }) - if (!cultivationOptions.includes(formValues.b_lu_catalogue)) { - return dataWithError( - `Cultivation ${formValues.b_lu_catalogue} is not available`, - "Gewas is onbekend. Kies een gewas uit de lijst", - ) - } - - const b_name = formValues.b_name - const b_id_source = formValues.b_id_source - const b_lu_catalogue = formValues.b_lu_catalogue - // Parse the geometry string twice to get the actual GeoJSON object - const b_geometry = JSON.parse( - JSON.parse(String(formValues.b_geometry)), - ) as Polygon - const currentYear = new Date().getFullYear() - const defaultDate = timeframe.start - ? timeframe.start - : `${currentYear}-01-01` - const b_start = defaultDate - const b_lu_start = defaultDate - const b_lu_end = undefined - const b_end = undefined - const b_acquiring_method = "unknown" - - const b_id = await addField( - fdm, - session.principal_id, - b_id_farm, - b_name, - b_id_source, - b_geometry, - b_start, - b_acquiring_method, - b_end, - ) - await addCultivation( - fdm, - session.principal_id, - b_lu_catalogue, - b_id, - b_lu_start, - b_lu_end, - ) - - if (nmiApiKey) { - const estimates = await getSoilParameterEstimates( - b_geometry, - nmiApiKey, - ) - - await addSoilAnalysis( - fdm, - session.principal_id, - undefined, - estimates.a_source, - b_id, - estimates.a_depth, - undefined, - { - a_p_al: estimates.a_p_al, - a_p_cc: estimates.a_p_cc, - a_som_loi: estimates.a_som_loi, - b_soiltype_agr: estimates.b_soiltype_agr, - b_gwl_class: estimates.b_gwl_class, - }, - ) - } - - return redirectWithSuccess( - `/farm/${b_id_farm}/${calendar}/field/${b_id}/fertilizer`, - { - message: `${b_name} is toegevoegd! 🎉`, - }, - ) - } catch (error) { - throw handleActionError(error) - } + // Get the farm id + const b_id_farm = params.b_id_farm; + if (!b_id_farm) { + throw data("Farm ID is required", { + status: 400, + statusText: "Farm ID is required", + }); + } + + try { + // Get the session + const session = await getSession(request); + + // Get the timeframe + const timeframe = getTimeframe(params); + const calendar = getCalendar(params); + + const nmiApiKey = getNmiApiKey(); + + // Get form values + const formValues = await extractFormValuesFromRequest(request, FormSchema); + + // Check if cultivation is available + let cultivationOptions = []; + const cultivationsCatalogue = await getCultivationsFromCatalogue( + fdm, + session.principal_id, + b_id_farm, + ); + cultivationOptions = cultivationsCatalogue + .filter( + (cultivation) => cultivation?.b_lu_catalogue && cultivation?.b_lu_name, + ) + .map((cultivation) => { + return cultivation.b_lu_catalogue; + }); + if (!cultivationOptions.includes(formValues.b_lu_catalogue)) { + return dataWithError( + `Cultivation ${formValues.b_lu_catalogue} is not available`, + "Gewas is onbekend. Kies een gewas uit de lijst", + ); + } + + const b_name = formValues.b_name; + const b_id_source = formValues.b_id_source; + const b_lu_catalogue = formValues.b_lu_catalogue; + // Parse the geometry string twice to get the actual GeoJSON object + const b_geometry = JSON.parse( + JSON.parse(String(formValues.b_geometry)), + ) as Polygon; + const currentYear = new Date().getFullYear(); + const defaultDate = timeframe.start + ? timeframe.start + : `${currentYear}-01-01`; + const b_start = defaultDate; + const b_lu_start = defaultDate; + const b_lu_end = undefined; + const b_end = undefined; + const b_acquiring_method = "unknown"; + + const b_id = await addField( + fdm, + session.principal_id, + b_id_farm, + b_name, + b_id_source, + b_geometry, + b_start, + b_acquiring_method, + b_end, + ); + await addCultivation( + fdm, + session.principal_id, + b_lu_catalogue, + b_id, + b_lu_start, + b_lu_end, + ); + + if (nmiApiKey) { + const estimates = await getSoilParameterEstimates(field, nmiApiKey); + + await addSoilAnalysis( + fdm, + session.principal_id, + undefined, + estimates.a_source, + b_id, + estimates.a_depth_lower, + undefined, + estimates, + ); + } + + return redirectWithSuccess( + `/farm/${b_id_farm}/${calendar}/field/${b_id}/fertilizer`, + { + message: `${b_name} is toegevoegd! 🎉`, + }, + ); + } catch (error) { + throw handleActionError(error); + } } From c794b5b4de62711485e4c4087e94c02710c118ab Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:49:21 +0200 Subject: [PATCH 2/3] fix: typo for variable --- fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new.tsx index 1285b5649..014e3ed87 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new.tsx @@ -406,7 +406,7 @@ export async function action({ request, params }: ActionFunctionArgs) { ); if (nmiApiKey) { - const estimates = await getSoilParameterEstimates(field, nmiApiKey); + const estimates = await getSoilParameterEstimates(b_geometry, nmiApiKey); await addSoilAnalysis( fdm, From 7cd0eb9e95f5b082280a922ed724b5384a2e93dd Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:59:56 +0200 Subject: [PATCH 3/3] chore: bump version number and update changeset --- .changeset/cold-icons-vanish.md | 5 ----- fdm-app/CHANGELOG.md | 6 ++++++ fdm-app/package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/cold-icons-vanish.md diff --git a/.changeset/cold-icons-vanish.md b/.changeset/cold-icons-vanish.md deleted file mode 100644 index 4cb3336a0..000000000 --- a/.changeset/cold-icons-vanish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@svenvw/fdm-app": patch ---- - -Fix storing additional soil parameters when adding a new field (not in the farm create wizard) diff --git a/fdm-app/CHANGELOG.md b/fdm-app/CHANGELOG.md index 853f01511..230cc21ac 100644 --- a/fdm-app/CHANGELOG.md +++ b/fdm-app/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog fdm-app +## 0.20.4 + +### Patch Changes + +- f589317: Fix storing additional soil parameters when adding a new field (not in the farm create wizard) + ## 0.20.3 ### Patch Changes diff --git a/fdm-app/package.json b/fdm-app/package.json index ee263903a..30528ce96 100644 --- a/fdm-app/package.json +++ b/fdm-app/package.json @@ -1,6 +1,6 @@ { "name": "@svenvw/fdm-app", - "version": "0.20.3", + "version": "0.20.4", "private": true, "sideEffects": false, "type": "module",