diff --git a/fdm-app/CHANGELOG.md b/fdm-app/CHANGELOG.md
index 96eacc5c4..0ff0b04be 100644
--- a/fdm-app/CHANGELOG.md
+++ b/fdm-app/CHANGELOG.md
@@ -1,5 +1,26 @@
# Changelog fdm-app
+## 0.4.0
+
+### Minor Changes
+
+- bee0e62: Add `FertilizerApplicationsForm` to list, add and delete fertilizer applications
+- 4112897: Remove selection of fertilizers for acquiring but select all fertilizers
+
+### Patch Changes
+
+- Updated dependencies [7af3fda]
+- Updated dependencies [bc4e75f]
+- Updated dependencies [a948c61]
+- Updated dependencies [efa423d]
+- Updated dependencies [b0c001e]
+- Updated dependencies [6ef3d44]
+- Updated dependencies [61da12f]
+- Updated dependencies [5be0abc]
+- Updated dependencies [4189f5d]
+ - @svenvw/fdm-core@0.7.0
+ - @svenvw/fdm-data@1.0.0
+
## 0.3.1
### Patch Changes
diff --git a/fdm-app/app/components/blocks/farm.tsx b/fdm-app/app/components/blocks/farm.tsx
index 2173ba79f..6ecd11dc0 100644
--- a/fdm-app/app/components/blocks/farm.tsx
+++ b/fdm-app/app/components/blocks/farm.tsx
@@ -1,4 +1,3 @@
-import { useState } from "react";
import { Form } from "react-router";
import { zodResolver } from "@hookform/resolvers/zod"
import { useRemixForm, RemixFormProvider } from "remix-hook-form"
@@ -14,7 +13,6 @@ import {
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
import {
FormControl,
FormDescription,
@@ -23,11 +21,8 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form"
-
-import { MultiSelect } from "@/components/custom/multi-select"
import { LoadingSpinner } from "../custom/loadingspinner";
-
export interface fertilizersListType {
value: string
label: string
@@ -35,10 +30,6 @@ export interface fertilizersListType {
export interface farmType {
b_name_farm: string | null
- b_fertilizers_organic: string[]
- b_fertilizers_mineral: string[]
- organicFertilizersList: fertilizersListType[]
- mineralFertilizersList: fertilizersListType[]
action: "/app/addfarm/new"
FormSchema: z.Schema
}
@@ -49,11 +40,7 @@ export interface farmType {
* @returns The JSX element representing the farm form.
*/
export function Farm(props: farmType) {
- const organicFertilizersList = props.organicFertilizersList
- const mineralFertilizersList = props.mineralFertilizersList
const FormSchema = props.FormSchema
- const [selectedOrganicFertilizers, setOrganicFertilizers] = useState(props.b_fertilizers_organic);
- const [selectedMineralFertilizers, setMineralFertilizers] = useState(props.b_fertilizers_mineral);
const form = useRemixForm>({
mode: "onTouched",
@@ -90,36 +77,9 @@ export function Farm(props: farmType) {
-
)}
/>
-
-
-
-
-
-
-
-
diff --git a/fdm-app/app/components/custom/combobox-fertilizers.tsx b/fdm-app/app/components/custom/combobox-fertilizers.tsx
deleted file mode 100644
index 3a2d755bf..000000000
--- a/fdm-app/app/components/custom/combobox-fertilizers.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-import { Button } from "@/components/ui/button"
-import { Separator } from "../ui/separator"
-import { Input } from "../ui/input"
-import { Label } from "../ui/label"
-
-import { Combobox } from "../custom/combobox"
-
-export function ComboboxFertilizers(props: { options: { value: string, label: string }[], defaultValue?: string }) {
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/*
Meststoffen
*/}
-
-
-
-
- Runderdrijfmest
-
- {/*
m@example.com
*/}
-
-
-
-
-
-
- {/*
*/}
-
-
-
-
- Runderdrijfmest
-
- {/*
m@example.com
*/}
-
-
-
-
-
-
-
-
-
-
-
- )
-}
\ No newline at end of file
diff --git a/fdm-app/app/components/custom/combobox.tsx b/fdm-app/app/components/custom/combobox.tsx
index 7c88e51be..2a207d279 100644
--- a/fdm-app/app/components/custom/combobox.tsx
+++ b/fdm-app/app/components/custom/combobox.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react"
+import { ReactNode, useState } from "react"
import { Button } from "@/components/ui/button"
import {
@@ -14,6 +14,14 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
+import {
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
import { ChevronsUpDown, Check } from "lucide-react"
import { cn } from "@/lib/utils"
@@ -24,71 +32,76 @@ type optionType = {
interface ComboboxProps {
options: { value: string, label: string }[]
- value?: string
- defaultValue?: string
- onChange?: (value: string) => void
- onOpenChange?: (open: boolean) => void
+ form: any
+ name: string
+ label: ReactNode
}
export function Combobox({
options,
- value: controlledValue,
- defaultValue,
- onChange,
- onOpenChange
+ form,
+ name,
+ label
}: ComboboxProps) {
const [open, setOpen] = useState(false)
- const [internalValue, setInternalValue] = useState(defaultValue ?? "")
-
- const value = controlledValue ?? internalValue
- const handleValueChange = (newValue: string) => {
- setInternalValue(newValue)
- onChange?.(newValue)
- }
- const name = "combobox"
return (
-
-
-
-
-
-
-
-
- Niks gevonden
-
- {options.map((option: optionType) => (
- {
- setInternalValue(currentValue === value ? "" : currentValue)
- setOpen(false)
- }}
+ (
+
+ {label}
+
+
+
+
- ))}
-
-
-
-
-
+ {options.find(option => option.value === field.value)?.label || "Begin met typen..."}
+
+
+
+
+
+
+
+
+ Niks gevonden
+
+ {options.map((option: optionType) => (
+ {
+ form.setValue(name, option.value)
+ setOpen(false)
+ }}
+ >
+ {option.label}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )}
+ />
)
}
\ No newline at end of file
diff --git a/fdm-app/app/components/custom/fertilizer-applications.tsx b/fdm-app/app/components/custom/fertilizer-applications.tsx
new file mode 100644
index 000000000..c574e30ed
--- /dev/null
+++ b/fdm-app/app/components/custom/fertilizer-applications.tsx
@@ -0,0 +1,233 @@
+import { Form, useFetcher } from "react-router"
+import { format } from "date-fns"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useRemixForm, RemixFormProvider } from "remix-hook-form"
+import { z } from "zod"
+
+// Components
+import { CalendarIcon } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Separator } from "@/components/ui/separator"
+import { Input } from "@/components/ui/input"
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
+import { Calendar } from "@/components/ui/calendar"
+import {
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Combobox } from "@/components/custom/combobox"
+
+import { cn } from "@/lib/utils"
+import { LoadingSpinner } from "./loadingspinner"
+import { useEffect } from "react"
+
+export const FormSchema = z.object({
+ p_app_amount: z.coerce.number({
+ required_error: "Hoeveelheid is verplicht",
+ invalid_type_error: "Hoeveelheid moet een getal zijn",
+ }).positive({
+ message: "Hoeveelheid moet positief zijn",
+ }).finite({
+ message: "Hoeveelheid moet een geheel getal zijn",
+ }).safe({
+ message: "Hoeveelheid moet een safe getal zijn",
+ }),
+ p_app_date: z.coerce.date({
+ required_error: "Datum is verplicht",
+ invalid_type_error: "Datum is ongeldig",
+ }),
+ p_id: z.coerce.string({ // TODO: Validate against the options that are available
+ required_error: "Keuze van meststof is verplicht",
+ invalid_type_error: "Meststof is ongeldig",
+ })
+})
+interface FertilizerApplication {
+ p_app_id: string;
+ p_app_ids: string[];
+ p_name_nl: string;
+ p_app_amount: number;
+ p_app_date: Date;
+ }
+
+ interface FertilizerOption {
+ value: string;
+ label: string;
+ }
+
+ interface FertilizerApplicationsFormProps {
+ fertilizerApplications: FertilizerApplication[];
+ options: FertilizerOption[];
+ defaultValue?: string;
+ action: string;
+ }
+
+export function FertilizerApplicationsForm(props: FertilizerApplicationsFormProps) {
+ const fetcher = useFetcher();
+
+ const form = useRemixForm>({
+ mode: "onTouched",
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ p_app_amount: undefined,
+ p_app_date: new Date(),
+ }
+ })
+
+ useEffect(() => {
+ if (form.formState.isSubmitSuccessful) {
+ form.reset()
+ }
+ }, [form.formState])
+
+ const handleDelete = (p_app_ids: string[]) => {
+ if (fetcher.state === 'submitting') return;
+
+ fetcher.submit(
+ { p_app_ids },
+ { method: "delete", action: props.action }
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+ {/*
Meststoffen
*/}
+
+ {props.fertilizerApplications.map((application) => (
+
+
+
+ {application.p_name_nl}
+
+ {/*
m@example.com
*/}
+
+
+
+ {application.p_app_amount} ton / ha
+
+
+
+
+ {format(application.p_app_date, "yyyy-MM-dd")}
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/fdm-app/app/components/ui/pagination.tsx b/fdm-app/app/components/ui/pagination.tsx
new file mode 100644
index 000000000..e9120dcc8
--- /dev/null
+++ b/fdm-app/app/components/ui/pagination.tsx
@@ -0,0 +1,116 @@
+import * as React from "react"
+import { cn } from "~/lib/utils"
+import { ButtonProps, buttonVariants } from "~/components/ui/button"
+import { ChevronLeftIcon, ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
+
+const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
+
+)
+Pagination.displayName = "Pagination"
+
+const PaginationContent = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+PaginationContent.displayName = "PaginationContent"
+
+const PaginationItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+))
+PaginationItem.displayName = "PaginationItem"
+
+type PaginationLinkProps = {
+ isActive?: boolean
+} & Pick &
+ React.ComponentProps<"a">
+
+const PaginationLink = ({
+ className,
+ isActive,
+ size = "icon",
+ ...props
+}: PaginationLinkProps) => (
+
+)
+PaginationLink.displayName = "PaginationLink"
+
+const PaginationPrevious = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+
+ Previous
+
+)
+PaginationPrevious.displayName = "PaginationPrevious"
+
+const PaginationNext = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+ Next
+
+
+)
+PaginationNext.displayName = "PaginationNext"
+
+const PaginationEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More pages
+
+)
+PaginationEllipsis.displayName = "PaginationEllipsis"
+
+export {
+ Pagination,
+ PaginationContent,
+ PaginationLink,
+ PaginationItem,
+ PaginationPrevious,
+ PaginationNext,
+ PaginationEllipsis,
+}
diff --git a/fdm-app/app/components/ui/sonner.tsx b/fdm-app/app/components/ui/sonner.tsx
new file mode 100644
index 000000000..1128edfce
--- /dev/null
+++ b/fdm-app/app/components/ui/sonner.tsx
@@ -0,0 +1,29 @@
+import { useTheme } from "next-themes"
+import { Toaster as Sonner } from "sonner"
+
+type ToasterProps = React.ComponentProps
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+
+ )
+}
+
+export { Toaster }
diff --git a/fdm-app/app/root.tsx b/fdm-app/app/root.tsx
index 357f7db9b..015693686 100644
--- a/fdm-app/app/root.tsx
+++ b/fdm-app/app/root.tsx
@@ -1,5 +1,9 @@
-import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
-import type { LinksFunction } from "react-router";
+import { useEffect } from "react";
+import { data, Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from "react-router";
+import type { LinksFunction, LoaderFunctionArgs } from "react-router";
+import { getToast } from "remix-toast";
+import { Toaster } from "@/components/ui/sonner"
+import { toast as notify } from "sonner";
import styles from "~/tailwind.css?url";
@@ -17,7 +21,29 @@ export const links: LinksFunction = () => [
},
];
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ try {
+ const { toast, headers } = await getToast(request);
+ return data({ toast }, { headers });
+ } catch (error) {
+ console.error('Failed to get toast:', error);
+ return data({ toast: null }, {});
+ }
+}
+
export function Layout() {
+ const { toast } = useLoaderData();
+
+ // Hook to show the toasts
+ useEffect(() => {
+ if (toast?.type === "error") {
+ notify.error(toast.message);
+ }
+ if (toast?.type === "success") {
+ notify.success(toast.message);
+ }
+ }, [toast]);
+
return (
@@ -26,8 +52,9 @@ export function Layout() {
-
+
+
diff --git a/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.covercrop.tsx b/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.covercrop.tsx
new file mode 100644
index 000000000..6ea513492
--- /dev/null
+++ b/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.covercrop.tsx
@@ -0,0 +1,10 @@
+export default function Index() {
+
+ return (
+
+
+ Vanggewas wordt binnenkort toegevoegd
+
+
+
)
+}
\ No newline at end of file
diff --git a/fdm-app/app/components/blocks/cultivation-plan.tsx b/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.crop.tsx
similarity index 69%
rename from fdm-app/app/components/blocks/cultivation-plan.tsx
rename to fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.crop.tsx
index 8ba341eef..b2daf3bef 100644
--- a/fdm-app/app/components/blocks/cultivation-plan.tsx
+++ b/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.crop.tsx
@@ -1,65 +1,24 @@
import { useState } from "react";
-import { Form, useLocation, useNavigation } from "react-router";
+import { Form, useNavigation } from "react-router";
-import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
-import { buttonVariants } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
-// import { nl } from "react-day-picker/locale" // Could not be found somehow
-interface SidebarNavProps extends React.HTMLAttributes {
- items: {
- href: string
- title: string
- }[]
-}
-
-export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
- const { pathname } = useLocation();
-
- return (
-
- )
-}
-
-export default function Cultivation(props: { action: string | undefined; }) {
+export default function Index(props: { action: string | undefined; }) {
const navigation = useNavigation();
// Get sowing and harvesting dates
const [dateSowing, setDateSowing] = useState(new Date('2024-03-01'))
const [dateHarvesting, setDateHarvesting] = useState(new Date('2024-10-01'))
-
return (
-
+
Werk de opbrengst, stikstofgehalte en zaai- en oogstdatum bij voor dit gewas.
-
+
)
-}
\ No newline at end of file
+}
diff --git a/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.fertilizers.tsx b/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.fertilizers.tsx
new file mode 100644
index 000000000..0fe4564b8
--- /dev/null
+++ b/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.fertilizers.tsx
@@ -0,0 +1,181 @@
+import { useLoaderData, type LoaderFunctionArgs, data, ActionFunctionArgs } from "react-router";
+import { dataWithError, dataWithSuccess } from "remix-toast";
+
+// Components
+import { FertilizerApplicationsForm } from "@/components/custom/fertilizer-applications";
+import { extractFormValuesFromRequest } from "@/lib/form";
+import { FormSchema } from "@/components/custom/fertilizer-applications";
+
+// FDM
+import { fdm } from "../services/fdm.server";
+import { getCultivationPlan, getFertilizers, addFertilizerApplication, getFertilizer, removeFertilizerApplication } from "@svenvw/fdm-core";
+
+// Loader
+export async function loader({
+ request, params
+}: LoaderFunctionArgs) {
+
+ // Extract farm ID from URL parameters
+ 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" });
+ }
+
+ // Extract cultivation catalogue ID from URL parameters
+ const b_lu_catalogue = params.b_lu_catalogue
+ if (!b_lu_catalogue) {
+ throw data("Cultivation catalogue ID is required", { status: 400, statusText: "Cultivation catalogue ID is required" });
+ }
+
+ // Fetch available fertilizers for the farm
+ const fertilizers = await getFertilizers(fdm, b_id_farm)
+ // Map fertilizers to options for the combobox
+ const fertilizerOptions = fertilizers.map(fertilizer => {
+ return {
+ value: fertilizer.p_id,
+ label: fertilizer.p_name_nl
+ }
+ })
+
+ // Fetch the cultivation plan for the farm
+ const cultivationPlan = await getCultivationPlan(fdm, b_id_farm).catch(error => {
+ throw data("Failed to fetch cultivation plan", { status: 500, statusText: error.message });
+ });
+
+ // Find the target cultivation within the cultivation plan
+ const targetCultivation = cultivationPlan.find(c => c.b_lu_catalogue === b_lu_catalogue);
+ if (!targetCultivation) {
+ throw data("Cultivation not found", { status: 404 });
+ }
+
+ // Combine similar fertilizer applications across all fields of the target cultivation.
+ const fertilizerApplications = targetCultivation.fields.reduce((accumulator, field) => {
+ field.fertilizer_applications.forEach(app => {
+ // Create a key based on application properties to identify similar applications.
+ const isSimilarApplication = (app1: any, app2: any) =>
+ app1.p_id_catalogue === app2.p_id_catalogue &&
+ app1.p_app_amount === app2.p_app_amount &&
+ app1.p_app_method === app2.p_app_method &&
+ app1.p_app_date.getTime() === app2.p_app_date.getTime();
+
+ const existingApplication = accumulator.find(existingApp =>
+ isSimilarApplication(existingApp, app)
+ );
+
+ if (existingApplication) {
+ // If similar application exists, add the current p_app_id to its p_app_ids array.
+ existingApplication.p_app_ids.push(app.p_app_id);
+ } else {
+ // If it's a new application, add it to the accumulator with a new p_app_ids array.
+ accumulator.push({ ...app, p_app_ids: [app.p_app_id] });
+ }
+ });
+
+ return accumulator;
+ }, []);
+
+ return {
+ b_lu_catalogue: b_lu_catalogue,
+ b_id_farm: b_id_farm,
+ fertilizerOptions: fertilizerOptions,
+ fertilizerApplications: fertilizerApplications
+ };
+}
+
+export default function Index() {
+ const loaderData = useLoaderData();
+
+ return (
+
+
+ Vul de bemesting op bouwplanniveau in voor dit gewas.
+
+
+
+ )
+}
+
+export async function action({
+ request, params
+}: ActionFunctionArgs) {
+
+ // Get the Id of the farm
+ const b_id_farm = params.b_id_farm
+ if (!b_id_farm) {
+ throw data("Farm ID is required", { status: 400, statusText: "Farm ID is required" });
+ }
+
+ // Get the cultivation
+ const b_lu_catalogue = params.b_lu_catalogue
+ if (!b_lu_catalogue) {
+ throw data("Cultivation catalogue ID is required", { status: 400, statusText: "Cultivation catalogue ID is required" });
+ }
+
+ if (request.method == 'POST') {
+ // Collect form entry
+ const formValues = await extractFormValuesFromRequest(request, FormSchema)
+ const { p_id, p_app_amount, p_app_date } = formValues;
+
+ // Get the cultivation details for this cultivation
+ const cultivationPlan = await getCultivationPlan(fdm, b_id_farm).catch(error => {
+ throw data("Failed to fetch cultivation plan", { status: 500, statusText: error.message });
+ });
+
+ // Get the id of the fields with this cultivation
+ const fields = cultivationPlan.find(cultivation => cultivation.b_lu_catalogue === b_lu_catalogue).fields
+
+ fields.map(async (field) => {
+ const b_id = field.b_id
+ await addFertilizerApplication(
+ fdm,
+ b_id,
+ p_id,
+ p_app_amount,
+ undefined,
+ p_app_date
+ )
+ })
+
+ return dataWithSuccess({ result: "Data saved successfully" }, { message: "Bemesting is toegevoegd! 🎉" })
+
+ } else if (request.method == 'DELETE') {
+
+ const formData = await request.formData();
+ const rawAppIds = formData.get("p_app_ids");
+
+ if (!rawAppIds || typeof rawAppIds !== 'string') {
+ return dataWithError(
+ 'Invalid or missing p_app_ids value',
+ "Oops! Something went wrong. Please try again later."
+ );
+ }
+
+ try {
+
+ const p_app_ids = rawAppIds.split(',');
+ await Promise.all(
+ p_app_ids.map((p_app_id: string) =>
+ removeFertilizerApplication(fdm, p_app_id)
+ )
+ );
+
+ return dataWithSuccess({}, { message: "Bemesting is verwijderd" });
+ } catch (error) {
+ // Handle errors appropriately. Log the error for debugging purposes.
+ console.error("Error deleting fertilizer application:", error);
+ return dataWithError(
+ error instanceof Error ? error.message : "Unknown error",
+ "Er is een fout opgetreden bij het verwijderen van de bemesting. Probeer het later opnieuw."
+ );
+ }
+ }
+
+ // Handle other methods. This returns an error response for methods other than POST or DELETE, which may or may not be what's desired.
+ console.error(`${request.method} is not supported`)
+ return dataWithError(null, "Oops! Something went wrong. Please try again later.");
+
+}
\ No newline at end of file
diff --git a/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.tsx b/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.tsx
index 871bb5f06..12e81049e 100644
--- a/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.tsx
+++ b/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.tsx
@@ -1,13 +1,13 @@
-import { type MetaFunction, type LoaderFunctionArgs, data } from "react-router";
+import { type MetaFunction, type LoaderFunctionArgs, data, useLocation, Outlet } from "react-router";
import { useLoaderData } from "react-router";
// Components
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { ComboboxFertilizers } from "@/components/custom/combobox-fertilizers";
-import { ComboboxCultivations } from "@/components/custom/combobox-cultivations";
-
-// Blocks
-import Cultivation from "@/components/blocks/cultivation-plan";
+import {
+ Pagination,
+ PaginationContent,
+ PaginationItem,
+ PaginationLink,
+} from "@/components/ui/pagination"
// FDM
import { fdm } from "../services/fdm.server";
@@ -57,27 +57,17 @@ export async function loader({
}
})
- // Fertilizer options
- const fertilizersCatalogue = await getFertilizersFromCatalogue(fdm)
- const fertilizerOptions = fertilizersCatalogue.map(fertilizer => {
- return {
- value: fertilizer.p_id_catalogue,
- label: fertilizer.p_name_nl
- }
- })
-
return {
b_lu_catalogue: b_lu_catalogue,
b_id_farm: b_id_farm,
- cultivation: cultivation,
- fertilizerOptions: fertilizerOptions,
- cultivationOptions: cultivationOptions
+ cultivation: cultivation
}
}
// Main
export default function Index() {
const loaderData = useLoaderData();
+ const { pathname } = useLocation();
// Get field names
let fieldNames = loaderData.cultivation.fields.map(field => field.b_name)
@@ -86,6 +76,20 @@ export default function Index() {
fieldNames = fieldNames.replace(/,(?=[^,]+$)/, ', en') //Replace last comma with and
}
+ const items = [
+ {
+ title: 'Gewas',
+ href: `/app/addfarm/${loaderData.b_id_farm}/cultivations/${loaderData.b_lu_catalogue}/crop`
+ },
+ {
+ title: 'Bemesting',
+ href: `/app/addfarm/${loaderData.b_id_farm}/cultivations/${loaderData.b_lu_catalogue}/fertilizers`
+ },
+ {
+ title: 'Vanggewas',
+ href: `/app/addfarm/${loaderData.b_id_farm}/cultivations/${loaderData.b_lu_catalogue}/covercrop`
+ }
+ ]
return (
@@ -95,7 +99,28 @@ export default function Index() {
{fieldNames}
-
+
+
+
+
+ {items.map((item) => (
+
+
+ {item.title}
+
+
+ ))}
+
+
+
+
+ {/*
Hoofdgewas
Bemesting
@@ -114,6 +139,7 @@ export default function Index() {
Vul de bemesting op bouwplanniveau in voor dit gewas.
@@ -128,8 +154,7 @@ export default function Index() {
/>
-
+ */}
-
);
-}
\ No newline at end of file
+}
diff --git a/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.tsx b/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.tsx
index 8568b848d..c947cc622 100644
--- a/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.tsx
+++ b/fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.tsx
@@ -1,4 +1,4 @@
-import { type MetaFunction, type LoaderFunctionArgs, data } from "react-router";
+import { type MetaFunction, type LoaderFunctionArgs, data, useLocation } from "react-router";
import { Outlet, useLoaderData } from "react-router";
// Components
@@ -13,8 +13,8 @@ import { Toaster } from "@/components/ui/toaster"
// FDM
import { fdm } from "../services/fdm.server";
import { getCultivationPlan, getFarm } from "@svenvw/fdm-core";
-import { Button } from "@/components/ui/button";
-import { SidebarNav } from "@/components/blocks/cultivation-plan";
+import { Button, buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
// Meta
export const meta: MetaFunction = () => {
@@ -125,4 +125,41 @@ export default function Index() {
);
-}
\ No newline at end of file
+}
+
+interface SidebarNavProps extends React.HTMLAttributes {
+ items: {
+ href: string
+ title: string
+ }[]
+}
+
+export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
+ const { pathname } = useLocation();
+
+ return (
+
+ )
+}
diff --git a/fdm-app/app/routes/app.addfarm.new.tsx b/fdm-app/app/routes/app.addfarm.new.tsx
index 809b595f9..efe929be9 100644
--- a/fdm-app/app/routes/app.addfarm.new.tsx
+++ b/fdm-app/app/routes/app.addfarm.new.tsx
@@ -1,7 +1,7 @@
import type { MetaFunction, ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
-import { useLoaderData, redirect } from "react-router";
+import { useLoaderData, redirect, data } from "react-router";
import { z } from "zod"
-import { addFarm, getFertilizersFromCatalogue } from "@svenvw/fdm-core";
+import { addFarm, addFertilizer, getFertilizersFromCatalogue } from "@svenvw/fdm-core";
// Components
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
@@ -14,6 +14,7 @@ import { Farm } from "@/components/blocks/farm";
// Services
import { fdm } from "../services/fdm.server";
import { extractFormValuesFromRequest } from "@/lib/form";
+import { dataWithError, dataWithSuccess, redirectWithSuccess } from "remix-toast";
// Meta
export const meta: MetaFunction = () => {
@@ -25,9 +26,9 @@ export const meta: MetaFunction = () => {
const FormSchema = z.object({
b_name_farm: z.string({
- required_error: "Naam van bedrijf is verplicht",
+ required_error: "Naam van bedrijf is verplicht",
}).min(3, {
- message: "Naam van bedrijf moet minimaal 3 karakters bevatten",
+ message: "Naam van bedrijf moet minimaal 3 karakters bevatten",
}),
})
@@ -35,35 +36,9 @@ const FormSchema = z.object({
export async function loader({
request,
}: LoaderFunctionArgs) {
- const fertilizers = await getFertilizersFromCatalogue(fdm);
-
- const organicFertilizersList = fertilizers
- .filter(x => { return (x.p_type_manure || x.p_type_compost) })
- .map(x => {
- return {
- value: x.p_id_catalogue,
- label: x.p_name_nl
- }
- })
-
- const mineralFertilizersList = fertilizers
- .filter(x => { return (x.p_type_mineral) })
- .map(x => {
- return {
- value: x.p_id_catalogue,
- label: x.p_name_nl
- }
- })
return {
- values: {
- b_name_farm: null,
- b_fertilizers_organic: null,
- },
- lists: {
- organicFertilizersList: organicFertilizersList,
- mineralFertilizersList: mineralFertilizersList
- }
+ b_name_farm: null,
};
}
@@ -97,11 +72,7 @@ export default function AddFarmPage() {
@@ -123,7 +94,17 @@ export async function action({
const { b_name_farm } = formValues;
// Create a farm
- const b_id_farm = await addFarm(fdm, b_name_farm, null)
-
- return redirect(`../addfarm/${b_id_farm}/map`)
+ try {
+ const b_id_farm = await addFarm(fdm, b_name_farm, null);
+ const fertilizers = await getFertilizersFromCatalogue(fdm);
+ await Promise.all(
+ fertilizers.map(fertilizer =>
+ addFertilizer(fdm, fertilizer.p_id_catalogue, b_id_farm)
+ )
+ );
+ return redirectWithSuccess(`../addfarm/${b_id_farm}/map`, { message: "Bedrijf is toegevoegd! 🎉" });
+ } catch (error) {
+ console.error('Failed to create farm with fertilizers:', error);
+ return dataWithError(null, "Er is iets misgegaan bij het aanmaken van het bedrijf.");
+ }
}
\ No newline at end of file
diff --git a/fdm-app/package.json b/fdm-app/package.json
index 42f9bb807..afd7d6570 100644
--- a/fdm-app/package.json
+++ b/fdm-app/package.json
@@ -1,6 +1,6 @@
{
"name": "@svenvw/fdm-app",
- "version": "0.3.1",
+ "version": "0.4.0",
"private": true,
"sideEffects": false,
"type": "module",
@@ -37,6 +37,7 @@
"isbot": "^5.1.17",
"lucide-react": "^0.468.0",
"mapbox-gl": "^3.8.0",
+ "next-themes": "^0.4.4",
"react": "^18.3.1",
"react-day-picker": "8.10.1",
"react-dom": "^18.3.1",
@@ -45,7 +46,9 @@
"react-router": "^7.0.2",
"react-router-dom": "^7.0.2",
"remix-hook-form": "6.0.0",
+ "remix-toast": "^2.0.0",
"remix-utils": "^7.7.0",
+ "sonner": "^1.7.1",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"wkx": "^0.5.0",
@@ -74,8 +77,8 @@
"vite-tsconfig-paths": "^4.3.2"
},
"peerDependencies": {
- "@svenvw/fdm-core": "workspace:>=0.6.1",
- "@svenvw/fdm-data": "workspace:>=0.4.0"
+ "@svenvw/fdm-core": "workspace:>=0.7.0",
+ "@svenvw/fdm-data": "workspace:>=0.5.0"
},
"engines": {
"node": ">=20.0.0"
diff --git a/fdm-calculator/CHANGELOG.md b/fdm-calculator/CHANGELOG.md
index 7999c41c6..f77b9b7d1 100644
--- a/fdm-calculator/CHANGELOG.md
+++ b/fdm-calculator/CHANGELOG.md
@@ -1,5 +1,12 @@
# fdm-calculator
+## 0.0.2
+
+### Patch Changes
+
+- Upgrade to use ES2022
+
+
## 0.0.1
### Patch Changes
diff --git a/fdm-calculator/package.json b/fdm-calculator/package.json
index 5da7f6149..d8d09afd3 100644
--- a/fdm-calculator/package.json
+++ b/fdm-calculator/package.json
@@ -1,7 +1,7 @@
{
"name": "fdm-calculator",
"private": true,
- "version": "0.0.1",
+ "version": "0.0.2",
"description": "Calculate various insights based on the Farm Data Model",
"license": "MIT",
"homepage": "https://github.com/SvenVw/fdm",
diff --git a/fdm-calculator/tsconfig.json b/fdm-calculator/tsconfig.json
index 0511b9f0e..8f3e48303 100644
--- a/fdm-calculator/tsconfig.json
+++ b/fdm-calculator/tsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
- "target": "ES2020",
+ "target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
diff --git a/fdm-core/CHANGELOG.md b/fdm-core/CHANGELOG.md
index 0e8ff5d5c..f2c9b8796 100644
--- a/fdm-core/CHANGELOG.md
+++ b/fdm-core/CHANGELOG.md
@@ -1,5 +1,23 @@
# Changelog fdm-core
+## 0.7.0
+
+### Minor Changes
+
+- 7af3fda: Rename `p_amount` at `fertilizer_acquiring` to `p_acquiring_amount`
+- bc4e75f: Rename `p_date_acquiring` to `p_acquiring_date` anc convert type from timestamp to date
+- efa423d: Export `getFertilizer` and `getFertilizers`
+- b0c001e: Add functions `addFertilizerApplication`, `updateFertilizerApplication`, `removeFertilizerApplication`, `getFertilizerApplication` and `getFertilizerApplications`
+- 6ef3d44: Alter `p_acquiring_date` and `p_picking_date` from date to timestamptz
+- 61da12f: Add to output of `getCultivationPlan` the `fertilizer_applications`
+- 5be0abc: `getFertilizers` returns the details of the fertilizers similiar as `getFertilizer`
+- 4189f5d: Add `fertilizer_application` table
+
+### Patch Changes
+
+- a948c61: Fix by adding `b_name` to output type of `getCultivationPlan`
+- Upgrade to use ES2022
+
## 0.6.1
### Patch Changes
diff --git a/fdm-core/package.json b/fdm-core/package.json
index 22faf7ba3..7be338694 100644
--- a/fdm-core/package.json
+++ b/fdm-core/package.json
@@ -1,7 +1,7 @@
{
"name": "@svenvw/fdm-core",
"private": false,
- "version": "0.6.1",
+ "version": "0.7.0",
"description": "Interface for the Farm Data Model",
"license": "MIT",
"homepage": "https://svenvw.github.io/fdm/",
diff --git a/fdm-core/src/cultivation.d.ts b/fdm-core/src/cultivation.d.ts
index 6b6c19d91..2c5a20ac4 100644
--- a/fdm-core/src/cultivation.d.ts
+++ b/fdm-core/src/cultivation.d.ts
@@ -11,3 +11,21 @@ export interface getCultivationType {
b_sowing_date: schema.fieldSowingTypeSelect['b_sowing_date'],
b_id: schema.fieldSowingTypeSelect['b_id'],
}
+
+export interface cultivationPlanType {
+ b_lu_catalogue: schema.cultivationsCatalogueTypeSelect['b_lu_catalogue']
+ b_lu_name: schema.cultivationsCatalogueTypeSelect['b_lu_name']
+ fields: Array<{
+ b_lu: schema.cultivationsTypeSelect['b_lu']
+ b_id: schema.fieldsTypeSelect['b_id']
+ b_name: schema.fieldsTypeSelect['b_name']
+ fertilizer_applications: Array<{
+ p_id_catalogue: schema.fertilizersCatalogueTypeSelect['p_id_catalogue']
+ p_name_nl: schema.fertilizersCatalogueTypeSelect['p_name_nl']
+ p_app_amount: schema.fertilizerApplicationTypeSelect['p_app_amount']
+ p_app_method: schema.fertilizerApplicationTypeSelect['p_app_method']
+ p_app_date: schema.fertilizerApplicationTypeSelect['p_app_date']
+ p_app_id: schema.fertilizerApplicationTypeSelect['p_app_id']
+ }>;
+ }>;
+}
\ No newline at end of file
diff --git a/fdm-core/src/cultivation.test.ts b/fdm-core/src/cultivation.test.ts
index 057ca1a31..cfa8d10c4 100644
--- a/fdm-core/src/cultivation.test.ts
+++ b/fdm-core/src/cultivation.test.ts
@@ -1,10 +1,11 @@
-import { describe, expect, it, afterAll, beforeEach } from 'vitest'
+import { describe, expect, it, afterAll, beforeEach, beforeAll } from 'vitest'
import { createFdmServer, migrateFdmServer } from './fdm-server'
import { type FdmServerType } from './fdm-server.d'
import { addCultivationToCatalogue, getCultivationsFromCatalogue, addCultivation, removeCultivation, getCultivation, getCultivations, getCultivationPlan } from './cultivation'
import { addFarm } from './farm'
import { addField } from './field'
import { nanoid } from 'nanoid'
+import { addFertilizer, addFertilizerApplication, addFertilizerToCatalogue } from './fertilizer'
describe('Cultivation Data Model', () => {
let fdm: FdmServerType
@@ -83,7 +84,7 @@ describe('Cultivation Data Model', () => {
it('should throw an error when adding a cultivation with an invalid catalogue ID', async () => {
const invalid_b_lu_catalogue = 'invalid-catalogue-id';
const b_sowing_date = '2024-01-01';
-
+
await expect(
addCultivation(fdm, invalid_b_lu_catalogue, b_id, b_sowing_date)
).rejects.toThrow('Cultivation in catalogue does not exist');
@@ -118,10 +119,10 @@ describe('Cultivation Data Model', () => {
b_lu_hcat3: 'test-hcat3',
b_lu_hcat3_name: 'test-hcat3-name'
});
-
+
const b_sowing_date = '2024-01-01';
await addCultivation(fdm, b_lu_catalogue, b_id, b_sowing_date);
-
+
// Attempt to add the same cultivation again
await expect(
addCultivation(fdm, b_lu_catalogue, b_id, b_sowing_date)
@@ -137,10 +138,10 @@ describe('Cultivation Data Model', () => {
b_lu_hcat3: 'test-hcat3',
b_lu_hcat3_name: 'test-hcat3-name'
});
-
+
const b_sowing_date = '2024-01-01';
const invalid_b_id = 'invalid-field-id';
-
+
await expect(
addCultivation(fdm, b_lu_catalogue, invalid_b_id, b_sowing_date)
).rejects.toThrow('Field does not exist');
@@ -155,9 +156,9 @@ describe('Cultivation Data Model', () => {
b_lu_hcat3: 'test-hcat3',
b_lu_hcat3_name: 'test-hcat3-name'
});
-
+
const invalid_b_sowing_date = 'invalid-date';
-
+
await expect(
addCultivation(fdm, b_lu_catalogue, b_id, invalid_b_sowing_date)
).rejects.toThrow('Invalid sowing date');
@@ -203,64 +204,149 @@ describe('Cultivation Data Model', () => {
})
- describe('getCultivationPlan', () => {
- it('should return an empty array if no cultivations are found', async () => {
- const cultivationPlan = await getCultivationPlan(fdm, b_id_farm);
- expect(cultivationPlan).toEqual([]);
- });
-
- it('should return a cultivation plan with unique cultivations and their fields', async () => {
- const catalogueItem1 = nanoid()
- const catalogueItem2 = nanoid()
+ describe('Cultivation Plan', () => {
+ let b_id_farm: string;
+ let b_id: string;
+ let b_lu_catalogue: string;
+ let b_lu: string;
+ let p_id: string;
+ beforeEach(async () => {
+ b_id_farm = await addFarm(fdm, 'test farm', 'arable');
+ b_id = await addField(
+ fdm,
+ b_id_farm,
+ 'test field',
+ 'test source',
+ 'POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))',
+ '2023-01-01',
+ '2024-01-01',
+ 'owner',
+ );
+ b_lu_catalogue = nanoid()
await addCultivationToCatalogue(fdm, {
- b_lu_catalogue: catalogueItem1,
- b_lu_source: 'test-source-1',
- b_lu_name: 'test-name-1',
- b_lu_name_en: 'test-name-en-1',
- b_lu_hcat3: 'test-hcat3-1',
- b_lu_hcat3_name: 'test-hcat3-name-1'
- })
- await addCultivationToCatalogue(fdm, {
- b_lu_catalogue: catalogueItem2,
- b_lu_source: 'test-source-2',
- b_lu_name: 'test-name-2',
- b_lu_name_en: 'test-name-en-2',
- b_lu_hcat3: 'test-hcat3-2',
- b_lu_hcat3_name: 'test-hcat3-name-2'
+ b_lu_catalogue: b_lu_catalogue,
+ b_lu_source: 'test',
+ b_lu_name: 'Wheat',
+ b_lu_name_en: 'Wheat',
+ b_lu_hcat3: '1',
+ b_lu_hcat3_name: "test"
})
+ b_lu = await addCultivation(fdm, b_lu_catalogue, b_id, '2024-03-01')
+
+ // Add fertilizer to catalogue (needed for fertilizer application)
+ const p_id_catalogue = nanoid();
+ const p_source = 'custom';
+ const p_name_nl = 'Test Fertilizer';
+ const p_name_en = 'Test Fertilizer (EN)';
+ const p_description = 'This is a test fertilizer';
+ const p_acquiring_amount = 1000;
+ const p_acquiring_date = new Date();
+
+ await addFertilizerToCatalogue(
+ fdm, {
+ p_id_catalogue,
+ p_source,
+ p_name_nl,
+ p_name_en,
+ p_description,
+ p_dm: 37,
+ p_density: 20,
+ p_om: 20,
+ p_a: 30,
+ p_hc: 40,
+ p_eom: 50,
+ p_eoc: 60,
+ p_c_rt: 70,
+ p_c_of: 80,
+ p_c_if: 90,
+ p_c_fr: 100,
+ p_cn_of: 110,
+ p_n_rt: 120,
+ p_n_if: 130,
+ p_n_of: 140,
+ p_n_wc: 150,
+ p_p_rt: 160,
+ p_k_rt: 170,
+ p_mg_rt: 180,
+ p_ca_rt: 190,
+ p_ne: 200,
+ p_s_rt: 210,
+ p_s_wc: 220,
+ p_cu_rt: 230,
+ p_zn_rt: 240,
+ p_na_rt: 250,
+ p_si_rt: 260,
+ p_b_rt: 270,
+ p_mn_rt: 280,
+ p_ni_rt: 290,
+ p_fe_rt: 300,
+ p_mo_rt: 310,
+ p_co_rt: 320,
+ p_as_rt: 330,
+ p_cd_rt: 340,
+ pr_cr_rt: 350,
+ p_cr_vi: 360,
+ p_pb_rt: 370,
+ p_hg_rt: 380,
+ p_cl_rt: 390,
+ p_type_manure: true,
+ p_type_mineral: false,
+ p_type_compost: false
+ })
- const b_sowing_date = '2024-01-01'
- await addCultivation(fdm, catalogueItem1, b_id, b_sowing_date)
- const b_id2 = await addField(
+ p_id = await addFertilizer(
fdm,
+ p_id_catalogue,
b_id_farm,
- 'test field 2',
- 'test source 2',
- 'POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))',
- '2023-01-01',
- '2023-12-31',
- 'owner'
- )
- await addCultivation(fdm, catalogueItem1, b_id2, b_sowing_date)
- await addCultivation(fdm, catalogueItem2, b_id, b_sowing_date)
+ p_acquiring_amount,
+ p_acquiring_date
+ );
+ });
+ it('should get cultivation plan for a farm', async () => {
+ const p_app_id1 = await addFertilizerApplication(fdm, b_id, p_id, 100, 'broadcasting', new Date('2024-03-15'));
+ const p_app_id2 = await addFertilizerApplication(fdm, b_id, p_id, 200, 'broadcasting', new Date('2024-04-15'));
+
const cultivationPlan = await getCultivationPlan(fdm, b_id_farm);
- expect(cultivationPlan.length).toBe(2);
+ expect(cultivationPlan).toBeDefined();
+ expect(cultivationPlan.length).toBeGreaterThan(0);
+
+ const wheatCultivation = cultivationPlan.find((c) => c.b_lu_catalogue === b_lu_catalogue);
+ expect(wheatCultivation).toBeDefined();
+
+ expect(wheatCultivation?.fields.length).toBeGreaterThan(0);
+ const fieldInPlan = wheatCultivation?.fields.find(f => f.b_id === b_id);
+ expect(fieldInPlan).toBeDefined();
+
+ expect(fieldInPlan?.fertilizer_applications.length).toEqual(2);
- const cultivation1 = cultivationPlan.find(item => item.b_lu_catalogue === catalogueItem1);
- expect(cultivation1).toBeDefined();
- expect(cultivation1?.fields.length).toBe(2);
+ const fertilizerApp1 = fieldInPlan!.fertilizer_applications.find(fa => fa.p_app_id === p_app_id1)
- const cultivation2 = cultivationPlan.find(item => item.b_lu_catalogue === catalogueItem2);
- expect(cultivation2).toBeDefined();
- expect(cultivation2?.fields.length).toBe(1);
+ //Check for some key fertilizer application details (adapt as needed based on your data)
+ expect(fertilizerApp1!.p_app_amount).toEqual(100)
+ expect(fertilizerApp1!.p_app_method).toEqual('broadcasting')
+
+ const fertilizerApp2 = fieldInPlan!.fertilizer_applications.find(fa => fa.p_app_id === p_app_id2)
+
+ //Check for some key fertilizer application details (adapt as needed based on your data)
+ expect(fertilizerApp2!.p_app_amount).toEqual(200)
+ expect(fertilizerApp2!.p_app_method).toEqual('broadcasting')
+
+ });
+
+
+ it('should return an empty array if no cultivations are found for the farm', async () => {
+ const emptyPlan = await getCultivationPlan(fdm, nanoid()); // Use a non-existent farm ID
+ expect(emptyPlan).toEqual([]);
});
+
+
});
})
\ No newline at end of file
diff --git a/fdm-core/src/cultivation.ts b/fdm-core/src/cultivation.ts
index b4c742d9c..617936066 100644
--- a/fdm-core/src/cultivation.ts
+++ b/fdm-core/src/cultivation.ts
@@ -3,8 +3,7 @@ import { nanoid } from 'nanoid'
import * as schema from './db/schema'
import { type FdmType } from './fdm'
-import { getCultivationType } from './cultivation.d'
-import { b } from 'vitest/dist/chunks/suite.BMWOKiTe.js'
+import { cultivationPlanType, getCultivationType } from './cultivation.d'
/**
* Retrieves cultivations available in the catalogue.
@@ -42,7 +41,7 @@ export async function addCultivationToCatalogue(
b_lu_hcat3_name: schema.cultivationsCatalogueTypeInsert['b_lu_hcat3_name']
}
): Promise {
- await fdm.transaction(async (tx) => {
+ await fdm.transaction(async (tx: FdmType) => {
// Check for existing cultivation
const existing = await tx
.select()
@@ -50,8 +49,8 @@ export async function addCultivationToCatalogue(
.where(eq(schema.cultivationsCatalogue.b_lu_catalogue, properties.b_lu_catalogue))
.limit(1)
- if (existing.length > 0) {
- throw new Error('Cultivation already exists in catalogue')
+ if (existing.length > 0) {
+ throw new Error('Cultivation already exists in catalogue')
}
// Insert the cultivation in the db
@@ -148,7 +147,7 @@ export async function addCultivation(
})
} catch (error) {
- throw new Error(`addCultivation failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error })
+ throw new Error(`addCultivation failed: ${error instanceof Error ? error.message : String(error)}`)
}
})
@@ -198,6 +197,7 @@ export async function getCultivation(fdm: FdmType, b_lu: schema.cultivationsType
* @param fdm The FDM instance.
* @param b_id The ID of the field.
* @returns A Promise that resolves with an array of cultivation details.
+ * @alpha
*/
export async function getCultivations(fdm: FdmType, b_id: schema.fieldSowingTypeSelect['b_id']): Promise {
@@ -226,20 +226,29 @@ export async function getCultivations(fdm: FdmType, b_id: schema.fieldSowingType
*
* The cultivation plan is an array of objects, where each object represents a unique cultivation
* identified by its `b_lu_catalogue`. Each cultivation object also contains a `fields` array,
- * listing the fields associated with that specific cultivation. The `fields` array contains objects,
- * each specifying the `b_lu` (cultivation ID) and `b_id` (field ID) combination.
+ * listing the fields associated with that specific cultivation. Within each field object, there's
+ * a `fertilizer_applications` array detailing the fertilizers applied to that field.
*
* @param fdm The FDM instance.
* @param b_id_farm The ID of the farm for which to retrieve the cultivation plan.
- * @returns A Promise that resolves with an array representing the cultivation plan.
+ * @returns A Promise that resolves with an array representing the cultivation plan.
* Each element in the array is an object with the following structure:
* ```
* {
* b_lu_catalogue: string; // Unique ID of the cultivation catalogue item
* b_lu_name: string; // Name of the cultivation
* fields: { // Array of fields associated with this cultivation
- * b_lu: string; // Unique ID of the cultivation
+ * b_lu: string; // Unique ID of the cultivation
* b_id: string; // Unique ID of the field
+ * b_name: string; // Name of the field
+ * fertilizer_applications: { // Array of fertilizer applications on this field
+ * p_id_catalogue: string; // Fertilizer catalogue ID
+ * p_name_nl: string; // Fertilizer name (Dutch)
+ * p_app_amount: number; // Amount applied
+ * p_app_method: string; // Application method
+ * p_app_date: Date; // Application date
+ * p_app_id: string; // Unique ID of the application
+ * }[]
* }[];
* }
* ```
@@ -248,21 +257,14 @@ export async function getCultivations(fdm: FdmType, b_id: schema.fieldSowingType
* ```typescript
* const cultivationPlan = await getCultivationPlan(fdm, 'farm123');
* if (cultivationPlan.length > 0) {
- * console.log("Cultivation Plan:", cultivationPlan);
+ * console.log("Cultivation Plan:", cultivationPlan);
* } else {
* console.log("No cultivations found for this farm.");
* }
* ```
* @alpha
*/
-export async function getCultivationPlan(fdm: FdmType, b_id_farm: schema.farmsTypeSelect['b_id_farm']): Promise;
-}>> {
+export async function getCultivationPlan(fdm: FdmType, b_id_farm: schema.farmsTypeSelect['b_id_farm']): Promise {
if (!b_id_farm) {
throw new Error('Farm ID is required')
}
@@ -274,7 +276,13 @@ export async function getCultivationPlan(fdm: FdmType, b_id_farm: schema.farmsTy
b_lu_name: schema.cultivationsCatalogue.b_lu_name,
b_lu: schema.cultivations.b_lu,
b_id: schema.fields.b_id,
- b_name: schema.fields.b_name
+ b_name: schema.fields.b_name,
+ p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue,
+ p_name_nl: schema.fertilizersCatalogue.p_name_nl,
+ p_app_amount: schema.fertilizerApplication.p_app_amount,
+ p_app_method: schema.fertilizerApplication.p_app_method,
+ p_app_date: schema.fertilizerApplication.p_app_date,
+ p_app_id: schema.fertilizerApplication.p_app_id
})
.from(schema.farms)
.leftJoin(schema.farmManaging, eq(schema.farms.b_id_farm, schema.farmManaging.b_id_farm))
@@ -282,24 +290,51 @@ export async function getCultivationPlan(fdm: FdmType, b_id_farm: schema.farmsTy
.leftJoin(schema.fieldSowing, eq(schema.fields.b_id, schema.fieldSowing.b_id))
.leftJoin(schema.cultivations, eq(schema.fieldSowing.b_lu, schema.cultivations.b_lu))
.leftJoin(schema.cultivationsCatalogue, eq(schema.cultivations.b_lu_catalogue, schema.cultivationsCatalogue.b_lu_catalogue))
+ .leftJoin(schema.fertilizerApplication, eq(schema.fertilizerApplication.b_id, schema.fields.b_id))
+ .leftJoin(schema.fertilizerPicking, eq(schema.fertilizerPicking.p_id, schema.fertilizerApplication.p_id))
+ .leftJoin(schema.fertilizersCatalogue, eq(schema.fertilizersCatalogue.p_id_catalogue, schema.fertilizerPicking.p_id_catalogue))
.where(and(
eq(schema.farms.b_id_farm, b_id_farm),
isNotNull(schema.cultivationsCatalogue.b_lu_catalogue))
)
- const cultivationPlan = cultivations.reduce((acc, curr) => {
- const existingCultivation = acc.find(item => item.b_lu_catalogue === curr.b_lu_catalogue)
- if (existingCultivation) {
- existingCultivation.fields.push({ b_lu: curr.b_lu, b_id: curr.b_id, b_name: curr.b_name })
- } else {
- acc.push({
- b_lu_catalogue: curr.b_lu_catalogue,
- b_lu_name: curr.b_lu_name,
- fields: [{ b_lu: curr.b_lu, b_id: curr.b_id, b_name: curr.b_name }]
- });
- }
- return acc
- }, []);
+ const cultivationPlan = cultivations.reduce((acc: cultivationPlanType[], curr: any) => {
+ let existingCultivation = acc.find(item => item.b_lu_catalogue === curr.b_lu_catalogue);
+
+ if (!existingCultivation) {
+ existingCultivation = {
+ b_lu_catalogue: curr.b_lu_catalogue,
+ b_lu_name: curr.b_lu_name,
+ fields: []
+ };
+ acc.push(existingCultivation);
+ }
+
+ let existingField = existingCultivation.fields.find(field => field.b_id === curr.b_id);
+
+ if (!existingField) {
+ existingField = {
+ b_lu: curr.b_lu,
+ b_id: curr.b_id,
+ b_name: curr.b_name,
+ fertilizer_applications: []
+ };
+ existingCultivation.fields.push(existingField);
+ }
+
+ if (curr.p_app_id) { // Only add if it's a fertilizer application
+ existingField.fertilizer_applications.push({
+ p_id_catalogue: curr.p_id_catalogue,
+ p_name_nl: curr.p_name_nl,
+ p_app_amount: curr.p_app_amount,
+ p_app_method: curr.p_app_method,
+ p_app_date: curr.p_app_date,
+ p_app_id: curr.p_app_id
+ });
+ }
+
+ return acc;
+ }, []);
return cultivationPlan
} catch (error) {
diff --git a/fdm-core/src/db/migrations/0001_needy_thena.sql b/fdm-core/src/db/migrations/0001_quiet_mysterio.sql
similarity index 87%
rename from fdm-core/src/db/migrations/0001_needy_thena.sql
rename to fdm-core/src/db/migrations/0001_quiet_mysterio.sql
index 926df34b5..925ea3938 100644
--- a/fdm-core/src/db/migrations/0001_needy_thena.sql
+++ b/fdm-core/src/db/migrations/0001_quiet_mysterio.sql
@@ -1,5 +1,6 @@
CREATE SCHEMA "fdm-dev";
--> statement-breakpoint
+CREATE TYPE "fdm-dev"."p_app_method" AS ENUM('slotted coulter', 'incorporation', 'injection', 'spraying', 'broadcasting', 'spoke wheel', 'pocket placement');--> statement-breakpoint
CREATE TYPE "fdm-dev"."b_manage_type" AS ENUM('owner', 'lease');--> statement-breakpoint
CREATE TYPE "fdm-dev"."b_sector" AS ENUM('diary', 'arable', 'tree_nursery', 'bulbs');--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "fdm-dev"."cultivations" (
@@ -40,8 +41,19 @@ CREATE TABLE IF NOT EXISTS "fdm-dev"."farms" (
CREATE TABLE IF NOT EXISTS "fdm-dev"."fertilizer_aquiring" (
"b_id_farm" text NOT NULL,
"p_id" text NOT NULL,
- "p_amount" numeric,
- "p_date_acquiring" timestamp with time zone,
+ "p_acquiring_amount" numeric,
+ "p_acquiring_date" timestamp with time zone,
+ "created" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated" timestamp with time zone
+);
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS "fdm-dev"."fertilizer_applying" (
+ "p_app_id" text PRIMARY KEY NOT NULL,
+ "b_id" text NOT NULL,
+ "p_id" text NOT NULL,
+ "p_app_amount" numeric,
+ "p_app_method" "fdm-dev"."p_app_method",
+ "p_app_date" timestamp with time zone,
"created" timestamp with time zone DEFAULT now() NOT NULL,
"updated" timestamp with time zone
);
@@ -186,6 +198,18 @@ EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "fdm-dev"."fertilizer_applying" ADD CONSTRAINT "fertilizer_applying_b_id_fields_b_id_fk" FOREIGN KEY ("b_id") REFERENCES "fdm-dev"."fields"("b_id") ON DELETE no action ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "fdm-dev"."fertilizer_applying" ADD CONSTRAINT "fertilizer_applying_p_id_fertilizers_p_id_fk" FOREIGN KEY ("p_id") REFERENCES "fdm-dev"."fertilizers"("p_id") ON DELETE no action ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "fdm-dev"."fertilizer_picking" ADD CONSTRAINT "fertilizer_picking_p_id_fertilizers_p_id_fk" FOREIGN KEY ("p_id") REFERENCES "fdm-dev"."fertilizers"("p_id") ON DELETE no action ON UPDATE no action;
EXCEPTION
@@ -231,6 +255,7 @@ END $$;
CREATE UNIQUE INDEX IF NOT EXISTS "b_lu_idx" ON "fdm-dev"."cultivations" USING btree ("b_lu");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "b_lu_catalogue_idx" ON "fdm-dev"."cultivations_catalogue" USING btree ("b_lu_catalogue");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "b_id_farm_idx" ON "fdm-dev"."farms" USING btree ("b_id_farm");--> statement-breakpoint
+CREATE UNIQUE INDEX IF NOT EXISTS "p_app_idx" ON "fdm-dev"."fertilizer_applying" USING btree ("p_app_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "p_id_idx" ON "fdm-dev"."fertilizers" USING btree ("p_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "p_id_catalogue_idx" ON "fdm-dev"."fertilizers_catalogue" USING btree ("p_id_catalogue");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "b_id_idx" ON "fdm-dev"."fields" USING btree ("b_id");--> statement-breakpoint
diff --git a/fdm-core/src/db/migrations/meta/0001_snapshot.json b/fdm-core/src/db/migrations/meta/0001_snapshot.json
index 2ebb62e65..3b538b3ea 100644
--- a/fdm-core/src/db/migrations/meta/0001_snapshot.json
+++ b/fdm-core/src/db/migrations/meta/0001_snapshot.json
@@ -1,5 +1,5 @@
{
- "id": "3e245459-8868-4bb8-916c-287ad9d0b80a",
+ "id": "7193c998-34fa-4ea4-bdf9-ff503d3d7825",
"prevId": "a835be94-9cd8-4b1f-88c0-749e8bd36b22",
"version": "7",
"dialect": "postgresql",
@@ -301,14 +301,14 @@
"primaryKey": false,
"notNull": true
},
- "p_amount": {
- "name": "p_amount",
+ "p_acquiring_amount": {
+ "name": "p_acquiring_amount",
"type": "numeric",
"primaryKey": false,
"notNull": false
},
- "p_date_acquiring": {
- "name": "p_date_acquiring",
+ "p_acquiring_date": {
+ "name": "p_acquiring_date",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
@@ -362,6 +362,112 @@
"uniqueConstraints": {},
"checkConstraints": {}
},
+ "fdm-dev.fertilizer_applying": {
+ "name": "fertilizer_applying",
+ "schema": "fdm-dev",
+ "columns": {
+ "p_app_id": {
+ "name": "p_app_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "b_id": {
+ "name": "b_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "p_id": {
+ "name": "p_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "p_app_amount": {
+ "name": "p_app_amount",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "p_app_method": {
+ "name": "p_app_method",
+ "type": "p_app_method",
+ "typeSchema": "fdm-dev",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "p_app_date": {
+ "name": "p_app_date",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created": {
+ "name": "created",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated": {
+ "name": "updated",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "p_app_idx": {
+ "name": "p_app_idx",
+ "columns": [
+ {
+ "expression": "p_app_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fertilizer_applying_b_id_fields_b_id_fk": {
+ "name": "fertilizer_applying_b_id_fields_b_id_fk",
+ "tableFrom": "fertilizer_applying",
+ "tableTo": "fields",
+ "schemaTo": "fdm-dev",
+ "columnsFrom": [
+ "b_id"
+ ],
+ "columnsTo": [
+ "b_id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "fertilizer_applying_p_id_fertilizers_p_id_fk": {
+ "name": "fertilizer_applying_p_id_fertilizers_p_id_fk",
+ "tableFrom": "fertilizer_applying",
+ "tableTo": "fertilizers",
+ "schemaTo": "fdm-dev",
+ "columnsFrom": [
+ "p_id"
+ ],
+ "columnsTo": [
+ "p_id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
"fdm-dev.fertilizer_picking": {
"name": "fertilizer_picking",
"schema": "fdm-dev",
@@ -1175,6 +1281,19 @@
}
},
"enums": {
+ "fdm-dev.p_app_method": {
+ "name": "p_app_method",
+ "schema": "fdm-dev",
+ "values": [
+ "slotted coulter",
+ "incorporation",
+ "injection",
+ "spraying",
+ "broadcasting",
+ "spoke wheel",
+ "pocket placement"
+ ]
+ },
"fdm-dev.b_manage_type": {
"name": "b_manage_type",
"schema": "fdm-dev",
diff --git a/fdm-core/src/db/migrations/meta/_journal.json b/fdm-core/src/db/migrations/meta/_journal.json
index f7a5efd12..31bd61caf 100644
--- a/fdm-core/src/db/migrations/meta/_journal.json
+++ b/fdm-core/src/db/migrations/meta/_journal.json
@@ -12,8 +12,8 @@
{
"idx": 1,
"version": "7",
- "when": 1732783245788,
- "tag": "0001_needy_thena",
+ "when": 1734345091202,
+ "tag": "0001_quiet_mysterio",
"breakpoints": true
}
]
diff --git a/fdm-core/src/db/schema.ts b/fdm-core/src/db/schema.ts
index 3166cc4da..eb1665542 100644
--- a/fdm-core/src/db/schema.ts
+++ b/fdm-core/src/db/schema.ts
@@ -79,8 +79,8 @@ export type fertilizersTypeInsert = typeof fertilizers.$inferInsert
export const fertilizerAcquiring = fdmSchema.table('fertilizer_aquiring', {
b_id_farm: text().notNull().references(() => farms.b_id_farm),
p_id: text().notNull().references(() => fertilizers.p_id),
- p_amount: numericCasted(), //kg
- p_date_acquiring: timestamp({ withTimezone: true }),
+ p_acquiring_amount: numericCasted(), //kg
+ p_acquiring_date: timestamp({ withTimezone: true }),
created: timestamp({ withTimezone: true }).notNull().defaultNow(),
updated: timestamp({ withTimezone: true })
})
@@ -88,6 +88,26 @@ export const fertilizerAcquiring = fdmSchema.table('fertilizer_aquiring', {
export type fertilizerAcquiringTypeSelect = typeof fertilizerAcquiring.$inferSelect
export type fertilizerAcquiringTypeInsert = typeof fertilizerAcquiring.$inferInsert
+// Define fertilizers application table
+export const applicationMethodEnum = fdmSchema.enum("p_app_method", ["slotted coulter", "incorporation", "injection", "spraying", "broadcasting","spoke wheel", "pocket placement"])
+export const fertilizerApplication = fdmSchema.table('fertilizer_applying', {
+ p_app_id: text().primaryKey(),
+ b_id: text().notNull().references(() => fields.b_id),
+ p_id: text().notNull().references(() => fertilizers.p_id),
+ p_app_amount: numericCasted(), // kg / ha
+ p_app_method: applicationMethodEnum(),
+ p_app_date: timestamp({ withTimezone: true }),
+ created: timestamp({ withTimezone: true }).notNull().defaultNow(),
+ updated: timestamp({ withTimezone: true })
+}, (table) => {
+ return {
+ p_app_idx: uniqueIndex('p_app_idx').on(table.p_app_id)
+ }
+})
+
+export type fertilizerApplicationTypeSelect = typeof fertilizerApplication.$inferSelect
+export type fertilizerApplicationTypeInsert = typeof fertilizerApplication.$inferInsert
+
// Define fertilizers_catalogue table
export const fertilizersCatalogue = fdmSchema.table('fertilizers_catalogue', {
p_id_catalogue: text().primaryKey(),
diff --git a/fdm-core/src/fertilizer.d.ts b/fdm-core/src/fertilizer.d.ts
index a0611f919..2c8e2fdce 100644
--- a/fdm-core/src/fertilizer.d.ts
+++ b/fdm-core/src/fertilizer.d.ts
@@ -3,7 +3,7 @@ export interface getFertilizerType {
p_name_nl: string | null
p_name_en: string | null
p_description: string | null
- p_amount: number | null
+ p_app_amount: number | null
p_date_acquiring: Date | null
p_picking_date: Date | null
p_n_rt: number | null
@@ -34,8 +34,4 @@ export interface getFertilizerType {
p_pb_rt: number | null
p_hg_rt: number | null
p_cl_cr: number | null
-}
-
-export interface getFertilizersType {
- p_id: string
}
\ No newline at end of file
diff --git a/fdm-core/src/fertilizer.test.ts b/fdm-core/src/fertilizer.test.ts
index 988b5c0f8..36e69d9f6 100644
--- a/fdm-core/src/fertilizer.test.ts
+++ b/fdm-core/src/fertilizer.test.ts
@@ -1,9 +1,10 @@
-import { describe, expect, it, afterAll, beforeEach } from 'vitest'
+import { describe, expect, it, afterAll, beforeEach, beforeAll } from 'vitest'
import { createFdmServer, migrateFdmServer } from './fdm-server'
import { type FdmServerType } from './fdm-server.d'
-import { addFertilizerToCatalogue, getFertilizersFromCatalogue, addFertilizer, removeFertilizer, getFertilizer, getFertilizers } from './fertilizer'
+import { addFertilizerToCatalogue, getFertilizersFromCatalogue, addFertilizer, removeFertilizer, getFertilizer, getFertilizers, removeFertilizerApplication, addFertilizerApplication, getFertilizerApplication, updateFertilizerApplication, getFertilizerApplications } from './fertilizer'
import { addFarm } from './farm'
import { nanoid } from 'nanoid'
+import { addField } from './field'
describe('Fertilizer Data Model', () => {
let fdm: FdmServerType
@@ -33,6 +34,7 @@ describe('Fertilizer Data Model', () => {
afterAll(async () => {
// Clean up the database after each test
// await fdm.execute(sql`TRUNCATE TABLE fertilizers_catalogue CASCADE`)
+ // await fdm.execute(sql`TRUNCATE TABLE fertilizer_picking CASCADE`)
// await fdm.execute(sql`TRUNCATE TABLE fertilizers CASCADE`)
// await fdm.execute(sql`TRUNCATE TABLE farms CASCADE`)
})
@@ -179,14 +181,14 @@ describe('Fertilizer Data Model', () => {
}
)
- const p_amount = 1000
- const p_date_acquiring = new Date()
+ const p_acquiring_amount = 1000
+ const p_acquiring_date = new Date()
const p_id = await addFertilizer(
fdm,
p_id_catalogue,
b_id_farm,
- p_amount,
- p_date_acquiring
+ p_acquiring_amount,
+ p_acquiring_date
)
expect(p_id).toBeDefined()
@@ -258,12 +260,12 @@ describe('Fertilizer Data Model', () => {
}
)
- const p_amount = 1000
- const p_date_acquiring = new Date()
+ const p_acquiring_amount = 1000
+ const p_acquiring_date = new Date()
// Add two fertilizers to the farm
- await addFertilizer(fdm, p_id_catalogue, b_id_farm, p_amount, p_date_acquiring)
- await addFertilizer(fdm, p_id_catalogue, b_id_farm, 1500, p_date_acquiring)
+ await addFertilizer(fdm, p_id_catalogue, b_id_farm, p_acquiring_amount, p_acquiring_date)
+ await addFertilizer(fdm, p_id_catalogue, b_id_farm, 1500, p_acquiring_date)
const fertilizers = await getFertilizers(fdm, b_id_farm)
expect(fertilizers.length).toBe(2)
@@ -333,14 +335,14 @@ describe('Fertilizer Data Model', () => {
}
)
- const p_amount = 1000
- const p_date_acquiring = new Date()
+ const p_acquiring_amount = 1000
+ const p_acquiring_date = new Date()
const p_id = await addFertilizer(
fdm,
p_id_catalogue,
b_id_farm,
- p_amount,
- p_date_acquiring
+ p_acquiring_amount,
+ p_acquiring_date
)
expect(p_id).toBeDefined()
@@ -350,4 +352,173 @@ describe('Fertilizer Data Model', () => {
expect(fertilizer).toBeUndefined()
})
})
+
+ describe('Fertilizer Application', () => {
+ let b_id: string;
+ let p_id: string;
+ let p_id_catalogue: string;
+
+ beforeAll(async () => {
+
+ const b_id_farm = await addFarm(fdm, 'test farm', 'arable');
+
+ b_id = await addField(
+ fdm,
+ b_id_farm,
+ 'test field',
+ 'test source',
+ 'POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))',
+ '2023-01-01',
+ '2024-01-01',
+ 'owner',
+ );
+
+ // Add fertilizer to catalogue
+ p_id_catalogue = nanoid()
+ const p_source = 'custom'
+ const p_name_nl = 'Test Fertilizer'
+ const p_name_en = 'Test Fertilizer (EN)'
+ const p_description = 'This is a test fertilizer'
+ await addFertilizerToCatalogue(
+ fdm,
+ {
+ p_id_catalogue,
+ p_source,
+ p_name_nl,
+ p_name_en,
+ p_description,
+ p_dm: 37,
+ p_density: 20,
+ p_om: 20,
+ p_a: 30,
+ p_hc: 40,
+ p_eom: 50,
+ p_eoc: 60,
+ p_c_rt: 70,
+ p_c_of: 80,
+ p_c_if: 90,
+ p_c_fr: 100,
+ p_cn_of: 110,
+ p_n_rt: 120,
+ p_n_if: 130,
+ p_n_of: 140,
+ p_n_wc: 150,
+ p_p_rt: 160,
+ p_k_rt: 170,
+ p_mg_rt: 180,
+ p_ca_rt: 190,
+ p_ne: 200,
+ p_s_rt: 210,
+ p_s_wc: 220,
+ p_cu_rt: 230,
+ p_zn_rt: 240,
+ p_na_rt: 250,
+ p_si_rt: 260,
+ p_b_rt: 270,
+ p_mn_rt: 280,
+ p_ni_rt: 290,
+ p_fe_rt: 300,
+ p_mo_rt: 310,
+ p_co_rt: 320,
+ p_as_rt: 330,
+ p_cd_rt: 340,
+ pr_cr_rt: 350,
+ p_cr_vi: 360,
+ p_pb_rt: 370,
+ p_hg_rt: 380,
+ p_cl_rt: 390,
+ p_type_manure: true,
+ p_type_mineral: false,
+ p_type_compost: false
+ }
+ )
+
+ const p_acquiring_amount = 1000
+ const p_acquiring_date = new Date()
+ p_id = await addFertilizer(
+ fdm,
+ p_id_catalogue,
+ b_id_farm,
+ p_acquiring_amount,
+ p_acquiring_date
+ )
+
+ });
+
+ afterAll(async () => {
+ // Clean up the database after each test (optional)
+ });
+
+ it('should add a new fertilizer application', async () => {
+ const p_app_date = new Date('2024-03-15');
+
+ const new_p_app_id = await addFertilizerApplication(
+ fdm,
+ b_id,
+ p_id,
+ 100,
+ 'broadcasting',
+ p_app_date
+ );
+ expect(new_p_app_id).toBeDefined();
+
+ const fertilizerApplication = await getFertilizerApplication(fdm, new_p_app_id);
+ expect(fertilizerApplication).toBeDefined();
+ expect(fertilizerApplication?.b_id).toBe(b_id);
+ expect(fertilizerApplication?.p_id).toBe(p_id);
+ expect(fertilizerApplication?.p_app_amount).toBe(100);
+ expect(fertilizerApplication?.p_app_method).toBe('broadcasting');
+ expect(fertilizerApplication?.p_app_date).toEqual(p_app_date);
+ });
+
+
+ it('should update a fertilizer application', async () => {
+ const p_app_date1 = new Date('2024-03-15');
+ const p_app_date2 = new Date('2024-04-20');
+
+ const p_app_id = await addFertilizerApplication(fdm, b_id, p_id, 100, 'broadcasting', p_app_date1);
+
+ await updateFertilizerApplication(fdm, p_app_id, b_id, p_id, 200, 'injection', p_app_date2);
+
+ const updatedApplication = await getFertilizerApplication(fdm, p_app_id);
+ expect(updatedApplication?.p_app_amount).toBe(200);
+ expect(updatedApplication?.p_app_method).toBe('injection');
+ expect(updatedApplication?.p_app_date).toEqual(p_app_date2);
+
+ });
+
+
+ it('should remove a fertilizer application', async () => {
+ const new_p_app_id = await addFertilizerApplication(
+ fdm,
+ b_id,
+ p_id,
+ 100,
+ 'broadcasting',
+ new Date('2024-03-15'),
+ );
+
+ await removeFertilizerApplication(fdm, new_p_app_id);
+
+ const deletedApplication = await getFertilizerApplication(fdm, new_p_app_id);
+ expect(deletedApplication).toBeNull();
+ });
+
+ it('should get a fertilizer application', async () => {
+ const p_app_id = await addFertilizerApplication(fdm, b_id, p_id, 100, 'broadcasting', new Date('2024-03-15'));
+ const fertilizerApplication = await getFertilizerApplication(fdm, p_app_id);
+ expect(fertilizerApplication).toBeDefined();
+ expect(fertilizerApplication?.p_app_id).toBe(p_app_id);
+ });
+
+ it('should get fertilizer applications for a field', async () => {
+ await addFertilizerApplication(fdm, b_id, p_id, 100, 'broadcasting', new Date('2024-03-15'));
+ await addFertilizerApplication(fdm, b_id, p_id, 150, 'injection', new Date('2024-04-18'));
+
+
+ const fertilizerApplications = await getFertilizerApplications(fdm, b_id);
+ expect(fertilizerApplications.length).toBeGreaterThanOrEqual(2);
+ });
+
+ });
})
diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts
index 3830043a9..ccf025adf 100644
--- a/fdm-core/src/fertilizer.ts
+++ b/fdm-core/src/fertilizer.ts
@@ -3,13 +3,13 @@ import { nanoid } from 'nanoid'
import * as schema from './db/schema'
import { type FdmType } from './fdm'
-import { getFertilizersType, getFertilizerType } from './fertilizer.d'
+import { getFertilizerType } from './fertilizer.d'
/**
- * Get fertilizers available in catalogue
+ * Retrieves all fertilizers from the catalogue.
*
- * @param fdm -
- * @returns A Promise that resolves with an array of fertilizers and the details.
+ * @param fdm The FDM instance.
+ * @returns A Promise that resolves with an array of fertilizer catalogue entries.
* @alpha
*/
export async function getFertilizersFromCatalogue(fdm: FdmType): Promise {
@@ -22,10 +22,12 @@ export async function getFertilizersFromCatalogue(fdm: FdmType): Promise {
// Generate an ID for the fertilizer
@@ -114,8 +117,8 @@ export async function addFertilizer(
const fertilizerAcquiringData = {
b_id_farm: b_id_farm,
p_id: p_id,
- p_amount: p_amount,
- p_date_acquiring: p_date_acquiring
+ p_acquiring_amount: p_acquiring_amount,
+ p_acquiring_date: p_acquiring_date
}
const fertilizerPickingData = {
@@ -141,7 +144,7 @@ export async function addFertilizer(
.insert(schema.fertilizerPicking)
.values(fertilizerPickingData)
- } catch (error) {
+ } catch (error) {
throw new Error('Add fertilizer failed with error ' + error)
}
})
@@ -150,11 +153,13 @@ export async function addFertilizer(
}
/**
- * Get the details of a fertilizer
- *
- * @param fdm
- * @param p_id - ID of requested fertilizer
- * @returns A promise that resolves with properties of requested fertilizer
+ * Retrieves the details of a specific fertilizer.
+ *
+ * @param fdm The FDM instance.
+ * @param p_id The ID of the fertilizer.
+ * @returns A Promise that resolves with the fertilizer details.
+ * @throws If retrieving the fertilizer details fails or the fertilizer is not found.
+ * @alpha
*/
export async function getFertilizer(fdm: FdmType, p_id: schema.fertilizersTypeSelect['p_id']): Promise {
@@ -165,8 +170,8 @@ export async function getFertilizer(fdm: FdmType, p_id: schema.fertilizersTypeSe
p_name_nl: schema.fertilizersCatalogue.p_name_nl,
p_name_en: schema.fertilizersCatalogue.p_name_en,
p_description: schema.fertilizersCatalogue.p_description,
- p_amount: schema.fertilizerAcquiring.p_amount,
- p_date_acquiring: schema.fertilizerAcquiring.p_date_acquiring,
+ p_acquiring_amount: schema.fertilizerAcquiring.p_acquiring_amount,
+ p_acquiring_date: schema.fertilizerAcquiring.p_acquiring_date,
p_picking_date: schema.fertilizerPicking.p_picking_date,
p_n_rt: schema.fertilizersCatalogue.p_n_rt,
p_n_if: schema.fertilizersCatalogue.p_n_if,
@@ -207,26 +212,70 @@ export async function getFertilizer(fdm: FdmType, p_id: schema.fertilizersTypeSe
return fertilizer[0]
}
-export async function getFertilizers(fdm: FdmType, b_id_farm: schema.fertilizerAcquiringTypeSelect['b_id_farm']): Promise {
+/**
+ * Retrieves all fertilizer available for a given farm.
+ *
+ * @param fdm The FDM instance.
+ * @param b_id_farm The ID of the farm.
+ * @returns A Promise that resolves with an array of fertilizer IDs.
+ * @alpha
+ */
+export async function getFertilizers(fdm: FdmType, b_id_farm: schema.fertilizerAcquiringTypeSelect['b_id_farm']): Promise {
const fertilizers = await fdm
.select({
- p_id: schema.fertilizers.p_id
+ p_id: schema.fertilizers.p_id,
+ p_name_nl: schema.fertilizersCatalogue.p_name_nl,
+ p_name_en: schema.fertilizersCatalogue.p_name_en,
+ p_description: schema.fertilizersCatalogue.p_description,
+ p_acquiring_amount: schema.fertilizerAcquiring.p_acquiring_amount,
+ p_acquiring_date: schema.fertilizerAcquiring.p_acquiring_date,
+ p_picking_date: schema.fertilizerPicking.p_picking_date,
+ p_n_rt: schema.fertilizersCatalogue.p_n_rt,
+ p_n_if: schema.fertilizersCatalogue.p_n_if,
+ p_n_of: schema.fertilizersCatalogue.p_n_of,
+ p_n_wc: schema.fertilizersCatalogue.p_n_wc,
+ p_p_rt: schema.fertilizersCatalogue.p_p_rt,
+ p_k_rt: schema.fertilizersCatalogue.p_k_rt,
+ p_mg_rt: schema.fertilizersCatalogue.p_mg_rt,
+ p_ca_rt: schema.fertilizersCatalogue.p_ca_rt,
+ p_ne: schema.fertilizersCatalogue.p_ne,
+ p_s_rt: schema.fertilizersCatalogue.p_s_rt,
+ p_s_wc: schema.fertilizersCatalogue.p_s_wc,
+ p_cu_rt: schema.fertilizersCatalogue.p_cu_rt,
+ p_zn_rt: schema.fertilizersCatalogue.p_zn_rt,
+ p_na_rt: schema.fertilizersCatalogue.p_na_rt,
+ p_si_rt: schema.fertilizersCatalogue.p_si_rt,
+ p_b_rt: schema.fertilizersCatalogue.p_b_rt,
+ p_mn_rt: schema.fertilizersCatalogue.p_mn_rt,
+ p_ni_rt: schema.fertilizersCatalogue.p_ni_rt,
+ p_fe_rt: schema.fertilizersCatalogue.p_fe_rt,
+ p_mo_rt: schema.fertilizersCatalogue.p_mo_rt,
+ p_co_rt: schema.fertilizersCatalogue.p_co_rt,
+ p_as_rt: schema.fertilizersCatalogue.p_as_rt,
+ p_cd_rt: schema.fertilizersCatalogue.p_cd_rt,
+ p_cr_rt: schema.fertilizersCatalogue.p_cr_rt,
+ p_cr_vi: schema.fertilizersCatalogue.p_cr_vi,
+ p_pb_rt: schema.fertilizersCatalogue.p_pb_rt,
+ p_hg_rt: schema.fertilizersCatalogue.p_hg_rt,
+ p_cl_cr: schema.fertilizersCatalogue.p_cl_cr
})
.from(schema.fertilizers)
.leftJoin(schema.fertilizerAcquiring, eq(schema.fertilizers.p_id, schema.fertilizerAcquiring.p_id))
+ .leftJoin(schema.fertilizerPicking, eq(schema.fertilizers.p_id, schema.fertilizerPicking.p_id))
+ .leftJoin(schema.fertilizersCatalogue, eq(schema.fertilizerPicking.p_id_catalogue, schema.fertilizersCatalogue.p_id_catalogue))
.where(eq(schema.fertilizerAcquiring.b_id_farm, b_id_farm))
-
return fertilizers
}
/**
- * Remove fertilizer from farm
+ * Removes a fertilizer from a farm.
*
- * @param fdm -
- * @param p_id - ID of the fertilizer to be remove
- * @returns A Promise that resolves when the fertilizer is removed from the farm
+ * @param fdm The FDM instance.
+ * @param p_id The ID of the fertilizer to remove.
+ * @returns A Promise that resolves when the fertilizer has been removed.
+ * @throws If removing the fertilizer fails.
* @alpha
*/
export async function removeFertilizer(
@@ -248,8 +297,167 @@ export async function removeFertilizer(
.delete(schema.fertilizers)
.where(eq(schema.fertilizers.p_id, p_id))
}
- catch (error) {
+ catch (error) {
throw new Error('Remove fertilizer failed with error ' + error)
}
})
+}
+
+export type FertilizerApplicationType = schema.fertilizerApplicationTypeSelect;
+
+/**
+ * Adds a fertilizer application record.
+ *
+ * @param fdm The FDM instance.
+ * @param b_id The ID of the field.
+ * @param p_id The ID of the fertilizer.
+ * @param p_app_amount The amount of fertilizer applied.
+ * @param p_app_method The method of fertilizer application.
+ * @param p_app_date The date of fertilizer application.
+ * @returns A Promise that resolves with the ID of the fertilizer application record.
+ * @throws If adding the fertilizer application record fails.
+ */
+export async function addFertilizerApplication(
+ fdm: FdmType,
+ b_id: schema.fertilizerApplicationTypeInsert['b_id'],
+ p_id: schema.fertilizerApplicationTypeInsert['p_id'],
+ p_app_amount: schema.fertilizerApplicationTypeInsert['p_app_amount'],
+ p_app_method: schema.fertilizerApplicationTypeInsert['p_app_method'],
+ p_app_date: schema.fertilizerApplicationTypeInsert['p_app_date']
+): Promise {
+
+ // Validate that the field exists
+ const fieldExists = await fdm.select().from(schema.fields).where(eq(schema.fields.b_id, b_id)).limit(1);
+ if (fieldExists.length === 0) {
+ throw new Error(`Field with b_id ${b_id} does not exist`);
+ }
+
+ // Validate that the fertilizer exists
+ const fertilizerExists = await fdm.select().from(schema.fertilizers).where(eq(schema.fertilizers.p_id, p_id)).limit(1);
+ if (fertilizerExists.length === 0) {
+ throw new Error(`Fertilizer with p_id ${p_id} does not exist`);
+ }
+
+ const p_app_id = nanoid();
+
+ try {
+ await fdm.insert(schema.fertilizerApplication).values({
+ p_app_id,
+ b_id,
+ p_id,
+ p_app_amount,
+ p_app_method,
+ p_app_date,
+ });
+ } catch (error) {
+ throw new Error(`Failed to add fertilizer application: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
+ }
+
+ return p_app_id;
+}
+
+
+
+/**
+ * Updates a fertilizer application record.
+ *
+ * @param fdm The FDM instance.
+ * @param p_app_id The ID of the fertilizer application record to update.
+ * @param b_id The ID of the field.
+ * @param p_id The ID of the fertilizer.
+ * @param p_app_amount The amount of fertilizer applied.
+ * @param p_app_method The method of fertilizer application.
+ * @param p_app_date The date of fertilizer application.
+ * @returns A Promise that resolves when the record has been updated.
+ * @throws If updating the record fails.
+ */
+export async function updateFertilizerApplication(
+ fdm: FdmType,
+ p_app_id: schema.fertilizerApplicationTypeInsert['p_app_id'],
+ b_id: schema.fertilizerApplicationTypeInsert['b_id'],
+ p_id: schema.fertilizerApplicationTypeInsert['p_id'],
+ p_app_amount: schema.fertilizerApplicationTypeInsert['p_app_amount'],
+ p_app_method: schema.fertilizerApplicationTypeInsert['p_app_method'],
+ p_app_date: schema.fertilizerApplicationTypeInsert['p_app_date']
+): Promise {
+ try {
+ await fdm
+ .update(schema.fertilizerApplication)
+ .set({ b_id, p_id, p_app_amount, p_app_method, p_app_date })
+ .where(eq(schema.fertilizerApplication.p_app_id, p_app_id));
+ } catch (error) {
+ throw new Error(`Failed to update fertilizer application: ${error}`);
+ }
+}
+
+/**
+ * Removes a fertilizer application record.
+ *
+ * @param fdm The FDM instance.
+ * @param p_app_id The ID of the fertilizer application record to remove.
+ * @returns A Promise that resolves when the record has been removed.
+ * @throws If removing the record fails.
+ */
+export async function removeFertilizerApplication(
+ fdm: FdmType,
+ p_app_id: schema.fertilizerApplicationTypeInsert['p_app_id']
+): Promise {
+ try {
+ await fdm
+ .delete(schema.fertilizerApplication)
+ .where(eq(schema.fertilizerApplication.p_app_id, p_app_id));
+ } catch (error) {
+ throw new Error(`Failed to remove fertilizer application: ${error}`);
+ }
+}
+
+
+/**
+ * Retrieves a fertilizer application record.
+ *
+ * @param fdm The FDM instance.
+ * @param p_app_id The ID of the fertilizer application record to retrieve.
+ * @returns A Promise that resolves with the fertilizer application record, or null if not found.
+ * @throws If retrieving the record fails.
+ */
+export async function getFertilizerApplication(
+ fdm: FdmType,
+ p_app_id: schema.fertilizerApplicationTypeSelect['p_app_id']
+): Promise {
+ try {
+ const result = await fdm
+ .select()
+ .from(schema.fertilizerApplication)
+ .where(eq(schema.fertilizerApplication.p_app_id, p_app_id));
+
+ return result[0] || null;
+ } catch (error) {
+ throw new Error(`Failed to get fertilizer application: ${error}`);
+ }
+}
+
+
+
+/**
+ * Retrieves all fertilizer applications for a given field
+ *
+ * @param fdm The FDM instance.
+ * @param b_id The ID of the field.
+ * @returns A Promise that resolves with an array of fertilizer application records.
+ * @throws If retrieving the records fails.
+ */
+export async function getFertilizerApplications(
+ fdm: FdmType,
+ b_id: schema.fertilizerApplicationTypeSelect['b_id'],
+): Promise {
+
+ try {
+ const fertilizerApplications = await fdm
+ .select()
+ .from(schema.fertilizerApplication)
+ .where(eq(schema.fertilizerApplication.b_id, b_id));
+ return fertilizerApplications;
+ } catch (error) {
+ throw new Error(`Failed to get fertilizer applications: ${error}`);
+ }
}
\ No newline at end of file
diff --git a/fdm-core/src/index.ts b/fdm-core/src/index.ts
index 681188cd2..d3aa330e8 100644
--- a/fdm-core/src/index.ts
+++ b/fdm-core/src/index.ts
@@ -21,6 +21,6 @@ export { createFdmServer, migrateFdmServer } from './fdm-server'
// export { createFdmLocal, migrateFdmLocal } from './fdm-local'
export { addFarm, getFarm, updateFarm, } from './farm'
export { addField, getField, getFields, updateField } from './field'
-export { addFertilizerToCatalogue, getFertilizersFromCatalogue, addFertilizer, removeFertilizer } from './fertilizer'
+export { addFertilizerToCatalogue, getFertilizersFromCatalogue, addFertilizer, removeFertilizer, getFertilizer, getFertilizers, addFertilizerApplication, updateFertilizerApplication, removeFertilizerApplication, getFertilizerApplication, getFertilizerApplications} from './fertilizer'
export { addCultivationToCatalogue, getCultivationsFromCatalogue, addCultivation, removeCultivation, getCultivation, getCultivations, getCultivationPlan } from './cultivation'
export { signUpUser, getUserFromSession} from './iam'
\ No newline at end of file
diff --git a/fdm-core/tsconfig.json b/fdm-core/tsconfig.json
index 0511b9f0e..8f3e48303 100644
--- a/fdm-core/tsconfig.json
+++ b/fdm-core/tsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
- "target": "ES2020",
+ "target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
diff --git a/fdm-data/CHANGELOG.md b/fdm-data/CHANGELOG.md
index 34d7a6b0a..22388f46b 100644
--- a/fdm-data/CHANGELOG.md
+++ b/fdm-data/CHANGELOG.md
@@ -1,5 +1,21 @@
# fdm-data
+## 0.5.0
+
+### Patch Changes
+
+- Upgrade to use ES2022
+- Updated dependencies [7af3fda]
+- Updated dependencies [bc4e75f]
+- Updated dependencies [a948c61]
+- Updated dependencies [efa423d]
+- Updated dependencies [b0c001e]
+- Updated dependencies [6ef3d44]
+- Updated dependencies [61da12f]
+- Updated dependencies [5be0abc]
+- Updated dependencies [4189f5d]
+ - @svenvw/fdm-core@0.7.0
+
## 0.4.0
### Minor Changes
diff --git a/fdm-data/package.json b/fdm-data/package.json
index 3e902a03e..d88a95c14 100644
--- a/fdm-data/package.json
+++ b/fdm-data/package.json
@@ -1,7 +1,7 @@
{
"name": "@svenvw/fdm-data",
"private": false,
- "version": "0.4.0",
+ "version": "0.5.0",
"description": "Extend Farm Data Model with catalogue data",
"license": "MIT",
"homepage": "https://github.com/SvenVw/fdm",
@@ -58,7 +58,7 @@
"vitest": "^2.1.3"
},
"peerDependencies": {
- "@svenvw/fdm-core": "workspace:>=0.6.1"
+ "@svenvw/fdm-core": "workspace:>=0.7.0"
},
"packageManager": "pnpm@9.14.2",
"publishConfig": {
diff --git a/fdm-data/tsconfig.json b/fdm-data/tsconfig.json
index 0511b9f0e..8f3e48303 100644
--- a/fdm-data/tsconfig.json
+++ b/fdm-data/tsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
- "target": "ES2020",
+ "target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ef23bcdd8..8eb44bfb3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -97,6 +97,9 @@ importers:
mapbox-gl:
specifier: ^3.8.0
version: 3.8.0
+ next-themes:
+ specifier: ^0.4.4
+ version: 0.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react:
specifier: ^18.3.1
version: 18.3.1
@@ -121,9 +124,15 @@ importers:
remix-hook-form:
specifier: 6.0.0
version: 6.0.0(react-dom@18.3.1(react@18.3.1))(react-hook-form@7.54.0(react@18.3.1))(react-router@7.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
+ remix-toast:
+ specifier: ^2.0.0
+ version: 2.0.0(react-router@7.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
remix-utils:
specifier: ^7.7.0
version: 7.7.0(@remix-run/node@2.15.1(typescript@5.7.2))(@remix-run/react@2.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@remix-run/router@1.21.0)(react@18.3.1)(zod@3.24.1)
+ sonner:
+ specifier: ^1.7.1
+ version: 1.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
tailwind-merge:
specifier: ^2.5.5
version: 2.5.5
@@ -6520,6 +6529,12 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
+ next-themes@0.4.4:
+ resolution: {integrity: sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==}
+ peerDependencies:
+ react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+
no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
@@ -7685,6 +7700,11 @@ packages:
react-hook-form: ^7.51.0
react-router: '>=7.0.0'
+ remix-toast@2.0.0:
+ resolution: {integrity: sha512-kLIiOKjR9syUxkXrqT/OtcYQ9PuuyYIiGhjzCGgplIHIq/JEdq6ff9TcDDlpSD3VvydOsIe4U5ZCGK00FCdXmw==}
+ peerDependencies:
+ react-router: '>=7.0.0'
+
remix-utils@7.7.0:
resolution: {integrity: sha512-J8NhP044nrNIam/xOT1L9a4RQ9FSaA2wyrUwmN8ZT+c/+CdAAf70yfaLnvMyKcV5U+8BcURQ/aVbth77sT6jGA==}
engines: {node: '>=18.0.0'}
@@ -7986,6 +8006,12 @@ packages:
sockjs@0.3.24:
resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==}
+ sonner@1.7.1:
+ resolution: {integrity: sha512-b6LHBfH32SoVasRFECrdY8p8s7hXPDn3OHUFbZZbiB1ctLS9Gdh6rpX2dVrpQA0kiL5jcRzDDldwwLkSKk3+QQ==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
sort-asc@0.2.0:
resolution: {integrity: sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==}
engines: {node: '>=0.10.0'}
@@ -16694,6 +16720,11 @@ snapshots:
neo-async@2.6.2: {}
+ next-themes@0.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
no-case@3.0.4:
dependencies:
lower-case: 2.0.2
@@ -17984,6 +18015,11 @@ snapshots:
react-hook-form: 7.54.0(react@18.3.1)
react-router: 7.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ remix-toast@2.0.0(react-router@7.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
+ dependencies:
+ react-router: 7.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ zod: 3.24.1
+
remix-utils@7.7.0(@remix-run/node@2.15.1(typescript@5.7.2))(@remix-run/react@2.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@remix-run/router@1.21.0)(react@18.3.1)(zod@3.24.1):
dependencies:
type-fest: 4.30.0
@@ -18318,6 +18354,11 @@ snapshots:
uuid: 8.3.2
websocket-driver: 0.7.4
+ sonner@1.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
sort-asc@0.2.0: {}
sort-css-media-queries@2.2.0: {}