diff --git a/fdm-app/CHANGELOG.md b/fdm-app/CHANGELOG.md index 682fb2815..48169a190 100644 --- a/fdm-app/CHANGELOG.md +++ b/fdm-app/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog fdm-app +## 0.28.1 + +### Patch Changes + +- [#495](https://github.com/nmi-agro/fdm/pull/495) [`9d5050a`](https://github.com/nmi-agro/fdm/commit/9d5050aef5f70636be638d2f1a4027ccd22f4189) Thanks [@SvenVw](https://github.com/SvenVw)! - Do not show the NavigationProgress for pages with their own loaders, like uploading files + +- [#495](https://github.com/nmi-agro/fdm/pull/495) [`9d5050a`](https://github.com/nmi-agro/fdm/commit/9d5050aef5f70636be638d2f1a4027ccd22f4189) Thanks [@SvenVw](https://github.com/SvenVw)! - Increase navigation progress time from 300 to 500ms + +- [#495](https://github.com/nmi-agro/fdm/pull/495) [`9d5050a`](https://github.com/nmi-agro/fdm/commit/9d5050aef5f70636be638d2f1a4027ccd22f4189) Thanks [@SvenVw](https://github.com/SvenVw)! - Improve DatePickers and Forms to use contextual default dates based on the selected calendar year. Forms now default to domain-specific dates (e.g., March 1st for fertilizer and cultivation-specific harvest defaults in non-current years), and DatePickers now resolve partial text entries (like "15 april") to the active calendar year instead of the current real-world year. + +- [#495](https://github.com/nmi-agro/fdm/pull/495) [`9d5050a`](https://github.com/nmi-agro/fdm/commit/9d5050aef5f70636be638d2f1a4027ccd22f4189) Thanks [@SvenVw](https://github.com/SvenVw)! - Fix that link for going back for fertilizer application modification goes back to rotation + +- [#495](https://github.com/nmi-agro/fdm/pull/495) [`9d5050a`](https://github.com/nmi-agro/fdm/commit/9d5050aef5f70636be638d2f1a4027ccd22f4189) Thanks [@SvenVw](https://github.com/SvenVw)! - Fix that going to Fertilizers page does not reset the selected calendar year to current year + +- [#495](https://github.com/nmi-agro/fdm/pull/495) [`9d5050a`](https://github.com/nmi-agro/fdm/commit/9d5050aef5f70636be638d2f1a4027ccd22f4189) Thanks [@SvenVw](https://github.com/SvenVw)! - At the Sentry metric for NavigationProgress include a tag for the page + +- Updated dependencies [[`9d5050a`](https://github.com/nmi-agro/fdm/commit/9d5050aef5f70636be638d2f1a4027ccd22f4189)]: + - @nmi-agro/fdm-calculator@0.12.1 + ## 0.28.0 ### Minor Changes diff --git a/fdm-app/app/components/blocks/fertilizer-applications/card.tsx b/fdm-app/app/components/blocks/fertilizer-applications/card.tsx index f504b6e3b..624113312 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/card.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/card.tsx @@ -5,6 +5,7 @@ import { Plus } from "lucide-react" import { useEffect, useRef, useState } from "react" import { useFetcher, useLocation, useNavigation, useParams } from "react-router" import { useFieldFertilizerFormStore } from "@/app/store/field-fertilizer-form" +import { useCalendarStore } from "~/store/calendar" import { Button } from "~/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card" import { @@ -75,11 +76,13 @@ export function FertilizerApplicationCard({ }, [navigation.state]) const fieldFertilizerFormStore = useFieldFertilizerFormStore() + const { calendar } = useCalendarStore() const savedFormValues = params.b_id_farm && b_id_or_b_lu_catalogue ? fieldFertilizerFormStore.load( params.b_id_farm, b_id_or_b_lu_catalogue, + calendar, ) : null @@ -98,6 +101,7 @@ export function FertilizerApplicationCard({ fieldFertilizerFormStore.delete( params.b_id_farm || "", b_id_or_b_lu_catalogue || "", + calendar, ) } }, [ @@ -139,6 +143,7 @@ export function FertilizerApplicationCard({ fieldFertilizerFormStore.delete( params.b_id_farm, b_id_or_b_lu_catalogue, + calendar, ) } diff --git a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx index ef2a4b300..061ac8ed8 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx @@ -9,6 +9,8 @@ import type { Navigation } from "react-router" import { Form, useNavigate, useSearchParams } from "react-router" import { RemixFormProvider, useRemixForm } from "remix-hook-form" import { useFieldFertilizerFormStore } from "@/app/store/field-fertilizer-form" +import { useCalendarStore } from "~/store/calendar" +import { getContextualDate } from "~/lib/calendar" import { Combobox } from "~/components/custom/combobox" import { DatePicker } from "~/components/custom/date-picker-v2" import { Button } from "~/components/ui/button" @@ -78,6 +80,7 @@ export function FertilizerApplicationForm({ const navigate = useNavigate() const [searchParams] = useSearchParams() const formId = useId() + const { calendar } = useCalendarStore() const form = useRemixForm({ mode: "onTouched", resolver: zodResolver( @@ -94,7 +97,7 @@ export function FertilizerApplicationForm({ ? fertilizerApplication.p_app_date : exampleFertilizerApplication ? undefined - : new Date(), + : getContextualDate(calendar, 3, 1), }, submitConfig: { method: fertilizerApplication ? "PUT" : "POST", @@ -113,6 +116,24 @@ export function FertilizerApplicationForm({ } }, [p_id, fertilizerApplication, form.setValue]) + useEffect(() => { + const currentValue = form.getValues("p_app_date") + const { isDirty } = form.getFieldState("p_app_date") + if ( + !fertilizerApplication?.p_app_date && + !exampleFertilizerApplication && + !currentValue && + !isDirty + ) { + form.setValue("p_app_date", getContextualDate(calendar, 3, 1)) + } + }, [ + calendar, + exampleFertilizerApplication, + fertilizerApplication?.p_app_date, + form.setValue, + ]) + const fieldFertilizerFormStore = useFieldFertilizerFormStore() useEffect(() => { @@ -120,6 +141,7 @@ export function FertilizerApplicationForm({ const savedFormValues = fieldFertilizerFormStore.load( b_id_farm, b_id_or_b_lu_catalogue, + calendar, ) if (savedFormValues) { for (const [k, v] of Object.entries(savedFormValues)) { @@ -137,6 +159,7 @@ export function FertilizerApplicationForm({ b_id_or_b_lu_catalogue, form.setValue, fieldFertilizerFormStore.load, + calendar, ]) useEffect(() => { @@ -156,13 +179,18 @@ export function FertilizerApplicationForm({ useEffect(() => { if (form.formState.isSubmitSuccessful) { - fieldFertilizerFormStore.delete(b_id_farm, b_id_or_b_lu_catalogue) + fieldFertilizerFormStore.delete( + b_id_farm, + b_id_or_b_lu_catalogue, + calendar, + ) } }, [ form.formState.isSubmitSuccessful, b_id_farm, b_id_or_b_lu_catalogue, fieldFertilizerFormStore.delete, + calendar, ]) function handleManageFertilizers(_e: MouseEvent) { @@ -171,6 +199,7 @@ export function FertilizerApplicationForm({ b_id_farm, b_id_or_b_lu_catalogue, form.getValues(), + calendar, ) } navigate( diff --git a/fdm-app/app/components/blocks/harvest/form.tsx b/fdm-app/app/components/blocks/harvest/form.tsx index 46cc181e5..bceb83841 100644 --- a/fdm-app/app/components/blocks/harvest/form.tsx +++ b/fdm-app/app/components/blocks/harvest/form.tsx @@ -7,6 +7,7 @@ import { useEffect, useState } from "react" import { Controller } from "react-hook-form" import { Form, useFetcher, useNavigate } from "react-router" import { RemixFormProvider, useRemixForm } from "remix-hook-form" +import type { UseRemixFormReturn } from "remix-hook-form" import type { z } from "zod" import { cn } from "@/app/lib/utils" import { DatePicker } from "~/components/custom/date-picker-v2" @@ -34,6 +35,7 @@ import { } from "~/components/ui/field" import { Input } from "~/components/ui/input" import { Spinner } from "~/components/ui/spinner" +import { useCalendarStore } from "~/store/calendar" import { getHarvestParameterLabel } from "./parameters" import { FormSchema } from "./schema" @@ -42,6 +44,7 @@ type HarvestFormDialogProps = { exampleHarvestableAnalysis?: Partial example_b_lu_harvest_date?: Date | null b_lu_harvest_date: Date | string | null | undefined // Changed to allow Date or string + b_date_harvest_default?: string | null // MM-dd format from cultivation catalogue b_lu_yield: number | undefined b_lu_yield_fresh: number | undefined b_lu_yield_bruto: number | undefined @@ -62,6 +65,7 @@ type HarvestFormDialogProps = { function useHarvestRemixForm({ harvestParameters, b_lu_harvest_date, + b_date_harvest_default, b_lu_yield, b_lu_yield_fresh, b_lu_yield_bruto, @@ -77,6 +81,37 @@ function useHarvestRemixForm({ example_b_lu_harvest_date, handleConfirmation, }: HarvestFormDialogProps) { + const { calendar } = useCalendarStore() + const currentYear = new Date().getFullYear() + const parsedCalendar = calendar ? Number(calendar) : Number.NaN + const calendarYear = Number.isNaN(parsedCalendar) + ? currentYear + : parsedCalendar + + // Compute default harvest date from catalogue's MM-dd + calendar year + // Only apply when creating a new harvest (no existing date) and not in current year + function getDefaultHarvestDate(): Date | undefined { + if (b_lu_harvest_date) { + return new Date(b_lu_harvest_date) + } + if (example_b_lu_harvest_date) { + // Bulk edit: leave empty + return undefined + } + if (calendarYear === currentYear) { + // Current year: leave empty (calendar opens at today) + return undefined + } + if (b_date_harvest_default) { + // Parse MM-dd and combine with calendar year + const [month, day] = b_date_harvest_default.split("-").map(Number) + if (month && day) { + return new Date(calendarYear, month - 1, day) + } + } + return undefined + } + const form = useRemixForm>({ mode: "onSubmit", resolver: async (values, bypass, options) => { @@ -115,9 +150,7 @@ function useHarvestRemixForm({ return validation }, defaultValues: { - b_lu_harvest_date: b_lu_harvest_date - ? new Date(b_lu_harvest_date) - : undefined, + b_lu_harvest_date: getDefaultHarvestDate(), b_lu_yield: harvestParameters.includes("b_lu_yield") ? b_lu_yield : undefined, @@ -151,6 +184,22 @@ function useHarvestRemixForm({ }, }) + // When the calendar store is populated after initial render, re-evaluate the + // default harvest date, but only if the user has not already entered a value. + useEffect(() => { + const currentValue = form.getValues("b_lu_harvest_date") + const { isDirty } = form.getFieldState("b_lu_harvest_date") + if ( + !b_lu_harvest_date && + !example_b_lu_harvest_date && + !currentValue && + !isDirty + ) { + form.setValue("b_lu_harvest_date", getDefaultHarvestDate()) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [calendar]) + return form } @@ -162,7 +211,7 @@ function HarvestFields({ example_b_lu_harvest_date, b_lu_harvest_date, }: HarvestFormDialogProps & { - form: ReturnType + form: UseRemixFormReturn> className: React.ComponentProps["className"] }) { const formatted_b_lu_harvest_date = example_b_lu_harvest_date diff --git a/fdm-app/app/components/blocks/soil/form.tsx b/fdm-app/app/components/blocks/soil/form.tsx index c621982a9..af993df18 100644 --- a/fdm-app/app/components/blocks/soil/form.tsx +++ b/fdm-app/app/components/blocks/soil/form.tsx @@ -26,6 +26,8 @@ import { } from "~/components/ui/select" import { Spinner } from "~/components/ui/spinner" import { cn } from "~/lib/utils" +import { getContextualDate } from "~/lib/calendar" +import { useCalendarStore } from "~/store/calendar" export function SoilAnalysisForm(props: { soilAnalysis: SoilAnalysis | undefined @@ -35,6 +37,7 @@ export function SoilAnalysisForm(props: { }) { const { soilAnalysis, soilParameterDescription, editable = true } = props + const { calendar } = useCalendarStore() const defaultValues: { [key: string]: string | number | Date | undefined | null } = {} @@ -50,6 +53,10 @@ export function SoilAnalysisForm(props: { defaultValue = "" } + if (defaultValue === undefined && x.type === "date" && !soilAnalysis) { + defaultValue = getContextualDate(calendar, 2, 1) + } + defaultValues[x.parameter] = defaultValue } defaultValues.a_id = undefined diff --git a/fdm-app/app/components/custom/date-picker-v2.tsx b/fdm-app/app/components/custom/date-picker-v2.tsx index 5ecdfe61c..7ad3a214d 100644 --- a/fdm-app/app/components/custom/date-picker-v2.tsx +++ b/fdm-app/app/components/custom/date-picker-v2.tsx @@ -26,6 +26,7 @@ import { PopoverTrigger, } from "~/components/ui/popover" import { endMonth } from "~/lib/calendar" +import { useCalendarStore } from "~/store/calendar" import { cn } from "~/lib/utils" type DatePickerProps = { @@ -49,30 +50,37 @@ export function DatePicker({ required, className, }: DatePickerProps) { + const { calendar } = useCalendarStore() + const calendarYear = calendar ? Number(calendar) : new Date().getFullYear() + const referenceDate = new Date(calendarYear, 0, 1) + const [open, setOpen] = useState(false) const initialDate = - (field.value && parseDateText(field.value)) || defaultValue + (field.value && parseDateText(field.value, calendarYear)) || + defaultValue const [inputValue, setInputValue] = useState( initialDate ? formatDate(initialDate) : "", ) const [selectedDate, setSelectedDate] = useState( initialDate || undefined, ) - const [month, setMonth] = useState(selectedDate) + const [month, setMonth] = useState( + selectedDate ?? referenceDate, + ) // biome-ignore lint/correctness/useExhaustiveDependencies: onChange is stable across renders for react-hook-form controllers useEffect(() => { if (field.value && field.value instanceof Date) { field.onChange(field.value.toISOString()) } else if (field.value) { - const date = parseDateText(field.value) + const date = parseDateText(field.value, calendarYear) setSelectedDate(date || undefined) setInputValue(date ? formatDate(date) : "") - setMonth(date || undefined) + setMonth(date || referenceDate) } else { setInputValue("") setSelectedDate(undefined) - setMonth(undefined) + setMonth(referenceDate) } }, [field.value]) @@ -87,7 +95,7 @@ export function DatePicker({ } const handleInputBlur = () => { - const date = parseDateText(inputValue) + const date = parseDateText(inputValue, calendarYear) if (date) { setSelectedDate(date) setMonth(date) @@ -177,7 +185,7 @@ function formatDate(date: Date | undefined) { return format(date, "PPP", { locale: nl }) } -function parseDateText(date: string | Date | undefined): Date | undefined { +function parseDateText(date: string | Date | undefined, calendarYear?: number): Date | undefined { if (date instanceof Date) { return date } @@ -185,18 +193,68 @@ function parseDateText(date: string | Date | undefined): Date | undefined { return undefined } - // Attempt to parse as ISO string first - const isoDate = new Date(date) - if (!Number.isNaN(isoDate.getTime())) { - return isoDate + const currentYear = new Date().getFullYear() + const targetYear = calendarYear ?? currentYear + + // Only treat as ISO string when it matches YYYY-MM-DD... to avoid JS's lenient Date parsing + // (e.g. new Date("1-4-2025") returns January 4th — American order — before Dutch pre-processor runs). + if (/^\d{4}-\d{2}-\d{2}/.test(date)) { + const isoDate = new Date(date) + if (!Number.isNaN(isoDate.getTime())) { + return isoDate + } } - // Fallback to chrono-node for localized strings - const referenceDate = new Date() - const parsedDate = chrono.nl.parseDate(date, referenceDate) - if (!parsedDate) { + // Dutch numeric format: DD-MM, DD-MM-YY, DD-MM-YYYY (e.g. "1-4", "1-4-25", "01-04-2025") + // Processed before chrono-node because chrono-node uses American MM-DD order for numeric dates. + const dutchNumeric = parseDutchNumericDate(date, targetYear) + if (dutchNumeric) { + return dutchNumeric + } + + // Chrono-node always uses actual today as reference so relative terms ("gisteren", "vandaag") + // resolve to the correct real-world date. + const results = chrono.nl.parse(date, new Date()) + if (!results?.length) { return undefined } + const result = results[0] + const parsedDate = result.start.date() + + // When a specific date was mentioned (month or day explicit) but no year was stated, + // override the year with the active calendar year. Skip for pure relative terms like + // "gisteren" where neither month nor day is explicit. + if ( + targetYear !== currentYear && + !result.start.isCertain("year") && + (result.start.isCertain("month") || result.start.isCertain("day")) + ) { + parsedDate.setFullYear(targetYear) + } return parsedDate } + +// Parses Dutch numeric date format DD-MM, DD-MM-YY or DD-MM-YYYY. +// Returns undefined when the input doesn't match or produces an invalid date. +function parseDutchNumericDate(text: string, targetYear: number): Date | undefined { + const match = text.trim().match(/^(\d{1,2})[./-](\d{1,2})(?:[./-](\d{2,4}))?$/) + if (!match) { + return undefined + } + const day = Number(match[1]) + const month = Number(match[2]) + if (day < 1 || day > 31 || month < 1 || month > 12) { + return undefined + } + let year = targetYear + if (match[3]) { + const y = Number(match[3]) + year = match[3].length <= 2 ? 2000 + y : y + } + const result = new Date(year, month - 1, day) + if (Number.isNaN(result.getTime()) || result.getMonth() !== month - 1) { + return undefined + } + return result +} diff --git a/fdm-app/app/components/custom/date-picker.tsx b/fdm-app/app/components/custom/date-picker.tsx index 68a807da6..d7d292126 100644 --- a/fdm-app/app/components/custom/date-picker.tsx +++ b/fdm-app/app/components/custom/date-picker.tsx @@ -22,22 +22,64 @@ import { } from "~/components/ui/popover" import { endMonth } from "~/lib/calendar" +import { useCalendarStore } from "~/store/calendar" -function parseDateString(dateString: string): Date | undefined { +function parseDateString(dateString: string, calendarYear: number): Date | undefined { if (!dateString) { return undefined } - // Attempt to parse using chrono-node (Dutch) - const referenceDate = new Date() - const parsedDate = chrono.nl.parseDate(dateString, referenceDate) - if (parsedDate) { - return parsedDate + const currentYear = new Date().getFullYear() + + // Dutch numeric format: DD-MM, DD-MM-YY, DD-MM-YYYY + const dutchNumeric = parseDutchNumericDate(dateString, calendarYear) + if (dutchNumeric) { + return dutchNumeric + } + + // Chrono-node always uses today as reference so relative terms ("gisteren") work correctly. + const results = chrono.nl.parse(dateString, new Date()) + if (!results?.length) { + // Fallback to default Date parsing for ISO-like strings + const defaultDate = new Date(dateString) + return isValidDate(defaultDate) ? defaultDate : undefined + } + const result = results[0] + const parsedDate = result.start.date() + + // Override with calendar year only for explicit partial dates (month/day stated, year not). + if ( + calendarYear !== currentYear && + !result.start.isCertain("year") && + (result.start.isCertain("month") || result.start.isCertain("day")) + ) { + parsedDate.setFullYear(calendarYear) } - // Fallback to default Date parsing for other formats - const defaultDate = new Date(dateString) - return isValidDate(defaultDate) ? defaultDate : undefined + return isValidDate(parsedDate) ? parsedDate : undefined +} + +// Parses Dutch numeric date format DD-MM, DD-MM-YY or DD-MM-YYYY. +function parseDutchNumericDate(text: string, targetYear: number): Date | undefined { + const match = text.trim().match(/^(\d{1,2})[./-](\d{1,2})(?:[./-](\d{2,4}))?$/) + if (!match) { + return undefined + } + const day = Number(match[1]) + const month = Number(match[2]) + if (day < 1 || day > 31 || month < 1 || month > 12) { + return undefined + } + let year = targetYear + if (match[3]) { + const y = Number(match[3]) + year = match[3].length <= 2 ? 2000 + y : y + } + const result = new Date(year, month - 1, day) + if (Number.isNaN(result.getTime()) || result.getMonth() !== month - 1) { + return undefined + } + return result } function formatDate(date: Date | undefined) { @@ -70,11 +112,15 @@ export function DatePicker({ description, disabled = false, }: DatePickerProps) { + const { calendar } = useCalendarStore() + const calendarYear = calendar ? Number(calendar) : new Date().getFullYear() + const referenceDate = new Date(calendarYear, 0, 1) + const [open, setOpen] = React.useState(false) const [date, setDate] = React.useState( form.getValues(name), ) - const [month, setMonth] = React.useState(date || new Date()) // Initialize month to current date if 'date' is undefined + const [month, setMonth] = React.useState(date || referenceDate) const [value, setValue] = React.useState(formatDate(date)) const [isInputValid, setIsInputValid] = React.useState(true) @@ -91,7 +137,7 @@ export function DatePicker({ } else if (date !== undefined) { // If formDate is undefined or invalid, and date was previously defined setDate(undefined) - setMonth(new Date()) // Reset month to current month + setMonth(referenceDate) // Reset month to calendar context month setValue("") // Clear input value setIsInputValid(true) } @@ -130,7 +176,7 @@ export function DatePicker({ return } - const newDate = parseDateString(text) + const newDate = parseDateString(text, calendarYear) if (newDate && isValidDate(newDate)) { setDate(newDate) setMonth(newDate) diff --git a/fdm-app/app/components/custom/navigation-progress.tsx b/fdm-app/app/components/custom/navigation-progress.tsx index a6a59e73a..fa5848c2e 100644 --- a/fdm-app/app/components/custom/navigation-progress.tsx +++ b/fdm-app/app/components/custom/navigation-progress.tsx @@ -2,31 +2,50 @@ import * as Sentry from "@sentry/react-router" import { AnimatePresence, motion } from "framer-motion" import { Loader2 } from "lucide-react" import { useEffect, useRef, useState } from "react" -import { useNavigation } from "react-router" +import { useLocation, useMatches, useNavigation } from "react-router" import { clientConfig } from "~/lib/config" /** - * Shows a blurred overlay with a loading card when navigation takes longer than 300ms. + * Shows a blurred overlay with a loading card when navigation takes longer than 500ms. * Fast navigations never trigger the indicator. - * Tracks show frequency and duration as Sentry metrics. + * Tracks show frequency and duration as Sentry metrics, tagged with the source page pathname. + * + * Routes can opt out by exporting `export const handle = { hideNavigationProgress: true }`. */ export function NavigationProgress() { const { state } = useNavigation() + const { pathname } = useLocation() + const matches = useMatches() + const hideProgress = matches.some( + (m) => + m.handle !== null && + typeof m.handle === "object" && + (m.handle as Record).hideNavigationProgress === + true, + ) const [show, setShow] = useState(false) const startTimeRef = useRef(null) + const startPathnameRef = useRef(null) - // Show after 300ms — emit a count metric when it appears + // Show after 500ms — emit a count metric when it appears useEffect(() => { - if (state !== "idle") { + if (state !== "idle" && !hideProgress) { if (startTimeRef.current === null) { startTimeRef.current = Date.now() + startPathnameRef.current = pathname } const timer = setTimeout(() => { setShow(true) if (clientConfig.analytics.sentry) { - Sentry.metrics.count("navigation_progress.shown", 1) + Sentry.withScope((scope) => { + scope.setTag( + "page", + startPathnameRef.current ?? pathname, + ) + Sentry.metrics.count("navigation_progress.shown", 1) + }) } - }, 300) + }, 500) return () => clearTimeout(timer) } @@ -34,15 +53,22 @@ export function NavigationProgress() { if (show && startTimeRef.current !== null) { const duration = Date.now() - startTimeRef.current if (clientConfig.analytics.sentry) { - Sentry.metrics.distribution( - "navigation_progress.duration_ms", - duration, - ) - } + Sentry.withScope((scope) => { + scope.setTag( + "page", + startPathnameRef.current ?? pathname, + ) + Sentry.metrics.distribution( + "navigation_progress.duration_ms", + duration, + ) + }) + } } setShow(false) startTimeRef.current = null - }, [state, show]) + startPathnameRef.current = null + }, [state, show, hideProgress, pathname]) return ( diff --git a/fdm-app/app/lib/calendar.ts b/fdm-app/app/lib/calendar.ts index bd5c2b4b1..907e5ab41 100644 --- a/fdm-app/app/lib/calendar.ts +++ b/fdm-app/app/lib/calendar.ts @@ -39,6 +39,34 @@ export function getTimeframe(params: Params): Timeframe { return timeframe } +/** + * Returns a context-aware default date based on the active cultivation calendar year. + * + * - If the calendar year matches the current real-world year, returns today's date. + * - Otherwise, returns a fixed date (defaultMonth/defaultDay) within the calendar year. + * + * @param calendar - Active calendar year as a string (e.g. "2023"), or undefined. + * @param defaultMonth - 1-indexed month for the fallback date (e.g. 3 for March). + * @param defaultDay - Day of the month for the fallback date (e.g. 1 for the 1st). + */ +export function getContextualDate( + calendar: string | undefined, + defaultMonth: number, + defaultDay: number, +): Date { + const currentYear = new Date().getFullYear() + const parsedYear = calendar ? Number(calendar) : currentYear + const calendarYear = Number.isInteger(parsedYear) + ? parsedYear + : currentYear + + if (calendarYear === currentYear) { + return new Date() + } + + return new Date(calendarYear, defaultMonth - 1, defaultDay) +} + export function getCalendarSelection(): string[] { // Create array of years from 2020 to next year const years = [] diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.new.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.new.tsx index 516819101..83f52ed1f 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.new.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.new.tsx @@ -69,6 +69,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) { cultivation.b_lu_catalogue, cultivationsCatalogue, ) + const b_date_harvest_default = + cultivationsCatalogue.find( + (item) => item.b_lu_catalogue === cultivation.b_lu_catalogue, + )?.b_date_harvest_default ?? null return { b_id_farm, @@ -76,6 +80,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { cultivation, harvestParameters, defaultHarvestParameters, + b_date_harvest_default, } } catch (error) { throw handleLoaderError(error) @@ -89,6 +94,7 @@ export default function HarvestNewBlock() {
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation_.harvest._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation_.harvest._index.tsx index bf637a397..83a547319 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation_.harvest._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation_.harvest._index.tsx @@ -326,6 +326,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const harvestParameters = getParametersForHarvestCat( targetCultivation.b_lu_harvestcat, ) + const b_date_harvest_default = + cultivationCatalogueData.find( + (item) => + item.b_lu_catalogue === targetCultivation.b_lu_catalogue, + )?.b_date_harvest_default ?? null // Return user information from loader return { @@ -352,6 +357,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { harvestParameters: harvestParameters, b_lu_start: b_lu_start, b_lu_end: b_lu_end, + b_date_harvest_default: b_date_harvest_default, create: url.searchParams.has("create"), } } catch (error) { @@ -720,6 +726,9 @@ export default function FarmRotationHarvestAddIndex() { loaderData.harvestApplication .b_lu_harvest_date } + b_date_harvest_default={ + loaderData.b_date_harvest_default + } b_lu_yield={ loaderData.harvestableAnalysis .b_lu_yield diff --git a/fdm-app/app/routes/farm.$b_id_farm.soil-analysis.bulk.tsx b/fdm-app/app/routes/farm.$b_id_farm.soil-analysis.bulk.tsx index f642e26a4..45652609d 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.soil-analysis.bulk.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.soil-analysis.bulk.tsx @@ -30,6 +30,8 @@ import { getSession } from "~/lib/auth.server" import { handleActionError, handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" +export const handle = { hideNavigationProgress: true } + export async function loader({ request, params }: LoaderFunctionArgs) { try { const b_id_farm = params.b_id_farm diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.soil-analysis.bulk.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.soil-analysis.bulk.tsx index 0940c62b4..d15699aa3 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.soil-analysis.bulk.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.soil-analysis.bulk.tsx @@ -29,6 +29,8 @@ import { getCalendar, getTimeframe } from "~/lib/calendar" import { handleActionError, handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" +export const handle = { hideNavigationProgress: true } + export async function loader({ request, params }: LoaderFunctionArgs) { try { const b_id_farm = params.b_id_farm 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 60449e523..83d3abe12 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 @@ -29,6 +29,8 @@ import { clientConfig } from "~/lib/config" import { handleActionError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" +export const handle = { hideNavigationProgress: true } + // Meta export const meta: MetaFunction = () => { return [ diff --git a/fdm-app/app/routes/farm.tsx b/fdm-app/app/routes/farm.tsx index 629e3a448..7a055ce1e 100644 --- a/fdm-app/app/routes/farm.tsx +++ b/fdm-app/app/routes/farm.tsx @@ -106,7 +106,9 @@ export default function App() { const setCalendar = useCalendarStore((state) => state.setCalendar) useEffect(() => { - setCalendar(initialCalendar) + if (initialCalendar !== undefined) { + setCalendar(initialCalendar) + } }, [initialCalendar, setCalendar]) // Identify user if PostHog is configured diff --git a/fdm-app/app/store/field-fertilizer-form.tsx b/fdm-app/app/store/field-fertilizer-form.tsx index 75dedf5f7..383b23d9f 100644 --- a/fdm-app/app/store/field-fertilizer-form.tsx +++ b/fdm-app/app/store/field-fertilizer-form.tsx @@ -8,34 +8,36 @@ interface FieldFertilizerFormStore { b_id_farm: string, b_id_or_b_lu_catalogue: string, formData: Partial, + calendar?: string, ): void load( b_id_farm: string, b_id_or_b_lu_catalogue: string, + calendar?: string, ): Partial | undefined - delete(b_id_farm: string, b_id_or_b_lu_catalogue: string): void + delete(b_id_farm: string, b_id_or_b_lu_catalogue: string, calendar?: string): void } -function makeId(b_id_farm: string, b_id: string) { - return `${b_id_farm}/${b_id}` +function makeId(b_id_farm: string, b_id: string, calendar?: string) { + return calendar ? `${b_id_farm}/${b_id}/${calendar}` : `${b_id_farm}/${b_id}` } export const useFieldFertilizerFormStore = create()( persist( (set, get) => ({ db: {}, - save(b_id_farm, b_id_or_b_lu_catalogue, formData) { + save(b_id_farm, b_id_or_b_lu_catalogue, formData, calendar) { const db = { ...get().db, - [makeId(b_id_farm, b_id_or_b_lu_catalogue)]: formData, + [makeId(b_id_farm, b_id_or_b_lu_catalogue, calendar)]: formData, } set({ db }) }, - load(b_id_farm, b_id_or_b_lu_catalogue) { - return get().db[makeId(b_id_farm, b_id_or_b_lu_catalogue)] + load(b_id_farm, b_id_or_b_lu_catalogue, calendar) { + return get().db[makeId(b_id_farm, b_id_or_b_lu_catalogue, calendar)] }, - delete(b_id_farm, b_id_or_b_lu_catalogue) { + delete(b_id_farm, b_id_or_b_lu_catalogue, calendar) { const db = { ...get().db } - delete db[makeId(b_id_farm, b_id_or_b_lu_catalogue)] + delete db[makeId(b_id_farm, b_id_or_b_lu_catalogue, calendar)] set({ db }) }, }), diff --git a/fdm-app/package.json b/fdm-app/package.json index 20c510389..5b4a2a710 100644 --- a/fdm-app/package.json +++ b/fdm-app/package.json @@ -1,6 +1,6 @@ { "name": "@nmi-agro/fdm-app", - "version": "0.28.0", + "version": "0.28.1", "private": true, "sideEffects": false, "type": "module", diff --git a/fdm-calculator/CHANGELOG.md b/fdm-calculator/CHANGELOG.md index d635aa58b..bdccbf3b2 100644 --- a/fdm-calculator/CHANGELOG.md +++ b/fdm-calculator/CHANGELOG.md @@ -1,5 +1,11 @@ # fdm-calculator +## 0.12.1 + +### Patch Changes + +- [#495](https://github.com/nmi-agro/fdm/pull/495) [`9d5050a`](https://github.com/nmi-agro/fdm/commit/9d5050aef5f70636be638d2f1a4027ccd22f4189) Thanks [@SvenVw](https://github.com/SvenVw)! - Fixes for farm nitrogen balance to exclude nitrate leaching + ## 0.12.0 ### Minor Changes diff --git a/fdm-calculator/package.json b/fdm-calculator/package.json index 4afd02159..f87e80fea 100644 --- a/fdm-calculator/package.json +++ b/fdm-calculator/package.json @@ -1,7 +1,7 @@ { "name": "@nmi-agro/fdm-calculator", "private": false, - "version": "0.12.0", + "version": "0.12.1", "description": "Calculate various insights based on the Farm Data Model", "license": "MIT", "homepage": "https://github.com/nmi-agro/fdm", diff --git a/fdm-calculator/src/balance/nitrogen/index.ts b/fdm-calculator/src/balance/nitrogen/index.ts index ce3b1e6dd..511dfe273 100644 --- a/fdm-calculator/src/balance/nitrogen/index.ts +++ b/fdm-calculator/src/balance/nitrogen/index.ts @@ -488,10 +488,10 @@ export function calculateNitrogenBalancesFieldToFarm( : ammoniaByFertilizerType[fertilizerType].dividedBy(totalFarmArea) } - // Calculate the average balance at farm level (Supply + Removal + Emission) + // Calculate the average balance at farm level (Supply + Removal + Ammonia Emission) const avgFarmBalance = avgFarmSupply .add(avgFarmRemoval) - .add(avgFarmEmission) + .add(avgFarmEmissionAmmonia) // Return the farm with average balances per hectare const farmWithBalance = {