diff --git a/.changeset/tiny-words-drum.md b/.changeset/tiny-words-drum.md new file mode 100644 index 000000000..2349062c0 --- /dev/null +++ b/.changeset/tiny-words-drum.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Improve the design of the error block to fit better in the page when shown diff --git a/fdm-app/app/components/blocks/header/automatic.tsx b/fdm-app/app/components/blocks/header/automatic.tsx new file mode 100644 index 000000000..a5897ff09 --- /dev/null +++ b/fdm-app/app/components/blocks/header/automatic.tsx @@ -0,0 +1,196 @@ +import type { ReactNode } from "react" +import { type UIMatch, useLocation, useMatches, useParams } from "react-router" +import { useCalendarStore } from "@/app/store/calendar" +import { useFarmFieldOptionsStore } from "@/app/store/farm-field-options" +import type { FertilizerOption } from "../farm/farm" +import { HeaderAtlas } from "./atlas" +import { HeaderBalance } from "./balance" +import { Header } from "./base" +import { HeaderFarmCreate } from "./create-farm" +import { HeaderFarm, type HeaderFarmOption } from "./farm" +import { HeaderFertilizer } from "./fertilizer" +import { HeaderField, type HeaderFieldOption } from "./field" +import { HeaderNorms } from "./norms" +import { HeaderNutrientAdvice } from "./nutrient-advice" + +export default function HeaderAutomatic() { + interface LoaderDataCandidate { + b_name_farm?: string + farmOptions?: HeaderFarmOption[] + fieldOptions?: HeaderFieldOption[] + fertilizerOptions?: FertilizerOption[] + } + const matches = useMatches() as UIMatch[] + const params = useParams() + const location = useLocation() + const farmFieldOptionsStore = useFarmFieldOptionsStore() + const storedCalendar = useCalendarStore((s) => s.calendar) + + // Find the farm and field options. + let farmOptions: HeaderFarmOption[] | undefined + let fieldOptions: HeaderFieldOption[] | undefined + let fertilizerOptions: unknown[] | undefined + let b_name_farm: string | undefined + + for (const match of matches) { + if (match.loaderData) { + b_name_farm ??= match.loaderData.b_name_farm + farmOptions ??= match.loaderData.farmOptions + fieldOptions ??= match.loaderData.fieldOptions + fertilizerOptions ??= match.loaderData.fertilizerOptions + } + } + + farmOptions ??= farmFieldOptionsStore.farmOptions + fieldOptions ??= farmFieldOptionsStore.fieldOptions + b_name_farm ??= + farmFieldOptionsStore.getFarmById(params.b_id_farm)?.b_name_farm ?? + undefined + + const calendar = params.calendar ?? storedCalendar + + if (/\/create\//.test(location.pathname)) { + return ( +
+ +
+ ) + } + + const variants: Record ReactNode> = { + "routes/farm._index": () => ( +
+ +
+ ), + "routes/farm.$b_id_farm.settings": () => ( +
+ +
+ ), + "routes/farm.$b_id_farm.$calendar.field.new": () => + variants["routes/farm.$b_id_farm.$calendar.field._index"](), + "routes/farm.$b_id_farm.$calendar.field.$b_id": () => + variants["routes/farm.$b_id_farm.$calendar.field._index"](), + "routes/farm.$b_id_farm.$calendar.field._index": () => ( +
+ + +
+ ), + "routes/farm.$b_id_farm.fertilizers": () => ( +
+ + +
+ ), + "routes/farm.$b_id_farm.$calendar.atlas": () => { + const isFieldDetailsPage = + location.pathname.includes("/atlas/fields/") && + location.pathname.split("/atlas/fields/")[1]?.includes(",") + let headerAction: + | { to: string; label: string; disabled: boolean } + | undefined + if (isFieldDetailsPage) { + headerAction = { + to: `/farm/${params.b_id_farm}/${calendar}/atlas/fields`, + label: "Terug", + disabled: false, + } + } + return ( +
+ + +
+ ) + }, + "routes/farm.$b_id_farm.$calendar.balance.nitrogen": () => ( +
+ + +
+ ), + "routes/farm.$b_id_farm.$calendar.nutrient_advice": () => ( +
+ + +
+ ), + "routes/farm.$b_id_farm.$calendar.norms": () => ( +
+ + +
+ ), + } + + let chosenVariant: (() => ReactNode) | undefined + + for (const match of matches) { + if (match.id in variants) { + chosenVariant = variants[match.id] + break + } + } + + return chosenVariant ? chosenVariant() : null +} diff --git a/fdm-app/app/components/blocks/header/create-farm.tsx b/fdm-app/app/components/blocks/header/create-farm.tsx index d5662dab2..09fbbf45d 100644 --- a/fdm-app/app/components/blocks/header/create-farm.tsx +++ b/fdm-app/app/components/blocks/header/create-farm.tsx @@ -1,4 +1,6 @@ -import { useLocation } from "react-router" +import { useEffect } from "react" +import { useLocation, useParams } from "react-router" +import { useFarmFieldOptionsStore } from "@/app/store/farm-field-options" import { BreadcrumbItem, BreadcrumbLink, @@ -11,6 +13,16 @@ export function HeaderFarmCreate({ b_name_farm: string | undefined | null }) { const location = useLocation() + const params = useParams() + const addFarmOptionToTheStore = useFarmFieldOptionsStore( + (s) => s.addFarmOption, + ) + useEffect(() => { + if (params.b_id_farm && b_name_farm) { + addFarmOptionToTheStore(params.b_id_farm, b_name_farm) + } + }, [params.b_id_farm, b_name_farm, addFarmOptionToTheStore]) + const currentPath = String(location.pathname) return ( diff --git a/fdm-app/app/components/blocks/header/farm.tsx b/fdm-app/app/components/blocks/header/farm.tsx index 594d30b1b..c90753c22 100644 --- a/fdm-app/app/components/blocks/header/farm.tsx +++ b/fdm-app/app/components/blocks/header/farm.tsx @@ -1,5 +1,6 @@ import { ChevronDown } from "lucide-react" import { NavLink, useLocation } from "react-router" +import { useFarmFieldOptionsStore } from "@/app/store/farm-field-options" import { BreadcrumbItem, BreadcrumbLink, @@ -20,6 +21,7 @@ export function HeaderFarm({ farmOptions: HeaderFarmOption[] }) { const location = useLocation() + const currentPath = String(location.pathname) return ( @@ -75,7 +77,7 @@ export function HeaderFarm({ ) } -type HeaderFarmOption = { +export type HeaderFarmOption = { b_id_farm: string b_name_farm: string | undefined | null } diff --git a/fdm-app/app/components/blocks/header/fertilizer.tsx b/fdm-app/app/components/blocks/header/fertilizer.tsx index 84f27b537..a9853eca2 100644 --- a/fdm-app/app/components/blocks/header/fertilizer.tsx +++ b/fdm-app/app/components/blocks/header/fertilizer.tsx @@ -1,5 +1,6 @@ import { ChevronDown } from "lucide-react" import { NavLink, useLocation } from "react-router" +import { create } from "zustand" import { BreadcrumbItem, BreadcrumbLink, @@ -12,6 +13,16 @@ import { DropdownMenuTrigger, } from "~/components/ui/dropdown-menu" +const useFertilizerOptionsStore = create<{ + fertilizerOptions: HeaderFertilizerOption[] | undefined + setFertilizerOptions(fertilizerOptions: HeaderFertilizerOption[]): void +}>((set) => ({ + fertilizerOptions: undefined, + setFertilizerOptions(fertilizerOptions) { + set({ fertilizerOptions }) + }, +})) + export function HeaderFertilizer({ b_id_farm, p_id, @@ -19,20 +30,21 @@ export function HeaderFertilizer({ }: { b_id_farm: string p_id: string | undefined - fertilizerOptions: HeaderFertilizerOption[] + fertilizerOptions: HeaderFertilizerOption[] | undefined }) { const location = useLocation() + const currentPath = String(location.pathname) return ( <> - + Meststof - {fertilizerOptions.length > 0 ? ( + {fertilizerOptions && fertilizerOptions.length > 0 ? ( <> @@ -70,16 +82,18 @@ export function HeaderFertilizer({ ) : ( - <> - - - - Nieuwe meststof - - - + fertilizerOptions && ( + <> + + + + Nieuwe meststof + + + + ) )} ) diff --git a/fdm-app/app/components/blocks/header/field.tsx b/fdm-app/app/components/blocks/header/field.tsx index ec91b9f20..86476564c 100644 --- a/fdm-app/app/components/blocks/header/field.tsx +++ b/fdm-app/app/components/blocks/header/field.tsx @@ -1,6 +1,8 @@ import { ChevronDown } from "lucide-react" +import { useEffect } from "react" import { NavLink, useLocation } from "react-router" import { useCalendarStore } from "@/app/store/calendar" +import { useFarmFieldOptionsStore } from "@/app/store/farm-field-options" import { BreadcrumbItem, BreadcrumbLink, @@ -91,7 +93,7 @@ export function HeaderField({ ) } -type HeaderFieldOption = { +export type HeaderFieldOption = { b_id: string b_name: string | undefined | null } diff --git a/fdm-app/app/components/custom/error.tsx b/fdm-app/app/components/custom/error.tsx index 4206dc1e9..e489a708c 100644 --- a/fdm-app/app/components/custom/error.tsx +++ b/fdm-app/app/components/custom/error.tsx @@ -1,22 +1,21 @@ -import { ArrowLeft, Copy, Home } from "lucide-react" +import { Copy, Home, RefreshCw } from "lucide-react" import { useEffect, useState } from "react" import { NavLink } from "react-router" import { Button } from "~/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "~/components/ui/card" +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "~/components/ui/accordion" +import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert" /** - * Displays a full-screen error block with tailored messaging and navigation options. - * - * Depending on the provided error status, this component renders: - * - A specific message and navigation buttons for a 404 error, indicating that the page does not exist. - * - A generic error message along with a button to copy the formatted error details (including status, message, stack trace, page, and timestamp) to the clipboard for other errors. - * - * If an error message is available, the component also displays the error details formatted as pretty-printed JSON. Otherwise, it shows a fallback message for non-404 errors. + * Displays an error block with tailored messaging and navigation options. + * It can be used as a full-screen error or within a component. * * @param status - HTTP status code of the error or null. * @param message - Detailed error message, or null if not available. * @param stacktrace - Optional stack trace providing additional error context. * @param page - The page where the error occurred. * @param timestamp - The timestamp when the error was recorded. + * @param actions - Optional array of action buttons to display. Each action should have a `label`, `onClick` function, and an optional `icon`. */ export function ErrorBlock({ status, @@ -24,12 +23,14 @@ export function ErrorBlock({ stacktrace, page, timestamp, + actions, }: { status: number | null message: string | null stacktrace: string | null | undefined page: string timestamp: string + actions?: { label: string; onClick: () => void; icon?: React.ReactNode; variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" }[] }) { const [isCopied, setIsCopied] = useState(false) @@ -51,73 +52,86 @@ export function ErrorBlock({ null, 2, ) - const copyStackTrace = () => { - navigator.clipboard.writeText(errorDetails) - setIsCopied(true) + + const isNetworkError = status === 502 || status === 503 || status === 504 || !status; + + const defaultActions = []; + + if (isNetworkError) { + defaultActions.push({ + label: "Pagina herladen", + onClick: () => window.location.reload(), + icon: , + variant: "default" as const, + }); } + + defaultActions.push( + { + label: "Terug naar de hoofdpagina", + onClick: () => { + window.location.href = "/" + }, + icon: , + variant: "outline" as const, + }, + { + label: isCopied ? "Gekopieerd!" : "Kopieer foutmelding", + onClick: () => { + navigator.clipboard.writeText(errorDetails) + setIsCopied(true) + }, + icon: , + variant: "ghost" as const, + }, + ); + + const currentActions = actions || defaultActions; + return ( -
-
- A red tractor doing a wheelie -
-

- {status === 404 - ? "Aii, deze pagina bestaat niet." - : "Oeps, er lijkt iets mis te zijn."} -

-

- {status === 404 - ? "Het lijkt erop dat de pagina die je zoekt niet bestaat." - : "Er is onverwachts wat fout gegaan. Probeer eerst opnieuw. Als het niet opnieuw lukt, kopieer dan de foutmelding en neem contact op met Ondersteuning."} -

+
+ {/* Added overflow-hidden to Card */} + +
{/* Removed overflow-hidden from inner div */} +
+ 🚜 +
+
+ + {status === 404 + ? "Oeps, deze pagina bestaat niet." + : "Er ging iets mis."} + + + {status === 404 + ? "Het lijkt erop dat de pagina die je zoekt niet bestaat. Geen zorgen, we helpen je graag verder!" + : "Er is een onverwachte fout opgetreden. Probeer het opnieuw of neem contact op met ondersteuning als het probleem aanhoudt."} + +
+ +
+ {currentActions.map((action, index) => ( + + ))} +
- {status === 404 ? ( -
- - -
- ) : ( -
- - -
- )} - {message ? ( -
-

- Foutmelding: -

-
-                        {errorDetails}
-                    
-
- ) : status === 404 ? null : ( -

- Er zijn helaas geen details over de fout beschikbaar. -

- )} + {message && ( + + + Foutmelding details + +
+                                        {errorDetails}
+                                    
+
+
+
+ )} +
+
) } diff --git a/fdm-app/app/components/custom/inline-error-boundary.tsx b/fdm-app/app/components/custom/inline-error-boundary.tsx new file mode 100644 index 000000000..8bb0312cd --- /dev/null +++ b/fdm-app/app/components/custom/inline-error-boundary.tsx @@ -0,0 +1,87 @@ +import * as Sentry from "@sentry/react-router" +import { isRouteErrorResponse, redirect, useLocation } from "react-router" +import type { Route } from "../../+types/root" +import { ErrorBlock } from "./error" + +/** + * Renders an error boundary that handles and displays error information based on the provided error. + * + * This component distinguishes between route error responses and generic errors: + * - For route errors: + * - Redirects to the signin page if the error status is 401. + * - Renders a 404 error block for client errors with status 400, 403, or 404. + * - Logs other route errors to the error tracking service and renders an error block reflecting the specific status. + * - For generic Error instances, it logs the error and renders a 500 error block with the error message and stack trace. + * - If the error is null, no error UI is rendered. + * - For any other cases, it logs the error and displays an error block with a 500 status and a generic message. + * + * @param error - The error encountered during route processing, either as a route error response or a generic Error. + */ +export function InlineErrorBoundary({ error }: Route.ErrorBoundaryProps) { + const location = useLocation() + const page = location.pathname + const timestamp = new Date().toISOString() + + if (isRouteErrorResponse(error)) { + // Redirect to signin page if authentication is not provided + if (error.status === 401) { + // Get the current path the user tried to access + const currentPath = + location.pathname + location.search + location.hash + // Construct the sign-in URL with the redirectTo parameter + const signInUrl = `./signin?redirectTo=${encodeURIComponent(currentPath)}` + // Throw the redirect response to be caught by React Router + throw redirect(signInUrl) + } + + const clientErrors = [400, 403, 404] + if (clientErrors.includes(error.status)) { + return ( + + ) + } + + Sentry.captureException(error) + return ( + + ) + } + if (error instanceof Error) { + Sentry.captureException(error) + return ( + + ) + } + if (error === null) { + return null + } + + Sentry.captureException(error) + return ( + + ) +} diff --git a/fdm-app/app/lib/form.ts b/fdm-app/app/lib/form.ts index 431b567e1..0f5975570 100644 --- a/fdm-app/app/lib/form.ts +++ b/fdm-app/app/lib/form.ts @@ -70,6 +70,6 @@ export async function extractFormValuesFromRequest( return parsedData.data as z.infer } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/root.tsx b/fdm-app/app/root.tsx index 23409bf6f..7f376f11a 100644 --- a/fdm-app/app/root.tsx +++ b/fdm-app/app/root.tsx @@ -1,15 +1,12 @@ -import * as Sentry from "@sentry/react-router" import mapBoxStyle from "mapbox-gl/dist/mapbox-gl.css?url" import posthog from "posthog-js" import { useEffect } from "react" import type { LinksFunction, LoaderFunctionArgs } from "react-router" import { data, - isRouteErrorResponse, Links, Meta, Outlet, - redirect, Scripts, ScrollRestoration, useLoaderData, @@ -17,8 +14,8 @@ import { } from "react-router" import { getToast } from "remix-toast" import { toast as notify } from "sonner" +import { InlineErrorBoundary } from "@/app/components/custom/inline-error-boundary" import { Banner } from "~/components/custom/banner" -import { ErrorBlock } from "~/components/custom/error" import { Toaster } from "~/components/ui/sonner" import { clientConfig } from "~/lib/config" import { useChangelogStore } from "~/store/changelog" @@ -175,85 +172,6 @@ export default function App() { return } -/** - * Renders an error boundary that handles and displays error information based on the provided error. - * - * This component distinguishes between route error responses and generic errors: - * - For route errors: - * - Redirects to the signin page if the error status is 401. - * - Renders a 404 error block for client errors with status 400, 403, or 404. - * - Logs other route errors to the error tracking service and renders an error block reflecting the specific status. - * - For generic Error instances, it logs the error and renders a 500 error block with the error message and stack trace. - * - If the error is null, no error UI is rendered. - * - For any other cases, it logs the error and displays an error block with a 500 status and a generic message. - * - * @param error - The error encountered during route processing, either as a route error response or a generic Error. - */ -export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { - const location = useLocation() - const page = location.pathname - const timestamp = new Date().toISOString() - - if (isRouteErrorResponse(error)) { - // Redirect to signin page if authentication is not provided - if (error.status === 401) { - // Get the current path the user tried to access - const currentPath = - location.pathname + location.search + location.hash - // Construct the sign-in URL with the redirectTo parameter - const signInUrl = `./signin?redirectTo=${encodeURIComponent(currentPath)}` - // Throw the redirect response to be caught by React Router - throw redirect(signInUrl) - } - - const clientErrors = [400, 403, 404] - if (clientErrors.includes(error.status)) { - return ( - - ) - } - - Sentry.captureException(error) - return ( - - ) - } - if (error instanceof Error) { - Sentry.captureException(error) - return ( - - ) - } - if (error === null) { - return null - } - - Sentry.captureException(error) - return ( - - ) +export function ErrorBoundary(props: Route.ErrorBoundaryProps) { + return } diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx index 225a0cb62..e3bf70553 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx @@ -8,9 +8,6 @@ import { useLocation, } from "react-router" import { ClientOnly } from "remix-utils/client-only" -import { HeaderAtlas } from "~/components/blocks/header/atlas" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarm } from "~/components/blocks/header/farm" import { SidebarInset } from "~/components/ui/sidebar" import { Skeleton } from "~/components/ui/skeleton" import { getSession } from "~/lib/auth.server" @@ -111,13 +108,6 @@ export default function FarmContentBlock() { return ( -
- - -
} diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.tsx index fcf2d0678..42381e560 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.tsx @@ -1,19 +1,15 @@ -import { getFarm, getFarms, getFields } from "@svenvw/fdm-core" +import { getFields } from "@svenvw/fdm-core" import { data, type LoaderFunctionArgs, type MetaFunction, Outlet, - useLoaderData, } from "react-router" import { FarmTitle } from "~/components/blocks/farm/farm-title" -import { HeaderBalance } from "~/components/blocks/header/balance" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarm } from "~/components/blocks/header/farm" import { SidebarInset } from "~/components/ui/sidebar" +import { clientConfig } from "~/lib/config" import { getSession } from "~/lib/auth.server" import { getTimeframe } from "~/lib/calendar" -import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" @@ -53,40 +49,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }) } - // Get the field id - const b_id = params.b_id - // Get the session const session = await getSession(request) // Get timeframe from calendar store const timeframe = getTimeframe(params) - // 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 the fields to be selected const fields = await getFields( fdm, @@ -107,10 +75,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Return user information from loader return { - farm: farm, - b_id_farm: b_id_farm, - b_id: b_id, - farmOptions: farmOptions, fieldOptions: fieldOptions, } } catch (error) { @@ -125,21 +89,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { * It also renders a main section containing the farm title, description, nested routes via an Outlet, and a notification toaster. */ export default function FarmBalanceNitrogenBlock() { - const loaderData = useLoaderData() - return ( -
- - -
{ @@ -78,25 +117,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const calendar = getCalendar(params) const timeframe = getTimeframe(params) - // Get a list of possible farms of the user - const farms = await getFarms(fdm, session.principal_id) - - // Redirect to farms overview if user has no farm - if (farms.length === 0) { - return redirect("farm") - } - - // Get farms to be selected - 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, - } - }) - // Get the fields to be selected const fields = await getFields( fdm, @@ -128,37 +148,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } // Create the items for sidebar page - const sidebarPageItems = [ - { - to: `/farm/${b_id_farm}/${calendar}/field/${b_id}/overview`, - title: "Overzicht", - }, - { - to: `/farm/${b_id_farm}/${calendar}/field/${b_id}/cultivation`, - title: "Gewassen", - }, - { - to: `/farm/${b_id_farm}/${calendar}/field/${b_id}/fertilizer`, - title: "Bemesting", - }, - { - to: `/farm/${b_id_farm}/${calendar}/field/${b_id}/soil`, - title: "Bodem", - }, - { - to: `/farm/${b_id_farm}/${calendar}/field/${b_id}/atlas`, - title: "Kaart", - }, - { - to: `/farm/${b_id_farm}/${calendar}/field/${b_id}/delete`, - title: "Verwijderen", - }, - ] + const sidebarPageItems = getSidebarPageItems(b_id_farm, calendar, b_id) // Return user information from loader return { b_id_farm: b_id_farm, - farmOptions: farmOptions, fieldOptions: fieldOptions, field: field, b_id: b_id, @@ -181,27 +175,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { */ export default function FarmFieldIndex() { const loaderData = useLoaderData() - const calendar = useCalendarStore((state) => state.calendar) return ( -
- - -
) } + +export function ErrorBoundary(props: Route.ErrorBoundaryProps) { + const farmFieldOptionsStore = useFarmFieldOptionsStore() + const { params } = props + + const cachedField = farmFieldOptionsStore.getFieldById(params.b_id) + const cachedFieldName = cachedField + ? (cachedField.b_name ?? "Naam Onbekend") + : "Onbekend Perceel" + + return ( + + {params.b_id_farm && params.calendar && params.b_id ? ( +
+ + + + +
+ ) : ( +
+ +
+ )} +
+ ) +} diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx index 6a561b644..87e9b2585 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx @@ -8,9 +8,6 @@ import { useLoaderData, } from "react-router" import { FarmTitle } from "~/components/blocks/farm/farm-title" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarm } from "~/components/blocks/header/farm" -import { HeaderField } from "~/components/blocks/header/field" import { Button } from "~/components/ui/button" import { Card, @@ -81,17 +78,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return redirect("./farm") } - // Get farms to be selected - 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, - } - }) - // Get the fields to be selected const fields = await getFields( fdm, @@ -116,7 +102,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Return user information from loader return { b_id_farm: b_id_farm, - farmOptions: farmOptions, fieldOptions: fieldOptions, userName: session.userName, } @@ -142,23 +127,6 @@ export default function FarmFieldIndex() { return ( -
- - -
{loaderData.fieldOptions.length === 0 ? ( <> 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 dbec73054..62fab0c9a 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 @@ -4,7 +4,6 @@ import { addSoilAnalysis, getCultivationsFromCatalogue, getFarm, - getFarms, getFields, } from "@svenvw/fdm-core" import type { Feature, FeatureCollection, Polygon } from "geojson" @@ -37,9 +36,6 @@ 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" @@ -51,7 +47,6 @@ import { clientConfig } from "~/lib/config" 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 = () => { @@ -91,18 +86,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const calendar = getCalendar(params) 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) { @@ -170,7 +153,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const mapboxStyle = getMapboxStyle() return { - farmOptions: farmOptions, b_id_farm: b_id_farm, b_name_farm: farm.b_name_farm, calendar: calendar, @@ -188,7 +170,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Main export default function Index() { const loaderData = useLoaderData() - const calendar = useCalendarStore((state) => state.calendar) const fieldsSavedId = "fieldsSaved" const fieldsSaved = loaderData.featureCollection @@ -226,23 +207,6 @@ export default function Index() { return ( -
- - -
{/* { @@ -82,13 +79,6 @@ 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 the fields to be selected const fields = await getFields( fdm, @@ -180,7 +170,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { b_id_farm: b_id_farm, b_id: b_id, calendar: calendar, - farmOptions: farmOptions, fieldOptions: fieldOptions, asyncData, } @@ -194,13 +183,6 @@ export default function FarmNormsBlock() { return ( -
- - -
{ @@ -260,3 +262,7 @@ function FieldNutrientAdvice({ /> ) } + +export function ErrorBoundary(props: Route.ErrorBoundaryProps) { + return +} diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.tsx index 54e8cf3fb..309b14893 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.tsx @@ -7,15 +7,12 @@ import { useLoaderData, } from "react-router" import { FarmTitle } from "~/components/blocks/farm/farm-title" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarm } from "~/components/blocks/header/farm" -import { HeaderNutrientAdvice } from "~/components/blocks/header/nutrient-advice" import { SidebarInset } from "~/components/ui/sidebar" import { getSession } from "~/lib/auth.server" -import { getTimeframe } from "~/lib/calendar" import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" +import { getTimeframe } from "~/lib/calendar" // Meta export const meta: MetaFunction = () => { @@ -53,40 +50,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }) } - // Get the field id - const b_id = params.b_id - // Get the session const session = await getSession(request) // Get timeframe from calendar store const timeframe = getTimeframe(params) - // 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 the fields to be selected const fields = await getFields( fdm, @@ -107,10 +76,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Return user information from loader return { - farm: farm, - b_id_farm: b_id_farm, - b_id: b_id, - farmOptions: farmOptions, fieldOptions: fieldOptions, } } catch (error) { @@ -125,21 +90,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { * It also renders a main section containing the farm title, description, nested routes via an Outlet, and a notification toaster. */ export default function FarmBalanceNitrogenBlock() { - const loaderData = useLoaderData() - return ( -
- - -
{ - 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 || "", - } - }) - // Set editable status let editable = false if (fertilizer.p_source === b_id_farm) { @@ -113,11 +79,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // 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: editable, fertilizerParameters: fertilizerParameters, @@ -191,23 +152,6 @@ export default function FarmFertilizerBlock() { return ( -
- - -
{ - return { - b_id_farm: farm.b_id_farm, - b_name_farm: farm.b_name_farm, - } - }) - // Get the available fertilizers const fertilizers: Fertilizer[] = await getFertilizers( fdm, @@ -78,9 +50,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Return user information from loader return { - farm: farm, - b_id_farm: b_id_farm, - farmOptions: farmOptions, fertilizers: fertilizers, } } catch (error) { @@ -99,17 +68,6 @@ export default function FarmFertilizersBlock() { return ( -
- - -
{ - 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, @@ -275,6 +242,6 @@ export async function action({ request, params }: ActionFunctionArgs) { message: `${formValues.p_name_nl} is toegevoegd! 🎉`, }) } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } 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 index c50860dba..2a4fec72e 100644 --- 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 @@ -3,7 +3,6 @@ import { addFertilizer, addFertilizerToCatalogue, getFertilizerParametersDescription, - getFertilizers, } from "@svenvw/fdm-core" import { useEffect } from "react" import { @@ -102,22 +101,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { 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, } @@ -329,6 +314,6 @@ export async function action({ request, params }: ActionFunctionArgs) { message: `${formValues.p_name_nl} is toegevoegd! 🎉`, }) } catch (error) { - throw handleActionError(error) + return 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 d603a39da..41c9e0c53 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 @@ -4,12 +4,8 @@ import { type LoaderFunctionArgs, type MetaFunction, Outlet, - useLoaderData, } from "react-router" import { FarmTitle } from "~/components/blocks/farm/farm-title" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarm } from "~/components/blocks/header/farm" -import { HeaderFertilizer } from "~/components/blocks/header/fertilizer" import { SidebarInset } from "~/components/ui/sidebar" import { getSession } from "~/lib/auth.server" import { clientConfig } from "~/lib/config" @@ -58,20 +54,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { statusText: "not found: farms", }) } - - const farmOptions = farms.map((farm) => { - return { - b_id_farm: farm.b_id_farm, - b_name_farm: farm.b_name_farm || "", - } - }) - - // Return user information from loader - return { - farm: farm, - b_id_farm: b_id_farm, - farmOptions: farmOptions, - } } catch (error) { throw handleLoaderError(error) } @@ -84,27 +66,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { * 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() - return ( -
- - -
{ + return { + p_id: fertilizer.p_id, + p_name_nl: fertilizer.p_name_nl || "", + } + }) + + // Return user information from loader + return { + fertilizerOptions: fertilizerOptions, + } + } catch (error) { + throw handleLoaderError(error) + } +} diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx index e79c7c0dc..65742f0c6 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx @@ -81,7 +81,7 @@ export async function action({ request, params }: ActionFunctionArgs) { `Het is niet gelukt derogatie voor ${year} aan te passen.`, ) } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx index ecaa41a09..b9d509975 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx @@ -297,7 +297,7 @@ export async function action({ request, params }: ActionFunctionArgs) { message: `${formValues.b_name_farm} is bijgewerkt! 🎉`, }) } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.tsx index 2146bc176..628e5a0e7 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.tsx @@ -1,4 +1,4 @@ -import { getFarm, getFarms } from "@svenvw/fdm-core" +import { getFarm } from "@svenvw/fdm-core" import { data, type LoaderFunctionArgs, @@ -8,15 +8,12 @@ import { } from "react-router" import { FarmContent } from "~/components/blocks/farm/farm-content" import { FarmTitle } from "~/components/blocks/farm/farm-title" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarm } from "~/components/blocks/header/farm" import { SidebarInset } from "~/components/ui/sidebar" import { Toaster } from "~/components/ui/sonner" import { getSession } from "~/lib/auth.server" import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" -import { useCalendarStore } from "~/store/calendar" // Meta export const meta: MetaFunction = () => { @@ -66,22 +63,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }) } - // 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, - } - }) - // Create the items for sidebar page const sidebarPageItems = [ { @@ -104,9 +85,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Return user information from loader return { - farm: farm, - b_id_farm: b_id_farm, - farmOptions: farmOptions, sidebarPageItems: sidebarPageItems, } } catch (error) { @@ -123,22 +101,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { export default function FarmContentBlock() { const loaderData = useLoaderData() - const calendar = useCalendarStore((state) => state.calendar) - return ( -
- -
{ @@ -25,26 +28,19 @@ export const meta: MetaFunction = () => { * * @throws {Response} If the farm ID is not provided. */ -export async function loader({ request, params }: LoaderFunctionArgs) { +export async function loader({ request }: LoaderFunctionArgs) { try { - // 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", - }) - } - // Get the session - const session = await getSession(request) - - // Return the farm ID and session info - return { - farmId: b_id_farm, - session, - } + await getSession(request) } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } + +export function ErrorBoundary(props: Route.ErrorBoundaryProps) { + return ( + + + + ) +} diff --git a/fdm-app/app/routes/farm._index.tsx b/fdm-app/app/routes/farm._index.tsx index 2c182c72a..2825740e6 100644 --- a/fdm-app/app/routes/farm._index.tsx +++ b/fdm-app/app/routes/farm._index.tsx @@ -7,8 +7,6 @@ import { useLoaderData, } from "react-router" import { FarmTitle } from "~/components/blocks/farm/farm-title" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarm } from "~/components/blocks/header/farm" import { Badge } from "~/components/ui/badge" import { Button } from "~/components/ui/button" import { @@ -25,7 +23,9 @@ import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" import { getTimeBasedGreeting } from "~/lib/greetings" -import { getCalendarSelection } from "../lib/calendar" +import { getCalendarSelection } from "~/lib/calendar" +import { InlineErrorBoundary } from "~/components/custom/inline-error-boundary" +import { Route } from "../+types/root" // Meta export const meta: MetaFunction = () => { @@ -50,7 +50,7 @@ export const meta: MetaFunction = () => { * * @throws {Error} If retrieving the session or fetching the farm data fails. */ -export async function loader({ request, params }: LoaderFunctionArgs) { +export async function loader({ request }: LoaderFunctionArgs) { try { // Get the session const session = await getSession(request) @@ -60,17 +60,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Get a list of possible farms of the user const farms = await getFarms(fdm, session.principal_id) - const farmOptions = farms.map((farm) => { - return { - b_id_farm: farm.b_id_farm, - b_name_farm: farm.b_name_farm, - } - }) // Return user information from loader return { farms: farms, - farmOptions: farmOptions, calendar: calendar, username: session.userName, } @@ -92,12 +85,6 @@ export default function AppIndex() { return ( -
- -
{loaderData.farms.length === 0 ? (
@@ -409,3 +396,11 @@ export default function AppIndex() { ) } + +export function ErrorBoundary(props: Route.ErrorBoundaryProps) { + return ( + + + + ) +} diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar._index.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar._index.tsx index fa1f5f1c5..91ef7d9d3 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar._index.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar._index.tsx @@ -2,8 +2,6 @@ import { getFarm } from "@svenvw/fdm-core" import { Map as MapIcon, UploadCloud } from "lucide-react" import type { LoaderFunctionArgs, MetaFunction } from "react-router" import { data, NavLink, useLoaderData } from "react-router" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarmCreate } from "~/components/blocks/header/create-farm" import { Accordion, AccordionContent, @@ -20,8 +18,7 @@ import { } from "~/components/ui/card" import { SidebarInset } from "~/components/ui/sidebar" import { clientConfig } from "~/lib/config" -import { getSession } from "../lib/auth.server" -import { fdm } from "../lib/fdm.server" +import { getSession } from "~/lib/auth.server" // Meta export const meta: MetaFunction = () => { @@ -43,27 +40,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } // Get the session - const session = await getSession(request) - - const farm = await getFarm(fdm, session.principal_id, b_id_farm) - if (!farm) { - throw data("Farm not found", { - status: 404, - statusText: "Farm not found", - }) - } - - return { farm } + await getSession(request) } export default function ChooseFieldImportMethod() { - const { farm } = useLoaderData() - return ( -
- -

diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.access.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.access.tsx index 3cdf98ce2..1832c0b81 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.access.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.access.tsx @@ -33,9 +33,12 @@ import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" import { extractFormValuesFromRequest } from "~/lib/form" import { AccessFormSchema } from "~/lib/schemas/access.schema" -import { SidebarInset } from "../components/ui/sidebar" -import { Header } from "../components/blocks/header/base" -import { HeaderFarmCreate } from "../components/blocks/header/create-farm" +import { Route } from "../+types/root" +import { Header } from "~/components/blocks/header/base" +import { HeaderFarmCreate } from "~/components/blocks/header/create-farm" +import { InlineErrorBoundary } from "~/components/custom/inline-error-boundary" +import { SidebarInset } from "~/components/ui/sidebar" +import { useFarmFieldOptionsStore } from "~/store/farm-field-options" // Meta export const meta: MetaFunction = () => { @@ -59,7 +62,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (!b_id_farm) { throw data("Farm ID is required", { status: 400 }) } - const calendar = getCalendar(params) // Get calendar year const session = await getSession(request) @@ -83,10 +85,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return { b_id_farm: b_id_farm, - b_name_farm: farm.b_name_farm, principals: principals, hasSharePermission: hasSharePermission, - calendar: calendar, } } catch (error) { throw handleLoaderError(error) @@ -97,14 +97,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // TODO: Add wizard-specific layout/header/breadcrumbs // TODO: Add "Voltooien" button with correct navigation export default function CreateFarmAccessStep() { - const { b_id_farm, b_name_farm, principals, hasSharePermission, calendar } = + const { b_id_farm, principals, hasSharePermission } = useLoaderData() return ( -
- -
@@ -229,3 +226,25 @@ export async function action({ request, params }: ActionFunctionArgs) { // throw handleActionError(error) } } + +export function ErrorBoundary(props: Route.ErrorBoundaryProps) { + return ( +
+
+
+
+

+ Toegang instellen (Optioneel) +

+

+ Nodig nu alvast gebruikers of organisaties uit, + of voltooi de wizard. +

+
+
+ +
+ +
+ ) +} diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.atlas.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.atlas.tsx index 88ca355b7..c2b673454 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.atlas.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.atlas.tsx @@ -55,7 +55,7 @@ import { getCalendar, getTimeframe } from "~/lib/calendar" import { clientConfig } from "~/lib/config" import { handleActionError, handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" -import FieldDetailsInfoPopup from "../components/blocks/field/popup" +import FieldDetailsInfoPopup from "~/components/blocks/field/popup" // Meta export const meta: MetaFunction = () => { @@ -162,7 +162,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return { b_id_farm: farm.b_id_farm, - b_name_farm: farm.b_name_farm, fieldsSaved: fieldsSaved, timeframe: timeframe, calendar: calendar, @@ -219,9 +218,6 @@ export default function Index() { return ( -
- -
@@ -488,6 +484,6 @@ export async function action({ request, params }: ActionFunctionArgs) { }, ) } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop._index.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop._index.tsx index 3a39cf6fb..0d1c6c408 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop._index.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop._index.tsx @@ -272,6 +272,6 @@ export async function action({ request, params }: ActionFunctionArgs) { ) } } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop.harvest.$b_id_harvesting.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop.harvest.$b_id_harvesting.tsx index 65fe90b70..d3b15a1a4 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop.harvest.$b_id_harvesting.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop.harvest.$b_id_harvesting.tsx @@ -320,6 +320,6 @@ export async function action({ request, params }: ActionFunctionArgs) { throw new Error("Invalid request method") } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop.harvest.new.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop.harvest.new.tsx index d807527ed..001085ad9 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop.harvest.new.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop.harvest.new.tsx @@ -149,6 +149,6 @@ export async function action({ request, params }: ActionFunctionArgs) { message: "Oogst succesvol toegevoegd! 🎉", }) } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.tsx index 047f6e469..6209a4bd4 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.tsx @@ -8,13 +8,13 @@ import { } from "react-router" import { CultivationListPlan } from "~/components/blocks/cultivation/list-plan" import { FarmTitle } from "~/components/blocks/farm/farm-title" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarmCreate } from "~/components/blocks/header/create-farm" import { getSession } from "~/lib/auth.server" import { getCalendar, getTimeframe } from "~/lib/calendar" import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" +import type { Route } from "../+types/root" +import { InlineErrorBoundary } from "~/components/custom/inline-error-boundary" // Meta export const meta: MetaFunction = () => { @@ -101,35 +101,50 @@ export default function Index() { const loaderData = useLoaderData() return ( - <> -
- -
-
- -
-
- -
- -
+
+ +
+
+ +
+
-
- +
+
+ ) +} + +export function ErrorBoundary(props: Route.ErrorBoundaryProps) { + const { params } = props + + return ( +
+ + +
) } diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fertilizers.$b_lu_catalogue.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fertilizers.$b_lu_catalogue.tsx index 99817a2c7..f7be07c53 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fertilizers.$b_lu_catalogue.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fertilizers.$b_lu_catalogue.tsx @@ -72,22 +72,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { (x: { parameter: string }) => 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: string) => { - const meta = applicationMethods.options.find( - (x: { value: string }) => 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, - } - }) // Fetch the cultivation plan for the farm const cultivationPlan = await getCultivationPlan( @@ -143,7 +127,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return { b_lu_catalogue: b_lu_catalogue, b_id_farm: b_id_farm, - fertilizerOptions: fertilizerOptions, fertilizerApplications: fertilizerApplications, dose: dose.dose, applicationMethodOptions: applicationMethods.options, @@ -252,6 +235,6 @@ export async function action({ request, params }: ActionFunctionArgs) { } throw new Error(`${request.method} is not supported`) } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fertilizers.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fertilizers.tsx index ab7c172d4..84e3cdd53 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fertilizers.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fertilizers.tsx @@ -1,4 +1,4 @@ -import { getCultivationPlan, getFarm } from "@svenvw/fdm-core" +import { getCultivationPlan, getFarm, getFertilizers } from "@svenvw/fdm-core" import { data, type LoaderFunctionArgs, @@ -8,13 +8,13 @@ import { } from "react-router" import { CultivationListPlan } from "~/components/blocks/cultivation/list-plan" import { FarmTitle } from "~/components/blocks/farm/farm-title" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarmCreate } from "~/components/blocks/header/create-farm" import { getSession } from "~/lib/auth.server" import { getCalendar, getTimeframe } from "~/lib/calendar" import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" +import type { Route } from "../+types/root" +import { InlineErrorBoundary } from "~/components/custom/inline-error-boundary" // Meta export const meta: MetaFunction = () => { @@ -57,6 +57,20 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }) } + // 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 || "", + } + }) + const cultivationPlan = await getCultivationPlan( fdm, session.principal_id, @@ -67,7 +81,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return { cultivationPlan: cultivationPlan, b_id_farm: b_id_farm, - b_name_farm: farm.b_name_farm, + fertilizerOptions: fertilizerOptions, calendar: calendar, } } catch (error) { @@ -80,35 +94,46 @@ export default function Index() { const loaderData = useLoaderData() return ( - <> -
- -
-
- -
-
- -
- -
+
+ +
+
+ +
+
-
- +
+
+ ) +} + +export function ErrorBoundary(props: Route.ErrorBoundaryProps) { + const { params } = props + + return ( +
+ + +
) } diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id._index.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id._index.tsx index 0912e2943..5a86bb13a 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id._index.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id._index.tsx @@ -58,7 +58,9 @@ import { clientConfig } from "~/lib/config" import { handleActionError, handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" import { extractFormValuesFromRequest } from "~/lib/form" -import { FieldDeleteDialog } from "../components/blocks/field/delete" +import type { Route } from "../+types/root" +import { FieldDeleteDialog } from "~/components/blocks/field/delete" +import { InlineErrorBoundary } from "~/components/custom/inline-error-boundary" // Meta export const meta: MetaFunction = () => { @@ -521,6 +523,14 @@ export async function action({ request, params }: ActionFunctionArgs) { throw new Error("invalid method") } } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } + +export function ErrorBoundary(props: Route.ErrorBoundaryProps) { + return ( +
+ +
+ ) +} diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis.$analysis_type.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis.$analysis_type.tsx index 8ca187cfe..d8c9ff985 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis.$analysis_type.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis.$analysis_type.tsx @@ -5,6 +5,7 @@ import { data, type LoaderFunctionArgs, NavLink, + redirect, useLoaderData, } from "react-router" import { redirectWithSuccess } from "remix-toast" @@ -14,7 +15,7 @@ import { getSoilParametersForSoilAnalysisType } from "~/components/blocks/soil/p import { Button } from "~/components/ui/button" import { Separator } from "~/components/ui/separator" import { getSession } from "~/lib/auth.server" -import { handleActionError, handleLoaderError } from "~/lib/error" +import { handleActionError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" import { extractFormValuesFromRequest } from "~/lib/form" @@ -94,7 +95,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { soilParameterDescription: soilAnalysisParameterDescription, } } catch (error) { - throw handleLoaderError(error) + const response = await handleActionError(error) + if (response.init) { + return redirect( + `/farm/create/${params.b_id_farm}/${params.calendar}/fields/${params.b_id}/soil/analysis`, + response.init, + ) + } + return response } } @@ -192,6 +200,6 @@ export async function action({ request, params }: ActionFunctionArgs) { message: "Bodemanalyse is toegevoegd! 🎉", }) } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis._index.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis._index.tsx index 372966dc6..436480477 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis._index.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis._index.tsx @@ -1,11 +1,11 @@ import { getField } from "@svenvw/fdm-core" import { ArrowLeft } from "lucide-react" -import { data, type LoaderFunctionArgs, NavLink } from "react-router" +import { data, type LoaderFunctionArgs, NavLink, redirect } from "react-router" import { SoilAnalysisFormSelection } from "~/components/blocks/soil/form-selection" import { Button } from "~/components/ui/button" import { Separator } from "~/components/ui/separator" import { getSession } from "~/lib/auth.server" -import { handleLoaderError } from "~/lib/error" +import { handleActionError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" /** @@ -55,7 +55,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { field: field, } } catch (error) { - throw handleLoaderError(error) + const response = await handleActionError(error) + if (response.init) { + return redirect( + `/farm/create/${params.b_id_farm}/${params.calendar}/fields/${params.b_id}`, + response.init, + ) + } + return response } } diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis.upload.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis.upload.tsx index 5c973717d..da773a162 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis.upload.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis.upload.tsx @@ -10,6 +10,7 @@ import { type ActionFunctionArgs, data, type LoaderFunctionArgs, + redirect, } from "react-router" import { dataWithError, redirectWithSuccess } from "remix-toast" import { @@ -18,7 +19,7 @@ import { } from "~/components/blocks/soil/form-upload" import { extractSoilAnalysis } from "~/integrations/nmi" import { getSession } from "~/lib/auth.server" -import { handleActionError, handleLoaderError } from "~/lib/error" +import { handleActionError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" /** @@ -71,7 +72,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { soilParameterDescription: soilParameterDescription, } } catch (error) { - throw handleLoaderError(error) + const response = await handleActionError(error) + if (response.init) { + return redirect( + `/farm/create/${params.b_id_farm}/${params.calendar}/fields/${params.b_id}/soil/analysis`, + response.init, + ) + } + return response } } @@ -210,6 +218,6 @@ export async function action({ request, params }: ActionFunctionArgs) { ) } - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.tsx index 1cbce7a1a..ff2fc30d9 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.tsx @@ -8,8 +8,6 @@ import { Outlet, useLoaderData, } from "react-router" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarmCreate } from "~/components/blocks/header/create-farm" import { SidebarPage } from "~/components/custom/sidebar-page" import { Button } from "~/components/ui/button" import { Separator } from "~/components/ui/separator" @@ -20,6 +18,8 @@ import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" import { cn } from "~/lib/utils" +import type { Route } from "../+types/root" +import { InlineErrorBoundary } from "~/components/custom/inline-error-boundary" // Meta export const meta: MetaFunction = () => { @@ -108,9 +108,6 @@ export default function Index() { return ( -
- -
@@ -170,3 +167,27 @@ export default function Index() { ) } + +export function ErrorBoundary(props: Route.ErrorBoundaryProps) { + return ( + +
+
+
+
+

+ Percelen +

+

+ Pas de naam aan, controleer het gewas en + bodemgegevens +

+
+
+ +
+ +
+
+ ) +} diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.upload.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.upload.tsx index c2d06fe41..e2055a0e8 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.upload.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.upload.tsx @@ -18,8 +18,6 @@ import { data, useLoaderData } from "react-router" import { dataWithWarning, redirectWithSuccess } from "remix-toast" import { combine, parseDbf, parseShp } from "shpjs" import { MijnPercelenUploadForm } from "@/app/components/blocks/mijnpercelen/form-upload" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarmCreate } from "~/components/blocks/header/create-farm" import { SidebarInset } from "~/components/ui/sidebar" import { getNmiApiKey, getSoilParameterEstimates } from "~/integrations/nmi" import { getSession } from "~/lib/auth.server" @@ -64,17 +62,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const calendar = getCalendar(params) - return { b_id_farm, b_name_farm: farm.b_name_farm, calendar } + return { b_id_farm, calendar } } export default function UploadMijnPercelenPage() { - const { b_id_farm, calendar, b_name_farm } = useLoaderData() + const { b_id_farm, calendar } = useLoaderData() return ( -
- -
-
- -
@@ -428,6 +423,6 @@ export async function action({ request }: ActionFunctionArgs) { message: "Bedrijf is toegevoegd! 🎉 Selecteer nu de importmethode.", }) } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/farm.tsx b/fdm-app/app/routes/farm.tsx index a91909683..43fee093a 100644 --- a/fdm-app/app/routes/farm.tsx +++ b/fdm-app/app/routes/farm.tsx @@ -19,6 +19,9 @@ import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { useCalendarStore } from "~/store/calendar" import { useFarmStore } from "~/store/farm" +import HeaderAutomatic from "~/components/blocks/header/automatic" +import { getFarms } from "@svenvw/fdm-core" +import { fdm } from "~/lib/fdm.server" export const meta: MetaFunction = () => { return [ @@ -52,12 +55,28 @@ export async function loader({ request }: LoaderFunctionArgs) { return sessionCheckResponse } - // Return user information from loader - return { + const data = { user: session.user, userName: session.userName, initials: session.initials, + farmOptions: [], } + + try { + // Get a list of possible farms of the user + const farms = await getFarms(fdm, session.principal_id) + const farmOptions = farms.map((farm) => { + return { + b_id_farm: farm.b_id_farm, + b_name_farm: farm.b_name_farm, + } + }) + + data.farmOptions = farmOptions + } catch (_) {} + + // Return user information from loader + return data } catch (error) { // If getSession throws (e.g., invalid token), it might result in a 401 // We need to handle that case here as well, similar to the ErrorBoundary @@ -132,6 +151,7 @@ export default function App() { /> + diff --git a/fdm-app/app/routes/logout.tsx b/fdm-app/app/routes/logout.tsx index e079d092a..c8afb5237 100644 --- a/fdm-app/app/routes/logout.tsx +++ b/fdm-app/app/routes/logout.tsx @@ -29,6 +29,6 @@ export async function action({ request }: ActionFunctionArgs) { }) return redirect("/signin") } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/organization.$slug.tsx b/fdm-app/app/routes/organization.$slug.tsx index c9e93bdd6..5a1fb0632 100644 --- a/fdm-app/app/routes/organization.$slug.tsx +++ b/fdm-app/app/routes/organization.$slug.tsx @@ -380,7 +380,7 @@ const FormSchema = z.object({ export async function action({ request, params }: ActionFunctionArgs) { try { if (!params.slug) { - throw handleActionError("not found: organization") + return handleActionError("not found: organization") } const formValues = await extractFormValuesFromRequest( request, @@ -393,7 +393,7 @@ export async function action({ request, params }: ActionFunctionArgs) { session.user.id, ) if (!organization) { - throw handleActionError("not found: organization") + return handleActionError("not found: organization") } if (formValues.intent === "invite_user") { @@ -473,6 +473,6 @@ export async function action({ request, params }: ActionFunctionArgs) { } throw new Error("invalid intent") } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/organization.invitations.tsx b/fdm-app/app/routes/organization.invitations.tsx index d5c357512..125c3bdab 100644 --- a/fdm-app/app/routes/organization.invitations.tsx +++ b/fdm-app/app/routes/organization.invitations.tsx @@ -239,6 +239,6 @@ export async function action({ request }: ActionFunctionArgs) { } throw new Error("invalid intent") } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/routes/organization.new.tsx b/fdm-app/app/routes/organization.new.tsx index ce09d7417..957b6e032 100644 --- a/fdm-app/app/routes/organization.new.tsx +++ b/fdm-app/app/routes/organization.new.tsx @@ -260,7 +260,7 @@ export async function action({ request }: ActionFunctionArgs) { message: `Organisatie ${formValues.name} is aangemaakt! 🎉`, }) } catch (error) { - throw handleActionError(error) + return handleActionError(error) } } diff --git a/fdm-app/app/store/farm-field-options.ts b/fdm-app/app/store/farm-field-options.ts new file mode 100644 index 000000000..427b9bbe9 --- /dev/null +++ b/fdm-app/app/store/farm-field-options.ts @@ -0,0 +1,91 @@ +import { create } from "zustand" +import type { HeaderFarmOption } from "~/components/blocks/header/farm" +import type { HeaderFieldOption } from "~/components/blocks/header/field" + +/** + * Cache for farm and field options, to be used in error boundaries only. + * + * Do not use this store server-side + */ +interface FarmFieldOptionsStore { + /** + * Farm options cached when the farm breadcrumb was last rendered. + */ + farmOptions: HeaderFarmOption[] + /** + * Field options cached when the field breadcrumb was last rendered. + */ + fieldOptions: HeaderFieldOption[] + /** + * Sets the cached list of farms as provided by the farm breadcrumb. + * + * @param farmOptions the list of HeaderFarmOption + */ + setFarmOptions(farmOptions: HeaderFarmOption[]): void + /** + * Sets the cached list of fields as provided by the field breadcrumb. + * + * @param fieldOptions the list of HeaderFieldOption + */ + setFieldOptions(fieldOptions: HeaderFieldOption[]): void + /** + * Adds a singe farm option so it is later found by getFarmById + * + * @param b_id_farm The farm id + * @param b_name_farm The farm name + */ + addFarmOption(b_id_farm: string, b_name_farm: string): void + /** + * Tries to get the cached farm with the given id, otherwise returns undefined. + * + * @param b_id_farm The farm id + * @returns the HeaderFarmOption if found otherwise undefined + */ + getFarmById(b_id_farm: string | undefined): HeaderFarmOption | undefined + /** + * Tries to get the cached field with the given id, otherwise returns undefined. + * + * @param b_id The field id + * @returns the HeaderFieldOption if found otherwise undefined + */ + getFieldById(b_id: string | undefined): HeaderFieldOption | undefined +} + +export const useFarmFieldOptionsStore = create( + (set, get) => ({ + farmOptions: [], + fieldOptions: [], + setFarmOptions(farmOptions) { + set({ farmOptions }) + }, + setFieldOptions(fieldOptions) { + set({ fieldOptions }) + }, + addFarmOption(b_id_farm, b_name_farm) { + set((state) => { + const item = { b_id_farm, b_name_farm } + + const duplicateIndex = state.farmOptions.findIndex( + (f) => f.b_id_farm === b_id_farm, + ) + if (duplicateIndex > -1) { + const newFarmOptions = state.farmOptions.slice() + newFarmOptions[duplicateIndex] = item + return { farmOptions: newFarmOptions } + } + + return { farmOptions: [item, ...get().farmOptions] } + }) + }, + getFarmById(b_id_farm) { + if (b_id_farm) { + return get().farmOptions.find((f) => f.b_id_farm === b_id_farm) + } + }, + getFieldById(b_id) { + if (b_id) { + return get().fieldOptions.find((f) => f.b_id === b_id) + } + }, + }), +) diff --git a/fdm-app/app/tailwind.css b/fdm-app/app/tailwind.css index 870a9f029..83b4087ca 100644 --- a/fdm-app/app/tailwind.css +++ b/fdm-app/app/tailwind.css @@ -171,3 +171,16 @@ /* text-transform: uppercase; */ /* outline: none; */ } + +@keyframes tractor-drive { + 100% { + transform: translateX(calc(-100% - 5rem)); + } + 0% { + transform: translateX(calc(100% + 20rem)); /* Adjusted to move beyond the card width */ + } +} + +.animate-tractor-drive { + animation: tractor-drive 5s linear infinite; +}