Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/polite-falcons-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@svenvw/fdm-core": minor
"@svenvw/fdm-app": minor
---

There is a new year selection drop-down in the farm creation fields page in case the farm has fields from multiple years.
6 changes: 6 additions & 0 deletions .changeset/ten-days-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@svenvw/fdm-core": minor
"@svenvw/fdm-app": minor
---

When an user uploads their shapefile obtained from mijnpercelen, they are now redirected to a different year in case the farm doesn't have any fields from the current year still.
55 changes: 54 additions & 1 deletion fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getFarm, getFields } from "@svenvw/fdm-core"
import { getCalendarYears, getFarm, getFields } from "@svenvw/fdm-core"
import { ArrowLeft } from "lucide-react"
import {
data,
Expand All @@ -7,6 +7,7 @@ import {
NavLink,
Outlet,
useLoaderData,
useNavigate,
} from "react-router"
import { Header } from "~/components/blocks/header/base"
import { HeaderFarmCreate } from "~/components/blocks/header/create-farm"
Expand All @@ -20,6 +21,13 @@ import { clientConfig } from "~/lib/config"
import { handleLoaderError } from "~/lib/error"
import { fdm } from "~/lib/fdm.server"
import { cn } from "~/lib/utils"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select"

// Meta
export const meta: MetaFunction = () => {
Expand Down Expand Up @@ -91,11 +99,18 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
}
})

const calendarYears = await getCalendarYears(
fdm,
session.principal_id,
b_id_farm,
)

return {
sidebarPageItems: sidebarPageItems,
b_id_farm: b_id_farm,
b_name_farm: farm.b_name_farm,
calendar: calendar,
calendarYears: calendarYears,
}
} catch (error) {
throw handleLoaderError(error)
Expand All @@ -105,7 +120,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
// Main
export default function Index() {
const loaderData = useLoaderData<typeof loader>()
const navigate = useNavigate()

const handleCalendarSelect = (year: string) => {
navigate(`/farm/create/${loaderData.b_id_farm}/${year}/fields`)
}
return (
<SidebarInset>
<Header action={undefined}>
Expand Down Expand Up @@ -147,6 +166,40 @@ export default function Index() {
<div className="space-y-6 pb-0">
<div className="flex flex-col space-y-0 lg:flex-row lg:space-x-4 lg:space-y-0">
<aside className="lg:w-1/5">
{loaderData.calendarYears.length > 1 && (
<p className="flex flex-row items-baseline mb-4">
<label
htmlFor="calendar-select"
className="mr-2"
>
Kies een jaar:
</label>
<Select
defaultValue={loaderData.calendar}
name="role"
onValueChange={handleCalendarSelect}
>
<SelectTrigger
id="calendar-select"
className="flex-1"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{loaderData.calendarYears.map(
(year) => (
<SelectItem
key={year}
value={year}
>
{year}
</SelectItem>
),
)}
</SelectContent>
</Select>
</p>
)}
<SidebarPage
items={loaderData.sidebarPageItems}
>
Expand Down
39 changes: 38 additions & 1 deletion fdm-app/app/routes/farm.create.$b_id_farm.$calendar.upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import type {
MetaFunction,
} from "react-router"
import { data, useLoaderData } from "react-router"
import { dataWithWarning, redirectWithSuccess } from "remix-toast"
import {
dataWithWarning,
redirectWithError,
redirectWithSuccess,
} from "remix-toast"
import { combine, parseDbf, parseShp } from "shpjs"
import { MijnPercelenUploadForm } from "@/app/components/blocks/mijnpercelen/form-upload"
import { Header } from "~/components/blocks/header/base"
Expand Down Expand Up @@ -176,6 +180,10 @@ export async function action({ request, params }: ActionFunctionArgs) {
},
)

const calendarYears = new Set()
let minCalendarYear = Number.POSITIVE_INFINITY
let maxCalendarYear = 0

for (const feature of features) {
const { properties, geometry } = feature
const {
Expand Down Expand Up @@ -256,6 +264,35 @@ export async function action({ request, params }: ActionFunctionArgs) {
estimates,
)
}

calendarYears.add(b_start.getFullYear().toString())
minCalendarYear = Math.min(minCalendarYear, b_start.getFullYear())
maxCalendarYear = Math.max(maxCalendarYear, b_start.getFullYear())
}

if (calendarYears.size === 0) {
return redirectWithError(
`/farm/create/${b_id_farm}/${calendar}/fields`,
{
message: "Geen percelen zijn geïmporteerd.",
},
)
}

if (!calendarYears.has(calendar)) {
try {
const redirectCalendar = Math.max(
minCalendarYear,
Math.min(maxCalendarYear, Number.parseInt(calendar)),
)

return redirectWithSuccess(
`/farm/create/${b_id_farm}/${redirectCalendar}/fields`,
{
message: `Geen percelen in ${calendar} zijn geïmporteerd. Je bekijkt nu de percelen in ${redirectCalendar}.`,
},
)
} catch (_) {}
}
Comment on lines +282 to 296
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Clamping picks unavailable years; choose the nearest available year instead.

Current logic clamps to [min,max] but may still select a year that has no parcels (e.g., available {2018,2020,2022} and requested 2021 → 2021). Pick the closest available year.

Apply this diff:

-        if (!calendarYears.has(calendar)) {
-            try {
-                const redirectCalendar = Math.max(
-                    minCalendarYear,
-                    Math.min(maxCalendarYear, Number.parseInt(calendar)),
-                )
-
-                return redirectWithSuccess(
-                    `/farm/create/${b_id_farm}/${redirectCalendar}/fields`,
-                    {
-                        message: `Geen percelen in ${calendar} zijn geïmporteerd. Je bekijkt nu de percelen in ${redirectCalendar}.`,
-                    },
-                )
-            } catch (_) {}
-        }
+        if (!calendarYears.has(Number.parseInt(calendar, 10))) {
+            const years = Array.from(calendarYears).sort((a, b) => a - b)
+            const req = Number.parseInt(calendar, 10)
+            // Prefer the latest year <= requested; otherwise the earliest >= requested; fallback to most recent
+            const lower = years.filter((y) => y <= req).pop()
+            const higher = years.find((y) => y >= req)
+            const redirectCalendar = lower ?? higher ?? years[years.length - 1]
+
+            return redirectWithSuccess(
+                `/farm/create/${b_id_farm}/${redirectCalendar}/fields`,
+                {
+                    message: `Geen percelen in ${calendar} zijn geïmporteerd. Je bekijkt nu de percelen in ${redirectCalendar}.`,
+                },
+            )
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!calendarYears.has(calendar)) {
try {
const redirectCalendar = Math.max(
minCalendarYear,
Math.min(maxCalendarYear, Number.parseInt(calendar)),
)
return redirectWithSuccess(
`/farm/create/${b_id_farm}/${redirectCalendar}/fields`,
{
message: `Geen percelen in ${calendar} zijn geïmporteerd. Je bekijkt nu de percelen in ${redirectCalendar}.`,
},
)
} catch (_) {}
}
if (!calendarYears.has(Number.parseInt(calendar, 10))) {
const years = Array.from(calendarYears).sort((a, b) => a - b)
const req = Number.parseInt(calendar, 10)
// Prefer the latest year <= requested; otherwise the earliest >= requested; fallback to most recent
const lower = years.filter((y) => y <= req).pop()
const higher = years.find((y) => y >= req)
const redirectCalendar = lower ?? higher ?? years[years.length - 1]
return redirectWithSuccess(
`/farm/create/${b_id_farm}/${redirectCalendar}/fields`,
{
message: `Geen percelen in ${calendar} zijn geïmporteerd. Je bekijkt nu de percelen in ${redirectCalendar}.`,
},
)
}
🤖 Prompt for AI Agents
In fdm-app/app/routes/farm.create.$b_id_farm.$calendar.upload.tsx around lines
282-296, the code currently clamps the requested year to min/max which can still
select a year that has no parcels; instead compute the numeric requested year,
iterate the available calendarYears to find the single year with the smallest
absolute difference (tie-breaker: prefer the larger or smaller year? pick the
nearer; if equal choose the lower or higher consistently — pick the lower for
determinism), use that nearest available year as redirectCalendar, and return
redirectWithSuccess to `/farm/create/${b_id_farm}/${redirectCalendar}/fields`
with the same message; if calendarYears is empty fallback to the existing clamp
behavior or abort gracefully; remove the silent empty catch (or at least log the
error) so failures are visible.


return redirectWithSuccess(
Expand Down
43 changes: 43 additions & 0 deletions fdm-core/src/farm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,49 @@ export async function addFarm(
}
}

/**
* Retrieves the calendar years during which the farm has fields, after verifying that the requesting principal has read access.
*
* This function checks the principal's permissions before querying the database for the farm identified by the provided ID.
*
* @param fdm The FDM instance providing the connection to the database. The instance can be created with {@link createFdmServer}.
* @param principal_id - The identifier of the principal making the request.
* @param b_id_farm - The unique identifier of the farm to retrieve.
* @returns A Promise that resolves with a string array of calendar years.
* @throws {Error} If permission checks fail or if an error occurs while retrieving the calendar years.
* @alpha
*/
export async function getCalendarYears(
fdm: FdmType,
principal_id: string,
b_id_farm: string,
): Promise<string[]> {
try {
return await fdm.transaction(async (tx: FdmType) => {
await checkPermission(
tx,
"farm",
"read",
b_id_farm,
principal_id,
"getCalendarYears",
)

const results = await tx
.selectDistinct({ b_start: schema.fieldAcquiring.b_start })
.from(schema.fieldAcquiring)
.where(eq(schema.fieldAcquiring.b_id_farm, b_id_farm))
.orderBy(asc(schema.fieldAcquiring.b_start))

return results.map(({ b_start }) =>
b_start.getFullYear().toString(),
)
})
} catch (err) {
throw handleError(err, "Exception for getCalendarYears")
}
}
Comment on lines +75 to +116
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure unique, sorted years and avoid timezone drift.

Current query selects distinct b_start timestamps, then maps to years. Different dates within the same year will produce duplicate years; also .getFullYear() is locale‑dependent. Use UTC and dedupe/sort years before returning.

Apply this diff:

-            const results = await tx
-                .selectDistinct({ b_start: schema.fieldAcquiring.b_start })
-                .from(schema.fieldAcquiring)
-                .where(eq(schema.fieldAcquiring.b_id_farm, b_id_farm))
-                .orderBy(asc(schema.fieldAcquiring.b_start))
-
-            return results.map(({ b_start }) =>
-                b_start.getFullYear().toString(),
-            )
+            const results = await tx
+                .selectDistinct({ b_start: schema.fieldAcquiring.b_start })
+                .from(schema.fieldAcquiring)
+                .where(eq(schema.fieldAcquiring.b_id_farm, b_id_farm))
+
+            const years = Array.from(
+                new Set(results.map(({ b_start }) => b_start.getUTCFullYear())),
+            ).sort((a, b) => a - b)
+
+            return years.map(String)

Also add minimal context to the error for parity with other methods:

-    } catch (err) {
-        throw handleError(err, "Exception for getCalendarYears")
-    }
+    } catch (err) {
+        throw handleError(err, "Exception for getCalendarYears", {
+            b_id_farm,
+            principal_id,
+        })
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Retrieves the calendar years during which the farm has fields, after verifying that the requesting principal has read access.
*
* This function checks the principal's permissions before querying the database for the farm identified by the provided ID.
*
* @param fdm The FDM instance providing the connection to the database. The instance can be created with {@link createFdmServer}.
* @param principal_id - The identifier of the principal making the request.
* @param b_id_farm - The unique identifier of the farm to retrieve.
* @returns A Promise that resolves with a string array of calendar years.
* @throws {Error} If permission checks fail or if an error occurs while retrieving the calendar years.
* @alpha
*/
export async function getCalendarYears(
fdm: FdmType,
principal_id: string,
b_id_farm: string,
): Promise<string[]> {
try {
return await fdm.transaction(async (tx: FdmType) => {
await checkPermission(
tx,
"farm",
"read",
b_id_farm,
principal_id,
"getCalendarYears",
)
const results = await tx
.selectDistinct({ b_start: schema.fieldAcquiring.b_start })
.from(schema.fieldAcquiring)
.where(eq(schema.fieldAcquiring.b_id_farm, b_id_farm))
.orderBy(asc(schema.fieldAcquiring.b_start))
return results.map(({ b_start }) =>
b_start.getFullYear().toString(),
)
})
} catch (err) {
throw handleError(err, "Exception for getCalendarYears")
}
}
/**
* Retrieves the calendar years during which the farm has fields, after verifying that the requesting principal has read access.
*
* This function checks the principal's permissions before querying the database for the farm identified by the provided ID.
*
* @param fdm The FDM instance providing the connection to the database. The instance can be created with {@link createFdmServer}.
* @param principal_id - The identifier of the principal making the request.
* @param b_id_farm - The unique identifier of the farm to retrieve.
* @returns A Promise that resolves with a string array of calendar years.
* @throws {Error} If permission checks fail or if an error occurs while retrieving the calendar years.
* @alpha
*/
export async function getCalendarYears(
fdm: FdmType,
principal_id: string,
b_id_farm: string,
): Promise<string[]> {
try {
return await fdm.transaction(async (tx: FdmType) => {
await checkPermission(
tx,
"farm",
"read",
b_id_farm,
principal_id,
"getCalendarYears",
)
const results = await tx
.selectDistinct({ b_start: schema.fieldAcquiring.b_start })
.from(schema.fieldAcquiring)
.where(eq(schema.fieldAcquiring.b_id_farm, b_id_farm))
const years = Array.from(
new Set(results.map(({ b_start }) => b_start.getUTCFullYear())),
).sort((a, b) => a - b)
return years.map(String)
})
} catch (err) {
throw handleError(err, "Exception for getCalendarYears", {
b_id_farm,
principal_id,
})
}
}
🤖 Prompt for AI Agents
In fdm-core/src/farm.ts around lines 75 to 116, the function returns distinct
b_start timestamps which can map to duplicate years and .getFullYear() can be
affected by local timezone; change the logic to extract the UTC year (e.g.,
using getUTCFullYear or equivalent) from each b_start, collect the years into a
Set to dedupe, convert to an array, sort ascending, and return that array of
unique string years; also include minimal context in the thrown error (e.g.,
"Exception for getCalendarYears: ") when rethrowing via handleError so it
parallels other methods.


/**
* Retrieves a farm's details after verifying that the requesting principal has read access.
*
Expand Down
1 change: 1 addition & 0 deletions fdm-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export {
} from "./derogation"
export {
addFarm,
getCalendarYears,
getFarm,
getFarms,
grantRoleToFarm,
Expand Down