mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix issue
This commit is contained in:
@@ -94,6 +94,7 @@
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-pdf": "^10.4.1",
|
||||
"react-pdf-html": "^2.1.5",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"react-signature-canvas": "1.1.0-alpha.2",
|
||||
|
||||
@@ -17,11 +17,11 @@ import { ChevronDown, ChevronRight, Info, Train, Weight } from "lucide-react";
|
||||
import type { TrainScheduleDetail } from "@/types/trainScheduling";
|
||||
|
||||
/**
|
||||
* Per-leg capacity workspace tab. A multi-stop corridor (A→B→C→D→E) is
|
||||
* capacity-checked edge by edge, so this shows, for EVERY adjacent leg, the
|
||||
* wagons/weight/length the consist actually uses there — and for every
|
||||
* possible origin→destination pair (A→C, B→E, …) the room left, which is the
|
||||
* minimum over the legs the pair rides.
|
||||
* Per-leg capacity workspace tab, computed from the BOOKINGS themselves: every
|
||||
* adjacent leg (A→B, B→C, …) lists each booking riding it with the booking's
|
||||
* own wagon count and gross weight, and totals both. A through booking (A→C)
|
||||
* appears on every leg it rides — so leg totals are what each leg actually
|
||||
* hauls, independent of how the consist slots were stamped.
|
||||
*/
|
||||
|
||||
interface Stop {
|
||||
@@ -34,8 +34,6 @@ interface LegBookingUsage {
|
||||
reference: string;
|
||||
wagons: number;
|
||||
grossTons: number;
|
||||
/** Linked to the schedule but has NO wagon allocation — its weight is on no consist slot. */
|
||||
unallocated?: boolean;
|
||||
/** The booking's own origin → destination, so a sub-leg booking reads as such. */
|
||||
route?: string | null;
|
||||
}
|
||||
@@ -46,11 +44,7 @@ interface EdgeUsage {
|
||||
to: Stop;
|
||||
wagons: number;
|
||||
grossTons: number;
|
||||
lengthMeters: number;
|
||||
bookingRefs: string[];
|
||||
bookings: LegBookingUsage[];
|
||||
/** Gross tons of linked-but-unallocated bookings riding this leg — not yet on any slot. */
|
||||
pendingTons: number;
|
||||
}
|
||||
|
||||
const round1 = (n: number) => Math.round(n * 10) / 10;
|
||||
@@ -64,20 +58,6 @@ function utilizationColor(used: number, cap: number | null): string {
|
||||
return "teal";
|
||||
}
|
||||
|
||||
/** Mirrors the API's slotSpans: unknown/missing yard = the schedule endpoint. */
|
||||
function spanOf(
|
||||
boardYardId: string | null | undefined,
|
||||
alightYardId: string | null | undefined,
|
||||
indexOf: Map<string, number>,
|
||||
lastIdx: number,
|
||||
): { from: number; to: number } {
|
||||
const fromRaw = boardYardId ? indexOf.get(boardYardId) : 0;
|
||||
const toRaw = alightYardId ? indexOf.get(alightYardId) : lastIdx;
|
||||
const from = fromRaw != null && fromRaw >= 0 ? fromRaw : 0;
|
||||
const to = toRaw != null && toRaw > 0 ? toRaw : lastIdx;
|
||||
return { from, to };
|
||||
}
|
||||
|
||||
function UsageCell({
|
||||
used,
|
||||
cap,
|
||||
@@ -102,9 +82,7 @@ function UsageCell({
|
||||
|
||||
export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }) {
|
||||
const stops: Stop[] = schedule.stops ?? [];
|
||||
const wagons = schedule.trainSet?.wagons ?? [];
|
||||
const weightCap = schedule.maxGrossWeightTons ?? null;
|
||||
const lengthCap = schedule.maxLengthMeters ?? null;
|
||||
const wagonCap = schedule.maxWagons ?? null;
|
||||
const [expandedEdge, setExpandedEdge] = useState<number | null>(null);
|
||||
|
||||
@@ -112,125 +90,35 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
|
||||
if (stops.length < 2) return [];
|
||||
const indexOf = new Map(stops.map((s, i) => [s.yardId, i]));
|
||||
const lastIdx = stops.length - 1;
|
||||
// Booking legs, for slot-span fallback and for labelling dropdown rows.
|
||||
const bookingById = new Map((schedule.bookings ?? []).map((b) => [b.id, b]));
|
||||
const bookingSpan = (bookingId: string) => {
|
||||
const b = bookingById.get(bookingId);
|
||||
if (!b) return null;
|
||||
// A booking rides origin→destination; unknown/off-corridor yards fall back
|
||||
// to the schedule's own endpoints (through cargo).
|
||||
const spans = (schedule.bookings ?? []).map((b) => {
|
||||
const from = (b.originYardId ? indexOf.get(b.originYardId) : undefined) ?? 0;
|
||||
const toRaw = b.destinationYardId ? indexOf.get(b.destinationYardId) : lastIdx;
|
||||
const to = toRaw != null && toRaw > from ? toRaw : lastIdx;
|
||||
return { from, to };
|
||||
};
|
||||
// A slot rides its stamped board→alight span. Slots without a stamp (an
|
||||
// API build predating the fields, or plans written before spans existed)
|
||||
// fall back to the union of their OWN bookings' legs — an a→b-only wagon
|
||||
// must not count on the b→c leg. Empty stamped-less wagons ride everything.
|
||||
const spans = wagons.map((w) => {
|
||||
if (w.boardYardId || w.alightYardId) {
|
||||
return spanOf(w.boardYardId, w.alightYardId, indexOf, lastIdx);
|
||||
}
|
||||
const legs = (w.allocations ?? [])
|
||||
.map((a) => bookingSpan(a.bookingId))
|
||||
.filter((s): s is { from: number; to: number } => s != null);
|
||||
if (!legs.length) return { from: 0, to: lastIdx };
|
||||
return {
|
||||
from: Math.min(...legs.map((s) => s.from)),
|
||||
to: Math.max(...legs.map((s) => s.to)),
|
||||
};
|
||||
return { b, from, to };
|
||||
});
|
||||
return stops.slice(0, -1).map((from, edge) => {
|
||||
const active = wagons.filter(
|
||||
(_, i) => spans[i].from <= edge && edge < spans[i].to,
|
||||
);
|
||||
const refs = new Set<string>();
|
||||
let grossTons = 0;
|
||||
let lengthMeters = 0;
|
||||
// Per booking on this leg: wagon count (distinct wagons carrying at
|
||||
// least one of its allocations — a shared wagon counts for each
|
||||
// booking riding it, so per-booking wagon counts can sum to more than
|
||||
// the leg's total) and its allocated weight share.
|
||||
const byBooking = new Map<string, LegBookingUsage>();
|
||||
for (const w of active) {
|
||||
grossTons += (Number(w.tareWeightTons) || 0) + (Number(w.assignedWeightTons) || 0);
|
||||
lengthMeters += Number(w.lengthMeters) || 0;
|
||||
const bookingIdsOnWagon = new Set<string>();
|
||||
for (const a of w.allocations ?? []) {
|
||||
if (!a.bookingReference) continue;
|
||||
refs.add(a.bookingReference);
|
||||
const linked = bookingById.get(a.bookingId);
|
||||
const row = byBooking.get(a.bookingId) ?? {
|
||||
bookingId: a.bookingId,
|
||||
reference: a.bookingReference,
|
||||
wagons: 0,
|
||||
grossTons: 0,
|
||||
route:
|
||||
linked?.origin && linked?.destination
|
||||
? `${linked.origin} → ${linked.destination}`
|
||||
: null,
|
||||
};
|
||||
row.grossTons += Number(a.allocatedWeightTons) || 0;
|
||||
byBooking.set(a.bookingId, row);
|
||||
bookingIdsOnWagon.add(a.bookingId);
|
||||
}
|
||||
for (const bookingId of bookingIdsOnWagon) {
|
||||
const row = byBooking.get(bookingId);
|
||||
if (row) row.wagons += 1;
|
||||
}
|
||||
}
|
||||
// Linked bookings with NO wagon allocation ride their leg too — without
|
||||
// this they vanish from the tab entirely (two Dire→DCT bookings hidden
|
||||
// while a through booking showed alone). Flagged so staff see the gap;
|
||||
// their tonnage is deliberately NOT in the leg totals, which reflect
|
||||
// what is physically on consist slots.
|
||||
for (const b of schedule.bookings ?? []) {
|
||||
if (byBooking.has(b.id)) continue;
|
||||
const bFrom = (b.originYardId ? indexOf.get(b.originYardId) : undefined) ?? 0;
|
||||
const bToRaw = b.destinationYardId ? indexOf.get(b.destinationYardId) : lastIdx;
|
||||
const bTo = bToRaw != null && bToRaw > bFrom ? bToRaw : lastIdx;
|
||||
if (!(bFrom <= edge && edge < bTo)) continue;
|
||||
const reference = b.reference ?? b.id;
|
||||
refs.add(reference);
|
||||
byBooking.set(b.id, {
|
||||
const riding = spans.filter((s) => s.from <= edge && edge < s.to);
|
||||
const bookings: LegBookingUsage[] = riding
|
||||
.map(({ b }) => ({
|
||||
bookingId: b.id,
|
||||
reference,
|
||||
reference: b.reference ?? b.id,
|
||||
wagons: Number(b.wagonsRequired) || 0,
|
||||
grossTons: Number(b.weightTons) || 0,
|
||||
unallocated: true,
|
||||
route:
|
||||
b.origin && b.destination ? `${b.origin} → ${b.destination}` : null,
|
||||
});
|
||||
}
|
||||
const bookings = [...byBooking.values()]
|
||||
.map((b) => ({ ...b, grossTons: round1(b.grossTons) }))
|
||||
grossTons: round1(Number(b.weightTons) || 0),
|
||||
route: b.origin && b.destination ? `${b.origin} → ${b.destination}` : null,
|
||||
}))
|
||||
.sort((a, b) => b.grossTons - a.grossTons);
|
||||
const pendingTons = round1(
|
||||
bookings.filter((b) => b.unallocated).reduce((sum, b) => sum + b.grossTons, 0),
|
||||
);
|
||||
return {
|
||||
edge,
|
||||
from,
|
||||
to: stops[edge + 1],
|
||||
wagons: active.length,
|
||||
grossTons: round1(grossTons),
|
||||
lengthMeters: round1(lengthMeters),
|
||||
bookingRefs: [...refs],
|
||||
wagons: bookings.reduce((sum, b) => sum + b.wagons, 0),
|
||||
grossTons: round1(bookings.reduce((sum, b) => sum + b.grossTons, 0)),
|
||||
bookings,
|
||||
pendingTons,
|
||||
};
|
||||
});
|
||||
}, [stops, wagons, schedule.bookings]);
|
||||
|
||||
const unallocatedRefs = useMemo(
|
||||
() => [
|
||||
...new Set(
|
||||
edges.flatMap((e) =>
|
||||
e.bookings.filter((b) => b.unallocated).map((b) => b.reference),
|
||||
),
|
||||
),
|
||||
],
|
||||
[edges],
|
||||
);
|
||||
}, [stops, schedule.bookings]);
|
||||
|
||||
if (stops.length < 2) {
|
||||
return (
|
||||
@@ -240,21 +128,9 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
|
||||
);
|
||||
}
|
||||
|
||||
if (!wagons.length) {
|
||||
return (
|
||||
<Alert mt="lg" radius="lg" color="gray" icon={<Info size={16} />}>
|
||||
No wagon plan yet — leg utilization appears once bookings are allocated
|
||||
to wagons. The route strip in the header shows the booking-based
|
||||
estimate meanwhile.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const legStatus = (e: EdgeUsage) => {
|
||||
if (weightCap != null && e.grossTons > weightCap)
|
||||
return <Badge color="red" variant="filled">Overweight</Badge>;
|
||||
if (lengthCap != null && e.lengthMeters > lengthCap)
|
||||
return <Badge color="red" variant="filled">Over length</Badge>;
|
||||
const wagonsFree = wagonCap != null ? wagonCap - e.wagons : null;
|
||||
const tonsFree = weightCap != null ? round1(weightCap - e.grossTons) : null;
|
||||
if ((wagonsFree != null && wagonsFree <= 0) || (tonsFree != null && tonsFree <= 0))
|
||||
@@ -283,26 +159,19 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
|
||||
|
||||
return (
|
||||
<Stack gap="lg" mt="lg">
|
||||
{unallocatedRefs.length ? (
|
||||
<Alert radius="lg" color="orange" icon={<Info size={16} />}>
|
||||
{unallocatedRefs.join(", ")}{" "}
|
||||
{unallocatedRefs.length === 1 ? "is" : "are"} linked to this train but
|
||||
have no wagons allocated — their weight is not on any leg yet. Re-run
|
||||
allocation (or add them from the workspace) to place them.
|
||||
</Alert>
|
||||
) : null}
|
||||
<Paper radius="xl" p="lg" withBorder>
|
||||
<Stack gap="md">
|
||||
<Stack gap={2}>
|
||||
<Text fw={700}>Per-leg utilization</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Each adjacent leg is checked as its own train — wagon tare + cargo
|
||||
against the locomotive limits{weightCap != null ? ` (${weightCap}T` : ""}
|
||||
{weightCap != null && lengthCap != null ? `, ${lengthCap}m` : ""}
|
||||
{weightCap != null ? " incl. tolerance)" : ""}.
|
||||
Each adjacent leg lists every booking riding it — a through
|
||||
booking counts on all its legs. Totals are checked against the
|
||||
train limits{weightCap != null ? ` (${weightCap}T incl. tolerance` : ""}
|
||||
{weightCap != null && wagonCap != null ? `, ${wagonCap} wagons` : ""}
|
||||
{weightCap != null ? ")" : ""}.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Table.ScrollContainer minWidth={720}>
|
||||
<Table.ScrollContainer minWidth={640}>
|
||||
<Table verticalSpacing="sm" highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
@@ -310,7 +179,6 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
|
||||
<Table.Th>Leg</Table.Th>
|
||||
<Table.Th>Wagons</Table.Th>
|
||||
<Table.Th>Gross weight</Table.Th>
|
||||
<Table.Th>Length</Table.Th>
|
||||
<Table.Th>Bookings</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
@@ -348,14 +216,6 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<UsageCell used={e.grossTons} cap={weightCap} unit="T" />
|
||||
{e.pendingTons > 0 ? (
|
||||
<Text size="xs" c="orange.8" fw={600}>
|
||||
+{e.pendingTons}T unallocated
|
||||
</Text>
|
||||
) : null}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<UsageCell used={e.lengthMeters} cap={lengthCap} unit="m" />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{hasBookings ? (
|
||||
@@ -372,7 +232,7 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
|
||||
</Table.Tr>
|
||||
{hasBookings ? (
|
||||
<Table.Tr key={`${e.edge}-detail`}>
|
||||
<Table.Td colSpan={7} p={0} style={{ border: 0 }}>
|
||||
<Table.Td colSpan={6} p={0} style={{ border: 0 }}>
|
||||
<Collapse expanded={isOpen}>
|
||||
<Box
|
||||
p="sm"
|
||||
@@ -390,22 +250,15 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
|
||||
{e.bookings.map((b) => (
|
||||
<Table.Tr key={b.bookingId}>
|
||||
<Table.Td w="50%">
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Text size="sm" fw={500} style={{ whiteSpace: "nowrap" }}>
|
||||
{b.reference}
|
||||
{b.route ? (
|
||||
<Text span size="xs" c="dimmed">
|
||||
{" "}
|
||||
({b.route})
|
||||
</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
{b.unallocated ? (
|
||||
<Badge size="xs" color="orange" variant="filled">
|
||||
no wagons
|
||||
</Badge>
|
||||
<Text size="sm" fw={500} style={{ whiteSpace: "nowrap" }}>
|
||||
{b.reference}
|
||||
{b.route ? (
|
||||
<Text span size="xs" c="dimmed">
|
||||
{" "}
|
||||
({b.route})
|
||||
</Text>
|
||||
) : null}
|
||||
</Group>
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td w="25%">
|
||||
<Group gap={4} wrap="nowrap">
|
||||
@@ -413,7 +266,6 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
|
||||
<Text size="xs" c="dimmed">
|
||||
{b.wagons > 0 ? b.wagons : "—"} wagon
|
||||
{b.wagons === 1 ? "" : "s"}
|
||||
{b.unallocated && b.wagons > 0 ? " needed" : ""}
|
||||
</Text>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Grid,
|
||||
Group,
|
||||
Loader,
|
||||
@@ -15,17 +14,20 @@ import {
|
||||
Slider,
|
||||
Stack,
|
||||
Text,
|
||||
Textarea,
|
||||
ThemeIcon,
|
||||
} from "@mantine/core";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { ArrowRight, ArrowRightLeft, CheckCircle2, CircleSlash, Layers, Warehouse } from "lucide-react";
|
||||
import { ArrowRight, ArrowRightLeft, Layers, Warehouse } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import ReactQuill from "react-quill";
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
|
||||
import { api } from "@/services/api";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import type { Wagon } from "@/services/wagon.service";
|
||||
|
||||
const stripHtml = (html: string) => html.replace(/<[^>]*>/g, "").trim();
|
||||
|
||||
export interface WagonYardWorkspaceModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
@@ -139,13 +141,10 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
|
||||
const [transferYardId, setTransferYardId] = useState<string | null>(null);
|
||||
const [transferQty, setTransferQty] = useState(0);
|
||||
const [transferReason, setTransferReason] = useState("");
|
||||
const [toAssignedQty, setToAssignedQty] = useState(0);
|
||||
const [toAvailableQty, setToAvailableQty] = useState(0);
|
||||
|
||||
const createRequest = useMutation(
|
||||
api.wagonTransferRequests.create.mutationOptions(),
|
||||
);
|
||||
const setStatus = useMutation(api.wagons.bulkSetStatus.mutationOptions());
|
||||
|
||||
const yardName = useMemo(() => {
|
||||
const byId = new Map(yards.map((y) => [y.id, y.label || y.code || y.id]));
|
||||
@@ -242,8 +241,6 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
|
||||
setTransferYardId(null);
|
||||
setTransferQty(0);
|
||||
setTransferReason("");
|
||||
setToAssignedQty(0);
|
||||
setToAvailableQty(0);
|
||||
}, [yardId, typeId]);
|
||||
|
||||
// Reset the whole workspace when closed.
|
||||
@@ -254,13 +251,6 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
// Keep quantities within bounds as counts shift after each action. A TRANSFER
|
||||
// REQUEST is deliberately uncapped: OCC delivers in instalments, so asking for
|
||||
// 50 where 20 sit today is normal — only the status flips below are bounded by
|
||||
// what is physically in the yard.
|
||||
useEffect(() => setToAssignedQty((q) => Math.min(q, availableCount)), [availableCount]);
|
||||
useEffect(() => setToAvailableQty((q) => Math.min(q, assignedCount)), [assignedCount]);
|
||||
|
||||
const showError = (err: unknown, fallback: string) => {
|
||||
const message =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ?? fallback;
|
||||
@@ -275,7 +265,7 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
|
||||
!typeId ||
|
||||
!transferYardId ||
|
||||
transferQty < 1 ||
|
||||
!transferReason.trim()
|
||||
!stripHtml(transferReason)
|
||||
)
|
||||
return;
|
||||
try {
|
||||
@@ -284,7 +274,7 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
|
||||
toYardId: transferYardId,
|
||||
wagonTypeId: typeId,
|
||||
quantity: transferQty,
|
||||
reason: transferReason.trim(),
|
||||
reason: transferReason,
|
||||
});
|
||||
toast({
|
||||
title: `Requested ${transferQty} ${typeInfo.code(typeId)} wagon(s) · ${yardName(
|
||||
@@ -300,26 +290,7 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
|
||||
}
|
||||
};
|
||||
|
||||
const handleFlip = async (
|
||||
pool: Wagon[],
|
||||
qty: number,
|
||||
status: Freight.WagonStatus,
|
||||
label: string,
|
||||
reset: () => void,
|
||||
) => {
|
||||
if (qty < 1) return;
|
||||
const ids = pool.slice(0, qty).map((w) => w.id);
|
||||
if (!ids.length) return;
|
||||
try {
|
||||
const res = await setStatus.mutateAsync({ wagonIds: ids, status });
|
||||
toast({ title: `${res.updated} wagon(s) set to ${label}` });
|
||||
reset();
|
||||
} catch (err) {
|
||||
showError(err, "Status update failed");
|
||||
}
|
||||
};
|
||||
|
||||
const busy = createRequest.isPending || setStatus.isPending;
|
||||
const busy = createRequest.isPending;
|
||||
const pct = (n: number) => (total > 0 ? (n / total) * 100 : 0);
|
||||
|
||||
return (
|
||||
@@ -443,11 +414,8 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
|
||||
</Card>
|
||||
|
||||
{/* ---- Actions ---- */}
|
||||
<Grid gap="lg">
|
||||
{/* Transfer */}
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<Card withBorder radius="md" h="100%" padding="lg">
|
||||
<Group gap="xs" mb={4}>
|
||||
<Card withBorder radius="md" padding="lg">
|
||||
<Group gap="xs" mb={4}>
|
||||
<ThemeIcon variant="light" color="grape" radius="md" size="md">
|
||||
<ArrowRightLeft size={16} />
|
||||
</ThemeIcon>
|
||||
@@ -479,16 +447,17 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
|
||||
searchable
|
||||
radius="md"
|
||||
/>
|
||||
<Textarea
|
||||
label="Reason"
|
||||
placeholder="Why are these wagons needed?"
|
||||
value={transferReason}
|
||||
onChange={(e) => setTransferReason(e.currentTarget.value)}
|
||||
required
|
||||
autosize
|
||||
minRows={2}
|
||||
radius="md"
|
||||
/>
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb={4}>
|
||||
Reason <Text span c="red">*</Text>
|
||||
</Text>
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={transferReason}
|
||||
onChange={setTransferReason}
|
||||
placeholder="Why are these wagons needed?"
|
||||
/>
|
||||
</div>
|
||||
{transferYardId && transferQty > 0 ? (
|
||||
<Card bg="var(--mantine-color-gray-0)" radius="md" padding="sm" withBorder>
|
||||
<Group gap={8} wrap="nowrap">
|
||||
@@ -518,106 +487,15 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
|
||||
busy ||
|
||||
!transferYardId ||
|
||||
transferQty < 1 ||
|
||||
!transferReason.trim()
|
||||
!stripHtml(transferReason)
|
||||
}
|
||||
color="edr-green"
|
||||
>
|
||||
Request {transferQty > 0 ? `${transferQty} ` : ""}wagon
|
||||
{transferQty === 1 ? "" : "s"}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
{/* Re-status */}
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<Card withBorder radius="md" h="100%" padding="lg">
|
||||
<Group gap="xs" mb="md">
|
||||
<ThemeIcon variant="light" color="orange" radius="md" size="md">
|
||||
<ArrowRightLeft size={16} />
|
||||
</ThemeIcon>
|
||||
<Text fw={700}>Change availability</Text>
|
||||
</Group>
|
||||
<Stack gap="lg">
|
||||
<Box>
|
||||
<Group justify="space-between" mb={6}>
|
||||
<Group gap={6}>
|
||||
<ThemeIcon variant="light" color="blue" radius="sm" size="sm">
|
||||
<CircleSlash size={12} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" fw={600}>
|
||||
Available → Assigned
|
||||
</Text>
|
||||
</Group>
|
||||
<Badge color="teal" variant="light">
|
||||
{availableCount} free
|
||||
</Badge>
|
||||
</Group>
|
||||
<QuantityField
|
||||
value={toAssignedQty}
|
||||
onChange={setToAssignedQty}
|
||||
max={availableCount}
|
||||
/>
|
||||
<Button
|
||||
mt="sm"
|
||||
fullWidth
|
||||
variant="light"
|
||||
color="blue"
|
||||
disabled={busy || toAssignedQty < 1}
|
||||
loading={setStatus.isPending}
|
||||
onClick={() =>
|
||||
handleFlip(availableWagons, toAssignedQty, ASSIGNED, "Assigned", () =>
|
||||
setToAssignedQty(0),
|
||||
)
|
||||
}
|
||||
>
|
||||
Assign {toAssignedQty > 0 ? `${toAssignedQty} ` : ""}wagon
|
||||
{toAssignedQty === 1 ? "" : "s"}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Divider variant="dashed" />
|
||||
|
||||
<Box>
|
||||
<Group justify="space-between" mb={6}>
|
||||
<Group gap={6}>
|
||||
<ThemeIcon variant="light" color="teal" radius="sm" size="sm">
|
||||
<CheckCircle2 size={12} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" fw={600}>
|
||||
Assigned → Available
|
||||
</Text>
|
||||
</Group>
|
||||
<Badge color="blue" variant="light">
|
||||
{assignedCount} assigned
|
||||
</Badge>
|
||||
</Group>
|
||||
<QuantityField
|
||||
value={toAvailableQty}
|
||||
onChange={setToAvailableQty}
|
||||
max={assignedCount}
|
||||
/>
|
||||
<Button
|
||||
mt="sm"
|
||||
fullWidth
|
||||
variant="light"
|
||||
color="teal"
|
||||
disabled={busy || toAvailableQty < 1}
|
||||
loading={setStatus.isPending}
|
||||
onClick={() =>
|
||||
handleFlip(assignedWagons, toAvailableQty, AVAILABLE, "Available", () =>
|
||||
setToAvailableQty(0),
|
||||
)
|
||||
}
|
||||
>
|
||||
Free up {toAvailableQty > 0 ? `${toAvailableQty} ` : ""}wagon
|
||||
{toAvailableQty === 1 ? "" : "s"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
83
pnpm-lock.yaml
generated
83
pnpm-lock.yaml
generated
@@ -469,6 +469,9 @@ importers:
|
||||
react-pdf-html:
|
||||
specifier: ^2.1.5
|
||||
version: 2.1.5(@react-pdf/renderer@4.5.1(react@19.2.6))(react@19.2.6)
|
||||
react-quill:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
react-resizable-panels:
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
@@ -4805,6 +4808,9 @@ packages:
|
||||
'@types/qs@6.15.1':
|
||||
resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
|
||||
|
||||
'@types/quill@1.3.10':
|
||||
resolution: {integrity: sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==}
|
||||
|
||||
'@types/raf@3.4.3':
|
||||
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
|
||||
|
||||
@@ -6563,6 +6569,10 @@ packages:
|
||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
deep-equal@1.1.2:
|
||||
resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
@@ -7086,6 +7096,9 @@ packages:
|
||||
eventemitter2@6.4.9:
|
||||
resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==}
|
||||
|
||||
eventemitter3@2.0.3:
|
||||
resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==}
|
||||
|
||||
eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
|
||||
@@ -7211,6 +7224,9 @@ packages:
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fast-diff@1.1.2:
|
||||
resolution: {integrity: sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==}
|
||||
|
||||
fast-equals@5.4.0:
|
||||
resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -9372,6 +9388,10 @@ packages:
|
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
object-is@1.1.6:
|
||||
resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
object-keys@1.1.1:
|
||||
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -9512,6 +9532,9 @@ packages:
|
||||
pako@2.1.0:
|
||||
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
||||
|
||||
parchment@1.1.4:
|
||||
resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -9948,6 +9971,13 @@ packages:
|
||||
queue@6.0.2:
|
||||
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
|
||||
|
||||
quill-delta@3.6.3:
|
||||
resolution: {integrity: sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
quill@1.3.7:
|
||||
resolution: {integrity: sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==}
|
||||
|
||||
radix-ui@1.5.0:
|
||||
resolution: {integrity: sha512-Nzh2HNpClgB31FBHRqt2xG8XNUfVfQRpf34hACC5PNrXTd5JdXdqOXwLs3BL+D8CNYiNQiJiT8QGr5Q4vq+00w==}
|
||||
peerDependencies:
|
||||
@@ -10126,6 +10156,12 @@ packages:
|
||||
react: '>=16.8'
|
||||
react-dom: '>=16.8'
|
||||
|
||||
react-quill@2.0.0:
|
||||
resolution: {integrity: sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==}
|
||||
peerDependencies:
|
||||
react: ^16 || ^17 || ^18
|
||||
react-dom: ^16 || ^17 || ^18
|
||||
|
||||
react-redux@9.3.0:
|
||||
resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==}
|
||||
peerDependencies:
|
||||
@@ -16907,6 +16943,10 @@ snapshots:
|
||||
|
||||
'@types/qs@6.15.1': {}
|
||||
|
||||
'@types/quill@1.3.10':
|
||||
dependencies:
|
||||
parchment: 1.1.4
|
||||
|
||||
'@types/raf@3.4.3':
|
||||
optional: true
|
||||
|
||||
@@ -18824,6 +18864,15 @@ snapshots:
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
deep-equal@1.1.2:
|
||||
dependencies:
|
||||
is-arguments: 1.2.0
|
||||
is-date-object: 1.1.0
|
||||
is-regex: 1.2.1
|
||||
object-is: 1.1.6
|
||||
object-keys: 1.1.1
|
||||
regexp.prototype.flags: 1.5.4
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
deepmerge-ts@7.1.5: {}
|
||||
@@ -19490,6 +19539,8 @@ snapshots:
|
||||
|
||||
eventemitter2@6.4.9: {}
|
||||
|
||||
eventemitter3@2.0.3: {}
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
|
||||
eventemitter3@5.0.4: {}
|
||||
@@ -19730,6 +19781,8 @@ snapshots:
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-diff@1.1.2: {}
|
||||
|
||||
fast-equals@5.4.0: {}
|
||||
|
||||
fast-fifo@1.3.2: {}
|
||||
@@ -22109,6 +22162,11 @@ snapshots:
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
object-is@1.1.6:
|
||||
dependencies:
|
||||
call-bind: 1.0.9
|
||||
define-properties: 1.2.1
|
||||
|
||||
object-keys@1.1.1: {}
|
||||
|
||||
object-treeify@1.1.33: {}
|
||||
@@ -22291,6 +22349,8 @@ snapshots:
|
||||
|
||||
pako@2.1.0: {}
|
||||
|
||||
parchment@1.1.4: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@@ -22699,6 +22759,21 @@ snapshots:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
|
||||
quill-delta@3.6.3:
|
||||
dependencies:
|
||||
deep-equal: 1.1.2
|
||||
extend: 3.0.2
|
||||
fast-diff: 1.1.2
|
||||
|
||||
quill@1.3.7:
|
||||
dependencies:
|
||||
clone: 2.1.2
|
||||
deep-equal: 1.1.2
|
||||
eventemitter3: 2.0.3
|
||||
extend: 3.0.2
|
||||
parchment: 1.1.4
|
||||
quill-delta: 3.6.3
|
||||
|
||||
radix-ui@1.5.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.4
|
||||
@@ -23022,6 +23097,14 @@ snapshots:
|
||||
react: 19.2.6
|
||||
react-dom: 19.2.6(react@19.2.6)
|
||||
|
||||
react-quill@2.0.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||
dependencies:
|
||||
'@types/quill': 1.3.10
|
||||
lodash: 4.18.1
|
||||
quill: 1.3.7
|
||||
react: 19.2.6
|
||||
react-dom: 19.2.6(react@19.2.6)
|
||||
|
||||
react-redux@9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1):
|
||||
dependencies:
|
||||
'@types/use-sync-external-store': 0.0.6
|
||||
|
||||
Reference in New Issue
Block a user