Merge pull request #926 from Tria-plc/freight_feature/usermanagement

implement freight permissions for trains, wagons, and routes
This commit is contained in:
marshal
2026-07-23 08:57:59 +03:00
committed by GitHub
19 changed files with 369 additions and 237 deletions

View File

@@ -271,19 +271,19 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
label: "Routes",
href: "/dashboard/routes",
icon: <Network />,
permission: FREIGHT_PERMS.fleet.view,
permission: [FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view],
},
{
label: "Locomotives",
href: "/dashboard/locomotives",
icon: <Train />,
permission: FREIGHT_PERMS.fleet.view,
permission: [FREIGHT_PERMS.locomotives.view, FREIGHT_PERMS.fleet.view],
},
{
label: "Train Builder",
href: "/dashboard/train-builder",
icon: <Hammer />,
permission: FREIGHT_PERMS.fleet.view,
permission: [FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view],
},
// {
@@ -295,7 +295,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
label: "Wagons",
href: "/dashboard/wagons",
icon: <Truck />,
permission: FREIGHT_PERMS.fleet.view,
permission: [FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view],
},
{
label: "Vehicles",
@@ -1113,7 +1113,7 @@ const App = () => {
<Route
path="routes"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view]}>
<RoutesPage />
</RequirePermission>
}
@@ -1121,7 +1121,7 @@ const App = () => {
<Route
path="locomotives"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.locomotives.view, FREIGHT_PERMS.fleet.view]}>
<FleetResourcePage />
</RequirePermission>
}
@@ -1129,7 +1129,7 @@ const App = () => {
<Route
path="trains"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
<FleetResourcePage />
</RequirePermission>
}
@@ -1137,7 +1137,7 @@ const App = () => {
<Route
path="trains/:id"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
<TrainDetailPage />
</RequirePermission>
}
@@ -1145,7 +1145,7 @@ const App = () => {
<Route
path="train-builder"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
<TrainBuilderListPage />
</RequirePermission>
}
@@ -1153,7 +1153,7 @@ const App = () => {
<Route
path="train-builder/:id"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
<TrainBuilderDetailPage />
</RequirePermission>
}
@@ -1161,7 +1161,7 @@ const App = () => {
<Route
path="wagons"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view]}>
<FleetResourcePage />
</RequirePermission>
}
@@ -1169,7 +1169,7 @@ const App = () => {
<Route
path="containers"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.containers.view, FREIGHT_PERMS.fleet.view]}>
<FleetResourcePage />
</RequirePermission>
}
@@ -1177,7 +1177,7 @@ const App = () => {
<Route
path="cargoes"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.cargoes.view, FREIGHT_PERMS.fleet.view]}>
<FleetResourcePage />
</RequirePermission>
}
@@ -1263,7 +1263,7 @@ const App = () => {
<Route
path="routes"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view]}>
<RoutesPage />
</RequirePermission>
}
@@ -1351,7 +1351,7 @@ const App = () => {
<Route
path="locomotives"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.locomotives.view, FREIGHT_PERMS.fleet.view]}>
<FleetResourcePage />
</RequirePermission>
}
@@ -1359,7 +1359,7 @@ const App = () => {
<Route
path="trains"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
<FleetResourcePage />
</RequirePermission>
}
@@ -1367,7 +1367,7 @@ const App = () => {
<Route
path="trains/:id"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
<TrainDetailPage />
</RequirePermission>
}
@@ -1375,7 +1375,7 @@ const App = () => {
<Route
path="train-builder"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
<TrainBuilderListPage />
</RequirePermission>
}
@@ -1383,7 +1383,7 @@ const App = () => {
<Route
path="train-builder/:id"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
<TrainBuilderDetailPage />
</RequirePermission>
}
@@ -1391,7 +1391,7 @@ const App = () => {
<Route
path="wagons"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view]}>
<FleetResourcePage />
</RequirePermission>
}
@@ -1399,7 +1399,7 @@ const App = () => {
<Route
path="containers"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.containers.view, FREIGHT_PERMS.fleet.view]}>
<FleetResourcePage />
</RequirePermission>
}
@@ -1407,7 +1407,7 @@ const App = () => {
<Route
path="cargoes"
element={
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
<RequirePermission permission={[FREIGHT_PERMS.cargoes.view, FREIGHT_PERMS.fleet.view]}>
<FleetResourcePage />
</RequirePermission>
}

View File

@@ -19,8 +19,9 @@ export interface FleetCardGridProps {
pageCount: number;
totalCount: number;
onPaginationChange: OnChangeFn<PaginationState>;
onEdit: (record: FleetRecord) => void;
onRemove: (record: FleetRecord) => void;
/** Omit to hide the action (caller lacks the update/delete permission). */
onEdit?: (record: FleetRecord) => void;
onRemove?: (record: FleetRecord) => void;
}
const FleetCardGrid = ({

View File

@@ -8,8 +8,9 @@ import type { FleetRecord } from "@/services/fleet/fleet.service";
export interface FleetRecordActionsProps {
record: FleetRecord;
config: FleetResourceConfig;
onEdit: (record: FleetRecord) => void;
onRemove: (record: FleetRecord) => void;
/** Omit to hide the action (caller lacks the update/delete permission). */
onEdit?: (record: FleetRecord) => void;
onRemove?: (record: FleetRecord) => void;
onAssignDriver?: (record: FleetRecord) => void;
onHistory?: (record: FleetRecord) => void;
onViewDetail?: (record: FleetRecord) => void;
@@ -42,6 +43,17 @@ const FleetRecordActions = ({
navigate(config.detailPath.replace(":id", String(record.id)));
};
if (
!onEdit &&
!onRemove &&
!showDetail &&
!showViewDetail &&
!showHistory &&
!(isVehicle && onAssignDriver)
) {
return null;
}
if (layout === "compact") {
return (
<Menu position="bottom-end" withinPortal shadow="md">
@@ -61,12 +73,14 @@ const FleetRecordActions = ({
Assign Driver
</MenuItem>
) : null}
<MenuItem
onClick={() => onEdit(record)}
leftSection={<Edit2 size={14} strokeWidth={2} />}
>
Edit
</MenuItem>
{onEdit ? (
<MenuItem
onClick={() => onEdit(record)}
leftSection={<Edit2 size={14} strokeWidth={2} />}
>
Edit
</MenuItem>
) : null}
{showViewDetail ? (
<MenuItem
onClick={() => onViewDetail?.(record)}
@@ -91,13 +105,15 @@ const FleetRecordActions = ({
View details
</MenuItem>
) : null}
<MenuItem
color="red"
onClick={() => onRemove(record)}
leftSection={<Trash2 size={14} strokeWidth={2} />}
>
{removeLabel}
</MenuItem>
{onRemove ? (
<MenuItem
color="red"
onClick={() => onRemove(record)}
leftSection={<Trash2 size={14} strokeWidth={2} />}
>
{removeLabel}
</MenuItem>
) : null}
</Menu.Dropdown>
</Menu>
);
@@ -121,12 +137,14 @@ const FleetRecordActions = ({
Assign Driver
</MenuItem>
) : null}
<MenuItem
onClick={() => onEdit(record)}
leftSection={<Edit2 size={14} strokeWidth={2} />}
>
Edit
</MenuItem>
{onEdit ? (
<MenuItem
onClick={() => onEdit(record)}
leftSection={<Edit2 size={14} strokeWidth={2} />}
>
Edit
</MenuItem>
) : null}
{showViewDetail ? (
<MenuItem
onClick={() => onViewDetail?.(record)}
@@ -143,13 +161,15 @@ const FleetRecordActions = ({
View details
</MenuItem>
) : null}
<MenuItem
color="red"
onClick={() => onRemove(record)}
leftSection={<Trash2 size={14} strokeWidth={2} />}
>
{removeLabel}
</MenuItem>
{onRemove ? (
<MenuItem
color="red"
onClick={() => onRemove(record)}
leftSection={<Trash2 size={14} strokeWidth={2} />}
>
{removeLabel}
</MenuItem>
) : null}
</Menu.Dropdown>
</Menu>
);

View File

@@ -507,6 +507,31 @@ export function canViewFleet(user: AuthUser | null | undefined): boolean {
return hasPermission(user, FREIGHT_PERMS.fleet.view);
}
export type FleetCrudResource =
| "locomotives"
| "wagons"
| "trains"
| "routes"
| "containers"
| "cargoes"
| "vehicles"
| "drivers";
/**
* Per-resource fleet CRUD check. The legacy coarse fleet:manage key still
* grants every action (mirrors the API's one-of guard fallback).
*/
export function canFleetAction(
user: AuthUser | null | undefined,
resource: FleetCrudResource,
action: "create" | "update" | "delete",
): boolean {
return (
hasPermission(user, FREIGHT_PERMS[resource][action]) ||
hasPermission(user, FREIGHT_PERMS.fleet.manage)
);
}
export function isFreightAdmin(user: AuthUser | null | undefined): boolean {
return hasPermission(user, FREIGHT_PERMS.admin);
}

View File

@@ -3,6 +3,8 @@ import { Box, Button, Card, Container, Group, Modal, Select, Stack, Text, Title
import { useMutation, useQuery } from "@tanstack/react-query";
import { api } from "@/services/api";
import { useAuth } from "@/auth/useAuth";
import { canFleetAction } from "@/lib/permissions";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { Inbox, Plus, Warehouse } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
@@ -37,6 +39,10 @@ const FleetResourcePage = () => {
const slug = getFleetSlugFromPath(location.pathname) ?? DEFAULT_SLUG;
const config = getFleetResource(slug);
const { toast } = useToast();
const { user } = useAuth();
const canCreate = canFleetAction(user, slug, "create");
const canUpdate = canFleetAction(user, slug, "update");
const canDelete = canFleetAction(user, slug, "delete");
const { pagination, setPagination } = usePagination({ pageSize: 10 });
const [search, setSearch] = useState("");
@@ -278,12 +284,16 @@ const FleetResourcePage = () => {
record={row.original}
config={config}
layout="compact"
onEdit={(record) => {
setEditing(record);
setFormOpen(true);
}}
onRemove={setRemoveTarget}
onAssignDriver={setAssigningDriver}
onEdit={
canUpdate
? (record) => {
setEditing(record);
setFormOpen(true);
}
: undefined
}
onRemove={canDelete ? setRemoveTarget : undefined}
onAssignDriver={canUpdate ? setAssigningDriver : undefined}
onHistory={setHistoryTarget}
/>
</div>
@@ -291,7 +301,7 @@ const FleetResourcePage = () => {
});
return base;
}, [config, dynamicOptions.yards]);
}, [config, dynamicOptions.yards, canUpdate, canDelete]);
const tableStatus = isLoading ? "loading" : isError ? "error" : "success";
@@ -390,15 +400,17 @@ const FleetResourcePage = () => {
<Group gap="sm">
{slug === "wagons" ? (
<>
<Button
variant="light"
color="edr-green"
leftSection={<Warehouse size={16} />}
styles={{ label: { fontWeight: 500 } }}
onClick={() => setWagonWorkspaceOpen(true)}
>
Yard Workspace
</Button>
{canUpdate ? (
<Button
variant="light"
color="edr-green"
leftSection={<Warehouse size={16} />}
styles={{ label: { fontWeight: 500 } }}
onClick={() => setWagonWorkspaceOpen(true)}
>
Yard Workspace
</Button>
) : null}
<Button
variant="light"
color="grape"
@@ -410,12 +422,14 @@ const FleetResourcePage = () => {
</Button>
</>
) : null}
<Button leftSection={<Plus size={16} />} styles={{ label: { fontWeight: 500 } }} onClick={() => {
setEditing(null);
setFormOpen(true);
}}>
{config.addLabel}
</Button>
{canCreate ? (
<Button leftSection={<Plus size={16} />} styles={{ label: { fontWeight: 500 } }} onClick={() => {
setEditing(null);
setFormOpen(true);
}}>
{config.addLabel}
</Button>
) : null}
</Group>
</Group>
@@ -529,11 +543,15 @@ const FleetResourcePage = () => {
pageCount={pageCount}
totalCount={filteredRows.length}
onPaginationChange={setPagination}
onEdit={(record) => {
setEditing(record);
setFormOpen(true);
}}
onRemove={setRemoveTarget}
onEdit={
canUpdate
? (record) => {
setEditing(record);
setFormOpen(true);
}
: undefined
}
onRemove={canDelete ? setRemoveTarget : undefined}
/>
)}
</Stack>

View File

@@ -36,6 +36,8 @@ import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import { api } from "@/services/api";
import { ruleEngineService } from "@/services/ruleEngine/ruleEngine.service";
import { useAuth } from "@/auth/useAuth";
import { canFleetAction } from "@/lib/permissions";
import { useToast } from "@/hooks/use-toast";
import {
formatRouteLabel,
@@ -160,6 +162,10 @@ export default function RoutesPage() {
const { viewMode, setViewMode } = useFleetViewMode("routes");
const { pagination, setPagination } = usePagination({ pageSize: 10 });
const { toast } = useToast();
const { user } = useAuth();
const canCreate = canFleetAction(user, "routes", "create");
const canUpdate = canFleetAction(user, "routes", "update");
const canDelete = canFleetAction(user, "routes", "delete");
const routesQuery = useQuery(api.routes.list.queryOptions());
const yardsQuery = useQuery(api.routes.yards.queryOptions());
@@ -441,26 +447,30 @@ export default function RoutesPage() {
<Eye size={16} />
</ActionIcon>
</Tooltip>
<Tooltip label="Edit">
<ActionIcon variant="subtle" color="gray" onClick={() => openEdit(row.original)}>
<Edit size={16} />
</ActionIcon>
</Tooltip>
<Tooltip label="Mark stop working">
<ActionIcon
variant="subtle"
color="red"
disabled={row.original.status === "STOP_WORKING" || deactivateMutation.isPending}
onClick={() => handleDeactivate(row.original)}
>
<Trash2 size={16} />
</ActionIcon>
</Tooltip>
{canUpdate ? (
<Tooltip label="Edit">
<ActionIcon variant="subtle" color="gray" onClick={() => openEdit(row.original)}>
<Edit size={16} />
</ActionIcon>
</Tooltip>
) : null}
{canDelete ? (
<Tooltip label="Mark stop working">
<ActionIcon
variant="subtle"
color="red"
disabled={row.original.status === "STOP_WORKING" || deactivateMutation.isPending}
onClick={() => handleDeactivate(row.original)}
>
<Trash2 size={16} />
</ActionIcon>
</Tooltip>
) : null}
</Group>
),
},
];
}, [deactivateMutation.isPending]);
}, [deactivateMutation.isPending, canUpdate, canDelete]);
return (
<PageContainer>
@@ -468,9 +478,11 @@ export default function RoutesPage() {
title="Routes"
subtitle="Define rail corridors, segment distances, and operational status for train scheduling."
action={
<Button leftSection={<Plus size={18} />} onClick={openCreate}>
Add route
</Button>
canCreate ? (
<Button leftSection={<Plus size={18} />} onClick={openCreate}>
Add route
</Button>
) : undefined
}
/>
@@ -555,9 +567,11 @@ export default function RoutesPage() {
<Button variant="light" size="compact-sm" onClick={() => setViewing(route)}>
View
</Button>
<Button variant="light" size="compact-sm" onClick={() => openEdit(route)}>
Edit
</Button>
{canUpdate ? (
<Button variant="light" size="compact-sm" onClick={() => openEdit(route)}>
Edit
</Button>
) : null}
</Group>
</Stack>
</Card>

View File

@@ -45,6 +45,8 @@ import {
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
import { api } from "@/services/api";
import { useAuth } from "@/auth/useAuth";
import { canFleetAction, FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
import { useToast } from "@/hooks/use-toast";
const parseError = (error: unknown, fallback: string) => {
@@ -76,6 +78,12 @@ export default function TrainBuilderDetailPage() {
const [yardModalOpen, setYardModalOpen] = useState(false);
const [disbandOpen, setDisbandOpen] = useState(false);
const [deactivateOpen, setDeactivateOpen] = useState(false);
const { user } = useAuth();
const canUpdate = canFleetAction(user, "trains", "update");
const canDelete = canFleetAction(user, "trains", "delete");
const canAssign =
hasPermission(user, FREIGHT_PERMS.trains.assignWagons) ||
hasPermission(user, FREIGHT_PERMS.fleet.manage);
const compositionQuery = useQuery(
api.trainBuilder.composition.queryOptions({ input: { id }, enabled: Boolean(id) }),
@@ -162,59 +170,67 @@ export default function TrainBuilderDetailPage() {
</Group>
}
action={
<Menu position="bottom-end" withinPortal shadow="md" width={220}>
<Menu.Target>
<Button variant="default" rightSection={<MoreHorizontal size={16} />}>
Actions
</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<Replace size={15} />}
disabled={!composition.editable}
onClick={() => setLocoModalOpen(true)}
>
Change locomotives
</Menu.Item>
<Menu.Item
leftSection={<MapPin size={15} />}
disabled={!composition.editable}
onClick={() => setYardModalOpen(true)}
>
Change yard
</Menu.Item>
{composition.status === "DEACTIVATED" ? (
<Menu.Item
leftSection={<Power size={15} />}
disabled={blockingLocomotives.length > 0}
onClick={() =>
void withToast(async () => {
await activate.mutateAsync(composition.id);
toast({ title: `Train ${composition.code} reactivated` });
}, "Could not reactivate train")
}
>
Reactivate train
</Menu.Item>
) : (
<Menu.Item
leftSection={<PowerOff size={15} />}
disabled={composition.activeSchedules.length > 0}
onClick={() => setDeactivateOpen(true)}
>
Deactivate train
</Menu.Item>
)}
<Menu.Item
color="red"
leftSection={<Trash2 size={15} />}
disabled={composition.activeSchedules.length > 0}
onClick={() => setDisbandOpen(true)}
>
Disband train
</Menu.Item>
</Menu.Dropdown>
</Menu>
canUpdate || canDelete ? (
<Menu position="bottom-end" withinPortal shadow="md" width={220}>
<Menu.Target>
<Button variant="default" rightSection={<MoreHorizontal size={16} />}>
Actions
</Button>
</Menu.Target>
<Menu.Dropdown>
{canUpdate ? (
<>
<Menu.Item
leftSection={<Replace size={15} />}
disabled={!composition.editable}
onClick={() => setLocoModalOpen(true)}
>
Change locomotives
</Menu.Item>
<Menu.Item
leftSection={<MapPin size={15} />}
disabled={!composition.editable}
onClick={() => setYardModalOpen(true)}
>
Change yard
</Menu.Item>
{composition.status === "DEACTIVATED" ? (
<Menu.Item
leftSection={<Power size={15} />}
disabled={blockingLocomotives.length > 0}
onClick={() =>
void withToast(async () => {
await activate.mutateAsync(composition.id);
toast({ title: `Train ${composition.code} reactivated` });
}, "Could not reactivate train")
}
>
Reactivate train
</Menu.Item>
) : (
<Menu.Item
leftSection={<PowerOff size={15} />}
disabled={composition.activeSchedules.length > 0}
onClick={() => setDeactivateOpen(true)}
>
Deactivate train
</Menu.Item>
)}
</>
) : null}
{canDelete ? (
<Menu.Item
color="red"
leftSection={<Trash2 size={15} />}
disabled={composition.activeSchedules.length > 0}
onClick={() => setDisbandOpen(true)}
>
Disband train
</Menu.Item>
) : null}
</Menu.Dropdown>
</Menu>
) : undefined
}
/>
@@ -259,16 +275,18 @@ export default function TrainBuilderDetailPage() {
.
</Text>
<Group gap="xs">
<Button
size="compact-sm"
variant="light"
color="red"
leftSection={<Replace size={14} />}
disabled={!composition.editable}
onClick={() => setLocoModalOpen(true)}
>
Detach & replace locomotives
</Button>
{canUpdate ? (
<Button
size="compact-sm"
variant="light"
color="red"
leftSection={<Replace size={14} />}
disabled={!composition.editable}
onClick={() => setLocoModalOpen(true)}
>
Detach & replace locomotives
</Button>
) : null}
<Button
size="compact-sm"
variant="subtle"
@@ -330,7 +348,7 @@ export default function TrainBuilderDetailPage() {
</Stack>
<Grid gap="lg" align="stretch">
{composition.editable ? (
{composition.editable && canAssign ? (
<Grid.Col span={{ base: 12, md: 5 }}>
<Card h="100%">
<Stack gap="sm">
@@ -355,7 +373,7 @@ export default function TrainBuilderDetailPage() {
</Card>
</Grid.Col>
) : null}
<Grid.Col span={{ base: 12, md: composition.editable ? 7 : 12 }}>
<Grid.Col span={{ base: 12, md: composition.editable && canAssign ? 7 : 12 }}>
<Card h="100%">
<Stack gap="sm">
<Text fw={600}>Wagon order</Text>
@@ -364,7 +382,7 @@ export default function TrainBuilderDetailPage() {
</Text>
<ConsistWagonList
wagons={composition.wagons}
editable={composition.editable}
editable={composition.editable && canAssign}
busy={busy}
onReorder={(wagonIds) =>
void withToast(

View File

@@ -35,6 +35,8 @@ import {
trainStatusLabel,
} from "@/components/trainBuilder/trainStatus";
import { api } from "@/services/api";
import { useAuth } from "@/auth/useAuth";
import { canFleetAction } from "@/lib/permissions";
import type {
BuiltTrainListFilters,
BuiltTrainStatus,
@@ -54,6 +56,9 @@ export default function TrainBuilderListPage() {
const [yardFilter, setYardFilter] = useState("ALL");
const [buildOpen, setBuildOpen] = useState(false);
const [editTarget, setEditTarget] = useState<BuiltTrainSummary | null>(null);
const { user } = useAuth();
const canCreate = canFleetAction(user, "trains", "create");
const canUpdate = canFleetAction(user, "trains", "update");
const resetPage = useCallback(() => {
setPagination((prev) =>
@@ -240,24 +245,25 @@ export default function TrainBuilderListPage() {
id: "actions",
header: "",
meta: { headerClassName, cellClassName },
cell: ({ row }) => (
<ActionIcon
variant="subtle"
color="gray"
aria-label={`Edit train ${row.original.code}`}
title="Edit name & train numbers"
onClick={(e) => {
// Row click navigates to the detail page — keep the edit local.
e.stopPropagation();
setEditTarget(row.original);
}}
>
<Pencil size={15} />
</ActionIcon>
),
cell: ({ row }) =>
canUpdate ? (
<ActionIcon
variant="subtle"
color="gray"
aria-label={`Edit train ${row.original.code}`}
title="Edit name & train numbers"
onClick={(e) => {
// Row click navigates to the detail page — keep the edit local.
e.stopPropagation();
setEditTarget(row.original);
}}
>
<Pencil size={15} />
</ActionIcon>
) : null,
},
];
}, []);
}, [canUpdate]);
const tableStatus = trainsQuery.isLoading
? "loading"
@@ -271,9 +277,11 @@ export default function TrainBuilderListPage() {
title="Train Builder"
subtitle="Assemble coded trains from locomotives and wagons in a yard, ready to schedule as a unit."
action={
<Button leftSection={<Hammer size={18} />} onClick={() => setBuildOpen(true)}>
Build train
</Button>
canCreate ? (
<Button leftSection={<Hammer size={18} />} onClick={() => setBuildOpen(true)}>
Build train
</Button>
) : undefined
}
/>