feat(permissions): add granular permissions for train management actions

feat(train-builder): update permissions checks for train actions in UI
feat(contracts): enhance contract view and shipment request pages with new features
test(shipment-preview): add tests for customs clearing logic in booking preview
style(contract-sign-bar): create CSS for fixed sign bar layout
This commit is contained in:
marshalyordanos
2026-08-11 11:48:16 +03:00
parent ea6eccbf09
commit 07a120af5e
9 changed files with 267 additions and 73 deletions

View File

@@ -188,6 +188,11 @@ export const FREIGHT_PERMS = {
update: "edr_freight_app:trains:update",
delete: "edr_freight_app:trains:delete",
assignWagons: "edr_freight_app:trains:assign_wagons",
/** Train-builder detail Actions menu — each item its own grant. */
changeLocomotives: "edr_freight_app:trains:change_locomotives",
changeYard: "edr_freight_app:trains:change_yard",
toggleActive: "edr_freight_app:trains:toggle_active",
disband: "edr_freight_app:trains:disband",
},
routes: {
view: "edr_freight_app:routes:view",

View File

@@ -47,7 +47,7 @@ import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompo
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 { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
import { useToast } from "@/hooks/use-toast";
import type { TrainCompositionWagon } from "@/services/trainBuilder.service";
@@ -83,9 +83,11 @@ export default function TrainBuilderDetailPage() {
const [maintenanceTarget, setMaintenanceTarget] =
useState<TrainCompositionWagon | null>(null);
const { user } = useAuth();
const canUpdate = canFleetAction(user, "trains", "update");
const canDelete = canFleetAction(user, "trains", "delete");
const canAssign = hasPermission(user, FREIGHT_PERMS.trains.assignWagons);
const canChangeLocomotives = hasPermission(user, FREIGHT_PERMS.trains.changeLocomotives);
const canChangeYard = hasPermission(user, FREIGHT_PERMS.trains.changeYard);
const canToggleActive = hasPermission(user, FREIGHT_PERMS.trains.toggleActive);
const canDisband = hasPermission(user, FREIGHT_PERMS.trains.disband);
const compositionQuery = useQuery(
api.trainBuilder.composition.queryOptions({ input: { id }, enabled: Boolean(id) }),
@@ -172,7 +174,7 @@ export default function TrainBuilderDetailPage() {
</Group>
}
action={
canUpdate || canDelete ? (
canChangeLocomotives || canChangeYard || canToggleActive || canDisband ? (
<Menu position="bottom-end" withinPortal shadow="md" width={220}>
<Menu.Target>
<Button variant="default" rightSection={<MoreHorizontal size={16} />}>
@@ -180,47 +182,49 @@ export default function TrainBuilderDetailPage() {
</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>
)}
</>
{canChangeLocomotives ? (
<Menu.Item
leftSection={<Replace size={15} />}
disabled={!composition.editable}
onClick={() => setLocoModalOpen(true)}
>
Change locomotives
</Menu.Item>
) : null}
{canDelete ? (
{canChangeYard ? (
<Menu.Item
leftSection={<MapPin size={15} />}
disabled={!composition.editable}
onClick={() => setYardModalOpen(true)}
>
Change yard
</Menu.Item>
) : null}
{canToggleActive ? (
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}
{canDisband ? (
<Menu.Item
color="red"
leftSection={<Trash2 size={15} />}
@@ -277,7 +281,7 @@ export default function TrainBuilderDetailPage() {
.
</Text>
<Group gap="xs">
{canUpdate ? (
{canChangeLocomotives ? (
<Button
size="compact-sm"
variant="light"