Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7827f3b
feat: setup a farm dashboard with a list of apps
SvenVw Sep 18, 2025
4c884eb
feat: add card to farm dashboard with farm data
SvenVw Sep 18, 2025
9762e13
feat: add a quick action menu for farm dashboard
SvenVw Sep 18, 2025
08f4b5b
feat: add some shadow on hover
SvenVw Sep 18, 2025
92e7b30
refactor: improve design of the new farm dashboard page
SvenVw Sep 18, 2025
b030040
feat: replace list of fields with a table
SvenVw Sep 19, 2025
b21a4ec
feat: show cultivations in table as well
SvenVw Sep 19, 2025
2ef8ff0
fix: name of column
SvenVw Sep 19, 2025
48b7d73
feat: enable filtering on cultivations as well
SvenVw Sep 19, 2025
78288a6
feat: use the cultivation color for the badge
SvenVw Sep 19, 2025
68a28fc
refactor: sort cultivation based on start date
SvenVw Sep 19, 2025
2bb6f34
feat: add fertilizer applications to the fields table as well
SvenVw Sep 19, 2025
6b0c9d3
feat: add alphabetically sorting to the table
SvenVw Sep 19, 2025
aa910e6
feat: add a_som_loi and b_soiltype_agr to table
SvenVw Sep 19, 2025
5d42a94
feat: add buttons to add fertilizer application and harvest for multi…
SvenVw Sep 19, 2025
12263f0
feat: enable shift selection and easier row selection
SvenVw Sep 19, 2025
925d6bf
feat: enable mobile view of table
SvenVw Sep 22, 2025
5c08685
feat: show redirect icon on hover
SvenVw Sep 22, 2025
bf6952a
feat: stick the table to top of tyhe page when scrolling down
SvenVw Sep 22, 2025
e7068ed
feat: improve format of text in table
SvenVw Sep 22, 2025
90c3cc4
feat: improve on mobile devices
SvenVw Sep 22, 2025
ef7e24f
refactor: add tooltip to new field button
SvenVw Sep 23, 2025
d0101a0
refactor: at Bekijk show the column names instead of id
SvenVw Sep 23, 2025
dd07ac8
refactor: improve page titles
SvenVw Sep 23, 2025
5c8c567
chore: remove unused imports
SvenVw Sep 23, 2025
3719eb1
feat: add page to add fertilizer application for multiple fields
SvenVw Sep 23, 2025
500db6f
refactor: improve quick actions
SvenVw Sep 23, 2025
6a9a744
refactor: improve on mobile
SvenVw Sep 23, 2025
ffc35a1
chore: fix imports
SvenVw Sep 23, 2025
47acfac
chore: add changesets
SvenVw Sep 23, 2025
2d3b89a
Merge branch 'development' into FDM-269
SvenVw Sep 23, 2025
3e0083b
Update fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx
SvenVw Sep 23, 2025
9d0fa31
nitpicks
SvenVw Sep 23, 2025
d8963c3
refactor: improve fuzzy search performance
SvenVw Sep 23, 2025
ed0219e
fix: import statement
SvenVw Sep 25, 2025
6ea7a67
fix: prevent double-toggle when clicking checkboxes/buttons/menus ins…
SvenVw Sep 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/purple-dryers-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@svenvw/fdm-app": minor
---

Adds a new farm dashboard page with an overview of the farm and links to apps, data pages, and quick actions.
5 changes: 5 additions & 0 deletions .changeset/shaggy-snails-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@svenvw/fdm-app": minor
---

Add a new page to apply a fertilizer application to multiple fields at once.
5 changes: 5 additions & 0 deletions .changeset/wicked-boats-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@svenvw/fdm-app": minor
---

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.
71 changes: 71 additions & 0 deletions fdm-app/app/components/blocks/fields/column-header.tsx
Original file line number Diff line number Diff line change
@@ -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<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>
title: string
}

export function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>
}

return (
<div className={cn("flex items-center space-x-2", className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
<ArrowDown />
) : column.getIsSorted() === "asc" ? (
<ArrowUp />
) : (
<ChevronsUpDown />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => column.toggleSorting(false)}
>
<ArrowUp className="h-3.5 w-3.5 text-muted-foreground/70" />
Oplopend
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => column.toggleSorting(true)}
>
<ArrowDown className="h-3.5 w-3.5 text-muted-foreground/70" />
Aflopend
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => column.toggleVisibility(false)}
>
<EyeOff className="h-3.5 w-3.5 text-muted-foreground/70" />
Verberg
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
259 changes: 259 additions & 0 deletions fdm-app/app/components/blocks/fields/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import type { ColumnDef } from "@tanstack/react-table"
import { MoreHorizontal, ArrowUpRightFromSquare } from "lucide-react"
import { NavLink } from "react-router-dom"
import { Badge } from "~/components/ui/badge"
import { DataTableColumnHeader } from "./column-header"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu"
import { Button } from "~/components/ui/button"
import { Checkbox } from "~/components/ui/checkbox"
import { getCultivationColor } from "~/components/custom/cultivation-colors"

export type FieldExtended = {
b_id: string
b_name: string
cultivations: {
b_lu_name: string
b_lu_croprotation: string
b_lu_start: Date
}[]
fertilizerApplications: {
p_name_nl: string
}[]
a_som_loi: number
b_soiltype_agr: string
b_area: number
}

export const columns: ColumnDef<FieldExtended>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "b_name",
enableSorting: true,
header: ({ column }) => {
return <DataTableColumnHeader column={column} title="Naam" />
},
cell: ({ row }) => {
const field = row.original

return (
<NavLink
to={`./${field.b_id}`}
className="group flex items-center hover:underline w-fit"
>
{field.b_name}
<ArrowUpRightFromSquare className="ml-2 h-4 w-4 text-gray-500 opacity-0 group-hover:opacity-100 transition-opacity" />
</NavLink>
)
},
},
{
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 <DataTableColumnHeader column={column} title="Gewassen" />
},
cell: ({ row }) => {
const field = row.original

const cultivationsSorted = [...field.cultivations].sort((a, b) =>
a.b_lu_name.localeCompare(b.b_lu_name),
)

return (
<div className="flex items-start flex-col space-y-2">
{cultivationsSorted.map((cultivation, idx) => (
<Badge
key={`${cultivation.b_lu_name}-${idx}`}
style={{
backgroundColor: getCultivationColor(
cultivation.b_lu_croprotation,
),
}}
className="text-white"
variant="default"
>
{cultivation.b_lu_name}
</Badge>
))}
</div>
)
},
enableHiding: true, // Enable hiding for mobile
},
{
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 (
<DataTableColumnHeader column={column} title="Bemesting met:" />
)
},
cell: ({ row }) => {
const field = row.original

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 (
<div className="flex items-start flex-col space-y-2">
{uniqueFertilizerNames.map((fertilizer) => (
<Badge key={fertilizer} variant="outline">
{fertilizer}
</Badge>
))}
</div>
)
},
enableHiding: true, // Enable hiding for mobile
},
{
accessorKey: "a_som_loi",
enableSorting: true,
sortingFn: "alphanumeric",
header: ({ column }) => {
return <DataTableColumnHeader column={column} title="OS" />
},
enableHiding: true, // Enable hiding for mobile
cell: ({ row }) => {
const field = row.original
return (
<p className="text-muted-foreground">
{`${field.a_som_loi.toFixed(2)} %`}
</p>
)
},
},
{
accessorKey: "b_soiltype_agr",
enableSorting: true,
sortingFn: "alphanumeric",
header: ({ column }) => {
return <DataTableColumnHeader column={column} title="Bodemtype" />
},
enableHiding: true, // Enable hiding for mobile
cell: ({ row }) => {
const field = row.original
return (
<p className="text-muted-foreground">{field.b_soiltype_agr}</p>
)
},
},
{
accessorKey: "b_area",
enableSorting: true,
sortingFn: "alphanumeric",
header: ({ column }) => {
return <DataTableColumnHeader column={column} title="Oppervlakte" />
},
enableHiding: true, // Enable hiding for mobile
cell: ({ row }) => {
const field = row.original
return (
<p className="text-muted-foreground">
{field.b_area < 0.1
? "< 0.1 ha"
: `${field.b_area.toFixed(1)} ha`}
</p>
)
},
},
{
id: "actions",
enableHiding: false,
cell: ({ row }) => {
const field = row.original

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{/* <DropdownMenuLabel>Acties</DropdownMenuLabel>
<DropdownMenuItem
onClick={() =>
navigator.clipboard.writeText(field.b_id)
}
>
Kopieer perceel id
</DropdownMenuItem>
<DropdownMenuSeparator /> */}
<DropdownMenuLabel>Gegevens</DropdownMenuLabel>
<DropdownMenuItem>
<NavLink to={`./${field.b_id}`}>Overzicht</NavLink>
</DropdownMenuItem>
<DropdownMenuItem>
<NavLink to={`./${field.b_id}/cultivation`}>
Gewassen
</NavLink>
</DropdownMenuItem>
<DropdownMenuItem>
<NavLink to={`./${field.b_id}/fertilizer`}>
Bemesting
</NavLink>
</DropdownMenuItem>
<DropdownMenuItem>
<NavLink to={`./${field.b_id}/soil`}>Bodem</NavLink>
</DropdownMenuItem>
<DropdownMenuItem>
<NavLink to={`./${field.b_id}/atlas`}>
Kaart
</NavLink>
</DropdownMenuItem>
<DropdownMenuItem>
<NavLink to={`./${field.b_id}/delete`}>
Verwijderen
</NavLink>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
},
]
Loading