From 7827f3b87721f5b94b4e655710e5e260b1db6d68 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 18 Sep 2025 15:09:56 +0200 Subject: [PATCH 01/35] feat: setup a farm dashboard with a list of apps --- fdm-app/app/routes/farm.$b_id_farm._index.tsx | 228 +++++++++++++++++- 1 file changed, 224 insertions(+), 4 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm._index.tsx b/fdm-app/app/routes/farm.$b_id_farm._index.tsx index 02b945df0..8b4037b9f 100644 --- a/fdm-app/app/routes/farm.$b_id_farm._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm._index.tsx @@ -1,6 +1,226 @@ -import { redirect } from "react-router" +import { + data, + NavLink, + Outlet, + useLoaderData, + type LoaderFunctionArgs, + type MetaFunction, +} from "react-router" +import { getSession } from "~/lib/auth.server" +import { clientConfig } from "~/lib/config" +import { handleActionError, handleLoaderError } from "~/lib/error" +import { useCalendarStore } from "../store/calendar" +import { SidebarInset } from "../components/ui/sidebar" +import { Header } from "../components/blocks/header/base" +import { HeaderFarm } from "../components/blocks/header/farm" +import { FarmTitle } from "../components/blocks/farm/farm-title" +import { FarmContent } from "../components/blocks/farm/farm-content" +import { getFarm, getFarms } from "@svenvw/fdm-core" +import { fdm } from "../lib/fdm.server" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "../components/ui/card" +import { Button } from "../components/ui/button" +import { ArrowRightLeft, BookOpenText, Landmark, MapIcon } from "lucide-react" -export async function loader() { - // Redirect to settings page - return redirect("./settings") +// Meta +export const meta: MetaFunction = () => { + return [ + { title: `Bedrijf | ${clientConfig.name}` }, + { + name: "description", + content: "Bekijk en bewerk de gegevens van je bedrijf.", + }, + ] +} + +/** + * Processes a request to retrieve a farm's session details. + * + * This function extracts the farm ID from the route parameters and throws an error with a 400 status + * if the ID is missing. When a valid farm ID is provided, it retrieves the session associated with the + * incoming request and returns an object containing both the farm ID and the session information. + * + * @returns An object with "farmId" and "session" properties. + * + * @throws {Response} If the farm ID is not provided. + */ +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + // Get the farm id + const b_id_farm = params.b_id_farm + if (!b_id_farm) { + throw data("Farm ID is required", { + status: 400, + statusText: "Farm ID is required", + }) + } + + // Get the session + const session = await getSession(request) + + // Get the farm details + const farm = await getFarm(fdm, session.principal_id, b_id_farm) + + // Get a list of possible farms of the user + const farms = await getFarms(fdm, session.principal_id) + const farmOptions = farms.map((farm) => { + return { + b_id_farm: farm.b_id_farm, + b_name_farm: farm.b_name_farm, + } + }) + + // Return the farm ID and session info + return { + b_id_farm: b_id_farm, + b_name_farm: farm.b_name_farm, + farmOptions: farmOptions, + roles: farm.roles, + } + } catch (error) { + throw handleLoaderError(error) + } +} + +export default function FarmDashboardIndex() { + const loaderData = useLoaderData() + + const calendar = useCalendarStore((state) => state.calendar) + + return ( + +
+ +
+
+ + + + + Apps + + Bekijk welke apps er beschikbaar zijn voor dit + bedrijf. + + + + + + +
+
+ +
+
+ + Nutriententenbalans + + + Inzicht in aanvoer, afvoer + en emissie van stikstof + +
+
+
+
+
+ + + +
+
+ +
+
+ + Bemestingsadvies + + + Advies volgens Handboek + Bodem en Bemesting (CBAV) en + Adviesbasis Bemesting + (CBGV). + +
+
+
+
+
+ + + +
+
+ +
+
+ + Gebruiksnormen + + + Gebruiksnormen op bedrijdfs- + en perceelsniveau + +
+
+
+
+
+ + + +
+
+ +
+
+ Atlas + + Bekijk gewaspercelen op de + kaart + +
+
+
+
+
+
+
+ + + + Overzicht + + + + Mijn rol: {loaderData.roles} + Adres: KvK nummer: Derogatie: + + + + + +
+
+
+ ) } From 4c884eb876363bddb5d4f7294217acf34a93c05d Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:18:32 +0200 Subject: [PATCH 02/35] feat: add card to farm dashboard with farm data --- fdm-app/app/routes/farm.$b_id_farm._index.tsx | 337 ++++++++++++------ 1 file changed, 235 insertions(+), 102 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm._index.tsx b/fdm-app/app/routes/farm.$b_id_farm._index.tsx index 8b4037b9f..3f0d7b1b3 100644 --- a/fdm-app/app/routes/farm.$b_id_farm._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm._index.tsx @@ -15,7 +15,7 @@ import { Header } from "../components/blocks/header/base" import { HeaderFarm } from "../components/blocks/header/farm" import { FarmTitle } from "../components/blocks/farm/farm-title" import { FarmContent } from "../components/blocks/farm/farm-content" -import { getFarm, getFarms } from "@svenvw/fdm-core" +import { getFarm, getFarms, getFields } from "@svenvw/fdm-core" import { fdm } from "../lib/fdm.server" import { Card, @@ -26,7 +26,18 @@ import { CardTitle, } from "../components/ui/card" import { Button } from "../components/ui/button" -import { ArrowRightLeft, BookOpenText, Landmark, MapIcon } from "lucide-react" +import { + ArrowRightLeft, + BookOpenText, + ChevronsUp, + ChevronUp, + Landmark, + MapIcon, + Settings, + Shapes, + Square, + UserRoundCheck, +} from "lucide-react" // Meta export const meta: MetaFunction = () => { @@ -67,6 +78,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Get the farm details const farm = await getFarm(fdm, session.principal_id, b_id_farm) + // Get the list of fields + const fields = await getFields(fdm, session.principal_id, b_id_farm) + + // Calculate total area for this farm + const farmArea = fields.reduce((acc, field) => acc + field.b_area, 0) + // Get a list of possible farms of the user const farms = await getFarms(fdm, session.principal_id) const farmOptions = farms.map((farm) => { @@ -80,6 +97,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return { b_id_farm: b_id_farm, b_name_farm: farm.b_name_farm, + fieldsNumber: fields.length, + farmArea: Math.round(farmArea), farmOptions: farmOptions, roles: farm.roles, } @@ -110,115 +129,229 @@ export default function FarmDashboardIndex() {
- - - Apps - - Bekijk welke apps er beschikbaar zijn voor dit - bedrijf. - - - - - - -
-
- +
+ + + Apps + + Bekijk welke apps er beschikbaar zijn voor + dit bedrijf. + + + + + + +
+
+ +
+
+ + Nutriententenbalans + + + Inzicht in aanvoer, + afvoer en emissie van + stikstof + +
-
- - Nutriententenbalans - - - Inzicht in aanvoer, afvoer - en emissie van stikstof - + + + + + + +
+
+ +
+
+ + Bemestingsadvies + + + Advies volgens Handboek + Bodem en Bemesting + (CBAV) en Adviesbasis + Bemesting (CBGV). + +
-
-
-
-
- - - -
-
- + + + + + + +
+
+ +
+
+ + Gebruiksnormen + + + Gebruiksnormen op + bedrijdfs- en + perceelsniveau + +
+
+
+
+
+ + + +
+
+ +
+
+ Atlas + + Bekijk gewaspercelen op + de kaart + +
-
- - Bemestingsadvies - - - Advies volgens Handboek - Bodem en Bemesting (CBAV) en - Adviesbasis Bemesting - (CBGV). - + + + + + + + + + Gegevens + + Bekijk welke gegevens er zijn over dit + bedrijf. + + + + + + +
+
+ +
+
+ + Percelen + + + {loaderData.fieldsNumber === + 0 + ? `Dit bedrijf heeft geen percelen ${calendar}` + : loaderData.fieldsNumber === + 1 + ? `Dit bedrijf heeft 1 perceel in ${calendar}` + : `Dit bedrijf heeft ${loaderData.fieldsNumber} percelen en ${loaderData.farmArea} ha in ${calendar}.`} + +
-
-
-
-
- - - -
-
- + + + + + + +
+
+ +
+
+ + Meststoffen + + + Bekijk en beheer de + meststoffen voor dit + bedrijf + +
-
- - Gebruiksnormen - - - Gebruiksnormen op bedrijdfs- - en perceelsniveau - + + + + + + +
+
+ +
+
+ + Toegang + + + {loaderData.roles[0] === + "owner" + ? "Je hebt de rol Eigenaar voor dit bedrijf." + : loaderData + .roles[0] === + "advisor" + ? "Je hebt de rol Adviseur voor dit bedrijf." + : loaderData + .roles[0] === + "researcher" + ? "Je hebt de rol Onderzoeker voor dit bedrijf." + : `Je hebt de rol ${loaderData.roles[0]} voor dit bedrijf.`} + +
-
-
-
-
- - - -
-
- + + + + + + +
+
+ +
+
+ + Instellingen + + + Pas de instellingen voor + dit bedrijf aan. + +
-
- Atlas - - Bekijk gewaspercelen op de - kaart - + + + + + + +
+
+ +
+
+ + Derogatie + + + Beheer derogatie voor + dit bedrijf. + +
-
-
-
-
- - - - - - Overzicht - - - - Mijn rol: {loaderData.roles} - Adres: KvK nummer: Derogatie: - - - - - + + +
From 9762e13a2f822ddb7c7a6c97df0aa7aa8fee834e Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:25:32 +0200 Subject: [PATCH 03/35] feat: add a quick action menu for farm dashboard --- fdm-app/app/routes/farm.$b_id_farm._index.tsx | 421 ++++++++++-------- 1 file changed, 242 insertions(+), 179 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm._index.tsx b/fdm-app/app/routes/farm.$b_id_farm._index.tsx index 3f0d7b1b3..37ba316ce 100644 --- a/fdm-app/app/routes/farm.$b_id_farm._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm._index.tsx @@ -33,10 +33,12 @@ import { ChevronUp, Landmark, MapIcon, + Plus, Settings, Shapes, Square, UserRoundCheck, + Zap, } from "lucide-react" // Meta @@ -129,92 +131,53 @@ export default function FarmDashboardIndex() {
-
+
- Apps - - Bekijk welke apps er beschikbaar zijn voor - dit bedrijf. - +
+
+ +
+
+ Snelle acties +
+
- +
- +
- Nutriententenbalans + Bemesting - Inzicht in aanvoer, - afvoer en emissie van - stikstof + Voeg een bemesting toe + voor één of meerdere + percelen.
- +
- +
- - Bemestingsadvies - + Oogst - Advies volgens Handboek - Bodem en Bemesting - (CBAV) en Adviesbasis - Bemesting (CBGV). - -
-
-
-
-
- - - -
-
- -
-
- - Gebruiksnormen - - - Gebruiksnormen op - bedrijdfs- en - perceelsniveau - -
-
-
-
-
- - - -
-
- -
-
- Atlas - - Bekijk gewaspercelen op - de kaart + Voeg een oogst toe voor + één of meerdere + percelen.
@@ -223,134 +186,234 @@ export default function FarmDashboardIndex() {
- - - - Gegevens - - Bekijk welke gegevens er zijn over dit - bedrijf. - - - - - - -
-
- +
+ + + Apps + + Bekijk welke apps er beschikbaar zijn + voor dit bedrijf. + + + + + + +
+
+ +
+
+ + Nutriententenbalans + + + Inzicht in aanvoer, + afvoer en emissie + van stikstof + +
-
- - Percelen - - - {loaderData.fieldsNumber === - 0 - ? `Dit bedrijf heeft geen percelen ${calendar}` - : loaderData.fieldsNumber === - 1 - ? `Dit bedrijf heeft 1 perceel in ${calendar}` - : `Dit bedrijf heeft ${loaderData.fieldsNumber} percelen en ${loaderData.farmArea} ha in ${calendar}.`} - -
-
- - - - - - -
-
- + + + + + + +
+
+ +
+
+ + Bemestingsadvies + + + Advies volgens + Handboek Bodem en + Bemesting (CBAV) en + Adviesbasis + Bemesting (CBGV). + +
-
- - Meststoffen - - - Bekijk en beheer de - meststoffen voor dit - bedrijf - + + + + + + +
+
+ +
+
+ + Gebruiksnormen + + + Gebruiksnormen op + bedrijdfs- en + perceelsniveau + +
-
-
-
-
- - - -
-
- + + + + + + +
+
+ +
+
+ + Atlas + + + Bekijk gewaspercelen + op de kaart + +
-
- - Toegang - - - {loaderData.roles[0] === - "owner" - ? "Je hebt de rol Eigenaar voor dit bedrijf." - : loaderData - .roles[0] === - "advisor" - ? "Je hebt de rol Adviseur voor dit bedrijf." - : loaderData - .roles[0] === - "researcher" - ? "Je hebt de rol Onderzoeker voor dit bedrijf." - : `Je hebt de rol ${loaderData.roles[0]} voor dit bedrijf.`} - + + + + + + + + + Gegevens + + Bekijk welke gegevens er zijn over dit + bedrijf. + + + + + + +
+
+ +
+
+ + Percelen + + + {loaderData.fieldsNumber === + 0 + ? `Dit bedrijf heeft geen percelen ${calendar}` + : loaderData.fieldsNumber === + 1 + ? `Dit bedrijf heeft 1 perceel in ${calendar}` + : `Dit bedrijf heeft ${loaderData.fieldsNumber} percelen en ${loaderData.farmArea} ha in ${calendar}.`} + +
-
-
-
-
- - - -
-
- + + + + + + +
+
+ +
+
+ + Meststoffen + + + Bekijk en beheer de + meststoffen voor dit + bedrijf + +
-
- - Instellingen - - - Pas de instellingen voor - dit bedrijf aan. - + + + + + + +
+
+ +
+
+ + Toegang + + + {loaderData + .roles[0] === + "owner" + ? "Je hebt de rol Eigenaar voor dit bedrijf." + : loaderData + .roles[0] === + "advisor" + ? "Je hebt de rol Adviseur voor dit bedrijf." + : loaderData + .roles[0] === + "researcher" + ? "Je hebt de rol Onderzoeker voor dit bedrijf." + : `Je hebt de rol ${loaderData.roles[0]} voor dit bedrijf.`} + +
-
-
-
-
- - - -
-
- + + + + + + +
+
+ +
+
+ + Instellingen + + + Pas de instellingen + voor dit bedrijf + aan. + +
-
- - Derogatie - - - Beheer derogatie voor - dit bedrijf. - + + + + + + +
+
+ +
+
+ + Derogatie + + + Beheer derogatie + voor dit bedrijf. + +
-
-
-
-
- - + + + + + +
From 08f4b5bdfaa19dd53171e8d43d03c5c2f9587aef Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:29:34 +0200 Subject: [PATCH 04/35] feat: add some shadow on hover --- fdm-app/app/routes/farm.$b_id_farm._index.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm._index.tsx b/fdm-app/app/routes/farm.$b_id_farm._index.tsx index 37ba316ce..e69d9d231 100644 --- a/fdm-app/app/routes/farm.$b_id_farm._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm._index.tsx @@ -145,7 +145,7 @@ export default function FarmDashboardIndex() { - +
@@ -166,7 +166,7 @@ export default function FarmDashboardIndex() { - +
@@ -197,7 +197,7 @@ export default function FarmDashboardIndex() { - +
@@ -218,7 +218,7 @@ export default function FarmDashboardIndex() { - +
@@ -241,7 +241,7 @@ export default function FarmDashboardIndex() { - +
@@ -262,7 +262,7 @@ export default function FarmDashboardIndex() { - +
@@ -294,7 +294,7 @@ export default function FarmDashboardIndex() { - +
@@ -319,7 +319,7 @@ export default function FarmDashboardIndex() { - +
@@ -340,7 +340,7 @@ export default function FarmDashboardIndex() { - +
@@ -371,7 +371,7 @@ export default function FarmDashboardIndex() { - +
@@ -392,7 +392,7 @@ export default function FarmDashboardIndex() { - +
From 92e7b30500b560821a1e8e073f4bbbfd0b03e2e4 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:56:45 +0200 Subject: [PATCH 05/35] refactor: improve design of the new farm dashboard page --- .changeset/purple-dryers-rule.md | 5 + fdm-app/app/routes/farm.$b_id_farm._index.tsx | 433 ++++++++++-------- 2 files changed, 235 insertions(+), 203 deletions(-) create mode 100644 .changeset/purple-dryers-rule.md diff --git a/.changeset/purple-dryers-rule.md b/.changeset/purple-dryers-rule.md new file mode 100644 index 000000000..8b5900541 --- /dev/null +++ b/.changeset/purple-dryers-rule.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Adds a new farm dashboard page with an overview of the farm and links to apps, pages for data and quick actions diff --git a/fdm-app/app/routes/farm.$b_id_farm._index.tsx b/fdm-app/app/routes/farm.$b_id_farm._index.tsx index e69d9d231..189829342 100644 --- a/fdm-app/app/routes/farm.$b_id_farm._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm._index.tsx @@ -26,20 +26,30 @@ import { CardTitle, } from "../components/ui/card" import { Button } from "../components/ui/button" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../components/ui/select" import { ArrowRightLeft, BookOpenText, ChevronsUp, ChevronUp, + Home, Landmark, MapIcon, Plus, Settings, Shapes, Square, + Trash2, UserRoundCheck, Zap, } from "lucide-react" +import { getCalendarSelection } from "../lib/calendar" // Meta export const meta: MetaFunction = () => { @@ -113,6 +123,8 @@ export default function FarmDashboardIndex() { const loaderData = useLoaderData() const calendar = useCalendarStore((state) => state.calendar) + const setCalendar = useCalendarStore((state) => state.setCalendar) + const years = getCalendarSelection() return ( @@ -131,288 +143,303 @@ export default function FarmDashboardIndex() {
-
- - -
-
- -
-
- Snelle acties -
-
-
- - - - -
-
- -
-
- - Bemesting - - - Voeg een bemesting toe - voor één of meerdere - percelen. - -
-
-
-
-
- - - -
-
- -
-
- Oogst - - Voeg een oogst toe voor - één of meerdere - percelen. - -
-
-
-
-
-
-
-
- - - Apps - - Bekijk welke apps er beschikbaar zijn - voor dit bedrijf. - - - - +
+ {/* Left Column */} +
+ {/* Quick Actions */} +
+

+ Snelle acties +

+
+ - +
-
- +
+
- Nutriententenbalans + Bemesting toevoegen - Inzicht in aanvoer, - afvoer en emissie - van stikstof - -
-
- - - - - - -
-
- -
-
- - Bemestingsadvies - - - Advies volgens - Handboek Bodem en - Bemesting (CBAV) en - Adviesbasis - Bemesting (CBGV). + Voor één of meerdere + percelen.
- + - +
-
- +
+
- Gebruiksnormen + Oogst toevoegen - Gebruiksnormen op - bedrijdfs- en - perceelsniveau + Voor één of meerdere + percelen.
- - - -
-
- -
-
- - Atlas - - - Bekijk gewaspercelen - op de kaart - -
-
-
-
-
- - +
+
- - - Gegevens - - Bekijk welke gegevens er zijn over dit - bedrijf. - - - - + {/* Apps */} +
+

+ Apps +

+
+ - +
-
- +
+
- Percelen + Nutriententenbalans - {loaderData.fieldsNumber === - 0 - ? `Dit bedrijf heeft geen percelen ${calendar}` - : loaderData.fieldsNumber === - 1 - ? `Dit bedrijf heeft 1 perceel in ${calendar}` - : `Dit bedrijf heeft ${loaderData.fieldsNumber} percelen en ${loaderData.farmArea} ha in ${calendar}.`} + Aanvoer, afvoer en + emissie van + stikstof.
- - - -
-
- -
-
- - Meststoffen - - - Bekijk en beheer de - meststoffen voor dit - bedrijf - -
-
-
-
-
- + - +
-
- +
+
- Toegang + Bemestingsadvies - {loaderData - .roles[0] === - "owner" - ? "Je hebt de rol Eigenaar voor dit bedrijf." - : loaderData - .roles[0] === - "advisor" - ? "Je hebt de rol Adviseur voor dit bedrijf." - : loaderData - .roles[0] === - "researcher" - ? "Je hebt de rol Onderzoeker voor dit bedrijf." - : `Je hebt de rol ${loaderData.roles[0]} voor dit bedrijf.`} + Volgens Handboek + Bodem en Bemesting + en Adviesbasis + Bemesting.
- + - +
-
- +
+
- Instellingen + Gebruiksnormen - Pas de instellingen - voor dit bedrijf - aan. + Normen op bedrijfs- + en perceelsniveau.
- + - +
-
- +
+
- Derogatie + Atlas - Beheer derogatie - voor dit bedrijf. + Gewaspercelen op de + kaart.
- - +
+
+
+ + {/* Right Column */} +
+ {/* Overview */} +
+

+ Overzicht +

+ + +
+
+ + Aantal percelen + + + {loaderData.fieldsNumber} + +
+
+ + Totale oppervlakte + + + {loaderData.farmArea} ha + +
+ +
+ + Rol + + + {loaderData.roles.includes( + "owner", + ) + ? "Eigenaar" + : loaderData.roles.includes( + "advisor", + ) + ? "Adviseur" + : loaderData.roles.includes( + "researcher", + ) + ? "Onderzoeker" + : loaderData + .roles[0]} + +
+
+ + Jaar + + +
+
+
+
+
+ + {/* Settings */} +
+

+ Gegevens +

+ + +
+ + + + + + +
+
+
+
From b03004054da8e851ecde3aedd57aaea20740a0a8 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 19 Sep 2025 11:53:55 +0200 Subject: [PATCH 06/35] feat: replace list of fields with a table --- .../blocks/fields/column-header.tsx | 71 ++++++++ .../app/components/blocks/fields/columns.tsx | 136 ++++++++++++++ .../app/components/blocks/fields/table.tsx | 171 ++++++++++++++++++ ...farm.$b_id_farm.$calendar.field._index.tsx | 72 ++------ ...arm.create.$b_id_farm.$calendar.access.tsx | 7 - 5 files changed, 389 insertions(+), 68 deletions(-) create mode 100644 fdm-app/app/components/blocks/fields/column-header.tsx create mode 100644 fdm-app/app/components/blocks/fields/columns.tsx create mode 100644 fdm-app/app/components/blocks/fields/table.tsx diff --git a/fdm-app/app/components/blocks/fields/column-header.tsx b/fdm-app/app/components/blocks/fields/column-header.tsx new file mode 100644 index 000000000..27922207e --- /dev/null +++ b/fdm-app/app/components/blocks/fields/column-header.tsx @@ -0,0 +1,71 @@ +import type { Column } from "@tanstack/react-table" +import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react" +import { Button } from "~/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu" +import { cn } from "~/lib/utils" + +interface DataTableColumnHeaderProps + extends React.HTMLAttributes { + column: Column + title: string +} + +export function DataTableColumnHeader({ + column, + title, + className, +}: DataTableColumnHeaderProps) { + if (!column.getCanSort()) { + return
{title}
+ } + + return ( +
+ + + + + + column.toggleSorting(false)} + > + + Oplopend + + column.toggleSorting(true)} + > + + Aflopend + + + column.toggleVisibility(false)} + > + + Verberg + + + +
+ ) +} diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx new file mode 100644 index 000000000..793a32075 --- /dev/null +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -0,0 +1,136 @@ +import type { ColumnDef } from "@tanstack/react-table" +import { ArrowRight, MoreHorizontal } from "lucide-react" +import { NavLink } from "react-router-dom" +import { Badge } from "~/components/ui/badge" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "~/components/ui/tooltip" +import { DataTableColumnHeader } from "./column-header" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../../ui/dropdown-menu" +import { Button } from "../../ui/button" + +export type FieldExtended = { + b_id: string + b_name: string + b_area: number +} + +export const columns: ColumnDef[] = [ + // { + // accessorKey: "p_id", + // header: "ID", + // }, + { + accessorKey: "b_name", + header: ({ column }) => { + return + }, + }, + { + accessorKey: "b_area", + header: ({ column }) => { + return + }, + }, + // { + // accessorKey: "Type", + // cell: ({ row }) => { + // const fertilizer = row.original + + // return ( + // + // {fertilizer.p_type_manure ? ( + // + // Mest + // + // ) : null} + // {fertilizer.p_type_compost ? ( + // + // Compost + // + // ) : null} + // {fertilizer.p_type_mineral ? ( + // + // Kunstmest + // + // ) : null} + // + // ) + // }, + // }, + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + const field = row.original + + return ( + + + + + + {/* Acties + + navigator.clipboard.writeText(field.b_id) + } + > + Kopieer perceel id + + */} + Gegevens + + Overzicht + + + + Gewassen + + + + + Bemesting + + + + Bodem + + + + Kaart + + + + + Verwijderen + + + + + ) + }, + }, +] diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx new file mode 100644 index 000000000..1cfe7a8c6 --- /dev/null +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -0,0 +1,171 @@ +import { + type ColumnDef, + type ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + type SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { ChevronDown, Plus } from "lucide-react" +import { useState } from "react" +import { NavLink } from "react-router" +import { Button } from "~/components/ui/button" +import { Input } from "~/components/ui/input" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "~/components/ui/table" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "../../ui/dropdown-menu" + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] +} + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const [sorting, setSorting] = useState([]) + const [columnFilters, setColumnFilters] = useState([]) + + const [columnVisibility, setColumnVisibility] = + useState({}) + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + state: { + sorting, + columnFilters, + columnVisibility, + }, + }) + + return ( +
+
+ + table + .getColumn("b_name") + ?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ) + })} + + +
+ + + +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + Geen resultaten. + + + )} + +
+
+
+ ) +} diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx index 2cad97af0..9e1b3afc5 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx @@ -27,6 +27,9 @@ import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" import { getTimeBasedGreeting } from "~/lib/greetings" +import { DataTable } from "../components/blocks/fields/table" +import { columns } from "../components/blocks/fields/columns" +import { FarmContent } from "../components/blocks/farm/farm-content" export const meta: MetaFunction = () => { return [ @@ -187,67 +190,14 @@ export default function FarmFieldIndex() { "Kies een perceel uit de lijst om verder te gaan of maak een nieuw perceel aan" } /> -
- - - {/* Bedrijven */} - - Kies een perceel om verder te gaan - - - -
-
- {loaderData.fieldOptions.map( - (option) => ( -
-
-

- {option.b_name} -

-

- {option.b_area && - option.b_area > - 0.1 - ? `${option.b_area} ha` - : "< 0.1 ha"} -

-
- -
- -
-
- ), - )} -
-
-
- - -

- Of maak een nieuw perceel aan: -

-
- - - -
-
-
-
+ +
+ +
+
)}
diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.access.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.access.tsx index 3cdf98ce2..d7c5c0196 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.access.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.access.tsx @@ -17,13 +17,6 @@ import { import { dataWithError, dataWithSuccess } from "remix-toast" import { AccessInfoCard } from "~/components/blocks/access/access-info-card" import { AccessManagementCard } from "~/components/blocks/access/access-management-card" -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbSeparator, -} from "~/components/ui/breadcrumb" import { Button } from "~/components/ui/button" import { Separator } from "~/components/ui/separator" import { getSession } from "~/lib/auth.server" From b21a4eca90cf19aebfd51cad0ebcee8be659dfe5 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:25:23 +0200 Subject: [PATCH 07/35] feat: show cultivations in table as well --- .../app/components/blocks/fields/columns.tsx | 38 ++++++++++++++++--- ...farm.$b_id_farm.$calendar.field._index.tsx | 31 +++++++++++++-- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index 793a32075..15eef14ff 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -2,12 +2,6 @@ import type { ColumnDef } from "@tanstack/react-table" import { ArrowRight, MoreHorizontal } from "lucide-react" import { NavLink } from "react-router-dom" import { Badge } from "~/components/ui/badge" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "~/components/ui/tooltip" import { DataTableColumnHeader } from "./column-header" import { DropdownMenu, @@ -22,6 +16,7 @@ import { Button } from "../../ui/button" export type FieldExtended = { b_id: string b_name: string + b_lu_name: string[] b_area: number } @@ -35,6 +30,37 @@ export const columns: ColumnDef[] = [ header: ({ column }) => { return }, + cell: ({ row }) => { + const field = row.original + + return ( + + {field.b_name} + + ) + }, + }, + { + accessorKey: "cultivationNames", + header: ({ column }) => { + return + }, + cell: ({ row }) => { + const field = row.original + + return ( +
+ {field.b_lu_name.map((name) => ( + + {name} + + ))} +
+ ) + }, }, { accessorKey: "b_area", diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx index 9e1b3afc5..c4412d172 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx @@ -1,4 +1,4 @@ -import { getFarms, getFields } from "@svenvw/fdm-core" +import { getCultivations, getFarms, getFields } from "@svenvw/fdm-core" import { data, type LoaderFunctionArgs, @@ -113,11 +113,34 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } }) + const fieldsExtended = await Promise.all( + fields.map(async (field) => { + const cultivations = await getCultivations( + fdm, + session.principal_id, + field.b_id, + timeframe, + ) + const cultivationNames = cultivations.reduce( + (x, currentValue) => [...x, currentValue.b_lu_name], + [], + ) + + return { + b_id: field.b_id, + b_name: field.b_name, + b_lu_name: cultivationNames, + b_area: Math.round(field.b_area * 10) / 10, + } + }), + ) + // Return user information from loader return { b_id_farm: b_id_farm, farmOptions: farmOptions, fieldOptions: fieldOptions, + fieldsExtended: fieldsExtended, userName: session.userName, } } catch (error) { @@ -191,11 +214,11 @@ export default function FarmFieldIndex() { } /> -
+
+ data={loaderData.fieldsExtended} + />
From 2ef8ff081663edd7fb8cab24dc7ab776a18015c3 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:53:44 +0200 Subject: [PATCH 08/35] fix: name of column --- fdm-app/app/components/blocks/fields/columns.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index 15eef14ff..f24f46b06 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -44,7 +44,7 @@ export const columns: ColumnDef[] = [ }, }, { - accessorKey: "cultivationNames", + accessorKey: "b_lu_name", header: ({ column }) => { return }, From 48b7d7355a30b6b3212e6a852cc6f65cb5021624 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:58:40 +0200 Subject: [PATCH 09/35] feat: enable filtering on cultivations as well --- .../app/components/blocks/fields/table.tsx | 25 +++++++++++-------- fdm-app/package.json | 1 + pnpm-lock.yaml | 8 ++++++ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index 1cfe7a8c6..0e46e8386 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -12,6 +12,7 @@ import { import { ChevronDown, Plus } from "lucide-react" import { useState } from "react" import { NavLink } from "react-router" +import fuzzysort from "fuzzysort" import { Button } from "~/components/ui/button" import { Input } from "~/components/ui/input" import { @@ -40,10 +41,17 @@ export function DataTable({ }: DataTableProps) { const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState([]) + const [globalFilter, setGlobalFilter] = useState("") const [columnVisibility, setColumnVisibility] = useState({}) + const fuzzyFilter = (row: any, columnId: string, filterValue: string) => { + const target = `${row.getValue("b_name")} ${row.getValue("b_lu_name")}` + const result = fuzzysort.go(filterValue, [target]) + return result.length > 0 + } + const table = useReactTable({ data, columns, @@ -53,10 +61,13 @@ export function DataTable({ onColumnFiltersChange: setColumnFilters, getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, + onGlobalFilterChange: setGlobalFilter, + globalFilterFn: fuzzyFilter, state: { sorting, columnFilters, columnVisibility, + globalFilter, }, }) @@ -64,17 +75,9 @@ export function DataTable({
- table - .getColumn("b_name") - ?.setFilterValue(event.target.value) - } + placeholder="Filter percelen of gewassen..." + value={globalFilter ?? ""} + onChange={(event) => setGlobalFilter(event.target.value)} className="max-w-sm" /> diff --git a/fdm-app/package.json b/fdm-app/package.json index 5a92d3ad3..ca36fe697 100644 --- a/fdm-app/package.json +++ b/fdm-app/package.json @@ -45,6 +45,7 @@ "file-type": "^21.0.0", "flatgeobuf": "^4.2.0", "framer-motion": "^12.23.14", + "fuzzysort": "^3.1.0", "isbot": "^5.1.30", "lodash.throttle": "^4.1.1", "lucide-react": "^0.544.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57eefdeec..83f716e8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -172,6 +172,9 @@ importers: framer-motion: specifier: ^12.23.14 version: 12.23.14(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + fuzzysort: + specifier: ^3.1.0 + version: 3.1.0 isbot: specifier: ^5.1.30 version: 5.1.30 @@ -6712,6 +6715,9 @@ packages: resolution: {integrity: sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==} engines: {node: '>= 0.6.0'} + fuzzysort@3.1.0: + resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -18744,6 +18750,8 @@ snapshots: fuzzy@0.1.3: {} + fuzzysort@3.1.0: {} + gensync@1.0.0-beta.2: {} geojson-equality-ts@1.0.2: From 78288a684654d423aa1b01ee230340fcbecbb703 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:21:40 +0200 Subject: [PATCH 10/35] feat: use the cultivation color for the badge --- .../app/components/blocks/fields/columns.tsx | 21 +++++++++++++------ .../app/components/blocks/fields/table.tsx | 5 ++++- ...farm.$b_id_farm.$calendar.field._index.tsx | 6 +----- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index f24f46b06..9464d715a 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -12,11 +12,15 @@ import { DropdownMenuTrigger, } from "../../ui/dropdown-menu" import { Button } from "../../ui/button" +import { getCultivationColor } from "../../custom/cultivation-colors" export type FieldExtended = { b_id: string b_name: string - b_lu_name: string[] + cultivations: { + b_lu_name: string + b_lu_croprotation: string + }[] b_area: number } @@ -44,18 +48,23 @@ export const columns: ColumnDef[] = [ }, }, { - accessorKey: "b_lu_name", + accessorKey: "cultivations", header: ({ column }) => { return }, cell: ({ row }) => { - const field = row.original + const field = row.original return (
- {field.b_lu_name.map((name) => ( - - {name} + {field.cultivations.map((cultivation) => ( + + {cultivation.b_lu_name} ))}
diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index 0e46e8386..3ce026d11 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -47,7 +47,10 @@ export function DataTable({ useState({}) const fuzzyFilter = (row: any, columnId: string, filterValue: string) => { - const target = `${row.getValue("b_name")} ${row.getValue("b_lu_name")}` + const cultivationNames = row.original.cultivations + .map((c: { b_lu_name: string }) => c.b_lu_name) + .join(" ") + const target = `${row.getValue("b_name")} ${cultivationNames}` const result = fuzzysort.go(filterValue, [target]) return result.length > 0 } diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx index c4412d172..f29505401 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx @@ -121,15 +121,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) { field.b_id, timeframe, ) - const cultivationNames = cultivations.reduce( - (x, currentValue) => [...x, currentValue.b_lu_name], - [], - ) return { b_id: field.b_id, b_name: field.b_name, - b_lu_name: cultivationNames, + cultivations: cultivations, b_area: Math.round(field.b_area * 10) / 10, } }), From 68a28fca797fcce605bd7d9cb65d7e67d63197c4 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:26:58 +0200 Subject: [PATCH 11/35] refactor: sort cultivation based on start date --- .../app/components/blocks/fields/columns.tsx | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index 9464d715a..97d9d563f 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -20,6 +20,7 @@ export type FieldExtended = { cultivations: { b_lu_name: string b_lu_croprotation: string + b_lu_start: Date }[] b_area: number } @@ -53,15 +54,29 @@ export const columns: ColumnDef[] = [ return }, cell: ({ row }) => { - const field = row.original + const field = row.original + + const cultivationsSorted = field.cultivations.sort((a, b) => { + if (a.b_lu_start.getTime() < b.b_lu_start.getTime()) { + return -1 + } + if (a.b_lu_start.getTime() > b.b_lu_start.getTime()) { + return 1 + } + return 0 + }) return ( -
- {field.cultivations.map((cultivation) => ( - + {cultivationsSorted.map((cultivation) => ( + {cultivation.b_lu_name} @@ -141,7 +156,7 @@ export const columns: ColumnDef[] = [ Overzicht - + Gewassen From 2bb6f3496edfc2e1035e6b9bc18682bc7086660e Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:38:11 +0200 Subject: [PATCH 12/35] feat: add fertilizer applications to the fields table as well --- .../app/components/blocks/fields/columns.tsx | 63 +++++++++---------- .../app/components/blocks/fields/table.tsx | 12 ++-- ...farm.$b_id_farm.$calendar.field._index.tsx | 16 ++++- 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index 97d9d563f..7ba30d971 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -22,6 +22,9 @@ export type FieldExtended = { b_lu_croprotation: string b_lu_start: Date }[] + fertilizerApplications: { + p_name_nl: string + }[] b_area: number } @@ -86,47 +89,37 @@ export const columns: ColumnDef[] = [ ) }, }, + { + accessorKey: "fertilizerApplications", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => { + const field = row.original + + const uniqueFertilizerNames = field.fertilizerApplications + .map((app) => app.p_name_nl) + .filter((name, index, self) => self.indexOf(name) === index) + + return ( +
+ {uniqueFertilizerNames.map((fertilizer) => ( + + {fertilizer} + + ))} +
+ ) + }, + }, { accessorKey: "b_area", header: ({ column }) => { return }, }, - // { - // accessorKey: "Type", - // cell: ({ row }) => { - // const fertilizer = row.original - - // return ( - // - // {fertilizer.p_type_manure ? ( - // - // Mest - // - // ) : null} - // {fertilizer.p_type_compost ? ( - // - // Compost - // - // ) : null} - // {fertilizer.p_type_mineral ? ( - // - // Kunstmest - // - // ) : null} - // - // ) - // }, - // }, { id: "actions", enableHiding: false, diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index 3ce026d11..cc6e68809 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -43,14 +43,18 @@ export function DataTable({ const [columnFilters, setColumnFilters] = useState([]) const [globalFilter, setGlobalFilter] = useState("") - const [columnVisibility, setColumnVisibility] = - useState({}) + const [columnVisibility, setColumnVisibility] = useState( + {}, + ) const fuzzyFilter = (row: any, columnId: string, filterValue: string) => { const cultivationNames = row.original.cultivations .map((c: { b_lu_name: string }) => c.b_lu_name) .join(" ") - const target = `${row.getValue("b_name")} ${cultivationNames}` + const fertilizerNames = row.original.fertilizerApplications + .map((f: { p_name_nl: string }) => f.p_name_nl) + .join(" ") + const target = `${row.getValue("b_name")} ${cultivationNames} ${fertilizerNames}` const result = fuzzysort.go(filterValue, [target]) return result.length > 0 } @@ -86,7 +90,7 @@ export function DataTable({ diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx index f29505401..052899b12 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx @@ -1,4 +1,10 @@ -import { getCultivations, getFarms, getFields } from "@svenvw/fdm-core" +import { + getCultivations, + getFarms, + getFertilizerApplication, + getFertilizerApplications, + getFields, +} from "@svenvw/fdm-core" import { data, type LoaderFunctionArgs, @@ -122,10 +128,18 @@ export async function loader({ request, params }: LoaderFunctionArgs) { timeframe, ) + const fertilizerApplications = await getFertilizerApplications( + fdm, + session.principal_id, + field.b_id, + timeframe, + ) + return { b_id: field.b_id, b_name: field.b_name, cultivations: cultivations, + fertilizerApplications: fertilizerApplications, b_area: Math.round(field.b_area * 10) / 10, } }), From 6b0c9d3765c064810702c92ff77877605f36d623 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:45:11 +0200 Subject: [PATCH 13/35] feat: add alphabetically sorting to the table --- .../app/components/blocks/fields/columns.tsx | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index 7ba30d971..25ad09eed 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -1,5 +1,5 @@ import type { ColumnDef } from "@tanstack/react-table" -import { ArrowRight, MoreHorizontal } from "lucide-react" +import { MoreHorizontal } from "lucide-react" import { NavLink } from "react-router-dom" import { Badge } from "~/components/ui/badge" import { DataTableColumnHeader } from "./column-header" @@ -8,7 +8,6 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu" import { Button } from "../../ui/button" @@ -35,6 +34,7 @@ export const columns: ColumnDef[] = [ // }, { accessorKey: "b_name", + enableSorting: true, header: ({ column }) => { return }, @@ -53,21 +53,21 @@ export const columns: ColumnDef[] = [ }, { accessorKey: "cultivations", + enableSorting: true, + sortingFn: (rowA, rowB, columnId) => { + const cultivationA = rowA.original.cultivations[0]?.b_lu_name || "" + const cultivationB = rowB.original.cultivations[0]?.b_lu_name || "" + return cultivationA.localeCompare(cultivationB) + }, header: ({ column }) => { return }, cell: ({ row }) => { const field = row.original - const cultivationsSorted = field.cultivations.sort((a, b) => { - if (a.b_lu_start.getTime() < b.b_lu_start.getTime()) { - return -1 - } - if (a.b_lu_start.getTime() > b.b_lu_start.getTime()) { - return 1 - } - return 0 - }) + const cultivationsSorted = [...field.cultivations].sort((a, b) => + a.b_lu_name.localeCompare(b.b_lu_name), + ) return (
@@ -91,6 +91,12 @@ export const columns: ColumnDef[] = [ }, { accessorKey: "fertilizerApplications", + enableSorting: true, + sortingFn: (rowA, rowB, columnId) => { + const fertilizerA = rowA.original.fertilizerApplications[0]?.p_name_nl || "" + const fertilizerB = rowB.original.fertilizerApplications[0]?.p_name_nl || "" + return fertilizerA.localeCompare(fertilizerB) + }, header: ({ column }) => { return ( @@ -99,9 +105,10 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => { const field = row.original - const uniqueFertilizerNames = field.fertilizerApplications + const uniqueFertilizerNames = [...field.fertilizerApplications] .map((app) => app.p_name_nl) .filter((name, index, self) => self.indexOf(name) === index) + .sort((a, b) => a.localeCompare(b)) return (
@@ -116,6 +123,8 @@ export const columns: ColumnDef[] = [ }, { accessorKey: "b_area", + enableSorting: true, + sortingFn: "alphanumeric", header: ({ column }) => { return }, From aa910e679e53f7c912a16d757029f573db9f2d47 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:13:18 +0200 Subject: [PATCH 14/35] feat: add a_som_loi and b_soiltype_agr to table --- .../app/components/blocks/fields/columns.tsx | 31 +++++++++++++++++-- .../app/components/blocks/fields/table.tsx | 8 ++--- ...farm.$b_id_farm.$calendar.field._index.tsx | 22 +++++++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index 25ad09eed..497665f31 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -24,6 +24,8 @@ export type FieldExtended = { fertilizerApplications: { p_name_nl: string }[] + a_som_loi: number + b_soiltype_agr: string b_area: number } @@ -93,8 +95,10 @@ export const columns: ColumnDef[] = [ accessorKey: "fertilizerApplications", enableSorting: true, sortingFn: (rowA, rowB, columnId) => { - const fertilizerA = rowA.original.fertilizerApplications[0]?.p_name_nl || "" - const fertilizerB = rowB.original.fertilizerApplications[0]?.p_name_nl || "" + const fertilizerA = + rowA.original.fertilizerApplications[0]?.p_name_nl || "" + const fertilizerB = + rowB.original.fertilizerApplications[0]?.p_name_nl || "" return fertilizerA.localeCompare(fertilizerB) }, header: ({ column }) => { @@ -121,12 +125,33 @@ export const columns: ColumnDef[] = [ ) }, }, + { + accessorKey: "a_som_loi", + enableSorting: true, + sortingFn: "alphanumeric", + header: ({ column }) => { + return + }, + }, + { + accessorKey: "b_soiltype_agr", + enableSorting: true, + sortingFn: "alphanumeric", + header: ({ column }) => { + return + }, + }, { accessorKey: "b_area", enableSorting: true, sortingFn: "alphanumeric", header: ({ column }) => { - return + return ( + + ) }, }, { diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index cc6e68809..028860134 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -54,7 +54,7 @@ export function DataTable({ const fertilizerNames = row.original.fertilizerApplications .map((f: { p_name_nl: string }) => f.p_name_nl) .join(" ") - const target = `${row.getValue("b_name")} ${cultivationNames} ${fertilizerNames}` + const target = `${row.getValue("b_name")} ${cultivationNames} ${fertilizerNames} ${row.getValue("b_soiltype_agr")}` const result = fuzzysort.go(filterValue, [target]) return result.length > 0 } @@ -79,10 +79,10 @@ export function DataTable({ }) return ( -
-
+
+
setGlobalFilter(event.target.value)} className="max-w-sm" diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx index 052899b12..204c146dc 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx @@ -1,5 +1,6 @@ import { getCultivations, + getCurrentSoilData, getFarms, getFertilizerApplication, getFertilizerApplications, @@ -135,11 +136,32 @@ export async function loader({ request, params }: LoaderFunctionArgs) { timeframe, ) + const currentSoilData = await getCurrentSoilData( + fdm, + session.principal_id, + field.b_id, + timeframe, + ) + const a_som_loi = currentSoilData.find((x) => { + if (x.parameter === "a_som_loi") { + return true + } + return false + }).value + const b_soiltype_agr = currentSoilData.find((x) => { + if (x.parameter === "b_soiltype_agr") { + return true + } + return false + }).value + return { b_id: field.b_id, b_name: field.b_name, cultivations: cultivations, fertilizerApplications: fertilizerApplications, + a_som_loi: a_som_loi, + b_soiltype_agr: b_soiltype_agr, b_area: Math.round(field.b_area * 10) / 10, } }), From 5d42a94f241b7f55d869c56053904289e4bf2499 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:31:24 +0200 Subject: [PATCH 15/35] feat: add buttons to add fertilizer application and harvest for multiple fields at once --- .../app/components/blocks/fields/columns.tsx | 29 +++++- .../app/components/blocks/fields/table.tsx | 99 ++++++++++++++++++- 2 files changed, 120 insertions(+), 8 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index 497665f31..413c50754 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -11,6 +11,7 @@ import { DropdownMenuTrigger, } from "../../ui/dropdown-menu" import { Button } from "../../ui/button" +import { Checkbox } from "~/components/ui/checkbox" import { getCultivationColor } from "../../custom/cultivation-colors" export type FieldExtended = { @@ -30,10 +31,30 @@ export type FieldExtended = { } export const columns: ColumnDef[] = [ - // { - // accessorKey: "p_id", - // header: "ID", - // }, + { + id: "select", + header: ({ table }) => ( + + table.toggleAllPageRowsSelected(!!value) + } + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, { accessorKey: "b_name", enableSorting: true, diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index 028860134..f3db18ccf 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -6,15 +6,23 @@ import { getFilteredRowModel, getSortedRowModel, type SortingState, + type RowSelectionState, useReactTable, VisibilityState, } from "@tanstack/react-table" import { ChevronDown, Plus } from "lucide-react" -import { useState } from "react" -import { NavLink } from "react-router" +import { useMemo, useState } from "react" +import { NavLink, useParams } from "react-router" import fuzzysort from "fuzzysort" import { Button } from "~/components/ui/button" import { Input } from "~/components/ui/input" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "~/components/ui/tooltip" +import { type FieldExtended } from "./columns" import { Table, TableBody, @@ -35,17 +43,20 @@ interface DataTableProps { data: TData[] } -export function DataTable({ +export function DataTable({ columns, data, }: DataTableProps) { const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState([]) const [globalFilter, setGlobalFilter] = useState("") - const [columnVisibility, setColumnVisibility] = useState( {}, ) + const [rowSelection, setRowSelection] = useState({}) + + const params = useParams() + const b_id_farm = params.b_id_farm const fuzzyFilter = (row: any, columnId: string, filterValue: string) => { const cultivationNames = row.original.cultivations @@ -69,15 +80,43 @@ export function DataTable({ getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, onGlobalFilterChange: setGlobalFilter, + onRowSelectionChange: setRowSelection, globalFilterFn: fuzzyFilter, state: { sorting, columnFilters, columnVisibility, globalFilter, + rowSelection, }, }) + const selectedFields = useMemo(() => { + return table.getFilteredSelectedRowModel().rows.map((row) => row.original) + }, [table.getFilteredSelectedRowModel().rows]) + + const canAddHarvest = useMemo(() => { + if (selectedFields.length === 0) return false + const firstCultivation = selectedFields[0]?.cultivations[0]?.b_lu_name + return selectedFields.every( + (field) => + field.cultivations.length > 0 && + field.cultivations[0]?.b_lu_name === firstCultivation, + ) + }, [selectedFields]) + + const selectedFieldIds = selectedFields.map((field) => field.b_id) + + const isFertilizerButtonDisabled = selectedFields.length === 0 + const fertilizerTooltipContent = isFertilizerButtonDisabled + ? "Selecteer één of meerdere percelen om bemesting toe te voegen" + : "Bemesting toevoegen aan geselecteerde percelen" + + const isHarvestButtonDisabled = !canAddHarvest + const harvestTooltipContent = isHarvestButtonDisabled + ? "Selecteer één of meerdere percelen met hetzelfde gewas om oogst toe te voegen" + : "Oogst toevoegen aan geselecteerde percelen" + return (
@@ -114,6 +153,58 @@ export function DataTable({ })} + + + +
+ {isFertilizerButtonDisabled ? ( + + ) : ( + + + + )} +
+
+ +

{fertilizerTooltipContent}

+
+
+
+ + + +
+ {isHarvestButtonDisabled ? ( + + ) : ( + + + + )} +
+
+ +

{harvestTooltipContent}

+
+
+
) }, + enableHiding: true, // Enable hiding for mobile }, { accessorKey: "fertilizerApplications", @@ -145,6 +157,7 @@ export const columns: ColumnDef[] = [
) }, + enableHiding: true, // Enable hiding for mobile }, { accessorKey: "a_som_loi", @@ -153,6 +166,7 @@ export const columns: ColumnDef[] = [ header: ({ column }) => { return }, + enableHiding: true, // Enable hiding for mobile }, { accessorKey: "b_soiltype_agr", @@ -161,6 +175,7 @@ export const columns: ColumnDef[] = [ header: ({ column }) => { return }, + enableHiding: true, // Enable hiding for mobile }, { accessorKey: "b_area", @@ -174,6 +189,7 @@ export const columns: ColumnDef[] = [ /> ) }, + enableHiding: true, // Enable hiding for mobile }, { id: "actions", diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index 98275507b..61fcee5b3 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -10,7 +10,7 @@ import { useReactTable, VisibilityState, } from "@tanstack/react-table" -import { ChevronDown, Plus } from "lucide-react" +import { ChevronDown, Plus, ChevronRight } from "lucide-react" import { useMemo, useRef, useState } from "react" import { NavLink, useParams } from "react-router" import fuzzysort from "fuzzysort" @@ -37,6 +37,10 @@ import { DropdownMenuContent, DropdownMenuTrigger, } from "../../ui/dropdown-menu" +import { useIsMobile } from "~/hooks/use-mobile" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/components/ui/collapsible" +import { Badge } from "~/components/ui/badge" +import { getCultivationColor } from "../../custom/cultivation-colors" interface DataTableProps { columns: ColumnDef[] @@ -54,10 +58,12 @@ export function DataTable({ {}, ) const [rowSelection, setRowSelection] = useState({}) + const [expandedRows, setExpandedRows] = useState>({}) const lastSelectedRowIndex = useRef(null) const params = useParams() const b_id_farm = params.b_id_farm + const isMobile = useIsMobile() const handleRowClick = ( row: any, @@ -79,6 +85,14 @@ export function DataTable({ return } + if (isMobile) { + setExpandedRows((prev) => ({ + ...prev, + [row.id]: !prev[row.id], + })) + return + } + if (event.shiftKey && lastSelectedRowIndex.current !== null) { const currentIndex = row.index const start = Math.min(currentIndex, lastSelectedRowIndex.current) @@ -127,7 +141,9 @@ export function DataTable({ state: { sorting, columnFilters, - columnVisibility, + columnVisibility: isMobile + ? { b_name: true, select: true, actions: true, cultivations: false, fertilizerApplications: false, a_som_loi: false, b_soiltype_agr: false, b_area: false } + : columnVisibility, globalFilter, rowSelection, }, @@ -168,33 +184,35 @@ export function DataTable({ onChange={(event) => setGlobalFilter(event.target.value)} className="max-w-sm" /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ) - })} - - + {!isMobile && ( + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ) + })} + + + )} @@ -280,22 +298,75 @@ export function DataTable({ {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - handleRowClick(row, event)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - + <> + handleRowClick(row, event)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + {isMobile && expandedRows[row.id] && ( + + + + +
+

Gewassen:

+
+ {row.original.cultivations.map((cultivation: any) => ( + + {cultivation.b_lu_name} + + ))} +
+
+
+

Bemesting met:

+
+ {row.original.fertilizerApplications.map((fertilizer: any) => ( + + {fertilizer.p_name_nl} + + ))} +
+
+
+

OS (%):

+

{row.original.a_som_loi}

+
+
+

Bodemtype:

+

{row.original.b_soiltype_agr}

+
+
+

Oppervlakte (ha):

+

{row.original.b_area}

+
+
+
+
+
+ )} + )) ) : ( From 5c08685b58c4d2969d73ac4be4b81d84094beede Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:32:55 +0200 Subject: [PATCH 18/35] feat: show redirect icon on hover --- .../app/components/blocks/fields/columns.tsx | 7 +- .../app/components/blocks/fields/table.tsx | 154 +++++++++++++----- 2 files changed, 118 insertions(+), 43 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index 8754b4ff4..489ca88c9 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -1,5 +1,5 @@ import type { ColumnDef } from "@tanstack/react-table" -import { MoreHorizontal, ChevronRight, ChevronDown } from "lucide-react" +import { MoreHorizontal, ChevronRight, ChevronDown, ArrowUpRightFromSquare } from "lucide-react" import { NavLink } from "react-router-dom" import { Badge } from "~/components/ui/badge" import { DataTableColumnHeader } from "./column-header" @@ -78,9 +78,10 @@ export const columns: ColumnDef[] = [ return ( {field.b_name} + ) }, @@ -168,7 +169,7 @@ export const columns: ColumnDef[] = [ }, enableHiding: true, // Enable hiding for mobile }, - { + { accessorKey: "b_soiltype_agr", enableSorting: true, sortingFn: "alphanumeric", diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index 61fcee5b3..1536f6cf1 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -10,7 +10,7 @@ import { useReactTable, VisibilityState, } from "@tanstack/react-table" -import { ChevronDown, Plus, ChevronRight } from "lucide-react" +import { ChevronDown, Plus } from "lucide-react" import { useMemo, useRef, useState } from "react" import { NavLink, useParams } from "react-router" import fuzzysort from "fuzzysort" @@ -22,7 +22,7 @@ import { TooltipProvider, TooltipTrigger, } from "~/components/ui/tooltip" -import { type FieldExtended } from "./columns" +import type { FieldExtended } from "./columns" import { Table, TableBody, @@ -38,7 +38,7 @@ import { DropdownMenuTrigger, } from "../../ui/dropdown-menu" import { useIsMobile } from "~/hooks/use-mobile" -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/components/ui/collapsible" +import { Collapsible, CollapsibleContent } from "~/components/ui/collapsible" import { Badge } from "~/components/ui/badge" import { getCultivationColor } from "../../custom/cultivation-colors" @@ -58,7 +58,9 @@ export function DataTable({ {}, ) const [rowSelection, setRowSelection] = useState({}) - const [expandedRows, setExpandedRows] = useState>({}) + const [expandedRows, setExpandedRows] = useState>( + {}, + ) const lastSelectedRowIndex = useRef(null) const params = useParams() @@ -142,7 +144,16 @@ export function DataTable({ sorting, columnFilters, columnVisibility: isMobile - ? { b_name: true, select: true, actions: true, cultivations: false, fertilizerApplications: false, a_som_loi: false, b_soiltype_agr: false, b_area: false } + ? { + b_name: true, + select: true, + actions: true, + cultivations: false, + fertilizerApplications: false, + a_som_loi: false, + b_soiltype_agr: false, + b_area: false, + } : columnVisibility, globalFilter, rowSelection, @@ -150,7 +161,9 @@ export function DataTable({ }) const selectedFields = useMemo(() => { - return table.getFilteredSelectedRowModel().rows.map((row) => row.original) + return table + .getFilteredSelectedRowModel() + .rows.map((row) => row.original) }, [table]) const canAddHarvest = useMemo(() => { @@ -218,7 +231,10 @@ export function DataTable({
{isFertilizerButtonDisabled ? ( - @@ -226,7 +242,7 @@ export function DataTable({ - @@ -244,7 +260,10 @@ export function DataTable({
{isHarvestButtonDisabled ? ( - @@ -252,7 +271,7 @@ export function DataTable({ - @@ -304,7 +323,9 @@ export function DataTable({ data-state={ row.getIsSelected() && "selected" } - onClick={(event) => handleRowClick(row, event)} + onClick={(event) => + handleRowClick(row, event) + } > {row.getVisibleCells().map((cell) => ( @@ -317,49 +338,102 @@ export function DataTable({ {isMobile && expandedRows[row.id] && ( - - + +
-

Gewassen:

+

+ Gewassen: +

- {row.original.cultivations.map((cultivation: any) => ( - - {cultivation.b_lu_name} - - ))} + {row.original.cultivations.map( + ( + cultivation: any, + ) => ( + + { + cultivation.b_lu_name + } + + ), + )}
-

Bemesting met:

+

+ Bemesting met: +

- {row.original.fertilizerApplications.map((fertilizer: any) => ( - - {fertilizer.p_name_nl} - - ))} + {row.original.fertilizerApplications.map( + ( + fertilizer: any, + ) => ( + + { + fertilizer.p_name_nl + } + + ), + )}
-

OS (%):

-

{row.original.a_som_loi}

+

+ OS (%): +

+

+ { + row.original + .a_som_loi + } +

-

Bodemtype:

-

{row.original.b_soiltype_agr}

+

+ Bodemtype: +

+

+ { + row.original + .b_soiltype_agr + } +

-

Oppervlakte (ha):

-

{row.original.b_area}

+

+ Oppervlakte + (ha): +

+

+ { + row.original + .b_area + } +

From bf6952ac5ce847a3fe8e7642aff093db45a479e6 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:15:40 +0200 Subject: [PATCH 19/35] feat: stick the table to top of tyhe page when scrolling down --- fdm-app/app/components/blocks/fields/table.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index 1536f6cf1..4db75623f 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -189,8 +189,8 @@ export function DataTable({ : "Oogst toevoegen aan geselecteerde percelen" return ( -
-
+
+
({
-
+
- + {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { From e7068edd51135d8d7c142af517942bf755c99352 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:22:04 +0200 Subject: [PATCH 20/35] feat: improve format of text in table --- .../app/components/blocks/fields/columns.tsx | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index 489ca88c9..077bde381 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -1,5 +1,10 @@ import type { ColumnDef } from "@tanstack/react-table" -import { MoreHorizontal, ChevronRight, ChevronDown, ArrowUpRightFromSquare } from "lucide-react" +import { + MoreHorizontal, + ChevronRight, + ChevronDown, + ArrowUpRightFromSquare, +} from "lucide-react" import { NavLink } from "react-router-dom" import { Badge } from "~/components/ui/badge" import { DataTableColumnHeader } from "./column-header" @@ -165,9 +170,17 @@ export const columns: ColumnDef[] = [ enableSorting: true, sortingFn: "alphanumeric", header: ({ column }) => { - return + return }, enableHiding: true, // Enable hiding for mobile + cell: ({ row }) => { + const field = row.original + return ( +

+ {`${field.a_som_loi.toFixed(2)} %`} +

+ ) + }, }, { accessorKey: "b_soiltype_agr", @@ -177,20 +190,31 @@ export const columns: ColumnDef[] = [ return }, enableHiding: true, // Enable hiding for mobile + cell: ({ row }) => { + const field = row.original + return ( +

{field.b_soiltype_agr}

+ ) + }, }, { accessorKey: "b_area", enableSorting: true, sortingFn: "alphanumeric", header: ({ column }) => { + return + }, + enableHiding: true, // Enable hiding for mobile + cell: ({ row }) => { + const field = row.original return ( - +

+ {field.b_area < 0.1 + ? "< 0.1 ha" + : `${field.b_area.toFixed(1)} ha`} +

) }, - enableHiding: true, // Enable hiding for mobile }, { id: "actions", From 90c3cc487471a6d1ead75e5eb0788105a637a485 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 22 Sep 2025 16:59:47 +0200 Subject: [PATCH 21/35] feat: improve on mobile devices --- .../app/components/blocks/fields/columns.tsx | 12 +- .../app/components/blocks/fields/table.tsx | 314 +++++------------- ...farm.$b_id_farm.$calendar.field._index.tsx | 13 +- 3 files changed, 98 insertions(+), 241 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index 077bde381..03bdead8e 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -36,17 +36,7 @@ export type FieldExtended = { b_area: number } -export const columns: ColumnDef[] = [ - { - id: "expander", - header: () => null, - cell: ({ row }) => { - const isMobile = useIsMobile() - if (!isMobile) return null - return row.getIsExpanded() ? : - }, - enableHiding: true, - }, +export const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index 4db75623f..b5aa104dd 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -11,8 +11,8 @@ import { VisibilityState, } from "@tanstack/react-table" import { ChevronDown, Plus } from "lucide-react" -import { useMemo, useRef, useState } from "react" -import { NavLink, useParams } from "react-router" +import { useEffect, useMemo, useRef, useState } from "react" +import { NavLink, useParams } from "react-router-dom" import fuzzysort from "fuzzysort" import { Button } from "~/components/ui/button" import { Input } from "~/components/ui/input" @@ -31,6 +31,7 @@ import { TableHeader, TableRow, } from "~/components/ui/table" +import { cn } from "@/app/lib/utils" import { DropdownMenu, DropdownMenuCheckboxItem, @@ -38,9 +39,6 @@ import { DropdownMenuTrigger, } from "../../ui/dropdown-menu" import { useIsMobile } from "~/hooks/use-mobile" -import { Collapsible, CollapsibleContent } from "~/components/ui/collapsible" -import { Badge } from "~/components/ui/badge" -import { getCultivationColor } from "../../custom/cultivation-colors" interface DataTableProps { columns: ColumnDef[] @@ -54,18 +52,25 @@ export function DataTable({ const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState([]) const [globalFilter, setGlobalFilter] = useState("") + const isMobile = useIsMobile() const [columnVisibility, setColumnVisibility] = useState( - {}, + isMobile + ? { a_som_loi: false, b_soiltype_agr: false, b_area: false } + : {}, ) const [rowSelection, setRowSelection] = useState({}) - const [expandedRows, setExpandedRows] = useState>( - {}, - ) const lastSelectedRowIndex = useRef(null) + useEffect(() => { + setColumnVisibility( + isMobile + ? { a_som_loi: false, b_soiltype_agr: false, b_area: false } + : {}, + ) + }, [isMobile]) + const params = useParams() const b_id_farm = params.b_id_farm - const isMobile = useIsMobile() const handleRowClick = ( row: any, @@ -87,14 +92,6 @@ export function DataTable({ return } - if (isMobile) { - setExpandedRows((prev) => ({ - ...prev, - [row.id]: !prev[row.id], - })) - return - } - if (event.shiftKey && lastSelectedRowIndex.current !== null) { const currentIndex = row.index const start = Math.min(currentIndex, lastSelectedRowIndex.current) @@ -143,18 +140,7 @@ export function DataTable({ state: { sorting, columnFilters, - columnVisibility: isMobile - ? { - b_name: true, - select: true, - actions: true, - cultivations: false, - fertilizerApplications: false, - a_som_loi: false, - b_soiltype_agr: false, - b_area: false, - } - : columnVisibility, + columnVisibility, globalFilter, rowSelection, }, @@ -164,7 +150,7 @@ export function DataTable({ return table .getFilteredSelectedRowModel() .rows.map((row) => row.original) - }, [table]) + }, [table, rowSelection]) const canAddHarvest = useMemo(() => { if (selectedFields.length === 0) return false @@ -190,19 +176,19 @@ export function DataTable({ return (
-
+
setGlobalFilter(event.target.value)} - className="max-w-sm" + className="w-full sm:w-auto sm:flex-grow" /> - {!isMobile && ( +
- @@ -225,82 +211,60 @@ export function DataTable({ })} - )} - - - -
- {isFertilizerButtonDisabled ? ( - - ) : ( - - - - )} -
-
- -

{fertilizerTooltipContent}

-
-
-
- - - -
- {isHarvestButtonDisabled ? ( - - ) : ( - - - - )} -
-
- -

{harvestTooltipContent}

-
-
-
-
+ ) : ( + + + + )} +
+ + +

{fertilizerTooltipContent}

+
+ +
-
+
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( - + {header.isPlaceholder ? null : flexRender( @@ -317,130 +281,32 @@ export function DataTable({ {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - <> - - handleRowClick(row, event) - } - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - {isMobile && expandedRows[row.id] && ( - - - - -
-

- Gewassen: -

-
- {row.original.cultivations.map( - ( - cultivation: any, - ) => ( - - { - cultivation.b_lu_name - } - - ), - )} -
-
-
-

- Bemesting met: -

-
- {row.original.fertilizerApplications.map( - ( - fertilizer: any, - ) => ( - - { - fertilizer.p_name_nl - } - - ), - )} -
-
-
-

- OS (%): -

-

- { - row.original - .a_som_loi - } -

-
-
-

- Bodemtype: -

-

- { - row.original - .b_soiltype_agr - } -

-
-
-

- Oppervlakte - (ha): -

-

- { - row.original - .b_area - } -

-
-
-
-
-
- )} - + + handleRowClick(row, event) + } + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + )) ) : ( diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx index 204c146dc..10acb5605 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx @@ -37,6 +37,7 @@ import { getTimeBasedGreeting } from "~/lib/greetings" import { DataTable } from "../components/blocks/fields/table" import { columns } from "../components/blocks/fields/columns" import { FarmContent } from "../components/blocks/farm/farm-content" +import { BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator } from "../components/ui/breadcrumb" export const meta: MetaFunction = () => { return [ @@ -208,11 +209,11 @@ export default function FarmFieldIndex() { b_id_farm={loaderData.b_id_farm} farmOptions={loaderData.farmOptions} /> - + + + + Percelen +
{loaderData.fieldOptions.length === 0 ? ( @@ -242,7 +243,7 @@ export default function FarmFieldIndex() { From ef7e24f244e473211f770f6693d2d893a56d56b7 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:40:41 +0200 Subject: [PATCH 22/35] refactor: add tooltip to new field button --- .../app/components/blocks/fields/table.tsx | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index b5aa104dd..88d7a744c 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -169,11 +169,6 @@ export function DataTable({ ? "Selecteer één of meerdere percelen om bemesting toe te voegen" : "Bemesting toevoegen aan geselecteerde percelen" - const isHarvestButtonDisabled = !canAddHarvest - const harvestTooltipContent = isHarvestButtonDisabled - ? "Selecteer één of meerdere percelen met hetzelfde gewas om oogst toe te voegen" - : "Oogst toevoegen aan geselecteerde percelen" - return (
@@ -241,12 +236,21 @@ export function DataTable({ - - - + + + + + + + + +

Voeg een nieuw perceel toe

+
+
+
@@ -260,9 +264,11 @@ export function DataTable({ key={header.id} className={cn({ "sticky left-0 bg-background": - header.column.id === "select", + header.column.id === + "select", "sticky right-0 bg-background": - header.column.id === "actions", + header.column.id === + "actions", })} > {header.isPlaceholder @@ -297,7 +303,8 @@ export function DataTable({ "sticky left-0 bg-background": cell.column.id === "select", "sticky right-0 bg-background": - cell.column.id === "actions", + cell.column.id === + "actions", })} > {flexRender( From d0101a07e72a15e0346532a688af04be2ffc1de5 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:43:41 +0200 Subject: [PATCH 23/35] refactor: at Bekijk show the column names instead of id --- fdm-app/app/components/blocks/fields/table.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index 88d7a744c..51bfe6f3d 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -191,6 +191,14 @@ export function DataTable({ .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { + const columnNames: Record = { + b_name: "Naam", + cultivations: "Gewassen", + fertilizerApplications: "Bemesting met:", + a_som_loi: "OS", + b_soiltype_agr: "Bodemtype", + b_area: "Oppervlakte", + } return ( ({ column.toggleVisibility(!!value) } > - {column.id} + {columnNames[column.id] ?? column.id} ) })} From dd07ac80adf8a219962796570118807cc87b80c8 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:11:31 +0200 Subject: [PATCH 24/35] refactor: improve page titles --- .../routes/farm.$b_id_farm.$calendar.field._index.tsx | 11 ++++------- fdm-app/app/routes/farm.$b_id_farm._index.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx index 10acb5605..5d9da095f 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx @@ -194,7 +194,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { */ export default function FarmFieldIndex() { const loaderData = useLoaderData() - const greeting = getTimeBasedGreeting() return ( @@ -219,8 +218,8 @@ export default function FarmFieldIndex() { {loaderData.fieldOptions.length === 0 ? ( <> farm.b_id_farm === loaderData.b_id_farm)?.b_name_farm}`} + description="Dit bedrijf heeft nog geen percelen" />
@@ -241,10 +240,8 @@ export default function FarmFieldIndex() { ) : ( <> farm.b_id_farm === loaderData.b_id_farm)?.b_name_farm}`} + description="Selecteer een perceel voor details of voeg een nieuw perceel toe." />
diff --git a/fdm-app/app/routes/farm.$b_id_farm._index.tsx b/fdm-app/app/routes/farm.$b_id_farm._index.tsx index 189829342..15d9dc4df 100644 --- a/fdm-app/app/routes/farm.$b_id_farm._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm._index.tsx @@ -141,7 +141,12 @@ export default function FarmDashboardIndex() { />
- +
{/* Left Column */} From 5c8c567ee336b98b74eb232cae179c57832c63fb Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:11:45 +0200 Subject: [PATCH 25/35] chore: remove unused imports --- fdm-app/app/components/blocks/fields/columns.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index 03bdead8e..f1e37104c 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -1,8 +1,6 @@ import type { ColumnDef } from "@tanstack/react-table" import { MoreHorizontal, - ChevronRight, - ChevronDown, ArrowUpRightFromSquare, } from "lucide-react" import { NavLink } from "react-router-dom" @@ -18,7 +16,6 @@ import { import { Button } from "../../ui/button" import { Checkbox } from "~/components/ui/checkbox" import { getCultivationColor } from "../../custom/cultivation-colors" -import { useIsMobile } from "~/hooks/use-mobile" export type FieldExtended = { b_id: string From 3719eb1b9dc8cc4fef2da97f2df3f41fded9ebf8 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:54:40 +0200 Subject: [PATCH 26/35] feat: add page to add fertilizer application for multiple fields --- .../app/components/blocks/fields/table.tsx | 37 +- ...farm.$b_id_farm.$calendar.field._index.tsx | 13 +- ....$b_id_farm.$calendar.field.fertilizer.tsx | 466 ++++++++++++++++++ 3 files changed, 483 insertions(+), 33 deletions(-) create mode 100644 fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer.tsx diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index 51bfe6f3d..5b72acb69 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -12,7 +12,7 @@ import { } from "@tanstack/react-table" import { ChevronDown, Plus } from "lucide-react" import { useEffect, useMemo, useRef, useState } from "react" -import { NavLink, useParams } from "react-router-dom" +import { Link, NavLink, useParams } from "react-router-dom" import fuzzysort from "fuzzysort" import { Button } from "~/components/ui/button" import { Input } from "~/components/ui/input" @@ -71,6 +71,7 @@ export function DataTable({ const params = useParams() const b_id_farm = params.b_id_farm + const calendar = params.calendar const handleRowClick = ( row: any, @@ -146,22 +147,13 @@ export function DataTable({ }, }) + // biome-ignore lint/correctness/useExhaustiveDependencies: rowSelection is needed for Bemesting button activation const selectedFields = useMemo(() => { return table .getFilteredSelectedRowModel() .rows.map((row) => row.original) }, [table, rowSelection]) - const canAddHarvest = useMemo(() => { - if (selectedFields.length === 0) return false - const firstCultivation = selectedFields[0]?.cultivations[0]?.b_lu_name - return selectedFields.every( - (field) => - field.cultivations.length > 0 && - field.cultivations[0]?.b_lu_name === firstCultivation, - ) - }, [selectedFields]) - const selectedFieldIds = selectedFields.map((field) => field.b_id) const isFertilizerButtonDisabled = selectedFields.length === 0 @@ -191,14 +183,16 @@ export function DataTable({ .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { - const columnNames: Record = { - b_name: "Naam", - cultivations: "Gewassen", - fertilizerApplications: "Bemesting met:", - a_som_loi: "OS", - b_soiltype_agr: "Bodemtype", - b_area: "Oppervlakte", - } + const columnNames: Record = + { + b_name: "Naam", + cultivations: "Gewassen", + fertilizerApplications: + "Bemesting met:", + a_som_loi: "OS", + b_soiltype_agr: "Bodemtype", + b_area: "Oppervlakte", + } return ( ({ column.toggleVisibility(!!value) } > - {columnNames[column.id] ?? column.id} + {columnNames[column.id] ?? + column.id} ) })} @@ -229,7 +224,7 @@ export function DataTable({ ) : ( +
+ )} + + + + + + + + + + Percelen selecteren + + + Selecteer de percelen voor + de bemesting. + + +
+
+ {loaderData.fieldOptions.map( + (field) => ( +
+ + toggleSelection( + field.b_id!, + ) + } + /> + + + { + field.b_area + }{" "} + ha + +
+ ), + )} +
+
+ + + +
+
+
+ + + + Bemesting toevoegen + + {loaderData.fieldAmount === 0 + ? "Selecteer eerst een of meerdere percelen." + : loaderData.fieldAmount === 1 + ? "Voeg een nieuwe bemestingstoepassing toe aan het geselecteerde perceel." + : `Voeg een nieuwe bemestingstoepassing toe aan de ${loaderData.fieldAmount} geselecteerde percelen.`} + + + + {loaderData.fieldAmount > 0 ? ( + + ) : ( +
+

+ Selecteer eerst percelen in de + linkerkolom. +

+
+ )} +
+
+
+
+
+
+ + ) +} + +export async function action({ request, params }: ActionFunctionArgs) { + try { + const { b_id_farm, calendar = "all" } = params + if (!b_id_farm) { + throw new Error("Farm ID is missing") + } + + const session = await getSession(request) + const url = new URL(request.url) + const fieldIds = + url.searchParams.get("fieldIds")?.split(",").filter(Boolean) ?? [] + + if (!fieldIds || fieldIds.length === 0) { + return dataWithError(null, "Selecteer eerst een perceel.") + } + + const validatedData = await extractFormValuesFromRequest( + request, + FormSchema, + ) + + for (const fieldId of fieldIds) { + await addFertilizerApplication( + fdm, + session.principal_id, + fieldId, + validatedData.p_id, + validatedData.p_app_amount, + validatedData.p_app_method, + validatedData.p_app_date, + ) + } + + return redirectWithSuccess(`/farm/${b_id_farm}/${calendar}/field`, { + message: `Bemesting succesvol toegevoegd aan ${fieldIds.length} ${fieldIds.length === 1 ? "perceel" : "percelen"}.`, + }) + } catch (error) { + if (error instanceof z.ZodError) { + return dataWithError( + null, + "Invoer is ongeldig. Controleer het formulier.", + ) + } + throw handleActionError(error) + } +} From 500db6f33383df2cf2eba1592c120af9ef8eb034 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:13:53 +0200 Subject: [PATCH 27/35] refactor: improve quick actions --- fdm-app/app/routes/farm.$b_id_farm._index.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm._index.tsx b/fdm-app/app/routes/farm.$b_id_farm._index.tsx index 15d9dc4df..66d77a336 100644 --- a/fdm-app/app/routes/farm.$b_id_farm._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm._index.tsx @@ -157,7 +157,9 @@ export default function FarmDashboardIndex() { Snelle acties
- +
@@ -177,20 +179,21 @@ export default function FarmDashboardIndex() { - +
- +
- Oogst toevoegen + Perceelsoverzicht - Voor één of meerdere - percelen. + Uitgebreide tabel met o.a. gewassen en meststoffen per perceel.
@@ -215,7 +218,7 @@ export default function FarmDashboardIndex() {
- Nutriententenbalans + Nutriëntenbalans Aanvoer, afvoer en From 6a9a7446dbaae558a5b3c7e14c82624d4a0c2bd2 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:17:57 +0200 Subject: [PATCH 28/35] refactor: improve on mobile --- .../farm.$b_id_farm.$calendar.field.fertilizer.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer.tsx index 64f177e8c..4cbaee38a 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer.tsx @@ -249,8 +249,8 @@ export default function FarmFieldFertilizerAddIndex() {
)} -
- +
+ Geselecteerde percelen @@ -262,7 +262,7 @@ export default function FarmFieldFertilizerAddIndex() { {loaderData.selectedFields.length > 0 ? ( -
+
{loaderData.selectedFields.map( (field) => (
) : ( -
+

Geen percelen geselecteerd @@ -325,7 +325,7 @@ export default function FarmFieldFertilizerAddIndex() {
-
+
{loaderData.fieldOptions.map( (field) => (
Date: Tue, 23 Sep 2025 12:20:08 +0200 Subject: [PATCH 29/35] chore: fix imports --- .../app/components/blocks/fields/columns.tsx | 6 ++--- .../app/components/blocks/fields/table.tsx | 2 +- ...farm.$b_id_farm.$calendar.field._index.tsx | 22 +++++++++---------- ....$b_id_farm.$calendar.field.fertilizer.tsx | 10 ++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index f1e37104c..8beff36d0 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -12,10 +12,10 @@ import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, -} from "../../ui/dropdown-menu" -import { Button } from "../../ui/button" +} from "~/components/ui/dropdown-menu" +import { Button } from "~/components/ui/button" import { Checkbox } from "~/components/ui/checkbox" -import { getCultivationColor } from "../../custom/cultivation-colors" +import { getCultivationColor } from "~/components/custom/cultivation-colors" export type FieldExtended = { b_id: string diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index 5b72acb69..95e71d9b7 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -12,7 +12,7 @@ import { } from "@tanstack/react-table" import { ChevronDown, Plus } from "lucide-react" import { useEffect, useMemo, useRef, useState } from "react" -import { Link, NavLink, useParams } from "react-router-dom" +import { NavLink, useParams } from "react-router-dom" import fuzzysort from "fuzzysort" import { Button } from "~/components/ui/button" import { Input } from "~/components/ui/input" diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx index b15c170d7..ac1ae5584 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx @@ -23,10 +23,10 @@ import { getTimeframe } from "~/lib/calendar" import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" -import { DataTable } from "../components/blocks/fields/table" -import { columns } from "../components/blocks/fields/columns" -import { FarmContent } from "../components/blocks/farm/farm-content" -import { BreadcrumbItem, BreadcrumbSeparator } from "../components/ui/breadcrumb" +import { DataTable } from "~/components/blocks/fields/table" +import { columns } from "~/components/blocks/fields/columns" +import { FarmContent } from "~/components/blocks/farm/farm-content" +import { BreadcrumbItem, BreadcrumbSeparator } from "~/components/ui/breadcrumb" export const meta: MetaFunction = () => { return [ @@ -151,7 +151,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { cultivations: cultivations, fertilizerApplications: fertilizerApplications, a_som_loi: a_som_loi, - b_soiltype_agr: b_soiltype_agr, + b_soiltype_agr: b_soiltype_agr, b_area: Math.round(field.b_area * 10) / 10, } }), @@ -197,17 +197,17 @@ export default function FarmFieldIndex() { b_id_farm={loaderData.b_id_farm} farmOptions={loaderData.farmOptions} /> - - - + + + Percelen - +
{loaderData.fieldOptions.length === 0 ? ( <> farm.b_id_farm === loaderData.b_id_farm)?.b_name_farm}`} + title={`Percelen van ${loaderData.farmOptions.find((farm) => farm.b_id_farm === loaderData.b_id_farm)?.b_name_farm}`} description="Dit bedrijf heeft nog geen percelen" />
@@ -229,7 +229,7 @@ export default function FarmFieldIndex() { ) : ( <> farm.b_id_farm === loaderData.b_id_farm)?.b_name_farm}`} + title={`Percelen van ${loaderData.farmOptions.find((farm) => farm.b_id_farm === loaderData.b_id_farm)?.b_name_farm}`} description="Selecteer een perceel voor details of voeg een nieuw perceel toe." /> diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer.tsx index 4cbaee38a..3901e6db3 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer.tsx @@ -27,12 +27,12 @@ import { getCalendar, getTimeframe } from "~/lib/calendar" import { clientConfig } from "~/lib/config" import { handleActionError, handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" -import { FarmContent } from "../components/blocks/farm/farm-content" +import { FarmContent } from "~/components/blocks/farm/farm-content" import { BreadcrumbItem, BreadcrumbSeparator, } from "../components/ui/breadcrumb" -import { FertilizerApplicationForm } from "../components/blocks/fertilizer-applications/form" +import { FertilizerApplicationForm } from "~/components/blocks/fertilizer-applications/form" import { Card, CardContent, @@ -40,7 +40,7 @@ import { CardFooter, CardHeader, CardTitle, -} from "../components/ui/card" +} from "~/components/ui/card" import { Badge } from "~/components/ui/badge" import { useState } from "react" import { @@ -54,12 +54,12 @@ import { } from "~/components/ui/dialog" import { Checkbox } from "~/components/ui/checkbox" import { Label } from "~/components/ui/label" -import { FormSchema } from "../components/blocks/fertilizer-applications/formschema.tsx" +import { FormSchema } from "~/components/blocks/fertilizer-applications/formschema" import { dataWithError, redirectWithSuccess } from "remix-toast" import { z } from "zod" import { Info } from "lucide-react" import { LoadingSpinner } from "~/components/custom/loadingspinner" -import { extractFormValuesFromRequest } from "../lib/form" +import { extractFormValuesFromRequest } from "~/lib/form" export const meta: MetaFunction = () => { return [ From 47acfac9356919a058cea586e265803d8c109c9c Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:23:33 +0200 Subject: [PATCH 30/35] chore: add changesets --- .changeset/shaggy-snails-tickle.md | 5 +++++ .changeset/wicked-boats-hope.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/shaggy-snails-tickle.md create mode 100644 .changeset/wicked-boats-hope.md diff --git a/.changeset/shaggy-snails-tickle.md b/.changeset/shaggy-snails-tickle.md new file mode 100644 index 000000000..292c63e3a --- /dev/null +++ b/.changeset/shaggy-snails-tickle.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Add new page to add fertilizer application to multiple fields at once diff --git a/.changeset/wicked-boats-hope.md b/.changeset/wicked-boats-hope.md new file mode 100644 index 000000000..489016749 --- /dev/null +++ b/.changeset/wicked-boats-hope.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Add new page showing an advanced table for the fields of the farm, including searching on field name, cultivations and fertilizers. It also included multiple selection of fields to add a new fertilizer application From 3e0083b47a1bfc19f12b144c7e460d33450ff801 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:30:02 +0200 Subject: [PATCH 31/35] Update fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Sven Verweij <37927107+SvenVw@users.noreply.github.com> --- .../farm.$b_id_farm.$calendar.field._index.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx index ac1ae5584..0dba144c0 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx @@ -132,18 +132,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { field.b_id, timeframe, ) - const a_som_loi = currentSoilData.find((x) => { - if (x.parameter === "a_som_loi") { - return true - } - return false - }).value - const b_soiltype_agr = currentSoilData.find((x) => { - if (x.parameter === "b_soiltype_agr") { - return true - } - return false - }).value + const a_som_loi = currentSoilData.find(x => x.parameter === "a_som_loi")?.value ?? null + const b_soiltype_agr = currentSoilData.find(x => x.parameter === "b_soiltype_agr")?.value ?? null return { b_id: field.b_id, From 9d0fa316db1f45d7ff8dea0d24cf687619806b1b Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:47:55 +0200 Subject: [PATCH 32/35] nitpicks --- .changeset/purple-dryers-rule.md | 2 +- .changeset/shaggy-snails-tickle.md | 2 +- .changeset/wicked-boats-hope.md | 2 +- fdm-app/app/components/blocks/fields/columns.tsx | 11 ++++------- fdm-app/app/components/blocks/fields/table.tsx | 12 +++++++----- .../farm.$b_id_farm.$calendar.field._index.tsx | 9 +++++++-- .../farm.$b_id_farm.$calendar.field.fertilizer.tsx | 9 ++++++--- fdm-app/app/routes/farm._index.tsx | 2 +- 8 files changed, 28 insertions(+), 21 deletions(-) diff --git a/.changeset/purple-dryers-rule.md b/.changeset/purple-dryers-rule.md index 8b5900541..6a28f6c51 100644 --- a/.changeset/purple-dryers-rule.md +++ b/.changeset/purple-dryers-rule.md @@ -2,4 +2,4 @@ "@svenvw/fdm-app": minor --- -Adds a new farm dashboard page with an overview of the farm and links to apps, pages for data and quick actions +Adds a new farm dashboard page with an overview of the farm and links to apps, data pages, and quick actions. \ No newline at end of file diff --git a/.changeset/shaggy-snails-tickle.md b/.changeset/shaggy-snails-tickle.md index 292c63e3a..60adfee65 100644 --- a/.changeset/shaggy-snails-tickle.md +++ b/.changeset/shaggy-snails-tickle.md @@ -2,4 +2,4 @@ "@svenvw/fdm-app": minor --- -Add new page to add fertilizer application to multiple fields at once +Add a new page to apply a fertilizer application to multiple fields at once. diff --git a/.changeset/wicked-boats-hope.md b/.changeset/wicked-boats-hope.md index 489016749..b7d61ee7c 100644 --- a/.changeset/wicked-boats-hope.md +++ b/.changeset/wicked-boats-hope.md @@ -2,4 +2,4 @@ "@svenvw/fdm-app": minor --- -Add new page showing an advanced table for the fields of the farm, including searching on field name, cultivations and fertilizers. It also included multiple selection of fields to add a new fertilizer application +Add a new page showing an advanced table for the fields of the farm, including searching on field name, cultivations, and fertilizers. It also includes multi‑selection of fields to add a new fertilizer application. diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index 8beff36d0..74003fe12 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -1,8 +1,5 @@ import type { ColumnDef } from "@tanstack/react-table" -import { - MoreHorizontal, - ArrowUpRightFromSquare, -} from "lucide-react" +import { MoreHorizontal, ArrowUpRightFromSquare } from "lucide-react" import { NavLink } from "react-router-dom" import { Badge } from "~/components/ui/badge" import { DataTableColumnHeader } from "./column-header" @@ -33,7 +30,7 @@ export type FieldExtended = { b_area: number } -export const columns: ColumnDef[] = [ +export const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -98,9 +95,9 @@ export const columns: ColumnDef[] = [ return (
- {cultivationsSorted.map((cultivation) => ( + {cultivationsSorted.map((cultivation, idx) => ( { @@ -74,7 +76,7 @@ export function DataTable({ const calendar = params.calendar const handleRowClick = ( - row: any, + row: Row, event: React.MouseEvent, ) => { // Check if the clicked element or any of its parents is an anchor tag @@ -114,7 +116,7 @@ export function DataTable({ lastSelectedRowIndex.current = row.index } - const fuzzyFilter = (row: any, columnId: string, filterValue: string) => { + const fuzzyFilter: FilterFn = (row, _columnId, filterValue) => { const cultivationNames = row.original.cultivations .map((c: { b_lu_name: string }) => c.b_lu_name) .join(" ") @@ -224,7 +226,7 @@ export function DataTable({ ) : (