diff --git a/apps/edr-freight-api/src/common/booking-guards.ts b/apps/edr-freight-api/src/common/booking-guards.ts index f9eab4d39..68bbdcba4 100644 --- a/apps/edr-freight-api/src/common/booking-guards.ts +++ b/apps/edr-freight-api/src/common/booking-guards.ts @@ -75,7 +75,7 @@ export const TrainSchedulingView = () => BookingStaff(FREIGHT_PERMS.trainScheduling.view); // Granular train-scheduling actions replace the retired coarse manage: -// create a schedule, update (assign/consist/loading/finalize/dispatch/arrive…), +// create a schedule, update (assign/consist/finalize/dispatch/arrive…), // cancel a schedule, reschedule (+ maintenance), and manage global rules. export const TrainSchedulingCreate = () => BookingStaff(FREIGHT_PERMS.trainScheduling.create); @@ -83,6 +83,18 @@ export const TrainSchedulingCreate = () => export const TrainSchedulingUpdate = () => BookingStaff(FREIGHT_PERMS.trainScheduling.update); +/** + * Confirm a booking's cargo loaded/unloaded at a yard — carved out of the + * coarse `update` so it can be granted independently of general schedule + * editing. Same two keys gate import, export, and intercity movements alike: + * the generic per-booking route and the intercity-specific one both use them. + */ +export const TrainSchedulingLoad = () => + BookingStaff(FREIGHT_PERMS.trainScheduling.load); + +export const TrainSchedulingUnload = () => + BookingStaff(FREIGHT_PERMS.trainScheduling.unload); + export const TrainSchedulingCancel = () => BookingStaff(FREIGHT_PERMS.trainScheduling.cancel); diff --git a/apps/edr-freight-api/src/modules/train-scheduling/controllers/train-scheduling.controller.ts b/apps/edr-freight-api/src/modules/train-scheduling/controllers/train-scheduling.controller.ts index 6afcf9380..03eb5ad88 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/controllers/train-scheduling.controller.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/controllers/train-scheduling.controller.ts @@ -16,7 +16,9 @@ import { TrainSchedulingCancel, TrainSchedulingCreate, TrainSchedulingEditTrainNumber, + TrainSchedulingLoad, TrainSchedulingReschedule, + TrainSchedulingUnload, TrainSchedulingRulesManage, TrainSchedulingUpdate, TrainSchedulingView, @@ -598,7 +600,7 @@ export class TrainSchedulingController { } @Post("schedules/:id/bookings/:bookingId/load") - @TrainSchedulingUpdate() + @TrainSchedulingLoad() @ApiOperation({ summary: "Confirm a booking's cargo loaded at its origin yard (any direction; train must be at that yard)", @@ -611,7 +613,7 @@ export class TrainSchedulingController { } @Post("schedules/:id/bookings/:bookingId/unload") - @TrainSchedulingUpdate() + @TrainSchedulingUnload() @ApiOperation({ summary: "Confirm a booking's cargo unloaded at its destination yard — per-booking arrival, may precede the train's final arrival", @@ -624,7 +626,7 @@ export class TrainSchedulingController { } @Post("schedules/:id/intercity/:bookingId/load") - @TrainSchedulingUpdate() + @TrainSchedulingLoad() @ApiOperation({ summary: "Confirm intercity cargo loaded (train must be at the booking's origin yard)", }) @@ -636,7 +638,7 @@ export class TrainSchedulingController { } @Post("schedules/:id/intercity/:bookingId/unload") - @TrainSchedulingUpdate() + @TrainSchedulingUnload() @ApiOperation({ summary: "Confirm intercity cargo unloaded at the booking's destination yard (completes the booking)", diff --git a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts index 5fde44f2d..8204ee2d3 100644 --- a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts +++ b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts @@ -1469,6 +1469,20 @@ export const SCHEDULING_EXTRA_PERMISSIONS: FreightPermissionSeed[] = [ "edr_freight_app:train_scheduling:rules_manage", "Manage global scheduling rules", ), + // Carved out of the coarse `update` — confirming a booking's cargo loaded/ + // unloaded at a yard, across import, export, and intercity movements alike + // (the same schedules/:id/bookings/:bookingId/{load,unload} + intercity + // routes serve all three directions). + perm( + "a2a00001-0001-4000-8000-000000000006", + "edr_freight_app:train_scheduling:load", + "Confirm cargo loaded (import, export, intercity)", + ), + perm( + "a2a00001-0001-4000-8000-000000000007", + "edr_freight_app:train_scheduling:unload", + "Confirm cargo unloaded (import, export, intercity)", + ), ]; // L. Administration & settings (split from the coarse admin umbrella) @@ -1996,6 +2010,15 @@ export const FREIGHT_PERMS = { cancel: "edr_freight_app:train_scheduling:cancel", reschedule: "edr_freight_app:train_scheduling:reschedule", rulesManage: "edr_freight_app:train_scheduling:rules_manage", + /** + * Confirm a booking's cargo loaded/unloaded at a yard — carved out of the + * coarse `update` so load/unload can be granted independently of general + * schedule editing. Covers import, export, and intercity alike: the + * generic per-booking route and the intercity-specific one both gate on + * these same two keys. + */ + load: "edr_freight_app:train_scheduling:load", + unload: "edr_freight_app:train_scheduling:unload", dispatch: "edr_freight_app:train_scheduling:dispatch", markPaid: "edr_freight_app:train_scheduling:mark_paid", expireBooking: "edr_freight_app:train_scheduling:expire_booking", @@ -2602,6 +2625,8 @@ export const ROLE_PERMISSION_PRESETS = { FREIGHT_PERMS.trainScheduling.view, FREIGHT_PERMS.trainScheduling.create, FREIGHT_PERMS.trainScheduling.update, + FREIGHT_PERMS.trainScheduling.load, + FREIGHT_PERMS.trainScheduling.unload, FREIGHT_PERMS.trainScheduling.cancel, FREIGHT_PERMS.trainScheduling.reschedule, FREIGHT_PERMS.trainScheduling.rulesManage, @@ -2822,6 +2847,8 @@ export const POSITION_PERMISSION_PRESETS = { FREIGHT_PERMS.trainScheduling.view, FREIGHT_PERMS.trainScheduling.create, FREIGHT_PERMS.trainScheduling.update, + FREIGHT_PERMS.trainScheduling.load, + FREIGHT_PERMS.trainScheduling.unload, FREIGHT_PERMS.trainScheduling.cancel, FREIGHT_PERMS.trainScheduling.reschedule, FREIGHT_PERMS.trainScheduling.rulesManage, diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/IntercityRideAlongPanel.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/IntercityRideAlongPanel.tsx index 1636cfa6d..0342b9ae9 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/IntercityRideAlongPanel.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/IntercityRideAlongPanel.tsx @@ -15,6 +15,8 @@ import { import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertCircle, ArrowRight, PackageCheck, PackageOpen, TrainFront } from "lucide-react"; +import { useAuth } from "@/auth/useAuth"; +import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from "@/lib/permissions"; import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; import type { @@ -130,6 +132,9 @@ export function IntercityRideAlongPanel({ direction: string | null | undefined; }) { const { toast } = useToast(); + const { user } = useAuth(); + const canLoad = hasFreightPermission(user, FREIGHT_PERMS.trainScheduling.load); + const canUnload = hasFreightPermission(user, FREIGHT_PERMS.trainScheduling.unload); const queryClient = useQueryClient(); const [selected, setSelected] = useState([]); @@ -378,12 +383,19 @@ export function IntercityRideAlongPanel({ {row.status === "PAID" && ( - + + + + void; }) { const { toast } = useToast(); + const { user } = useAuth(); + const canUnload = hasFreightPermission(user, FREIGHT_PERMS.warehouseInventory.unload); const { data: trains = [], isLoading } = useQuery( api.warehouses.importArriveQueue.queryOptions({ enabled }), ); @@ -2489,23 +2497,25 @@ export function ImportArriveQueueTab({ > Open - + + + diff --git a/apps/edr-freight-web/backoffice/src/lib/permissions.ts b/apps/edr-freight-web/backoffice/src/lib/permissions.ts index 177976b28..d8ef47f35 100644 --- a/apps/edr-freight-web/backoffice/src/lib/permissions.ts +++ b/apps/edr-freight-web/backoffice/src/lib/permissions.ts @@ -104,6 +104,9 @@ export const FREIGHT_PERMS = { view: "edr_freight_app:train_scheduling:view", create: "edr_freight_app:train_scheduling:create", update: "edr_freight_app:train_scheduling:update", + /** Confirm cargo loaded/unloaded at a yard — import, export, and intercity alike. */ + load: "edr_freight_app:train_scheduling:load", + unload: "edr_freight_app:train_scheduling:unload", cancel: "edr_freight_app:train_scheduling:cancel", reschedule: "edr_freight_app:train_scheduling:reschedule", rulesManage: "edr_freight_app:train_scheduling:rules_manage", diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/IntercityPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/IntercityPage.tsx index cfda31629..f756af834 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/IntercityPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/IntercityPage.tsx @@ -17,6 +17,8 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, PackageCheck, PackageOpen, TrainFront, Warehouse } from "lucide-react"; import { PageContainer, PageHeader } from "@/components/page"; +import { useAuth } from "@/auth/useAuth"; +import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from "@/lib/permissions"; import ListControls from "@/components/common/ListControls"; // Generic list footer — already shared by the fleet and train-scheduling lists // despite the ruleEngine path. @@ -93,6 +95,9 @@ const apiErrorMessage = (error: unknown) => { function Rows({ rows }: { rows: IntercityRideAlongRow[] }) { const { toast } = useToast(); + const { user } = useAuth(); + const canLoad = hasFreightPermission(user, FREIGHT_PERMS.trainScheduling.load); + const canUnload = hasFreightPermission(user, FREIGHT_PERMS.trainScheduling.unload); const queryClient = useQueryClient(); const refresh = () => queryClient.invalidateQueries({ @@ -201,31 +206,37 @@ function Rows({ rows }: { rows: IntercityRideAlongRow[] }) { {/* Work the cargo right here while the train is at the yard. */} {r.trainScheduleId && atOrigin(r) && isWaiting(r) && r.status === "PAID" && ( - + + + )} {r.trainScheduleId && atDestination(r) && isRiding(r) && ( - + + + )}