From b7adc202bacdf0d8714a980081237de7b22923d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 24 Nov 2025 17:15:03 +0100 Subject: [PATCH 01/45] Add hasPermission function --- fdm-core/src/authorization.ts | 157 ++++++++++++++++++++++++---------- fdm-core/src/index.ts | 1 + 2 files changed, 114 insertions(+), 44 deletions(-) diff --git a/fdm-core/src/authorization.ts b/fdm-core/src/authorization.ts index a6c4e5142..9965cf6dd 100644 --- a/fdm-core/src/authorization.ts +++ b/fdm-core/src/authorization.ts @@ -160,50 +160,19 @@ export async function checkPermission( resource_id: string, principal_id: PrincipalId, origin: string, -): Promise { +) { const start = performance.now() - - let isAllowed = false - let granting_resource = "" - let granting_resource_id = "" try { - const roles = getRolesForAction(action, resource) - const chain = await getResourceChain(fdm, resource, resource_id) - - // Convert principal_id to array - const principal_ids = Array.isArray(principal_id) - ? principal_id - : [principal_id] + const permission = await getPermission( + fdm, + resource, + action, + resource_id, + principal_id, + ) - await fdm.transaction(async (tx: FdmType) => { - for (const bead of chain) { - const check = await tx - .select({ - resource_id: authZSchema.role.resource_id, - }) - .from(authZSchema.role) - .where( - and( - eq(authZSchema.role.resource, bead.resource), - eq(authZSchema.role.resource_id, bead.resource_id), - inArray( - authZSchema.role.principal_id, - principal_ids, - ), - inArray(authZSchema.role.role, roles), - isNull(authZSchema.role.deleted), - ), - ) - .limit(1) - - if (check.length > 0) { - isAllowed = true - granting_resource = bead.resource - granting_resource_id = bead.resource_id - break - } - } - }) + const granting_resource = permission?.granting_resource ?? "" + const granting_resource_id = permission?.granting_resource_id ?? "" // Store check in audit await fdm.insert(authZSchema.audit).values({ @@ -215,15 +184,15 @@ export async function checkPermission( granting_resource: granting_resource, granting_resource_id: granting_resource_id, action: action, - allowed: isAllowed, + allowed: !!permission, duration: Math.round(performance.now() - start), }) - if (!isAllowed) { + if (!permission) { throw new Error("Permission denied") } - return isAllowed + return !!permission } catch (err) { let message = "Exception for checkPermission" if (err instanceof Error && err.message === "Permission denied") { @@ -239,6 +208,106 @@ export async function checkPermission( } } +/** + * Gets the granting resource type and ID if the principal has permission to perform the action in the given resource. + * + * If an action is actually going to be taken based on the result, you should use `checkPermission` instead. + * + * @param fdm The FDM instance providing the connection to the database. The instance can be created with {@link createFdmServer}. + * @param resource - The type of resource being accessed. + * @param action - The action the principal intends to perform. + * @param resource_id - The unique identifier of the specific resource. + * @param principal_id - The principal identifier(s); supports a single ID or an array. + * @returns Resolves to true if the principal is permitted to perform the action. + */ +export async function hasPermission( + fdm: FdmType, + resource: Resource, + action: Action, + resource_id: string, + principal_id: PrincipalId, +) { + try { + return !!(await getPermission( + fdm, + resource, + action, + resource_id, + principal_id, + )) + } catch (err) { + const message = "Exception for hasPermission" + throw handleError(err, message, { + resource: resource, + action: action, + resource_id: resource_id, + principal_id: principal_id, + }) + } +} + +/** + * Gets the granting resource type and ID if the principal has permission to perform the action in the given resource. + * + * @param fdm The FDM instance providing the connection to the database. The instance can be created with {@link createFdmServer}. + * @param resource - The type of resource being accessed. + * @param action - The action the principal intends to perform. + * @param resource_id - The unique identifier of the specific resource. + * @param principal_id - The principal identifier(s); supports a single ID or an array. + * @returns `granting_resource` is the resource type, `granting_resource_id` is the id of the specific granting resource. + * `null` is returned if the principal does not have the permission. + */ +async function getPermission( + fdm: FdmType, + resource: Resource, + action: Action, + resource_id: string, + principal_id: PrincipalId, +): Promise<{ + granting_resource: string + granting_resource_id: string +} | null> { + let isAllowed = false + let granting_resource = "" + let granting_resource_id = "" + const roles = getRolesForAction(action, resource) + const chain = await getResourceChain(fdm, resource, resource_id) + + // Convert principal_id to array + const principal_ids = Array.isArray(principal_id) + ? principal_id + : [principal_id] + + await fdm.transaction(async (tx: FdmType) => { + for (const bead of chain) { + const check = await tx + .select({ + resource_id: authZSchema.role.resource_id, + }) + .from(authZSchema.role) + .where( + and( + eq(authZSchema.role.resource, bead.resource), + eq(authZSchema.role.resource_id, bead.resource_id), + inArray(authZSchema.role.principal_id, principal_ids), + inArray(authZSchema.role.role, roles), + isNull(authZSchema.role.deleted), + ), + ) + .limit(1) + + if (check.length > 0) { + isAllowed = true + granting_resource = bead.resource + granting_resource_id = bead.resource_id + break + } + } + }) + + return isAllowed ? { granting_resource, granting_resource_id } : null +} + /** * Retrieves a list of roles a principal has for a specific resource. * diff --git a/fdm-core/src/index.ts b/fdm-core/src/index.ts index c0793f36c..544487f37 100644 --- a/fdm-core/src/index.ts +++ b/fdm-core/src/index.ts @@ -22,6 +22,7 @@ export { updateUserProfile, } from "./authentication" export type { FdmAuth } from "./authentication.d" +export { checkPermission, hasPermission } from "./authorization" export type { PrincipalId } from "./authorization.d" export { getCachedCalculation, From f1d03f09ab273d324981d75c035b7c2b4276f444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 24 Nov 2025 17:19:05 +0100 Subject: [PATCH 02/45] Add disabled buttons to field cultivations --- .../blocks/cultivation/card-details.tsx | 51 ++++++----- .../blocks/cultivation/card-harvests.tsx | 5 ++ .../blocks/cultivation/card-list.tsx | 3 + .../blocks/cultivation/form-add.tsx | 7 +- .../components/blocks/cultivation/types.tsx | 1 + .../app/components/blocks/harvest/form.tsx | 87 +++++++++++-------- fdm-app/app/components/custom/date-picker.tsx | 3 + ...ivation.$b_lu.harvest.$b_id_harvesting.tsx | 13 ++- ...calendar.field.$b_id.cultivation.$b_lu.tsx | 20 +++++ ...farm.$calendar.field.$b_id.cultivation.tsx | 11 +++ 10 files changed, 139 insertions(+), 62 deletions(-) diff --git a/fdm-app/app/components/blocks/cultivation/card-details.tsx b/fdm-app/app/components/blocks/cultivation/card-details.tsx index 487182ec8..74a682c95 100644 --- a/fdm-app/app/components/blocks/cultivation/card-details.tsx +++ b/fdm-app/app/components/blocks/cultivation/card-details.tsx @@ -30,13 +30,16 @@ import { export function CultivationDetailsCard({ cultivation, b_lu_variety_options, + editable = true, }: { cultivation: Cultivation b_lu_variety_options: { value: string; label: string }[] + editable?: boolean }) { const fetcher = useFetcher() const form = useRemixForm({ resolver: zodResolver(CultivationDetailsFormSchema), + disabled: !editable, mode: "onTouched", defaultValues: { b_lu_start: new Date(cultivation.b_lu_start), @@ -72,7 +75,7 @@ export function CultivationDetailsCard({ {cultivation.b_lu_name} - {!isCreateWizard ? ( + {!isCreateWizard && editable ? (
@@ -132,6 +137,7 @@ export function CultivationDetailsCard({ field.onChange } disabled={ + !editable || form.formState .isSubmitting || fetcher.state === @@ -158,8 +164,9 @@ export function CultivationDetailsCard({ onValueChange={field.onChange} value={field.value ?? undefined} disabled={ + !editable || b_lu_variety_options.length === - 0 + 0 } > @@ -196,25 +203,27 @@ export function CultivationDetailsCard({ )} />
-
- -
+ {editable && ( +
+ +
+ )} diff --git a/fdm-app/app/components/blocks/cultivation/card-harvests.tsx b/fdm-app/app/components/blocks/cultivation/card-harvests.tsx index 17817f8c3..ab75f6150 100644 --- a/fdm-app/app/components/blocks/cultivation/card-harvests.tsx +++ b/fdm-app/app/components/blocks/cultivation/card-harvests.tsx @@ -10,10 +10,12 @@ export function CultivationHarvestsCard({ harvests, b_lu_harvestable, harvestParameters, + editable = true, }: { harvests: Harvest[] b_lu_harvestable: HarvestableType harvestParameters: HarvestParameters + editable?: boolean }) { let canAddHarvest = false if (b_lu_harvestable === "once" && harvests.length === 0) { @@ -22,6 +24,9 @@ export function CultivationHarvestsCard({ if (b_lu_harvestable === "multiple") { canAddHarvest = true } + if (typeof editable !== "undefined") { + canAddHarvest = editable + } return ( diff --git a/fdm-app/app/components/blocks/cultivation/card-list.tsx b/fdm-app/app/components/blocks/cultivation/card-list.tsx index b7471aced..d3349ba0e 100644 --- a/fdm-app/app/components/blocks/cultivation/card-list.tsx +++ b/fdm-app/app/components/blocks/cultivation/card-list.tsx @@ -14,10 +14,12 @@ export function CultivationListCard({ cultivationsCatalogueOptions, cultivations, harvests, + editable = true, }: { cultivationsCatalogueOptions: CultivationOption[] cultivations: Cultivation[] harvests: Harvest[] + editable?: boolean }) { return ( @@ -28,6 +30,7 @@ export function CultivationListCard({ {cultivations.length !== 0 ? ( ) : null} diff --git a/fdm-app/app/components/blocks/cultivation/form-add.tsx b/fdm-app/app/components/blocks/cultivation/form-add.tsx index 435e28043..33ac01d67 100644 --- a/fdm-app/app/components/blocks/cultivation/form-add.tsx +++ b/fdm-app/app/components/blocks/cultivation/form-add.tsx @@ -17,13 +17,16 @@ import { import { CultivationAddFormSchema } from "./schema" import type { CultivationsFormProps } from "./types" -export function CultivationAddFormDialog({ options }: CultivationsFormProps) { +export function CultivationAddFormDialog({ + options, + editable = true, +}: CultivationsFormProps) { const [isOpen, setIsOpen] = useState(false) return ( - + diff --git a/fdm-app/app/components/blocks/cultivation/types.tsx b/fdm-app/app/components/blocks/cultivation/types.tsx index bf1804be8..2559c86b2 100644 --- a/fdm-app/app/components/blocks/cultivation/types.tsx +++ b/fdm-app/app/components/blocks/cultivation/types.tsx @@ -14,4 +14,5 @@ export interface CultivationOption { export interface CultivationsFormProps { options: CultivationOption[] + editable: boolean } diff --git a/fdm-app/app/components/blocks/harvest/form.tsx b/fdm-app/app/components/blocks/harvest/form.tsx index a73bd2663..de046e96b 100644 --- a/fdm-app/app/components/blocks/harvest/form.tsx +++ b/fdm-app/app/components/blocks/harvest/form.tsx @@ -50,8 +50,9 @@ type HarvestFormDialogProps = { b_lu_harvestable: "once" | "multiple" | "none" b_lu_start: Date | undefined | null b_lu_end: Date | undefined | null - action: string + action?: string handleConfirmation?: (data: z.infer) => Promise + editable?: boolean } function useHarvestRemixForm({ @@ -69,10 +70,12 @@ function useHarvestRemixForm({ b_lu_harvestable, b_lu_start, b_lu_end, + editable = true, handleConfirmation, }: HarvestFormDialogProps) { const form = useRemixForm>({ mode: "onTouched", + disabled: !editable, resolver: async (values, bypass, options) => { // Do the validation using Zod const validation = await zodResolver(FormSchema)( @@ -485,7 +488,7 @@ function HarvestFormExplainer() { ) } export function HarvestFormDialog(props: HarvestFormDialogProps) { - const { b_lu_harvest_date, action } = props + const { b_lu_harvest_date, action, editable = true } = props const navigate = useNavigate() const fetcher = useFetcher() const form = useHarvestRemixForm(props) @@ -524,26 +527,30 @@ export function HarvestFormDialog(props: HarvestFormDialogProps) { - + {editable && ( + + )} + {editable && ( + + )} diff --git a/fdm-app/app/components/custom/date-picker.tsx b/fdm-app/app/components/custom/date-picker.tsx index 594772cb5..99cae7c60 100644 --- a/fdm-app/app/components/custom/date-picker.tsx +++ b/fdm-app/app/components/custom/date-picker.tsx @@ -74,6 +74,7 @@ interface DatePickerProps { name: Path // Use Path for better type inference with react-hook-form label: string description: string + disabled?: boolean } export function DatePicker({ @@ -81,6 +82,7 @@ export function DatePicker({ name, label, description, + disabled = false, }: DatePickerProps) { const [open, setOpen] = React.useState(false) const [date, setDate] = React.useState( @@ -123,6 +125,7 @@ export function DatePicker({ value={value} placeholder="Kies een datum" className="bg-background pr-10" + disabled={disabled} onChange={(e) => { const newDate = parseDateString( e.target.value, diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.$b_id_harvesting.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.$b_id_harvesting.tsx index e130ba167..b046db558 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.$b_id_harvesting.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.$b_id_harvesting.tsx @@ -2,6 +2,7 @@ import { getCultivation, getHarvest, getParametersForHarvestCat, + hasPermission, removeHarvest, updateHarvest, } from "@svenvw/fdm-core" @@ -16,11 +17,11 @@ import { dataWithWarning, redirectWithSuccess } from "remix-toast" import { HarvestFormDialog } from "~/components/blocks/harvest/form" import { FormSchema } from "~/components/blocks/harvest/schema" import { getSession } from "~/lib/auth.server" +import { getCalendar } from "~/lib/calendar" import { clientConfig } from "~/lib/config" import { handleActionError, handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" import { extractFormValuesFromRequest } from "~/lib/form" -import { getCalendar } from "~/lib/calendar" import { getHarvestParameterLabel } from "../components/blocks/harvest/parameters" // Meta @@ -87,6 +88,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const session = await getSession(request) const calendar = getCalendar(params) + const harvestingWritePermission = hasPermission( + fdm, + "harvesting", + "write", + b_id_harvesting, + session.principal_id, + ) + // Get details of cultivation const cultivation = await getCultivation( fdm, @@ -120,6 +129,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { b_id_farm: b_id_farm, calendar: calendar, harvestParameters: harvestParameters, + harvestingWritePermission: await harvestingWritePermission, } } catch (error) { throw handleLoaderError(error) @@ -156,6 +166,7 @@ export default function FarmFieldsOverviewBlock() { b_lu_harvestable={loaderData.cultivation.b_lu_harvestable} b_lu_start={loaderData.cultivation.b_lu_start} b_lu_end={loaderData.cultivation.b_lu_end} + editable={loaderData.harvestingWritePermission} /> ) } diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx index 050cf90b1..1b33aaaf1 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx @@ -5,6 +5,7 @@ import { getField, getHarvests, getParametersForHarvestCat, + hasPermission, removeCultivation, updateCultivation, } from "@svenvw/fdm-core" @@ -166,6 +167,22 @@ export async function loader({ request, params }: LoaderFunctionArgs) { cultivation.b_lu_harvestcat, ) + const fieldWritePermission = hasPermission( + fdm, + "field", + "write", + b_id, + session.principal_id, + ) + + const cultivationWritePermission = hasPermission( + fdm, + "cultivation", + "write", + b_lu, + session.principal_id, + ) + // Return user information from loader return { field: field, @@ -177,6 +194,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { b_lu_variety_options: b_lu_variety_options, b_id_farm: b_id_farm, calendar: calendar, + cultivationWritePermission: await cultivationWritePermission, } } catch (error) { throw handleLoaderError(error) @@ -198,11 +216,13 @@ export default function FarmFieldsOverviewBlock() { harvests={loaderData.harvests} b_lu_harvestable={loaderData.b_lu_harvestable} b_lu_variety_options={loaderData.b_lu_variety_options} + editable={loaderData.cultivationWritePermission} /> diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx index 2dc597101..5d1b1a442 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx @@ -4,6 +4,7 @@ import { getCultivationsFromCatalogue, getField, getHarvests, + hasPermission, removeCultivation, } from "@svenvw/fdm-core" import { @@ -77,6 +78,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Get timeframe from calendar store const timeframe = getTimeframe(params) + const fieldWritePermission = await hasPermission( + fdm, + "field", + "write", + b_id, + session.principal_id, + ) + // Get details of field const field = await getField(fdm, session.principal_id, b_id) if (!field) { @@ -130,6 +139,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { cultivationsCatalogueOptions: cultivationsCatalogueOptions, cultivations: cultivations, harvests: harvests, + fieldWritePermission: fieldWritePermission, } } catch (error) { return handleLoaderError(error) @@ -155,6 +165,7 @@ export default function FarmFieldsOverviewBlock() { } cultivations={loaderData.cultivations} harvests={loaderData.harvests} + editable={loaderData.fieldWritePermission} /> From 52b2059e537447b44066846e8644e28619176b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 09:30:49 +0100 Subject: [PATCH 03/45] Add disabled buttons to field fertilizer applications --- .../blocks/fertilizer-applications/card.tsx | 9 +- .../blocks/fertilizer-applications/list.tsx | 89 +++++++++++-------- ...calendar.field.$b_id.fertilizer._index.tsx | 38 ++++++++ 3 files changed, 96 insertions(+), 40 deletions(-) diff --git a/fdm-app/app/components/blocks/fertilizer-applications/card.tsx b/fdm-app/app/components/blocks/fertilizer-applications/card.tsx index f086e2feb..556982aad 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/card.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/card.tsx @@ -24,6 +24,8 @@ export function FertilizerApplicationCard({ applicationMethodOptions, fertilizers, fertilizerOptions, + canCreateFertilizerApplication = true, + canModifyFertilizerApplication = {}, }: { fertilizerApplications: FertilizerApplication[] applicationMethodOptions: { @@ -34,6 +36,8 @@ export function FertilizerApplicationCard({ fertilizerOptions: FertilizerOption[] dose: Dose className?: string + canCreateFertilizerApplication?: boolean + canModifyFertilizerApplication?: Record }) { const fetcher = useFetcher() const location = useLocation() @@ -143,7 +147,7 @@ export function FertilizerApplicationCard({ onOpenChange={handleDialogOpenChange} > - @@ -181,6 +185,9 @@ export function FertilizerApplicationCard({ fertilizers={fertilizers} handleDelete={handleDelete} handleEdit={handleEdit} + canModifyFertilizerApplication={ + canModifyFertilizerApplication + } /> diff --git a/fdm-app/app/components/blocks/fertilizer-applications/list.tsx b/fdm-app/app/components/blocks/fertilizer-applications/list.tsx index 8c5d54bda..ac3f0e027 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/list.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/list.tsx @@ -32,6 +32,7 @@ export function FertilizerApplicationsList({ fertilizerApplications, applicationMethodOptions, fertilizers, + canModifyFertilizerApplication = {}, handleDelete, handleEdit, }: { @@ -41,6 +42,7 @@ export function FertilizerApplicationsList({ label: string }[] fertilizers: Fertilizer[] + canModifyFertilizerApplication?: Record handleDelete: (p_app_id: string | string[]) => void handleEdit: (fertilizerApplication: FertilizerApplication) => () => void }) { @@ -57,6 +59,10 @@ export function FertilizerApplicationsList({ if (!fertilizer) { return null } + const editable = + canModifyFertilizerApplication[ + application.p_app_id + ] ?? true return (
@@ -82,8 +88,9 @@ export function FertilizerApplicationsList({ variant="link" className="p-0 mt-0" disabled={ + !editable || fetcher.state === - "submitting" + "submitting" } onClick={handleEdit( application, @@ -117,45 +124,49 @@ export function FertilizerApplicationsList({

- - - - - - - -

Verwijder

-
-
-
-
+ onClick={() => { + if ( + application.p_app_ids + ) { + handleDelete( + application.p_app_ids, + ) + } else { + handleDelete( + [ + application.p_app_id, + ], + ) + } + }} + > + {fetcher.state === + "submitting" ? ( + + ) : ( + + )} + + + +

Verwijder

+
+ + + + )}
) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx index 1bdc0ea9d..7776852e6 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx @@ -5,6 +5,7 @@ import { getFertilizerParametersDescription, getFertilizers, getField, + hasPermission, removeFertilizerApplication, updateFertilizerApplication, } from "@svenvw/fdm-core" @@ -165,6 +166,34 @@ export async function loader({ request, params }: LoaderFunctionArgs) { calendar: getCalendar(params), } + const fieldWritePermission = hasPermission( + fdm, + "field", + "write", + b_id, + session.principal_id, + ) + + const fertilizerApplicationWritePermissionsEntries = await Promise.all( + fertilizerApplications.map( + async (app) => + [ + app.p_app_id, + await hasPermission( + fdm, + "fertilizer_application", + "write", + app.p_app_id, + session.principal_id, + ), + ] as [string, boolean], + ), + ) + + const fertilizerApplicationWritePermissions = Object.fromEntries( + fertilizerApplicationWritePermissionsEntries, + ) + // Return user information from loader, including the promises return { field: field, @@ -175,6 +204,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { applicationMethodOptions: applicationMethods.options, fertilizerApplicationMetricsData: fertilizerApplicationMetricsData, calendar: getCalendar(params), + fieldWritePermission: await fieldWritePermission, + fertilizerApplicationWritePermissions: + fertilizerApplicationWritePermissions, } } catch (error) { throw handleLoaderError(error) @@ -209,6 +241,12 @@ export default function FarmFieldsOverviewBlock() { fertilizers={loaderData.fertilizers} fertilizerOptions={loaderData.fertilizerOptions} dose={loaderData.dose} + canCreateFertilizerApplication={ + loaderData.fieldWritePermission + } + canModifyFertilizerApplication={ + loaderData.fertilizerApplicationWritePermissions + } />
From f66c71516ae62044fff0d793c681b007ff70c6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 10:03:54 +0100 Subject: [PATCH 04/45] Add disabled buttons to field soil analyses --- fdm-app/app/components/blocks/soil/list.tsx | 76 ++++++++++--------- ...farm.$calendar.field.$b_id.soil._index.tsx | 56 +++++++++++--- 2 files changed, 86 insertions(+), 46 deletions(-) diff --git a/fdm-app/app/components/blocks/soil/list.tsx b/fdm-app/app/components/blocks/soil/list.tsx index 4a288b476..733d55922 100644 --- a/fdm-app/app/components/blocks/soil/list.tsx +++ b/fdm-app/app/components/blocks/soil/list.tsx @@ -9,12 +9,14 @@ import type { SoilAnalysis } from "./types" export function SoilAnalysesList({ soilAnalyses, fetcher, + canModifySoilAnalysis = {}, }: { soilAnalyses: SoilAnalysis[] fetcher: { state: string submit: (data: { a_id: string }, options: { method: string }) => void } + canModifySoilAnalysis?: Record }) { const handleDelete = (a_id: string) => { if (fetcher.state === "submitting") return @@ -47,49 +49,53 @@ export function SoilAnalysesList({

{""}
-
-
- + {(canModifySoilAnalysis[analysis.a_id] ?? true) ? ( +
+
+ + + - - +
-
+ ) : null}
))} diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil._index.tsx index dc613d912..59b4281be 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil._index.tsx @@ -3,6 +3,7 @@ import { getField, getSoilAnalyses, getSoilParametersDescription, + hasPermission, removeSoilAnalysis, } from "@svenvw/fdm-core" import { Plus } from "lucide-react" @@ -95,12 +96,38 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Get soil parameter descriptions const soilParameterDescription = getSoilParametersDescription() + const fieldWritePermission = hasPermission( + fdm, + "field", + "write", + b_id, + session.principal_id, + ) + + const soilAnalysisWritePermissionsEntries = await Promise.all( + soilAnalyses.map(async (analysis) => [ + analysis.a_id, + await hasPermission( + fdm, + "soil_analysis", + "write", + analysis.a_id, + session.principal_id, + ), + ]), + ) + const soilAnalysisWritePermissions = Object.fromEntries( + soilAnalysisWritePermissionsEntries, + ) + // Return user information from loader return { field: field, + fieldWritePermission: await fieldWritePermission, currentSoilData: currentSoilData, soilParameterDescription: soilParameterDescription, soilAnalyses: soilAnalyses, + soilAnalysisWritePermissions: soilAnalysisWritePermissions, } } catch (error) { throw handleLoaderError(error) @@ -133,12 +160,14 @@ export default function FarmFieldSoilOverviewBlock() { Parameters Analyses - + {loaderData.fieldWritePermission && ( + + )} @@ -155,11 +184,13 @@ export default function FarmFieldSoilOverviewBlock() { bodem bij te houden

- + {loaderData.fieldWritePermission && ( + + )} ) : ( From e9baf560ed03ae9419437864b8b31c7df40d3ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 10:38:03 +0100 Subject: [PATCH 05/45] Remove remove field tab if the user has no permission --- .../farm.$b_id_farm.$calendar.field.$b_id.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.tsx index 9636fa63d..e92eaae45 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.tsx @@ -1,4 +1,4 @@ -import { getFarms, getField, getFields } from "@svenvw/fdm-core" +import { getFarms, getField, getFields, hasPermission } from "@svenvw/fdm-core" import { data, type LoaderFunctionArgs, @@ -150,11 +150,22 @@ export async function loader({ request, params }: LoaderFunctionArgs) { to: `/farm/${b_id_farm}/${calendar}/field/${b_id}/atlas`, title: "Kaart", }, - { + ] + + const fieldWritePermission = await hasPermission( + fdm, + "field", + "write", + b_id, + session.principal_id, + ) + + if (fieldWritePermission) { + sidebarPageItems.push({ to: `/farm/${b_id_farm}/${calendar}/field/${b_id}/delete`, title: "Verwijderen", - }, - ] + }) + } // Return user information from loader return { From 4474c0884acc7f3021a8d03f3fd30d5ba7c55f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 11:32:53 +0100 Subject: [PATCH 06/45] Add disabled buttons to the field table --- .../app/components/blocks/fields/columns.tsx | 15 ++-- .../app/components/blocks/fields/table.tsx | 89 ++++++++++--------- ...farm.$b_id_farm.$calendar.field._index.tsx | 19 ++++ 3 files changed, 76 insertions(+), 47 deletions(-) diff --git a/fdm-app/app/components/blocks/fields/columns.tsx b/fdm-app/app/components/blocks/fields/columns.tsx index cfc39f4ce..8ee09b3a1 100644 --- a/fdm-app/app/components/blocks/fields/columns.tsx +++ b/fdm-app/app/components/blocks/fields/columns.tsx @@ -28,7 +28,7 @@ export type FieldExtended = { b_lu_name: string b_lu_croprotation: string b_lu_start: Date - }[] + }[] fertilizers: { p_name_nl: string p_id: string @@ -37,6 +37,7 @@ export type FieldExtended = { a_som_loi: number b_soiltype_agr: string b_area: number + has_write_permission: boolean } export const columns: ColumnDef[] = [ @@ -265,11 +266,13 @@ export const columns: ColumnDef[] = [ Kaart - - - Verwijderen - - + {field.has_write_permission && ( + + + Verwijderen + + + )} ) diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index c8fe909a2..53e1cc0a4 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -46,11 +46,13 @@ import type { FieldExtended } from "./columns" interface DataTableProps { columns: ColumnDef[] data: TData[] + canAddItem: boolean } export function DataTable({ columns, data, + canAddItem, }: DataTableProps) { const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState([]) @@ -212,51 +214,56 @@ export function DataTable({ - - - -
- {isFertilizerButtonDisabled ? ( - - ) : ( - + {canAddItem && ( + <> + + + +
+ {isFertilizerButtonDisabled ? ( + + ) : ( + + + + )} +
+
+ +

{fertilizerTooltipContent}

+
+
+
+ + + + + - )} -
-
- -

{fertilizerTooltipContent}

-
-
-
- - - - - - - - -

Voeg een nieuw perceel toe

-
-
-
+ + +

Voeg een nieuw perceel toe

+
+ + + + )}
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 ff9bbf882..209519c41 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 @@ -5,6 +5,7 @@ import { getFertilizerApplications, getFertilizers, getFields, + hasPermission, } from "@svenvw/fdm-core" import { data, @@ -154,6 +155,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { (x) => x.parameter === "b_soiltype_agr", )?.value ?? null + const has_write_permission = await hasPermission( + fdm, + "field", + "write", + field.b_id, + session.principal_id, + ) return { b_id: field.b_id, b_name: field.b_name, @@ -163,10 +171,19 @@ export async function loader({ request, params }: LoaderFunctionArgs) { b_soiltype_agr: b_soiltype_agr, b_area: Math.round(field.b_area * 10) / 10, b_isproductive: field.b_isproductive ?? true, + has_write_permission: has_write_permission, } }), ) + const farmWritePermission = await hasPermission( + fdm, + "farm", + "write", + b_id_farm, + session.principal_id, + ) + // Return user information from loader return { b_id_farm: b_id_farm, @@ -174,6 +191,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { fieldOptions: fieldOptions, fieldsExtended: fieldsExtended, userName: session.userName, + farmWritePermission: farmWritePermission, } } catch (error) { throw handleLoaderError(error) @@ -262,6 +280,7 @@ export default function FarmFieldIndex() {
From 0cf89b6c8d8e5f4b5767c55060a371a1e4e1670f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 11:37:20 +0100 Subject: [PATCH 07/45] Add disabled buttons to the farm overview --- fdm-app/app/routes/farm.$b_id_farm._index.tsx | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 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 77b5858ec..3517bcc66 100644 --- a/fdm-app/app/routes/farm.$b_id_farm._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm._index.tsx @@ -1,5 +1,5 @@ import { cowHead } from "@lucide/lab" -import { getFarm, getFarms, getFields } from "@svenvw/fdm-core" +import { getFarm, getFarms, getFields, hasPermission } from "@svenvw/fdm-core" import { ArrowRightLeft, BookOpenText, @@ -103,6 +103,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } }) + const farmWritePermission = await hasPermission( + fdm, + "farm", + "write", + b_id_farm, + session.principal_id, + ) + // Return the farm ID and session info return { b_id_farm: b_id_farm, @@ -111,6 +119,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { farmArea: Math.round(farmArea), farmOptions: farmOptions, roles: farm.roles, + farmWritePermission: farmWritePermission, } } catch (error) { throw handleLoaderError(error) @@ -449,26 +458,30 @@ export default function FarmDashboardIndex() { Beweiding - - + {loaderData.farmWritePermission && ( + + )} + {loaderData.farmWritePermission && ( + + )}
From ff417f31ace88320a7a44062ee5662aad5c1849a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 12:00:00 +0100 Subject: [PATCH 08/45] Add disabled buttons to the cultivation overview --- .../app/components/blocks/rotation/table.tsx | 146 +++++++++--------- ...m.$b_id_farm.$calendar.rotation._index.tsx | 15 +- 2 files changed, 90 insertions(+), 71 deletions(-) diff --git a/fdm-app/app/components/blocks/rotation/table.tsx b/fdm-app/app/components/blocks/rotation/table.tsx index 83cfbdeb9..9c9da23a9 100644 --- a/fdm-app/app/components/blocks/rotation/table.tsx +++ b/fdm-app/app/components/blocks/rotation/table.tsx @@ -51,11 +51,13 @@ import { useFieldFilterStore } from "@/app/store/field-filter" interface DataTableProps { columns: ColumnDef[] data: TData[] + canAddItem: boolean } export function DataTable({ columns, data, + canAddItem, }: DataTableProps) { const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState([]) @@ -276,75 +278,81 @@ export function DataTable({ - - - -
- {isFertilizerButtonDisabled ? ( - - ) : ( - - - - )} -
-
- -

{fertilizerTooltipContent}

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

{harvestTooltipContent}

-
-
-
+ {canAddItem && ( + <> + + + +
+ {isFertilizerButtonDisabled ? ( + + ) : ( + + + + )} +
+
+ +

{fertilizerTooltipContent}

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

{harvestTooltipContent}

+
+
+
+ + )}
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx index 92d4594f9..abe1fdca5 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx @@ -8,6 +8,7 @@ import { getFertilizers, getFields, getHarvests, + hasPermission, } from "@svenvw/fdm-core" import { type LoaderFunctionArgs, @@ -18,13 +19,13 @@ import { } from "react-router" import { FarmContent } from "~/components/blocks/farm/farm-content" import { FarmTitle } from "~/components/blocks/farm/farm-title" +import { Header } from "~/components/blocks/header/base" +import { HeaderFarm } from "~/components/blocks/header/farm" import { columns, type RotationExtended, } from "~/components/blocks/rotation/columns" import { DataTable } from "~/components/blocks/rotation/table" -import { Header } from "~/components/blocks/header/base" -import { HeaderFarm } from "~/components/blocks/header/farm" import { BreadcrumbItem, BreadcrumbSeparator } from "~/components/ui/breadcrumb" import { Button } from "~/components/ui/button" import { SidebarInset } from "~/components/ui/sidebar" @@ -303,6 +304,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { fieldsExtended, cultivationCatalogue, ) + + const farmWritePermission = await hasPermission( + fdm, + "farm", + "write", + b_id_farm, + session.principal_id, + ) // Return user information from loader return { @@ -311,6 +320,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { fieldOptions: fieldOptions, rotationExtended: rotationExtended, // Return filtered data userName: session.userName, + farmWritePermission: farmWritePermission, } } catch (error) { throw handleLoaderError(error) @@ -389,6 +399,7 @@ export default function FarmRotationIndex() {
From 883680379c1fbf877c643064e1997086338cbeae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 12:08:01 +0100 Subject: [PATCH 09/45] Handle farm share and delete permissions properly --- fdm-app/app/routes/farm.$b_id_farm._index.tsx | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 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 3517bcc66..98eb8109f 100644 --- a/fdm-app/app/routes/farm.$b_id_farm._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm._index.tsx @@ -1,5 +1,11 @@ import { cowHead } from "@lucide/lab" -import { getFarm, getFarms, getFields, hasPermission } from "@svenvw/fdm-core" +import { + getFarm, + getFarms, + getFields, + hasPermission, + isAllowedToDeleteFarm, +} from "@svenvw/fdm-core" import { ArrowRightLeft, BookOpenText, @@ -103,13 +109,23 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } }) - const farmWritePermission = await hasPermission( + const farmSharePermission = hasPermission( fdm, "farm", - "write", + "share", b_id_farm, session.principal_id, ) + // Check if user is allowed to delete farm + let canDeleteFarm = await isAllowedToDeleteFarm( + fdm, + session.principal_id, + b_id_farm, + ) + // Add temporary workaround (until implemented in fdm-core) so that advisors are not able to delete a farm + if (canDeleteFarm && farm.roles.includes("advisor")) { + canDeleteFarm = false + } // Return the farm ID and session info return { @@ -119,7 +135,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { farmArea: Math.round(farmArea), farmOptions: farmOptions, roles: farm.roles, - farmWritePermission: farmWritePermission, + canDeleteFarm: canDeleteFarm, + farmSharePermission: await farmSharePermission, } } catch (error) { throw handleLoaderError(error) @@ -458,7 +475,7 @@ export default function FarmDashboardIndex() { Beweiding - {loaderData.farmWritePermission && ( + {loaderData.farmSharePermission && ( )} - {loaderData.farmWritePermission && ( + {loaderData.canDeleteFarm && ( - - + {canAddItem && ( +
+ + + +
+ )}
diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx index a352c8df7..69f6d63c6 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx @@ -5,6 +5,7 @@ import { getFertilizer, getFertilizerParametersDescription, getFertilizers, + hasPermission, updateFertilizerFromCatalogue, } from "@svenvw/fdm-core" import { @@ -110,6 +111,18 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (fertilizer.p_source === b_id_farm) { editable = true } + if ( + editable && + !(await hasPermission( + fdm, + "farm", + "write", + b_id_farm, + session.principal_id, + )) + ) { + editable = false + } // Return user information from loader return { diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx index 584be095b..ac9f30069 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx @@ -3,6 +3,7 @@ import { getFarms, getFertilizerParametersDescription, getFertilizers, + hasPermission, } from "@svenvw/fdm-core" import { data, @@ -102,12 +103,15 @@ export async function loader({ request, params }: LoaderFunctionArgs) { })), ) + const farmWritePermission = await hasPermission(fdm, "farm", "write", b_id_farm, session.principal_id) + // Return user information from loader return { farm: farm, b_id_farm: b_id_farm, farmOptions: farmOptions, fertilizers: fertilizers, + farmWritePermission: farmWritePermission, } } catch (error) { throw handleLoaderError(error) @@ -143,6 +147,7 @@ export default function FarmFertilizersIndexPage({ From 3337f33acc776a02f3930a8b3175854da123cc36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 12:39:52 +0100 Subject: [PATCH 11/45] Hide add cultivation button altogether if the user can't add a cultivation --- fdm-app/app/components/blocks/cultivation/card-list.tsx | 3 +-- fdm-app/app/components/blocks/cultivation/form-add.tsx | 8 ++++---- fdm-app/app/components/blocks/cultivation/types.tsx | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/fdm-app/app/components/blocks/cultivation/card-list.tsx b/fdm-app/app/components/blocks/cultivation/card-list.tsx index d3349ba0e..6f652eff0 100644 --- a/fdm-app/app/components/blocks/cultivation/card-list.tsx +++ b/fdm-app/app/components/blocks/cultivation/card-list.tsx @@ -27,10 +27,9 @@ export function CultivationListCard({ Gewassen - {cultivations.length !== 0 ? ( + {cultivations.length !== 0 && editable ? ( ) : null} diff --git a/fdm-app/app/components/blocks/cultivation/form-add.tsx b/fdm-app/app/components/blocks/cultivation/form-add.tsx index 33ac01d67..684e0669c 100644 --- a/fdm-app/app/components/blocks/cultivation/form-add.tsx +++ b/fdm-app/app/components/blocks/cultivation/form-add.tsx @@ -19,14 +19,13 @@ import type { CultivationsFormProps } from "./types" export function CultivationAddFormDialog({ options, - editable = true, }: CultivationsFormProps) { const [isOpen, setIsOpen] = useState(false) return ( - + @@ -44,7 +43,8 @@ export function CultivationAddFormDialog({ function CultivationAddForm({ options, onSuccess, -}: CultivationsFormProps & { onSuccess?: () => void }) { + editable = true, +}: CultivationsFormProps & { editable?: boolean; onSuccess?: () => void }) { const form = useRemixForm>({ mode: "onTouched", resolver: zodResolver(CultivationAddFormSchema), @@ -73,7 +73,7 @@ function CultivationAddForm({ onSubmit={form.handleSubmit} method="post" > -
+
Date: Tue, 25 Nov 2025 13:46:22 +0100 Subject: [PATCH 12/45] Disable calendar button on disabled v1 date pickers --- fdm-app/app/components/custom/date-picker.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fdm-app/app/components/custom/date-picker.tsx b/fdm-app/app/components/custom/date-picker.tsx index 99cae7c60..e160682e1 100644 --- a/fdm-app/app/components/custom/date-picker.tsx +++ b/fdm-app/app/components/custom/date-picker.tsx @@ -111,6 +111,12 @@ export function DatePicker({ } }, [form, name, date]) + React.useEffect(() => { + if (disabled && open) { + setOpen(false) + } + }, [disabled, open]) + return ( ({ id={`${field.name}-picker`} variant="ghost" className="absolute top-1/2 right-2 size-6 -translate-y-1/2" + disabled={disabled} > From cac548cddf98c100b40022df87aaf812076b6682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 13:48:08 +0100 Subject: [PATCH 13/45] Remove unused permission from loader --- ...$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx index 1b33aaaf1..c10777ad6 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx @@ -167,14 +167,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { cultivation.b_lu_harvestcat, ) - const fieldWritePermission = hasPermission( - fdm, - "field", - "write", - b_id, - session.principal_id, - ) - const cultivationWritePermission = hasPermission( fdm, "cultivation", From af614b31d17654986efc720b4b5e146a73cc0f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 14:05:40 +0100 Subject: [PATCH 14/45] Handle access change in fertilizer application card --- .../components/blocks/fertilizer-applications/card.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/fdm-app/app/components/blocks/fertilizer-applications/card.tsx b/fdm-app/app/components/blocks/fertilizer-applications/card.tsx index 556982aad..9f06fd419 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/card.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/card.tsx @@ -112,10 +112,16 @@ export function FertilizerApplicationCard({ if (savedFormValues && !isDialogOpen) { if (savedFormValues.p_app_id) { // Do not open the form if there is a risk it will create a new application - if (applicationToEdit) { + if ( + applicationToEdit && + (canModifyFertilizerApplication[ + applicationToEdit.p_app_id + ] ?? + true) + ) { setIsDialogOpen(true) } - } else { + } else if (canCreateFertilizerApplication) { setIsDialogOpen(true) } } From 91c357cce5861ebea9adf3bdffe00e97fd8ccf7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 14:08:25 +0100 Subject: [PATCH 15/45] Disable fieldset in cultivation form for consistency --- fdm-app/app/components/blocks/cultivation/card-details.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/components/blocks/cultivation/card-details.tsx b/fdm-app/app/components/blocks/cultivation/card-details.tsx index 74a682c95..30b0b97c5 100644 --- a/fdm-app/app/components/blocks/cultivation/card-details.tsx +++ b/fdm-app/app/components/blocks/cultivation/card-details.tsx @@ -39,7 +39,6 @@ export function CultivationDetailsCard({ const fetcher = useFetcher() const form = useRemixForm({ resolver: zodResolver(CultivationDetailsFormSchema), - disabled: !editable, mode: "onTouched", defaultValues: { b_lu_start: new Date(cultivation.b_lu_start), @@ -101,6 +100,7 @@ export function CultivationDetailsCard({
Date: Tue, 25 Nov 2025 14:23:42 +0100 Subject: [PATCH 16/45] Allow viewing the soil analysis details for users with no write permission --- fdm-app/app/components/blocks/soil/form.tsx | 9 ++-- fdm-app/app/components/blocks/soil/list.tsx | 50 +++++++++---------- ...lendar.field.$b_id.soil.analysis.$a_id.tsx | 5 ++ 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/fdm-app/app/components/blocks/soil/form.tsx b/fdm-app/app/components/blocks/soil/form.tsx index add378169..072d3971a 100644 --- a/fdm-app/app/components/blocks/soil/form.tsx +++ b/fdm-app/app/components/blocks/soil/form.tsx @@ -30,8 +30,9 @@ export function SoilAnalysisForm(props: { soilAnalysis: SoilAnalysis | undefined soilParameterDescription: SoilParameterDescription action: string + editable?: boolean }) { - const { soilAnalysis, soilParameterDescription } = props + const { soilAnalysis, soilParameterDescription, editable } = props const defaultValues: { [key: string]: string | number | Date | undefined | null @@ -71,7 +72,7 @@ export function SoilAnalysisForm(props: { onSubmit={form.handleSubmit} method="post" > -
+

Vul de gegevens van de bodemanalyse in. @@ -225,7 +226,7 @@ export function SoilAnalysisForm(props: { } })}

-
+ {editable &&
-
+
}
diff --git a/fdm-app/app/components/blocks/soil/list.tsx b/fdm-app/app/components/blocks/soil/list.tsx index 733d55922..9ee6984e0 100644 --- a/fdm-app/app/components/blocks/soil/list.tsx +++ b/fdm-app/app/components/blocks/soil/list.tsx @@ -49,31 +49,31 @@ export function SoilAnalysesList({

{""}
- {(canModifySoilAnalysis[analysis.a_id] ?? true) ? ( -
-
- +
+ + - + Bewerk + + + {(canModifySoilAnalysis[analysis.a_id] ?? + true) ? (
+ ) : null}
- ) : null} +
))} diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.$a_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.$a_id.tsx index 159030a65..29a9f78e4 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.$a_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.$a_id.tsx @@ -2,6 +2,7 @@ import { getField, getSoilAnalysis, getSoilParametersDescription, + hasPermission, updateSoilAnalysis, } from "@svenvw/fdm-core" import { ArrowLeft } from "lucide-react" @@ -98,11 +99,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { (item: { parameter: string }) => soilAnalysis[item.parameter], ) + const soilAnalysisWritePermission = await hasPermission(fdm, "soil_analysis", "write", a_id, session.principal_id) + // Return user information from loader return { field: field, soilParameterDescription: soilParameterDescription, soilAnalysis: soilAnalysis, + soilAnalysisWritePermission: soilAnalysisWritePermission, } } catch (error) { throw handleLoaderError(error) @@ -139,6 +143,7 @@ export default function FarmFieldSoilOverviewBlock() { soilAnalysis={loaderData.soilAnalysis} soilParameterDescription={loaderData.soilParameterDescription} action="." + editable={loaderData.soilAnalysisWritePermission} /> ) From 9dbee5933170c9325e360ec98fa741130d5631ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 14:28:46 +0100 Subject: [PATCH 17/45] Make entire harvest form disabled --- fdm-app/app/components/blocks/harvest/form.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fdm-app/app/components/blocks/harvest/form.tsx b/fdm-app/app/components/blocks/harvest/form.tsx index de046e96b..93fa12059 100644 --- a/fdm-app/app/components/blocks/harvest/form.tsx +++ b/fdm-app/app/components/blocks/harvest/form.tsx @@ -509,7 +509,9 @@ export function HarvestFormDialog(props: HarvestFormDialogProps) { method="post" action={action} > -
+
@@ -592,7 +594,7 @@ export function HarvestFormDialog(props: HarvestFormDialogProps) { } export function HarvestForm(props: HarvestFormDialogProps) { - const { b_lu_harvest_date, action } = props + const { b_lu_harvest_date, action, editable } = props const fetcher = useFetcher() const form = useHarvestRemixForm(props) @@ -614,7 +616,7 @@ export function HarvestForm(props: HarvestFormDialogProps) { action={action} >
Date: Tue, 25 Nov 2025 14:32:19 +0100 Subject: [PATCH 18/45] Disable calendar button on disabled v2 date pickers --- fdm-app/app/components/custom/date-picker-v2.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fdm-app/app/components/custom/date-picker-v2.tsx b/fdm-app/app/components/custom/date-picker-v2.tsx index 2a6132140..748c64124 100644 --- a/fdm-app/app/components/custom/date-picker-v2.tsx +++ b/fdm-app/app/components/custom/date-picker-v2.tsx @@ -63,6 +63,12 @@ export function DatePicker({ } }, [field.value]) + useEffect(() => { + if (field.disabled && open) { + setOpen(false) + } + }, [field.disabled, open]) + const handleInputChange = (e: ChangeEvent) => { setInputValue(e.target.value) } @@ -115,6 +121,7 @@ export function DatePicker({ id="date-picker" variant="ghost" className="absolute top-1/2 right-2 size-6 -translate-y-1/2" + disabled={field.disabled} > Kies een datum From 07ea0de4c58b3e4bb63db7a48a8c6a2b551750fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 14:57:25 +0100 Subject: [PATCH 19/45] Add some editable prop default values --- fdm-app/app/components/blocks/harvest/form.tsx | 4 +--- fdm-app/app/components/blocks/soil/form.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/fdm-app/app/components/blocks/harvest/form.tsx b/fdm-app/app/components/blocks/harvest/form.tsx index 93fa12059..b9e0bfdc1 100644 --- a/fdm-app/app/components/blocks/harvest/form.tsx +++ b/fdm-app/app/components/blocks/harvest/form.tsx @@ -70,12 +70,10 @@ function useHarvestRemixForm({ b_lu_harvestable, b_lu_start, b_lu_end, - editable = true, handleConfirmation, }: HarvestFormDialogProps) { const form = useRemixForm>({ mode: "onTouched", - disabled: !editable, resolver: async (values, bypass, options) => { // Do the validation using Zod const validation = await zodResolver(FormSchema)( @@ -594,7 +592,7 @@ export function HarvestFormDialog(props: HarvestFormDialogProps) { } export function HarvestForm(props: HarvestFormDialogProps) { - const { b_lu_harvest_date, action, editable } = props + const { b_lu_harvest_date, action, editable = true } = props const fetcher = useFetcher() const form = useHarvestRemixForm(props) diff --git a/fdm-app/app/components/blocks/soil/form.tsx b/fdm-app/app/components/blocks/soil/form.tsx index 072d3971a..4ecc888bb 100644 --- a/fdm-app/app/components/blocks/soil/form.tsx +++ b/fdm-app/app/components/blocks/soil/form.tsx @@ -32,7 +32,7 @@ export function SoilAnalysisForm(props: { action: string editable?: boolean }) { - const { soilAnalysis, soilParameterDescription, editable } = props + const { soilAnalysis, soilParameterDescription, editable = true } = props const defaultValues: { [key: string]: string | number | Date | undefined | null From 19a284429d52e553c11aeba841a960f88de71575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 15:33:11 +0100 Subject: [PATCH 20/45] Add fallback value support to hasPermission and make use of it --- ...ivation.$b_lu.harvest.$b_id_harvesting.tsx | 1 + ...calendar.field.$b_id.cultivation.$b_lu.tsx | 1 + ...farm.$calendar.field.$b_id.cultivation.tsx | 1 + ...calendar.field.$b_id.fertilizer._index.tsx | 2 + ...farm.$calendar.field.$b_id.soil._index.tsx | 2 + ...lendar.field.$b_id.soil.analysis.$a_id.tsx | 9 ++- .../farm.$b_id_farm.$calendar.field.$b_id.tsx | 1 + ...farm.$b_id_farm.$calendar.field._index.tsx | 2 + ...m.$b_id_farm.$calendar.rotation._index.tsx | 1 + fdm-app/app/routes/farm.$b_id_farm._index.tsx | 1 + .../farm.$b_id_farm.fertilizers.$p_id.tsx | 1 + .../farm.$b_id_farm.fertilizers._index.tsx | 9 ++- fdm-core/src/authorization.test.ts | 58 +++++++++++++++++++ fdm-core/src/authorization.ts | 12 +++- 14 files changed, 97 insertions(+), 4 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.$b_id_harvesting.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.$b_id_harvesting.tsx index b046db558..bf3175c2a 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.$b_id_harvesting.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.$b_id_harvesting.tsx @@ -94,6 +94,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", b_id_harvesting, session.principal_id, + { fallback: true }, ) // Get details of cultivation diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx index c10777ad6..0f0ebec1e 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx @@ -173,6 +173,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", b_lu, session.principal_id, + { fallback: true }, ) // Return user information from loader diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx index 5d1b1a442..418cad2cf 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx @@ -84,6 +84,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", b_id, session.principal_id, + { fallback: true }, ) // Get details of field diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx index 7776852e6..ae66a548f 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx @@ -172,6 +172,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", b_id, session.principal_id, + { fallback: true }, ) const fertilizerApplicationWritePermissionsEntries = await Promise.all( @@ -185,6 +186,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", app.p_app_id, session.principal_id, + { fallback: true }, ), ] as [string, boolean], ), diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil._index.tsx index 59b4281be..e734ee237 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil._index.tsx @@ -102,6 +102,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", b_id, session.principal_id, + { fallback: true }, ) const soilAnalysisWritePermissionsEntries = await Promise.all( @@ -113,6 +114,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", analysis.a_id, session.principal_id, + { fallback: true }, ), ]), ) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.$a_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.$a_id.tsx index 29a9f78e4..510f49865 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.$a_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.$a_id.tsx @@ -99,7 +99,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { (item: { parameter: string }) => soilAnalysis[item.parameter], ) - const soilAnalysisWritePermission = await hasPermission(fdm, "soil_analysis", "write", a_id, session.principal_id) + const soilAnalysisWritePermission = await hasPermission( + fdm, + "soil_analysis", + "write", + a_id, + session.principal_id, + { fallback: true }, + ) // Return user information from loader return { diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.tsx index e92eaae45..f1695b0e1 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.tsx @@ -158,6 +158,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", b_id, session.principal_id, + { fallback: true }, ) if (fieldWritePermission) { 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 209519c41..fa5de995b 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 @@ -161,6 +161,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", field.b_id, session.principal_id, + { fallback: true }, ) return { b_id: field.b_id, @@ -182,6 +183,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", b_id_farm, session.principal_id, + { fallback: true }, ) // Return user information from loader diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx index abe1fdca5..07d918979 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx @@ -311,6 +311,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", b_id_farm, session.principal_id, + { fallback: true }, ) // Return user information from loader 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 98eb8109f..fef7b17e7 100644 --- a/fdm-app/app/routes/farm.$b_id_farm._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm._index.tsx @@ -115,6 +115,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "share", b_id_farm, session.principal_id, + { fallback: true }, ) // Check if user is allowed to delete farm let canDeleteFarm = await isAllowedToDeleteFarm( diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx index 69f6d63c6..487655171 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx @@ -119,6 +119,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", b_id_farm, session.principal_id, + { fallback: true }, )) ) { editable = false diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx index ac9f30069..596d57776 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx @@ -103,7 +103,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { })), ) - const farmWritePermission = await hasPermission(fdm, "farm", "write", b_id_farm, session.principal_id) + const farmWritePermission = await hasPermission( + fdm, + "farm", + "write", + b_id_farm, + session.principal_id, + { fallback: true }, + ) // Return user information from loader return { diff --git a/fdm-core/src/authorization.test.ts b/fdm-core/src/authorization.test.ts index e095b9ad0..0d7f972b7 100644 --- a/fdm-core/src/authorization.test.ts +++ b/fdm-core/src/authorization.test.ts @@ -5,6 +5,7 @@ import { checkPermission, getRolesOfPrincipalForResource, grantRole, + hasPermission, listPrincipalsForResource, listResources, resources, @@ -150,6 +151,63 @@ describe("Authorization Functions", () => { }) }) + describe("hasPermission", () => { + it("should not store the audit log", async () => { + const principal_id_new = createId() + + await expect( + hasPermission(fdm, "farm", "read", farm_id, principal_id_new), + ).resolves.toBe(false) + + const auditLogs = await fdm + .select() + .from(authZSchema.audit) + .where(eq(authZSchema.audit.principal_id, principal_id_new)) + .orderBy(desc(authZSchema.audit.audit_timestamp)) + expect(auditLogs).toHaveLength(0) + }) + + it("should return the fallback value when specified", async () => { + await grantRole(fdm, "farm", "owner", farm_id, principal_id) + await expect( + hasPermission( + fdm, + // biome-ignore lint/suspicious/noExplicitAny: Used for testing validation + "unknown_resource" as any, + "read", + farm_id, + principal_id, + { fallback: true }, + ), + ).resolves.toBe(true) + await expect( + hasPermission( + fdm, + // biome-ignore lint/suspicious/noExplicitAny: Used for testing validation + "unknown_resource" as any, + "read", + farm_id, + principal_id, + { fallback: false }, + ), + ).resolves.toBe(false) + }) + + it("should throw an error if the fallback value is not specified", async () => { + await grantRole(fdm, "farm", "owner", farm_id, principal_id) + await expect( + hasPermission( + fdm, + // biome-ignore lint/suspicious/noExplicitAny: Used for testing validation + "unknown_resource" as any, + "read", + farm_id, + principal_id, + ), + ).rejects.toThrowError("Exception for checkPermission") + }) + }) + describe("grantRole", () => { it("should grant a role to a principal for a resource", async () => { await grantRole(fdm, "farm", "owner", farm_id, principal_id) diff --git a/fdm-core/src/authorization.ts b/fdm-core/src/authorization.ts index 9965cf6dd..1e6093065 100644 --- a/fdm-core/src/authorization.ts +++ b/fdm-core/src/authorization.ts @@ -209,7 +209,7 @@ export async function checkPermission( } /** - * Gets the granting resource type and ID if the principal has permission to perform the action in the given resource. + * Returns whether the principal has permission to perform the action in the given resource. * * If an action is actually going to be taken based on the result, you should use `checkPermission` instead. * @@ -218,7 +218,8 @@ export async function checkPermission( * @param action - The action the principal intends to perform. * @param resource_id - The unique identifier of the specific resource. * @param principal_id - The principal identifier(s); supports a single ID or an array. - * @returns Resolves to true if the principal is permitted to perform the action. + * @param options - Options to customize the behavior + * @returns Resolves to true if the principal is permitted to perform the action. Returns undefined in case of error. */ export async function hasPermission( fdm: FdmType, @@ -226,6 +227,10 @@ export async function hasPermission( action: Action, resource_id: string, principal_id: PrincipalId, + options?: { + /** The value to return in case of error. Omit if you would like the return promise to be rejected instead. */ + fallback?: boolean + }, ) { try { return !!(await getPermission( @@ -236,6 +241,9 @@ export async function hasPermission( principal_id, )) } catch (err) { + if (options && typeof options.fallback !== "undefined") { + return options.fallback + } const message = "Exception for hasPermission" throw handleError(err, message, { resource: resource, From f545dd437751ff09102d57650e9142d260f7d190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 15:39:14 +0100 Subject: [PATCH 21/45] Fix and improve tests --- fdm-core/src/authorization.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fdm-core/src/authorization.test.ts b/fdm-core/src/authorization.test.ts index 0d7f972b7..5bd60b782 100644 --- a/fdm-core/src/authorization.test.ts +++ b/fdm-core/src/authorization.test.ts @@ -167,7 +167,7 @@ describe("Authorization Functions", () => { expect(auditLogs).toHaveLength(0) }) - it("should return the fallback value when specified", async () => { + it("should return the fallback value on error when specified", async () => { await grantRole(fdm, "farm", "owner", farm_id, principal_id) await expect( hasPermission( @@ -193,7 +193,7 @@ describe("Authorization Functions", () => { ).resolves.toBe(false) }) - it("should throw an error if the fallback value is not specified", async () => { + it("should throw any error encountered if the fallback value is not specified", async () => { await grantRole(fdm, "farm", "owner", farm_id, principal_id) await expect( hasPermission( @@ -204,7 +204,7 @@ describe("Authorization Functions", () => { farm_id, principal_id, ), - ).rejects.toThrowError("Exception for checkPermission") + ).rejects.toThrowError("Exception for hasPermission") }) }) From fb7126b8024ce4286ff3761db2af98bb106b486e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 16:16:39 +0100 Subject: [PATCH 22/45] Call hasPermission concurrently in the loader loader for field cultivations --- ...$calendar.field.$b_id.cultivation.$b_lu.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx index 0f0ebec1e..039360df7 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx @@ -99,6 +99,15 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const timeframe = getTimeframe(params) const calendar = getCalendar(params) + const cultivationWritePermission = hasPermission( + fdm, + "cultivation", + "write", + b_lu, + session.principal_id, + { fallback: true }, + ) + // Get details of field const field = await getField(fdm, session.principal_id, b_id) if (!field) { @@ -167,15 +176,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { cultivation.b_lu_harvestcat, ) - const cultivationWritePermission = hasPermission( - fdm, - "cultivation", - "write", - b_lu, - session.principal_id, - { fallback: true }, - ) - // Return user information from loader return { field: field, From 1b0dff7a5747d42e7f33dec641f08d105655c986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 16:31:06 +0100 Subject: [PATCH 23/45] Update loader documentation for some routes --- .../farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx | 1 + .../routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx | 1 + fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx | 1 + fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx | 1 + 4 files changed, 4 insertions(+) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx index 039360df7..0351208f7 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.tsx @@ -60,6 +60,7 @@ export const meta: MetaFunction = () => { * - harvests: The list of harvests related to the cultivation. * - b_lu_harvestable: The harvestable type from the catalogue, or "none" if not applicable. * - b_id_farm: The farm ID. + * - cultivationWritePermission: A Boolean indicating if the user is able to edit or delete the cultivation or add harvests to it. Set to true if the information could not be obtained. * * @throws {Response} If the farm, field, or cultivation ID is missing (status 400) or if the field or cultivation cannot be found (status 404). */ diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx index 418cad2cf..26507c82f 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx @@ -49,6 +49,7 @@ export const meta: MetaFunction = () => { * - cultivationsCatalogueOptions: A list of catalogue options for cultivations, formatted for use in a combobox. * - cultivations: The list of cultivations associated with the field. * - harvests: The harvest data for the first collection of cultivation harvests, or an empty array if none are available. + * - fieldWritePermissions: A Boolean indicating if the user is able to add cultivations to the field. Set to true if the information could not be obtained. * * @throws {Response} When the "b_id_farm" or "b_id" parameters are missing or if the field is not found. */ 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 fa5de995b..0716c51dc 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 @@ -58,6 +58,7 @@ export const meta: MetaFunction = () => { * - farmOptions: An array of validated farm options. * - fieldOptions: A sorted array of processed field options. * - userName: The name of the current user. + * - farmWritePermission: A Boolean indicating if the user is able to add fields to the farm. Set to true if the information could not be obtained. */ export async function loader({ request, params }: LoaderFunctionArgs) { try { diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx index 07d918979..1184d2589 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx @@ -62,6 +62,7 @@ export const meta: MetaFunction = () => { * - farmOptions: An array of validated farm options. * - fieldOptions: A sorted array of processed field options. * - userName: The name of the current user. + * - farmWritePermission: A Boolean indicating if the user is able to add things to the farm. Set to true if the information could not be obtained. */ export async function loader({ request, params }: LoaderFunctionArgs) { try { From 8a189b4c15c14abf688dbc50cbf397770f411344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 16:41:16 +0100 Subject: [PATCH 24/45] Hide inline harvest form buttons if it is not editable --- .../app/components/blocks/harvest/form.tsx | 72 ++++++++++--------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/fdm-app/app/components/blocks/harvest/form.tsx b/fdm-app/app/components/blocks/harvest/form.tsx index b9e0bfdc1..7f09488f8 100644 --- a/fdm-app/app/components/blocks/harvest/form.tsx +++ b/fdm-app/app/components/blocks/harvest/form.tsx @@ -623,41 +623,43 @@ export function HarvestForm(props: HarvestFormDialogProps) { className="grid lg:grid-cols-2 items-center gap-y-6 gap-x-8" /> -
- - -
+ {editable && ( +
+ + +
+ )}
From fb4dfa5dead73e9c9ca5ff4cffba11a5328de400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 25 Nov 2025 16:52:36 +0100 Subject: [PATCH 25/45] Add disabled buttons to empty field and cultivation overview tables --- .../farm.$b_id_farm.$calendar.field._index.tsx | 12 +++++++----- .../farm.$b_id_farm.$calendar.rotation._index.tsx | 12 +++++++----- 2 files changed, 14 insertions(+), 10 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 0716c51dc..fdbcd6394 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 @@ -261,11 +261,13 @@ export default function FarmFieldIndex() { :( -
- - - -
+ {loaderData.farmWritePermission && ( +
+ + + +
+ )} {/*

*/} diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx index 1184d2589..ca6a1267b 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx @@ -381,11 +381,13 @@ export default function FarmRotationIndex() { :( -
- - - -
+ {loaderData.farmWritePermission && ( +
+ + + +
+ )} ) : ( From ab554d3d3d76fdff1fc130b6c957e7100548ddba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 13:49:53 +0100 Subject: [PATCH 26/45] Remove add button altogether in the field fertilizer applications --- .../blocks/fertilizer-applications/card.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/fdm-app/app/components/blocks/fertilizer-applications/card.tsx b/fdm-app/app/components/blocks/fertilizer-applications/card.tsx index 9f06fd419..b0fab86bf 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/card.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/card.tsx @@ -152,12 +152,14 @@ export function FertilizerApplicationCard({ open={isDialogOpen} onOpenChange={handleDialogOpenChange} > - - - + {canCreateFertilizerApplication && ( + + + + )} From 73f143610c44e3eae5a145f9a66d8e1c1124b1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 13:51:20 +0100 Subject: [PATCH 27/45] Add disabled buttons to the farm settings properties route --- .../farm.$b_id_farm.settings.properties.tsx | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx index ecaa41a09..64efcd9cc 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx @@ -1,5 +1,5 @@ import { zodResolver } from "@hookform/resolvers/zod" -import { getFarm, updateFarm } from "@svenvw/fdm-core" +import { getFarm, hasPermission, updateFarm } from "@svenvw/fdm-core" import { useEffect } from "react" import { Form } from "react-hook-form" import { @@ -80,10 +80,19 @@ export async function loader({ request, params }: LoaderFunctionArgs) { statusText: "Farm is not found", }) } + const farmWritePermission = await hasPermission( + fdm, + "farm", + "write", + b_id_farm, + session.principal_id, + { fallback: true }, + ) // Return user information from loader return { farm: farm, + farmWritePermission: farmWritePermission, } } catch (error) { throw handleLoaderError(error) @@ -145,7 +154,12 @@ export default function FarmSettingsPropertiesBlock() { onSubmit={form.handleSubmit} method="POST" > -
+

-
- -
+ {loaderData.farmWritePermission && ( +
+ +
+ )} From 84e8095f76947ab69b820c2ddc66b9393d9808d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 13:59:37 +0100 Subject: [PATCH 28/45] Do not hide any buttons in the farm overview --- fdm-app/app/routes/farm.$b_id_farm._index.tsx | 73 ++++++------------- 1 file changed, 21 insertions(+), 52 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 fef7b17e7..77b5858ec 100644 --- a/fdm-app/app/routes/farm.$b_id_farm._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm._index.tsx @@ -1,11 +1,5 @@ import { cowHead } from "@lucide/lab" -import { - getFarm, - getFarms, - getFields, - hasPermission, - isAllowedToDeleteFarm, -} from "@svenvw/fdm-core" +import { getFarm, getFarms, getFields } from "@svenvw/fdm-core" import { ArrowRightLeft, BookOpenText, @@ -109,25 +103,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } }) - const farmSharePermission = hasPermission( - fdm, - "farm", - "share", - b_id_farm, - session.principal_id, - { fallback: true }, - ) - // Check if user is allowed to delete farm - let canDeleteFarm = await isAllowedToDeleteFarm( - fdm, - session.principal_id, - b_id_farm, - ) - // Add temporary workaround (until implemented in fdm-core) so that advisors are not able to delete a farm - if (canDeleteFarm && farm.roles.includes("advisor")) { - canDeleteFarm = false - } - // Return the farm ID and session info return { b_id_farm: b_id_farm, @@ -136,8 +111,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { farmArea: Math.round(farmArea), farmOptions: farmOptions, roles: farm.roles, - canDeleteFarm: canDeleteFarm, - farmSharePermission: await farmSharePermission, } } catch (error) { throw handleLoaderError(error) @@ -476,30 +449,26 @@ export default function FarmDashboardIndex() { Beweiding - {loaderData.farmSharePermission && ( - - )} - {loaderData.canDeleteFarm && ( - - )} + + From 09688e63a0fec9aa656ea6798d36af3fd15630aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 14:10:20 +0100 Subject: [PATCH 29/45] Add disabled buttons to the farm derogation --- .../farm.$b_id_farm.settings.derogation.tsx | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx index c03a88750..11519db8e 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx @@ -1,5 +1,6 @@ import { addDerogation, + hasPermission, listDerogations, removeDerogation, } from "@svenvw/fdm-core" @@ -40,7 +41,15 @@ export async function loader({ request, params }: LoaderFunctionArgs) { session.principal_id, b_id_farm, ) - return { b_id_farm, derogations } + const farmWritePermission = await hasPermission( + fdm, + "farm", + "write", + b_id_farm, + session.principal_id, + { fallback: true }, + ) + return { b_id_farm, derogations, farmWritePermission } } catch (error) { throw handleLoaderError(error) } @@ -86,7 +95,7 @@ export async function action({ request, params }: ActionFunctionArgs) { } export default function DerogationSettings() { - const { derogations } = useLoaderData() + const { derogations, farmWritePermission } = useLoaderData() const fetcher = useFetcher() const years = getCalendarSelection() @@ -99,9 +108,11 @@ export default function DerogationSettings() { Derogatie - Schakel derogatie in voor de jaren waarvoor dit bedrijf - in aanmerking komt. Dit heeft invloed op de berekening - van je gebruiksruimte. + {farmWritePermission + ? "Schakel derogatie in voor de jaren waarvoor dit bedrijf in aanmerking komt." + : "Hieronder staan de jaren waarin dit bedrijf derogatie in aanmerking heeft."}{" "} + Dit heeft invloed op de berekening van je + gebruiksruimte. @@ -121,25 +132,33 @@ export default function DerogationSettings() { {year} - + {farmWritePermission ? ( + + { + fetcher.submit( + { + year: String( + year, + ), + hasDerogation: + String( + hasDerogation, + ), + }, + { + method: "post", + }, + ) + }} + /> + + ) : ( { - fetcher.submit( - { - year: String( - year, - ), - hasDerogation: - String( - hasDerogation, - ), - }, - { method: "post" }, - ) - }} /> - + )} ) From 3da135d5fb45396f21e799f0e140ba8006f47055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 14:24:14 +0100 Subject: [PATCH 30/45] Add disabled buttons to the farm bio-certificate settings --- ...id_farm.settings.organic-certification.tsx | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx index 39fa32106..3074d94f9 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx @@ -1,6 +1,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { addOrganicCertification, + hasPermission, listOrganicCertifications, removeOrganicCertification, } from "@svenvw/fdm-core" @@ -93,7 +94,15 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // For now we expect that a farm can have only 1 certification const organicCertification = organicCertifications[0] - return { b_id_farm, organicCertification } + const farmWritePermission = await hasPermission( + fdm, + "farm", + "write", + b_id_farm, + session.principal_id, + ) + + return { b_id_farm, organicCertification, farmWritePermission } } catch (error) { throw handleLoaderError(error) } @@ -155,7 +164,8 @@ export async function action({ request, params }: ActionFunctionArgs) { } export default function OrganicCertificationSettings() { - const { organicCertification } = useLoaderData() + const { organicCertification, farmWritePermission } = + useLoaderData() const navigation = useNavigation() const isDeleting = @@ -309,15 +319,16 @@ export default function OrganicCertificationSettings() { Dit bedrijf heeft geen bio-certificaat - Als dit bedrijf wel een bio-certificaat heeft, - kunt u deze toevoegen. + {farmWritePermission ? "Als dit bedrijf wel een bio-certificaat heeft, kunt u deze toevoegen." : "U hebt geen toestemming om een bio-certificaat voor dit bedrijf toe te voegen."} - - - - - + {farmWritePermission && ( + + + + + + )} From ca3826dc59e14d215405b1884d640be669ee8513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 14:30:29 +0100 Subject: [PATCH 31/45] Fix flaky getRolesOfPrincipalForResource test --- fdm-core/src/authorization.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fdm-core/src/authorization.test.ts b/fdm-core/src/authorization.test.ts index 9bb3b1595..cf78d0d1e 100644 --- a/fdm-core/src/authorization.test.ts +++ b/fdm-core/src/authorization.test.ts @@ -835,7 +835,9 @@ describe("Authorization Functions", () => { farm_id, organization_member_id, ) - expect(roles).toEqual(["owner", "researcher"]) + expect(roles.length).toBe(2) + expect(roles).toContain("owner") + expect(roles).toContain("researcher") }) it("should throw error with invalid resource", async () => { From 2ad7030425ce7d7f563f419303cd576de65ebefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 14:40:06 +0100 Subject: [PATCH 32/45] Add missing effect dependencies in fertilizer application card --- .../components/blocks/fertilizer-applications/card.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fdm-app/app/components/blocks/fertilizer-applications/card.tsx b/fdm-app/app/components/blocks/fertilizer-applications/card.tsx index b0fab86bf..a278b316d 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/card.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/card.tsx @@ -125,7 +125,13 @@ export function FertilizerApplicationCard({ setIsDialogOpen(true) } } - }, [savedFormValues, applicationToEdit, isDialogOpen]) + }, [ + savedFormValues, + applicationToEdit, + isDialogOpen, + canCreateFertilizerApplication, + canModifyFertilizerApplication, + ]) function handleDialogOpenChange(state: boolean) { if (!state && params.b_id_farm && b_id_or_b_lu_catalogue) { From 24ac7ab1c5efa5ebb38eeee4611d118f1fcb43c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 14:40:42 +0100 Subject: [PATCH 33/45] Update documentation for hasPermission --- fdm-core/src/authorization.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fdm-core/src/authorization.ts b/fdm-core/src/authorization.ts index a42aa099b..b630f8759 100644 --- a/fdm-core/src/authorization.ts +++ b/fdm-core/src/authorization.ts @@ -220,7 +220,8 @@ export async function checkPermission( * @param resource_id - The unique identifier of the specific resource. * @param principal_id - The principal identifier(s); supports a single ID or an array. * @param options - Options to customize the behavior - * @returns Resolves to true if the principal is permitted to perform the action. Returns undefined in case of error. + * @returns Resolves to true if the principal is permitted to perform the action. + * If an error occurs and `options.fallback` is provided, returns the fallback value; otherwise throws. */ export async function hasPermission( fdm: FdmType, From 6f464a12e3b602188093f1da4b63051176d6222a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 14:49:50 +0100 Subject: [PATCH 34/45] Add disabled buttons to the farm grazing intention settings --- ....$b_id_farm.settings.grazing-intention.tsx | 67 +++++++++++++------ 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.grazing-intention.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.grazing-intention.tsx index d5c04db3f..56cdcff45 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.grazing-intention.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.grazing-intention.tsx @@ -1,4 +1,8 @@ -import { getGrazingIntentions, setGrazingIntention } from "@svenvw/fdm-core" +import { + getGrazingIntentions, + hasPermission, + setGrazingIntention, +} from "@svenvw/fdm-core" import { type ActionFunctionArgs, type LoaderFunctionArgs, @@ -39,7 +43,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { session.principal_id, b_id_farm, ) - return { b_id_farm, grazingIntentions } + const farmWritePermission = await hasPermission( + fdm, + "farm", + "write", + b_id_farm, + session.principal_id, + ) + return { b_id_farm, grazingIntentions, farmWritePermission } } catch (error) { throw handleLoaderError(error) } @@ -78,7 +89,8 @@ export async function action({ request, params }: ActionFunctionArgs) { } export default function GrazingIntentionSettings() { - const { grazingIntentions } = useLoaderData() + const { grazingIntentions, farmWritePermission } = + useLoaderData() const fetcher = useFetcher() const currentYear = new Date().getFullYear() @@ -92,9 +104,10 @@ export default function GrazingIntentionSettings() { Beweiding - Geef hier aan of je voor een bepaald jaar hebt beweid of - van plan bent te gaan beweiden. Dit heeft invloed op de - berekeningen. + {farmWritePermission + ? "Geef hier aan of je voor een bepaald jaar hebt beweid of van plan bent te gaan beweiden." + : "Hieronder staan de jaren waarin beweiding gepland is of heeft plaatsgevonden."}{" "} + Dit heeft invloed op de berekeningen. @@ -117,27 +130,37 @@ export default function GrazingIntentionSettings() { {year} - + {farmWritePermission ? ( + + { + fetcher.submit( + { + year: String( + year, + ), + hasGrazingIntention: + String( + hasGrazingIntention, + ), + }, + { + method: "post", + }, + ) + }} + /> + + ) : ( { - fetcher.submit( - { - year: String( - year, - ), - hasGrazingIntention: - String( - hasGrazingIntention, - ), - }, - { method: "post" }, - ) - }} /> - + )} ) From bcc2f04eac8f218770b0615abc58f479f2620579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 14:52:58 +0100 Subject: [PATCH 35/45] Improve derogation text --- fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx index 11519db8e..dc7545263 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx @@ -110,7 +110,7 @@ export default function DerogationSettings() { {farmWritePermission ? "Schakel derogatie in voor de jaren waarvoor dit bedrijf in aanmerking komt." - : "Hieronder staan de jaren waarin dit bedrijf derogatie in aanmerking heeft."}{" "} + : "Hieronder staan de jaren waarin dit bedrijf derogatie heeft."}{" "} Dit heeft invloed op de berekening van je gebruiksruimte. From de0ae02cf50f0f120c52aa3084af05b90d94ef87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 15:04:06 +0100 Subject: [PATCH 36/45] Add missing fallback true --- .../app/routes/farm.$b_id_farm.settings.grazing-intention.tsx | 1 + .../routes/farm.$b_id_farm.settings.organic-certification.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.grazing-intention.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.grazing-intention.tsx index 56cdcff45..6c7fa2cf6 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.grazing-intention.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.grazing-intention.tsx @@ -49,6 +49,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", b_id_farm, session.principal_id, + { fallback: true }, ) return { b_id_farm, grazingIntentions, farmWritePermission } } catch (error) { diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx index 3074d94f9..b11c847d5 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx @@ -100,6 +100,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "write", b_id_farm, session.principal_id, + { fallback: true }, ) return { b_id_farm, organicCertification, farmWritePermission } From 05ecb4ae101034fbe5ae2ef37c72829deac25df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 15:33:37 +0100 Subject: [PATCH 37/45] Fix various issues about the field harvests --- .../blocks/cultivation/card-harvests.tsx | 33 ++-- .../app/components/blocks/harvest/form.tsx | 153 +++++++++--------- 2 files changed, 92 insertions(+), 94 deletions(-) diff --git a/fdm-app/app/components/blocks/cultivation/card-harvests.tsx b/fdm-app/app/components/blocks/cultivation/card-harvests.tsx index ab75f6150..644098e38 100644 --- a/fdm-app/app/components/blocks/cultivation/card-harvests.tsx +++ b/fdm-app/app/components/blocks/cultivation/card-harvests.tsx @@ -24,9 +24,7 @@ export function CultivationHarvestsCard({ if (b_lu_harvestable === "multiple") { canAddHarvest = true } - if (typeof editable !== "undefined") { - canAddHarvest = editable - } + canAddHarvest &&= editable return ( @@ -35,22 +33,25 @@ export function CultivationHarvestsCard({ {b_lu_harvestable === "multiple" ? "Oogsten" : "Oogst"}
- { - if (!canAddHarvest) { - e.preventDefault() - } - }} - className={!canAddHarvest ? "cursor-not-allowed" : ""} + - + +
diff --git a/fdm-app/app/components/blocks/harvest/form.tsx b/fdm-app/app/components/blocks/harvest/form.tsx index 7f09488f8..44dbb6b87 100644 --- a/fdm-app/app/components/blocks/harvest/form.tsx +++ b/fdm-app/app/components/blocks/harvest/form.tsx @@ -459,7 +459,10 @@ function HarvestFormExplainer() { return ( - +

Waarom zie ik deze oogstparameters?

@@ -507,84 +510,78 @@ export function HarvestFormDialog(props: HarvestFormDialogProps) { method="post" action={action} > -
- - - - {isHarvestUpdate - ? "Oogst bijwerken" - : "Oogst toevoegen"} - - - {isHarvestUpdate - ? "Werk de oogst bij van dit gewas. Vul de gegevens in, zodat deze gebruikt kunnen worden in de berekeningen." - : "Voeg een oogst toe aan dit gewas. Vul de gegevens in, zodat deze gebruikt kunnen worden in de berekeningen."} - - + + + + {isHarvestUpdate + ? "Oogst bijwerken" + : "Oogst toevoegen"} + + + {isHarvestUpdate + ? "Werk de oogst bij van dit gewas. Vul de gegevens in, zodat deze gebruikt kunnen worden in de berekeningen." + : "Voeg een oogst toe aan dit gewas. Vul de gegevens in, zodat deze gebruikt kunnen worden in de berekeningen."} + + +
- - - - {editable && ( - - )} - - - - {editable && ( - - )} - - - -
+
+ + + + {editable && ( + + )} + + + + {editable && ( + + )} + + +
From 0dca996b37345d4661592a9024f3508c3cc3a7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 15:43:26 +0100 Subject: [PATCH 38/45] Fix import in farm.tsx --- fdm-app/app/routes/farm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/routes/farm.tsx b/fdm-app/app/routes/farm.tsx index 904ab5319..4547118e4 100644 --- a/fdm-app/app/routes/farm.tsx +++ b/fdm-app/app/routes/farm.tsx @@ -20,7 +20,7 @@ import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { useCalendarStore } from "~/store/calendar" import { useFarmStore } from "~/store/farm" -import { fdm } from "../lib/fdm.server" +import { fdm } from "~/lib/fdm.server" export const meta: MetaFunction = () => { return [ From d8dcd23aaef8cf878b97b338960b55c03da0bc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 16:10:31 +0100 Subject: [PATCH 39/45] Add changeset --- .changeset/dark-wombats-slide.md | 5 +++++ .changeset/jolly-pens-crash.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/dark-wombats-slide.md create mode 100644 .changeset/jolly-pens-crash.md diff --git a/.changeset/dark-wombats-slide.md b/.changeset/dark-wombats-slide.md new file mode 100644 index 000000000..12f70c6f1 --- /dev/null +++ b/.changeset/dark-wombats-slide.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-core": minor +--- + +Added the `hasPermission` function that returns if the principal has permission to perform a specific action on a resource without throwing exceptions or creating an audit log. diff --git a/.changeset/jolly-pens-crash.md b/.changeset/jolly-pens-crash.md new file mode 100644 index 000000000..8a5b1be95 --- /dev/null +++ b/.changeset/jolly-pens-crash.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Various UI elements are now hidden if the current logged-in user doesn't have permission to perform the actions that they represent. From f4c2b6f76c13dd22cdd2d67b56cd4b8b1e3e8497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 16:31:33 +0100 Subject: [PATCH 40/45] Add disabled buttons to the single field overview page --- ...b_id_farm.$calendar.field.$b_id.overview.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.overview.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.overview.tsx index fce74bf66..38033c9bd 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.overview.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.overview.tsx @@ -1,6 +1,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { getField, + hasPermission, listAvailableAcquiringMethods, updateField, } from "@svenvw/fdm-core" @@ -41,6 +42,7 @@ import { clientConfig } from "~/lib/config" import { handleActionError, handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" import { extractFormValuesFromRequest } from "~/lib/form" +import { cn } from "~/lib/utils" export const meta: MetaFunction = () => { return [ @@ -86,9 +88,19 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }) } + const fieldWritePermission = await hasPermission( + fdm, + "field", + "write", + b_id, + session.principal_id, + { fallback: true }, + ) + // Return user information from loader return { field: field, + fieldWritePermission, acquiringMethodOptions: listAvailableAcquiringMethods(), } } catch (error) { @@ -233,7 +245,10 @@ export default function FarmFieldsOverviewBlock() { - - )} +
+ +
diff --git a/fdm-app/app/components/blocks/fertilizer-applications/card.tsx b/fdm-app/app/components/blocks/fertilizer-applications/card.tsx index a278b316d..b009e1afb 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/card.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/card.tsx @@ -15,6 +15,7 @@ import { DialogTitle, DialogTrigger, } from "~/components/ui/dialog" +import { cn } from "~/lib/utils" import { FertilizerApplicationForm } from "./form" import { FertilizerApplicationsList } from "./list" import type { FertilizerApplication, FertilizerOption } from "./types.d" @@ -158,14 +159,18 @@ export function FertilizerApplicationCard({ open={isDialogOpen} onOpenChange={handleDialogOpenChange} > - {canCreateFertilizerApplication && ( - - - - )} + + + diff --git a/fdm-app/app/components/blocks/fertilizer-applications/list.tsx b/fdm-app/app/components/blocks/fertilizer-applications/list.tsx index ac3f0e027..2dd9984e1 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/list.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/list.tsx @@ -4,6 +4,7 @@ import { format } from "date-fns" import { nl } from "date-fns/locale" import { Circle, Diamond, Square, Trash, Triangle } from "lucide-react" import { useFetcher } from "react-router" +import { LoadingSpinner } from "~/components/custom/loadingspinner" import { Button } from "~/components/ui/button" import { Empty, @@ -26,7 +27,7 @@ import { TooltipProvider, TooltipTrigger, } from "~/components/ui/tooltip" -import { LoadingSpinner } from "../../custom/loadingspinner" +import { cn } from "~/lib/utils" export function FertilizerApplicationsList({ fertilizerApplications, @@ -124,49 +125,49 @@ export function FertilizerApplicationsList({

- {editable && ( - - - - - - - -

Verwijder

-
-
-
-
- )} + }} + > + {fetcher.state === + "submitting" ? ( + + ) : ( + + )} + + + +

Verwijder

+
+ + + ) diff --git a/fdm-app/app/components/blocks/fertilizer/form.tsx b/fdm-app/app/components/blocks/fertilizer/form.tsx index 3b20583c0..ed471faf0 100644 --- a/fdm-app/app/components/blocks/fertilizer/form.tsx +++ b/fdm-app/app/components/blocks/fertilizer/form.tsx @@ -22,6 +22,7 @@ import { SelectTrigger, SelectValue, } from "~/components/ui/select" +import { cn } from "~/lib/utils" export interface FertilizerParameterDescriptionItem { parameter: FertilizerParameters @@ -258,19 +259,20 @@ export function FertilizerForm({ ))} - {editable && ( -
- -
- )} +
+ +
diff --git a/fdm-app/app/components/blocks/fertilizer/table.tsx b/fdm-app/app/components/blocks/fertilizer/table.tsx index 454e5a37b..e81c570e7 100644 --- a/fdm-app/app/components/blocks/fertilizer/table.tsx +++ b/fdm-app/app/components/blocks/fertilizer/table.tsx @@ -21,6 +21,7 @@ import { TableHeader, TableRow, } from "~/components/ui/table" +import { cn } from "~/lib/utils" interface DataTableProps { columns: ColumnDef[] @@ -67,16 +68,14 @@ export function DataTable({ } className="max-w-sm" /> - {canAddItem && ( -
- - - -
- )} +
+ + + +
diff --git a/fdm-app/app/components/blocks/fields/table.tsx b/fdm-app/app/components/blocks/fields/table.tsx index 53e1cc0a4..ebbd6db2e 100644 --- a/fdm-app/app/components/blocks/fields/table.tsx +++ b/fdm-app/app/components/blocks/fields/table.tsx @@ -214,56 +214,57 @@ export function DataTable({ - {canAddItem && ( - <> - - - -
- {isFertilizerButtonDisabled ? ( - - ) : ( - - - - )} -
-
- -

{fertilizerTooltipContent}

-
-
-
- - - - - + + + +
+ {isFertilizerButtonDisabled ? ( + + ) : ( + - - -

Voeg een nieuw perceel toe

-
- - - - )} + )} +
+
+ +

{fertilizerTooltipContent}

+
+
+
+ + + + + + + + + +

Voeg een nieuw perceel toe

+
+
+
diff --git a/fdm-app/app/components/blocks/harvest/form.tsx b/fdm-app/app/components/blocks/harvest/form.tsx index 44dbb6b87..106d122b9 100644 --- a/fdm-app/app/components/blocks/harvest/form.tsx +++ b/fdm-app/app/components/blocks/harvest/form.tsx @@ -531,28 +531,28 @@ export function HarvestFormDialog(props: HarvestFormDialogProps) { - {editable && ( - - )} + - )} + @@ -620,43 +619,43 @@ export function HarvestForm(props: HarvestFormDialogProps) { className="grid lg:grid-cols-2 items-center gap-y-6 gap-x-8" /> - {editable && ( -
- - -
- )} +
+ + +
diff --git a/fdm-app/app/components/blocks/rotation/table.tsx b/fdm-app/app/components/blocks/rotation/table.tsx index 9c9da23a9..fa4877077 100644 --- a/fdm-app/app/components/blocks/rotation/table.tsx +++ b/fdm-app/app/components/blocks/rotation/table.tsx @@ -278,81 +278,79 @@ export function DataTable({ - {canAddItem && ( - <> - - - -
- {isFertilizerButtonDisabled ? ( - - ) : ( - - - - )} -
-
- -

{fertilizerTooltipContent}

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

{harvestTooltipContent}

-
-
-
- - )} + + + +
+ {isFertilizerButtonDisabled ? ( + + ) : ( + + + + )} +
+
+ +

{fertilizerTooltipContent}

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

{harvestTooltipContent}

+
+
+
diff --git a/fdm-app/app/components/blocks/soil/form.tsx b/fdm-app/app/components/blocks/soil/form.tsx index 4ecc888bb..29bbbe440 100644 --- a/fdm-app/app/components/blocks/soil/form.tsx +++ b/fdm-app/app/components/blocks/soil/form.tsx @@ -6,6 +6,7 @@ import { RemixFormProvider, useRemixForm } from "remix-hook-form" import type { z } from "zod" import { FormSchema } from "@/app/components/blocks/soil/formschema" import type { SoilAnalysis } from "@/app/components/blocks/soil/types" +import { DatePicker } from "~/components/custom/date-picker" import { LoadingSpinner } from "~/components/custom/loadingspinner" import { Button } from "~/components/ui/button" import { @@ -24,7 +25,7 @@ import { SelectTrigger, SelectValue, } from "~/components/ui/select" -import { DatePicker } from "../../custom/date-picker" +import { cn } from "~/lib/utils" export function SoilAnalysisForm(props: { soilAnalysis: SoilAnalysis | undefined @@ -226,7 +227,12 @@ export function SoilAnalysisForm(props: { } })}
- {editable &&
+
-
} +
diff --git a/fdm-app/app/components/blocks/soil/list.tsx b/fdm-app/app/components/blocks/soil/list.tsx index 9ee6984e0..2e6f123f8 100644 --- a/fdm-app/app/components/blocks/soil/list.tsx +++ b/fdm-app/app/components/blocks/soil/list.tsx @@ -72,28 +72,28 @@ export function SoilAnalysesList({ Bewerk
- {(canModifySoilAnalysis[analysis.a_id] ?? - true) ? ( - - ) : null} + diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil._index.tsx index 676d6d9cb..d1aadc1e5 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil._index.tsx @@ -25,6 +25,7 @@ import { getSession } from "~/lib/auth.server" import { getTimeframe } from "~/lib/calendar" import { handleActionError, handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" +import { cn } from "~/lib/utils" /** * Loader function for the soil data page of a specific farm field. @@ -165,14 +166,17 @@ export default function FarmFieldSoilOverviewBlock() { Parameters Analyses - {loaderData.fieldWritePermission && ( - - )} + @@ -189,13 +193,18 @@ export default function FarmFieldSoilOverviewBlock() { bodem bij te houden

- {loaderData.fieldWritePermission && ( - - )} + ) : ( { return [ @@ -263,13 +264,20 @@ export default function FarmFieldIndex() { :( - {loaderData.farmWritePermission && ( -
+
+ + Maak een perceel -
- )} + +
{/*

*/} diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx index 2f24bb5b7..ebeba99ae 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation._index.tsx @@ -34,6 +34,7 @@ import { getCalendar, getTimeframe } from "~/lib/calendar" import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" +import { cn } from "~/lib/utils" export const meta: MetaFunction = () => { return [ @@ -382,13 +383,20 @@ export default function FarmRotationIndex() { :( - {loaderData.farmWritePermission && ( -
+
+ + Maak een perceel -
- )} + +
) : ( diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx index 91c6aa246..bdf442019 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx @@ -64,6 +64,7 @@ import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" import { extractFormValuesFromRequest } from "~/lib/form" +import { cn } from "~/lib/utils" export const meta: MetaFunction = () => { return [ @@ -321,16 +322,22 @@ export default function OrganicCertificationSettings() { Dit bedrijf heeft geen bio-certificaat - {farmWritePermission ? "Als dit bedrijf wel een bio-certificaat heeft, kunt u deze toevoegen." : "U hebt geen toestemming om een bio-certificaat voor dit bedrijf toe te voegen."} + {farmWritePermission + ? "Als dit bedrijf wel een bio-certificaat heeft, kunt u deze toevoegen." + : "U hebt geen toestemming om een bio-certificaat voor dit bedrijf toe te voegen."} - {farmWritePermission && ( - - - - - - )} + + + + + diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx index 177f0c5f1..08cbd1632 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx @@ -30,6 +30,7 @@ import { getSession } from "~/lib/auth.server" import { handleActionError, handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" import { extractFormValuesFromRequest } from "~/lib/form" +import { cn } from "~/lib/utils" const { isPostalCode } = validator @@ -253,20 +254,23 @@ Wageningen"
- {loaderData.farmWritePermission && ( -
- -
- )} +
+ +
From 17575749303c1c155e678e666bef860802cd10bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Thu, 27 Nov 2025 14:49:51 +0100 Subject: [PATCH 44/45] Update changeset --- .changeset/dark-wombats-slide.md | 2 +- .changeset/jolly-pens-crash.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/dark-wombats-slide.md b/.changeset/dark-wombats-slide.md index 12f70c6f1..80cc48679 100644 --- a/.changeset/dark-wombats-slide.md +++ b/.changeset/dark-wombats-slide.md @@ -2,4 +2,4 @@ "@svenvw/fdm-core": minor --- -Added the `hasPermission` function that returns if the principal has permission to perform a specific action on a resource without throwing exceptions or creating an audit log. +Added the optional `strict` parameter to the `checkPermission` function, which, when specified as false, disables audit logging, and throwing an exception if the principal has no permission. diff --git a/.changeset/jolly-pens-crash.md b/.changeset/jolly-pens-crash.md index 8a5b1be95..f817f18d5 100644 --- a/.changeset/jolly-pens-crash.md +++ b/.changeset/jolly-pens-crash.md @@ -2,4 +2,4 @@ "@svenvw/fdm-app": minor --- -Various UI elements are now hidden if the current logged-in user doesn't have permission to perform the actions that they represent. +Various UI elements are now hidden or grayed out if the current logged-in user doesn't have permission to perform the actions that they represent. From d9754841a07268cc421ffc526fb28edf5b919a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Thu, 27 Nov 2025 15:02:34 +0100 Subject: [PATCH 45/45] Fix typo --- .../routes/farm.$b_id_farm.settings.organic-certification.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx b/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx index bdf442019..66ca969eb 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.settings.organic-certification.tsx @@ -324,7 +324,7 @@ export default function OrganicCertificationSettings() { {farmWritePermission ? "Als dit bedrijf wel een bio-certificaat heeft, kunt u deze toevoegen." - : "U hebt geen toestemming om een bio-certificaat voor dit bedrijf toe te voegen."} + : "U heeft geen toestemming om een bio-certificaat voor dit bedrijf toe te voegen."}