mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Intercity andd import export gating
This commit is contained in:
@@ -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<string[]>([]);
|
||||
|
||||
@@ -378,12 +383,19 @@ export function IntercityRideAlongPanel({
|
||||
<Table.Td>
|
||||
<Group gap="xs" justify="flex-end">
|
||||
{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
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={<PackageCheck size={13} />}
|
||||
loading={load.isPending}
|
||||
disabled={!canLoad}
|
||||
onClick={() =>
|
||||
load.mutate({ scheduleId, bookingId: row.id })
|
||||
}
|
||||
@@ -393,13 +405,20 @@ export function IntercityRideAlongPanel({
|
||||
</Tooltip>
|
||||
)}
|
||||
{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
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="orange"
|
||||
leftSection={<PackageOpen size={13} />}
|
||||
loading={unload.isPending}
|
||||
disabled={!canUnload}
|
||||
onClick={() =>
|
||||
unload.mutate({ scheduleId, bookingId: row.id })
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ import { useEffect, useState } from "react";
|
||||
import { Freight } from "@edr/types";
|
||||
|
||||
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 { api } from "@/services/api";
|
||||
import type { TrackStation, YardWorkBookingRow } from "@/types/trainScheduling";
|
||||
@@ -111,6 +113,8 @@ export function LogPassYardWorkModal({
|
||||
alreadyLogged: boolean;
|
||||
}) {
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
const canLoad = hasFreightPermission(user, FREIGHT_PERMS.trainScheduling.load);
|
||||
const [justLogged, setJustLogged] = useState(false);
|
||||
// When the train was here — defaults to now, past allowed (recorded after the fact).
|
||||
const [passAt, setPassAt] = useState<Date | null>(null);
|
||||
@@ -353,18 +357,20 @@ export function LogPassYardWorkModal({
|
||||
{!row.loadedAt ? (
|
||||
<Tooltip
|
||||
label={
|
||||
!logged
|
||||
? "Log the pass first — the train must be at this yard"
|
||||
: !row.canLoad
|
||||
? "Booking is not ready to load (payment pending)"
|
||||
: "Confirm cargo loaded onto the train"
|
||||
!canLoad
|
||||
? "You don't have permission to load cargo"
|
||||
: !logged
|
||||
? "Log the pass first — the train must be at this yard"
|
||||
: !row.canLoad
|
||||
? "Booking is not ready to load (payment pending)"
|
||||
: "Confirm cargo loaded onto the train"
|
||||
}
|
||||
>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={<PackageCheck size={13} />}
|
||||
disabled={!logged || !row.canLoad}
|
||||
disabled={!canLoad || !logged || !row.canLoad}
|
||||
loading={
|
||||
load.isPending && load.variables?.bookingId === row.id
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ import { CountdownTimer } from "@edr/ui-common";
|
||||
|
||||
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
|
||||
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 { bookingsService } from "@/services/bookings.service";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
@@ -170,6 +172,9 @@ export function ScheduleWorkspacePanel({
|
||||
onChanged,
|
||||
}: ScheduleWorkspacePanelProps) {
|
||||
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 =
|
||||
schedule.freightType === "CONTAINER" || schedule.freightType === "BULK"
|
||||
@@ -773,13 +778,15 @@ export function ScheduleWorkspacePanel({
|
||||
{showLoad ? (
|
||||
<Tooltip
|
||||
label={
|
||||
boardHere
|
||||
? `Load cargo onto the train at ${group.label}`
|
||||
: passed
|
||||
? `Train already passed ${group.label} — this cargo missed its stop`
|
||||
: `Loads at ${group.label} — train is ${
|
||||
trainAtLabel ? `at ${trainAtLabel}` : "not there yet"
|
||||
}`
|
||||
!canLoad
|
||||
? "You don't have permission to load cargo"
|
||||
: boardHere
|
||||
? `Load cargo onto the train at ${group.label}`
|
||||
: passed
|
||||
? `Train already passed ${group.label} — this cargo missed its stop`
|
||||
: `Loads at ${group.label} — train is ${
|
||||
trainAtLabel ? `at ${trainAtLabel}` : "not there yet"
|
||||
}`
|
||||
}
|
||||
withArrow
|
||||
>
|
||||
@@ -788,7 +795,7 @@ export function ScheduleWorkspacePanel({
|
||||
variant="filled"
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
disabled={!boardHere}
|
||||
disabled={!boardHere || !canLoad}
|
||||
leftSection={<PackageCheck size={13} />}
|
||||
loading={
|
||||
loadJourney.isPending &&
|
||||
@@ -802,7 +809,11 @@ export function ScheduleWorkspacePanel({
|
||||
) : null}
|
||||
{showTruckToTrain ? (
|
||||
<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
|
||||
>
|
||||
<Button
|
||||
@@ -810,6 +821,7 @@ export function ScheduleWorkspacePanel({
|
||||
variant="light"
|
||||
color="blue"
|
||||
radius="md"
|
||||
disabled={!canLoad}
|
||||
leftSection={<Truck size={13} />}
|
||||
loading={truckToTrainPending === b.id}
|
||||
onClick={() => doTruckToTrain(b.id, ref)}
|
||||
@@ -821,9 +833,11 @@ export function ScheduleWorkspacePanel({
|
||||
{showUnload ? (
|
||||
<Tooltip
|
||||
label={
|
||||
alightHere
|
||||
? "Unload at this yard — stamps the booking's arrival"
|
||||
: "Unloads when the train reaches its destination yard"
|
||||
!canUnload
|
||||
? "You don't have permission to unload cargo"
|
||||
: alightHere
|
||||
? "Unload at this yard — stamps the booking's arrival"
|
||||
: "Unloads when the train reaches its destination yard"
|
||||
}
|
||||
withArrow
|
||||
>
|
||||
@@ -832,7 +846,7 @@ export function ScheduleWorkspacePanel({
|
||||
variant="light"
|
||||
color="orange"
|
||||
radius="md"
|
||||
disabled={!alightHere}
|
||||
disabled={!alightHere || !canUnload}
|
||||
leftSection={<PackageOpen size={13} />}
|
||||
loading={
|
||||
unloadJourney.isPending &&
|
||||
|
||||
@@ -51,6 +51,8 @@ import {
|
||||
|
||||
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 { QUERY_KEYS } from '@/constants/QUERY_KEYS';
|
||||
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. */
|
||||
function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: () => void }) {
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
const canLoad = hasFreightPermission(user, FREIGHT_PERMS.warehouseInventory.load);
|
||||
const { data: rows = [], isLoading } = useQuery(
|
||||
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</>
|
||||
)}
|
||||
</Text>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
variant="filled"
|
||||
color="teal"
|
||||
leftSection={<Truck size={14} />}
|
||||
disabled={rows.length === 0}
|
||||
onClick={() => setTrainPickerOpen(true)}
|
||||
>
|
||||
{selected.size > 0 ? `Load Selected (${selected.size})` : 'Auto Load Ready Items'}
|
||||
</Button>
|
||||
<Tooltip label={canLoad ? undefined : "You don't have permission to load cargo"} disabled={canLoad} withArrow>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
variant="filled"
|
||||
color="teal"
|
||||
leftSection={<Truck size={14} />}
|
||||
disabled={rows.length === 0 || !canLoad}
|
||||
onClick={() => setTrainPickerOpen(true)}
|
||||
>
|
||||
{selected.size > 0 ? `Load Selected (${selected.size})` : 'Auto Load Ready Items'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Modal
|
||||
@@ -2305,6 +2311,8 @@ export function ImportArriveQueueTab({
|
||||
onChanged?: () => 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
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
color={fullyUnloaded ? 'gray' : 'indigo'}
|
||||
leftSection={<Truck size={14} />}
|
||||
loading={busyId === t.scheduleId}
|
||||
disabled={fullyUnloaded || t.totalBookings === 0 || !readyBySchedule[t.scheduleId] || warehousesLoading}
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
title: 'Auto unload train',
|
||||
message: `Unload all arrived bookings from train ${t.trainNumber ?? t.scheduleId.slice(0, 8)} into their assigned warehouse locations?`,
|
||||
confirmLabel: 'Unload train',
|
||||
run: () => autoUnload(t),
|
||||
})
|
||||
}
|
||||
>
|
||||
{fullyUnloaded ? 'Already Unloaded' : 'Auto Unload Arrived Bookings'}
|
||||
</Button>
|
||||
<Tooltip label={canUnload ? undefined : "You don't have permission to unload cargo"} disabled={canUnload} withArrow>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
color={fullyUnloaded ? 'gray' : 'indigo'}
|
||||
leftSection={<Truck size={14} />}
|
||||
loading={busyId === t.scheduleId}
|
||||
disabled={fullyUnloaded || t.totalBookings === 0 || !readyBySchedule[t.scheduleId] || warehousesLoading || !canUnload}
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
title: 'Auto unload train',
|
||||
message: `Unload all arrived bookings from train ${t.trainNumber ?? t.scheduleId.slice(0, 8)} into their assigned warehouse locations?`,
|
||||
confirmLabel: 'Unload train',
|
||||
run: () => autoUnload(t),
|
||||
})
|
||||
}
|
||||
>
|
||||
{fullyUnloaded ? 'Already Unloaded' : 'Auto Unload Arrived Bookings'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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[] }) {
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
{/* Work the cargo right here while the train is at the yard. */}
|
||||
{r.trainScheduleId && atOrigin(r) && isWaiting(r) && r.status === "PAID" && (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={<PackageCheck size={13} />}
|
||||
loading={load.isPending}
|
||||
onClick={() =>
|
||||
load.mutate({ scheduleId: r.trainScheduleId as string, bookingId: r.bookingId })
|
||||
}
|
||||
>
|
||||
Load
|
||||
</Button>
|
||||
<Tooltip label={canLoad ? undefined : "You don't have permission to load cargo"} disabled={canLoad}>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={<PackageCheck size={13} />}
|
||||
loading={load.isPending}
|
||||
disabled={!canLoad}
|
||||
onClick={() =>
|
||||
load.mutate({ scheduleId: r.trainScheduleId as string, bookingId: r.bookingId })
|
||||
}
|
||||
>
|
||||
Load
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{r.trainScheduleId && atDestination(r) && isRiding(r) && (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="orange"
|
||||
leftSection={<PackageOpen size={13} />}
|
||||
loading={unload.isPending}
|
||||
onClick={() =>
|
||||
unload.mutate({ scheduleId: r.trainScheduleId as string, bookingId: r.bookingId })
|
||||
}
|
||||
>
|
||||
Unload
|
||||
</Button>
|
||||
<Tooltip label={canUnload ? undefined : "You don't have permission to unload cargo"} disabled={canUnload}>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="orange"
|
||||
leftSection={<PackageOpen size={13} />}
|
||||
loading={unload.isPending}
|
||||
disabled={!canUnload}
|
||||
onClick={() =>
|
||||
unload.mutate({ scheduleId: r.trainScheduleId as string, bookingId: r.bookingId })
|
||||
}
|
||||
>
|
||||
Unload
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
Reference in New Issue
Block a user