Intercity andd import export gating

This commit is contained in:
Hagernesh
2026-08-22 09:16:17 +00:00
parent e426e0c16e
commit 7f5e8349da
6 changed files with 134 additions and 71 deletions

View File

@@ -15,6 +15,8 @@ import {
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AlertCircle, ArrowRight, PackageCheck, PackageOpen, TrainFront } from "lucide-react"; 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 { api } from "@/services/api";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import type { import type {
@@ -130,6 +132,9 @@ export function IntercityRideAlongPanel({
direction: string | null | undefined; direction: string | null | undefined;
}) { }) {
const { toast } = useToast(); 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 queryClient = useQueryClient();
const [selected, setSelected] = useState<string[]>([]); const [selected, setSelected] = useState<string[]>([]);
@@ -378,12 +383,19 @@ export function IntercityRideAlongPanel({
<Table.Td> <Table.Td>
<Group gap="xs" justify="flex-end"> <Group gap="xs" justify="flex-end">
{row.status === "PAID" && ( {row.status === "PAID" && (
<Tooltip label="Train must be at the booking's origin yard"> <Tooltip
label={
canLoad
? "Train must be at the booking's origin yard"
: "You don't have permission to load cargo"
}
>
<Button <Button
size="compact-xs" size="compact-xs"
variant="light" variant="light"
leftSection={<PackageCheck size={13} />} leftSection={<PackageCheck size={13} />}
loading={load.isPending} loading={load.isPending}
disabled={!canLoad}
onClick={() => onClick={() =>
load.mutate({ scheduleId, bookingId: row.id }) load.mutate({ scheduleId, bookingId: row.id })
} }
@@ -393,13 +405,20 @@ export function IntercityRideAlongPanel({
</Tooltip> </Tooltip>
)} )}
{row.status === "IN_TRANSIT" && ( {row.status === "IN_TRANSIT" && (
<Tooltip label="Train must be at the booking's destination yard"> <Tooltip
label={
canUnload
? "Train must be at the booking's destination yard"
: "You don't have permission to unload cargo"
}
>
<Button <Button
size="compact-xs" size="compact-xs"
variant="light" variant="light"
color="orange" color="orange"
leftSection={<PackageOpen size={13} />} leftSection={<PackageOpen size={13} />}
loading={unload.isPending} loading={unload.isPending}
disabled={!canUnload}
onClick={() => onClick={() =>
unload.mutate({ scheduleId, bookingId: row.id }) unload.mutate({ scheduleId, bookingId: row.id })
} }

View File

@@ -25,6 +25,8 @@ import { useEffect, useState } from "react";
import { Freight } from "@edr/types"; import { Freight } from "@edr/types";
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge"; import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
import { useAuth } from "@/auth/useAuth";
import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from "@/lib/permissions";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { api } from "@/services/api"; import { api } from "@/services/api";
import type { TrackStation, YardWorkBookingRow } from "@/types/trainScheduling"; import type { TrackStation, YardWorkBookingRow } from "@/types/trainScheduling";
@@ -111,6 +113,8 @@ export function LogPassYardWorkModal({
alreadyLogged: boolean; alreadyLogged: boolean;
}) { }) {
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth();
const canLoad = hasFreightPermission(user, FREIGHT_PERMS.trainScheduling.load);
const [justLogged, setJustLogged] = useState(false); const [justLogged, setJustLogged] = useState(false);
// When the train was here — defaults to now, past allowed (recorded after the fact). // When the train was here — defaults to now, past allowed (recorded after the fact).
const [passAt, setPassAt] = useState<Date | null>(null); const [passAt, setPassAt] = useState<Date | null>(null);
@@ -353,7 +357,9 @@ export function LogPassYardWorkModal({
{!row.loadedAt ? ( {!row.loadedAt ? (
<Tooltip <Tooltip
label={ label={
!logged !canLoad
? "You don't have permission to load cargo"
: !logged
? "Log the pass first — the train must be at this yard" ? "Log the pass first — the train must be at this yard"
: !row.canLoad : !row.canLoad
? "Booking is not ready to load (payment pending)" ? "Booking is not ready to load (payment pending)"
@@ -364,7 +370,7 @@ export function LogPassYardWorkModal({
size="compact-xs" size="compact-xs"
variant="light" variant="light"
leftSection={<PackageCheck size={13} />} leftSection={<PackageCheck size={13} />}
disabled={!logged || !row.canLoad} disabled={!canLoad || !logged || !row.canLoad}
loading={ loading={
load.isPending && load.variables?.bookingId === row.id load.isPending && load.variables?.bookingId === row.id
} }

View File

@@ -38,6 +38,8 @@ import { CountdownTimer } from "@edr/ui-common";
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge"; import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
import { EntityLink } from "@/components/detail"; import { EntityLink } from "@/components/detail";
import { useAuth } from "@/auth/useAuth";
import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from "@/lib/permissions";
import { api } from "@/services/api"; import { api } from "@/services/api";
import { bookingsService } from "@/services/bookings.service"; import { bookingsService } from "@/services/bookings.service";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
@@ -170,6 +172,9 @@ export function ScheduleWorkspacePanel({
onChanged, onChanged,
}: ScheduleWorkspacePanelProps) { }: ScheduleWorkspacePanelProps) {
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth();
const canLoad = hasFreightPermission(user, FREIGHT_PERMS.trainScheduling.load);
const canUnload = hasFreightPermission(user, FREIGHT_PERMS.trainScheduling.unload);
const freightType: FreightType | undefined = const freightType: FreightType | undefined =
schedule.freightType === "CONTAINER" || schedule.freightType === "BULK" schedule.freightType === "CONTAINER" || schedule.freightType === "BULK"
@@ -773,7 +778,9 @@ export function ScheduleWorkspacePanel({
{showLoad ? ( {showLoad ? (
<Tooltip <Tooltip
label={ label={
boardHere !canLoad
? "You don't have permission to load cargo"
: boardHere
? `Load cargo onto the train at ${group.label}` ? `Load cargo onto the train at ${group.label}`
: passed : passed
? `Train already passed ${group.label} — this cargo missed its stop` ? `Train already passed ${group.label} — this cargo missed its stop`
@@ -788,7 +795,7 @@ export function ScheduleWorkspacePanel({
variant="filled" variant="filled"
color="edr-green" color="edr-green"
radius="md" radius="md"
disabled={!boardHere} disabled={!boardHere || !canLoad}
leftSection={<PackageCheck size={13} />} leftSection={<PackageCheck size={13} />}
loading={ loading={
loadJourney.isPending && loadJourney.isPending &&
@@ -802,7 +809,11 @@ export function ScheduleWorkspacePanel({
) : null} ) : null}
{showTruckToTrain ? ( {showTruckToTrain ? (
<Tooltip <Tooltip
label="Customer truck loaded straight onto the wagon — no warehouse receipt, no GRN. Sets direct truck-to-train handover and loads." label={
canLoad
? "Customer truck loaded straight onto the wagon — no warehouse receipt, no GRN. Sets direct truck-to-train handover and loads."
: "You don't have permission to load cargo"
}
withArrow withArrow
> >
<Button <Button
@@ -810,6 +821,7 @@ export function ScheduleWorkspacePanel({
variant="light" variant="light"
color="blue" color="blue"
radius="md" radius="md"
disabled={!canLoad}
leftSection={<Truck size={13} />} leftSection={<Truck size={13} />}
loading={truckToTrainPending === b.id} loading={truckToTrainPending === b.id}
onClick={() => doTruckToTrain(b.id, ref)} onClick={() => doTruckToTrain(b.id, ref)}
@@ -821,7 +833,9 @@ export function ScheduleWorkspacePanel({
{showUnload ? ( {showUnload ? (
<Tooltip <Tooltip
label={ label={
alightHere !canUnload
? "You don't have permission to unload cargo"
: alightHere
? "Unload at this yard — stamps the booking's arrival" ? "Unload at this yard — stamps the booking's arrival"
: "Unloads when the train reaches its destination yard" : "Unloads when the train reaches its destination yard"
} }
@@ -832,7 +846,7 @@ export function ScheduleWorkspacePanel({
variant="light" variant="light"
color="orange" color="orange"
radius="md" radius="md"
disabled={!alightHere} disabled={!alightHere || !canUnload}
leftSection={<PackageOpen size={13} />} leftSection={<PackageOpen size={13} />}
loading={ loading={
unloadJourney.isPending && unloadJourney.isPending &&

View File

@@ -51,6 +51,8 @@ import {
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useAuth } from '@/auth/useAuth';
import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from '@/lib/permissions';
import { api } from '@/services/api'; import { api } from '@/services/api';
import { QUERY_KEYS } from '@/constants/QUERY_KEYS'; import { QUERY_KEYS } from '@/constants/QUERY_KEYS';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
@@ -1566,6 +1568,8 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
/** Export items that passed inspection and are queued to be loaded onto a train. */ /** Export items that passed inspection and are queued to be loaded onto a train. */
function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: () => void }) { function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: () => void }) {
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth();
const canLoad = hasFreightPermission(user, FREIGHT_PERMS.warehouseInventory.load);
const { data: rows = [], isLoading } = useQuery( const { data: rows = [], isLoading } = useQuery(
api.warehouses.readyToLoadExport.queryOptions({ enabled }), api.warehouses.readyToLoadExport.queryOptions({ enabled }),
); );
@@ -1655,16 +1659,18 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
<><b>{controls.filteredRows.length}</b> item{controls.filteredRows.length !== 1 ? 's' : ''} ready to load</> <><b>{controls.filteredRows.length}</b> item{controls.filteredRows.length !== 1 ? 's' : ''} ready to load</>
)} )}
</Text> </Text>
<Tooltip label={canLoad ? undefined : "You don't have permission to load cargo"} disabled={canLoad} withArrow>
<Button <Button
size="compact-sm" size="compact-sm"
variant="filled" variant="filled"
color="teal" color="teal"
leftSection={<Truck size={14} />} leftSection={<Truck size={14} />}
disabled={rows.length === 0} disabled={rows.length === 0 || !canLoad}
onClick={() => setTrainPickerOpen(true)} onClick={() => setTrainPickerOpen(true)}
> >
{selected.size > 0 ? `Load Selected (${selected.size})` : 'Auto Load Ready Items'} {selected.size > 0 ? `Load Selected (${selected.size})` : 'Auto Load Ready Items'}
</Button> </Button>
</Tooltip>
</Group> </Group>
<Modal <Modal
@@ -2305,6 +2311,8 @@ export function ImportArriveQueueTab({
onChanged?: () => void; onChanged?: () => void;
}) { }) {
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth();
const canUnload = hasFreightPermission(user, FREIGHT_PERMS.warehouseInventory.unload);
const { data: trains = [], isLoading } = useQuery( const { data: trains = [], isLoading } = useQuery(
api.warehouses.importArriveQueue.queryOptions({ enabled }), api.warehouses.importArriveQueue.queryOptions({ enabled }),
); );
@@ -2489,12 +2497,13 @@ export function ImportArriveQueueTab({
> >
Open Open
</Button> </Button>
<Tooltip label={canUnload ? undefined : "You don't have permission to unload cargo"} disabled={canUnload} withArrow>
<Button <Button
size="compact-xs" size="compact-xs"
color={fullyUnloaded ? 'gray' : 'indigo'} color={fullyUnloaded ? 'gray' : 'indigo'}
leftSection={<Truck size={14} />} leftSection={<Truck size={14} />}
loading={busyId === t.scheduleId} loading={busyId === t.scheduleId}
disabled={fullyUnloaded || t.totalBookings === 0 || !readyBySchedule[t.scheduleId] || warehousesLoading} disabled={fullyUnloaded || t.totalBookings === 0 || !readyBySchedule[t.scheduleId] || warehousesLoading || !canUnload}
onClick={() => onClick={() =>
setConfirmAction({ setConfirmAction({
title: 'Auto unload train', title: 'Auto unload train',
@@ -2506,6 +2515,7 @@ export function ImportArriveQueueTab({
> >
{fullyUnloaded ? 'Already Unloaded' : 'Auto Unload Arrived Bookings'} {fullyUnloaded ? 'Already Unloaded' : 'Auto Unload Arrived Bookings'}
</Button> </Button>
</Tooltip>
</Group> </Group>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>

View File

@@ -104,6 +104,9 @@ export const FREIGHT_PERMS = {
view: "edr_freight_app:train_scheduling:view", view: "edr_freight_app:train_scheduling:view",
create: "edr_freight_app:train_scheduling:create", create: "edr_freight_app:train_scheduling:create",
update: "edr_freight_app:train_scheduling:update", 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", cancel: "edr_freight_app:train_scheduling:cancel",
reschedule: "edr_freight_app:train_scheduling:reschedule", reschedule: "edr_freight_app:train_scheduling:reschedule",
rulesManage: "edr_freight_app:train_scheduling:rules_manage", rulesManage: "edr_freight_app:train_scheduling:rules_manage",

View File

@@ -17,6 +17,8 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AlertTriangle, PackageCheck, PackageOpen, TrainFront, Warehouse } from "lucide-react"; import { AlertTriangle, PackageCheck, PackageOpen, TrainFront, Warehouse } from "lucide-react";
import { PageContainer, PageHeader } from "@/components/page"; 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"; import ListControls from "@/components/common/ListControls";
// Generic list footer — already shared by the fleet and train-scheduling lists // Generic list footer — already shared by the fleet and train-scheduling lists
// despite the ruleEngine path. // despite the ruleEngine path.
@@ -93,6 +95,9 @@ const apiErrorMessage = (error: unknown) => {
function Rows({ rows }: { rows: IntercityRideAlongRow[] }) { function Rows({ rows }: { rows: IntercityRideAlongRow[] }) {
const { toast } = useToast(); 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 queryClient = useQueryClient();
const refresh = () => const refresh = () =>
queryClient.invalidateQueries({ queryClient.invalidateQueries({
@@ -201,31 +206,37 @@ function Rows({ rows }: { rows: IntercityRideAlongRow[] }) {
<Group gap="xs" justify="flex-end" wrap="nowrap"> <Group gap="xs" justify="flex-end" wrap="nowrap">
{/* Work the cargo right here while the train is at the yard. */} {/* Work the cargo right here while the train is at the yard. */}
{r.trainScheduleId && atOrigin(r) && isWaiting(r) && r.status === "PAID" && ( {r.trainScheduleId && atOrigin(r) && isWaiting(r) && r.status === "PAID" && (
<Tooltip label={canLoad ? undefined : "You don't have permission to load cargo"} disabled={canLoad}>
<Button <Button
size="compact-xs" size="compact-xs"
variant="light" variant="light"
leftSection={<PackageCheck size={13} />} leftSection={<PackageCheck size={13} />}
loading={load.isPending} loading={load.isPending}
disabled={!canLoad}
onClick={() => onClick={() =>
load.mutate({ scheduleId: r.trainScheduleId as string, bookingId: r.bookingId }) load.mutate({ scheduleId: r.trainScheduleId as string, bookingId: r.bookingId })
} }
> >
Load Load
</Button> </Button>
</Tooltip>
)} )}
{r.trainScheduleId && atDestination(r) && isRiding(r) && ( {r.trainScheduleId && atDestination(r) && isRiding(r) && (
<Tooltip label={canUnload ? undefined : "You don't have permission to unload cargo"} disabled={canUnload}>
<Button <Button
size="compact-xs" size="compact-xs"
variant="light" variant="light"
color="orange" color="orange"
leftSection={<PackageOpen size={13} />} leftSection={<PackageOpen size={13} />}
loading={unload.isPending} loading={unload.isPending}
disabled={!canUnload}
onClick={() => onClick={() =>
unload.mutate({ scheduleId: r.trainScheduleId as string, bookingId: r.bookingId }) unload.mutate({ scheduleId: r.trainScheduleId as string, bookingId: r.bookingId })
} }
> >
Unload Unload
</Button> </Button>
</Tooltip>
)} )}
</Group> </Group>
</Table.Td> </Table.Td>