mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 07:38:10 +00:00
implement contract cancellation feature and update contract statuses
- Added functionality to cancel contracts, allowing users to provide a reason for cancellation. - Updated contract statuses to include SUSPENDED and changed CLOSED to COMPLETED. - Enhanced the UI to reflect the new cancellation option and updated messaging for contract statuses. - Refactored contract booking actions to accommodate changes in booking logic for ONE_TIME and GENERAL contracts. - Removed clearance document management from the contract detail page, as it is now handled per booking. - Introduced a SQL script to reset bookings and train schedules for development purposes.
This commit is contained in:
@@ -30,7 +30,7 @@ const parseError = (error: unknown, fallback: string) => {
|
||||
|
||||
/**
|
||||
* Step one of the Train Builder: pick the yard it is being assembled in and
|
||||
* couple at least two locomotives from that yard. The train code is assigned by
|
||||
* couple at least one locomotive from that yard. The train code is assigned by
|
||||
* the system. Wagons are attached afterwards on the composition page.
|
||||
*/
|
||||
export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrainModalProps) {
|
||||
@@ -86,9 +86,9 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
||||
}, [opened]);
|
||||
|
||||
const handleBuild = async () => {
|
||||
if (!yardId || locomotiveIds.length < 2) {
|
||||
if (!yardId || locomotiveIds.length < 1) {
|
||||
toast({
|
||||
title: "Pick a yard and couple at least two locomotives",
|
||||
title: "Pick a yard and couple at least one locomotive",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -185,17 +185,15 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
||||
/>
|
||||
<MultiSelect
|
||||
label="Locomotives"
|
||||
description="A train must be pulled by at least two locomotives (front and back). First pick becomes the lead."
|
||||
placeholder={yardId ? "Select at least two locomotives" : "Select a yard first"}
|
||||
description="A train must be pulled by at least one locomotive. First pick becomes the lead."
|
||||
placeholder={yardId ? "Select at least one locomotive" : "Select a yard first"}
|
||||
data={locomotiveOptions}
|
||||
value={locomotiveIds}
|
||||
onChange={setLocomotiveIds}
|
||||
searchable
|
||||
disabled={!yardId}
|
||||
error={
|
||||
locomotiveIds.length > 0 && locomotiveIds.length < 2
|
||||
? "Select at least two locomotives"
|
||||
: undefined
|
||||
locomotiveIds.length < 1 ? "Select at least one locomotive" : undefined
|
||||
}
|
||||
nothingFoundMessage={
|
||||
yardId ? "No available locomotives in this yard" : "Select a yard first"
|
||||
|
||||
@@ -16,7 +16,7 @@ const parseError = (error: unknown, fallback: string) => {
|
||||
return fallback;
|
||||
};
|
||||
|
||||
/** Swap the locomotive set of a built train (minimum 2, same-yard rule). */
|
||||
/** Swap the locomotive set of a built train (minimum 1, same-yard rule). */
|
||||
export default function ChangeLocomotivesModal({
|
||||
composition,
|
||||
opened,
|
||||
@@ -74,8 +74,8 @@ export default function ChangeLocomotivesModal({
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!composition) return;
|
||||
if (locomotiveIds.length < 2) {
|
||||
toast({ title: "A train needs at least two locomotives", variant: "destructive" });
|
||||
if (locomotiveIds.length < 1) {
|
||||
toast({ title: "A train needs at least one locomotive", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -112,9 +112,7 @@ export default function ChangeLocomotivesModal({
|
||||
onChange={setLocomotiveIds}
|
||||
searchable
|
||||
error={
|
||||
locomotiveIds.length > 0 && locomotiveIds.length < 2
|
||||
? "Select at least two locomotives"
|
||||
: undefined
|
||||
locomotiveIds.length < 1 ? "Select at least one locomotive" : undefined
|
||||
}
|
||||
nothingFoundMessage="No available locomotives in this yard"
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { type ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
import type { TrainCompositionWagon } from "@/services/trainBuilder.service";
|
||||
import { wagonTypeColor } from "./trainStatus";
|
||||
|
||||
/** Reparent dragged row to body — fixes position:fixed inside transformed parents. */
|
||||
const PortalAwareRow = ({
|
||||
@@ -58,11 +59,36 @@ export default function ConsistWagonList({
|
||||
);
|
||||
}
|
||||
|
||||
// Legend of the types actually coupled, in consist order — the colour code is
|
||||
// only readable if the row tints are keyed somewhere.
|
||||
const legend = [
|
||||
...new Map(
|
||||
wagons
|
||||
.filter((w) => w.wagonType)
|
||||
.map((w) => [w.wagonType!.code, w.wagonType!]),
|
||||
).values(),
|
||||
];
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="train-consist-wagons" isDropDisabled={!editable || busy}>
|
||||
{(dropProvided) => (
|
||||
<Stack gap="xs" ref={dropProvided.innerRef} {...dropProvided.droppableProps}>
|
||||
{legend.length > 1 ? (
|
||||
<Group gap={6} wrap="wrap">
|
||||
{legend.map((type) => (
|
||||
<Badge
|
||||
key={type.code}
|
||||
size="sm"
|
||||
radius="sm"
|
||||
variant="light"
|
||||
color={wagonTypeColor(type.code)}
|
||||
>
|
||||
{type.code} · {type.name}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
) : null}
|
||||
{wagons.map((wagon, index) => (
|
||||
<Draggable
|
||||
key={wagon.id}
|
||||
@@ -97,8 +123,8 @@ export interface ConsistWagonListProps {
|
||||
editable: boolean;
|
||||
onReorder: (wagonIds: string[]) => void;
|
||||
onRemove: (wagonId: string) => void;
|
||||
/** Detach the wagon and move it to MAINTENANCE status. */
|
||||
onMaintenance: (wagonId: string) => void;
|
||||
/** Detach the wagon and move it to MAINTENANCE status (page confirms first). */
|
||||
onMaintenance: (wagon: TrainCompositionWagon) => void;
|
||||
busy?: boolean;
|
||||
}
|
||||
|
||||
@@ -119,8 +145,10 @@ function WagonRow({
|
||||
editable: boolean;
|
||||
busy: boolean;
|
||||
onRemove: (wagonId: string) => void;
|
||||
onMaintenance: (wagonId: string) => void;
|
||||
onMaintenance: (wagon: TrainCompositionWagon) => void;
|
||||
}) {
|
||||
const color = wagonTypeColor(wagon.wagonType?.code);
|
||||
|
||||
return (
|
||||
<PortalAwareRow snapshot={snapshot}>
|
||||
<Group
|
||||
@@ -132,9 +160,14 @@ function WagonRow({
|
||||
p="sm"
|
||||
style={{
|
||||
...dragProvided.draggableProps.style,
|
||||
border: "1px solid var(--mantine-color-gray-3)",
|
||||
border: `1px solid var(--mantine-color-${color}-2)`,
|
||||
borderLeft: `4px solid var(--mantine-color-${color}-5)`,
|
||||
borderRadius: "var(--mantine-radius-md)",
|
||||
background: snapshot.isDragging ? "var(--mantine-color-gray-0)" : "white",
|
||||
// Tinted by wagon type so a mixed consist is scannable at a glance;
|
||||
// the drag state keeps its own neutral lift.
|
||||
background: snapshot.isDragging
|
||||
? "white"
|
||||
: `var(--mantine-color-${color}-0)`,
|
||||
boxShadow: snapshot.isDragging ? "0 8px 24px rgba(0, 0, 0, 0.12)" : undefined,
|
||||
cursor: editable ? (snapshot.isDragging ? "grabbing" : "grab") : "default",
|
||||
userSelect: "none",
|
||||
@@ -145,13 +178,20 @@ function WagonRow({
|
||||
<GripVertical size={18} />
|
||||
</Box>
|
||||
) : null}
|
||||
<Badge variant="light" color="gray" size="sm">
|
||||
<Badge variant="filled" color={color} size="sm">
|
||||
{index + 1}
|
||||
</Badge>
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="sm" fw={600} ff="monospace" truncate>
|
||||
{wagon.wagonNumber}
|
||||
</Text>
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Text size="sm" fw={600} ff="monospace" truncate>
|
||||
{wagon.wagonNumber}
|
||||
</Text>
|
||||
{wagon.wagonType ? (
|
||||
<Badge variant="light" color={color} size="xs" radius="sm">
|
||||
{wagon.wagonType.code}
|
||||
</Badge>
|
||||
) : null}
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed" truncate>
|
||||
{wagon.wagonType
|
||||
? `${wagon.wagonType.name} · ${wagon.wagonType.capacityTons}T cap · ${wagon.wagonType.lengthMeters}m`
|
||||
@@ -165,7 +205,7 @@ function WagonRow({
|
||||
variant="subtle"
|
||||
color="orange"
|
||||
disabled={busy}
|
||||
onClick={() => onMaintenance(wagon.id)}
|
||||
onClick={() => onMaintenance(wagon)}
|
||||
aria-label={`Send wagon ${wagon.wagonNumber} to maintenance`}
|
||||
>
|
||||
<Wrench size={16} />
|
||||
|
||||
@@ -56,6 +56,58 @@ export const locomotiveStatusLabel = (status: string): string =>
|
||||
.replace(/_/g, " ")
|
||||
.replace(/^\w/, (c) => c.toUpperCase());
|
||||
|
||||
/**
|
||||
* Hues for the wagon-type color code. No red or gray — red reads as a fault on
|
||||
* a consist row, gray is the "unknown type" fallback.
|
||||
*/
|
||||
const WAGON_TYPE_COLORS = [
|
||||
"blue",
|
||||
"teal",
|
||||
"grape",
|
||||
"orange",
|
||||
"cyan",
|
||||
"indigo",
|
||||
"pink",
|
||||
"lime",
|
||||
"violet",
|
||||
"yellow",
|
||||
];
|
||||
|
||||
/**
|
||||
* Fixed hue per seeded wagon-type code. Related families sit on neighbouring
|
||||
* hues (gondolas blue/indigo, hoppers grape/violet, flats teal/lime) so a
|
||||
* consist reads as groups, not confetti. Explicit rather than hashed because
|
||||
* hashing 10 codes into 10 hues collides — and two types sharing a colour is
|
||||
* exactly what a colour code must not do.
|
||||
*/
|
||||
const WAGON_TYPE_CODE_COLORS: Record<string, string> = {
|
||||
CW3: "blue", // Gondola open
|
||||
CW4: "indigo", // Gondola covered
|
||||
KW2: "grape", // Hopper covered
|
||||
KW3: "violet", // Hopper open
|
||||
NW5: "teal", // Flat
|
||||
NW6: "lime", // Flat (long)
|
||||
NW7: "pink", // Double deck sedan
|
||||
BW1: "cyan", // Refrigerated
|
||||
GW2: "orange", // Tank
|
||||
PW2: "yellow", // Box
|
||||
};
|
||||
|
||||
/**
|
||||
* Stable hue per wagon-type code. Unseeded codes fall back to a hash so a new
|
||||
* type still gets a consistent colour instead of collapsing to gray.
|
||||
*/
|
||||
export const wagonTypeColor = (code?: string | null): string => {
|
||||
if (!code) return "gray";
|
||||
const seeded = WAGON_TYPE_CODE_COLORS[code];
|
||||
if (seeded) return seeded;
|
||||
let hash = 0;
|
||||
for (let i = 0; i < code.length; i++) {
|
||||
hash = (hash * 31 + code.charCodeAt(i)) >>> 0;
|
||||
}
|
||||
return WAGON_TYPE_COLORS[hash % WAGON_TYPE_COLORS.length]!;
|
||||
};
|
||||
|
||||
/** Badge color per trade direction (Mantine palette keys). */
|
||||
export const directionColor = (direction?: string | null): string =>
|
||||
direction === "IMPORT" ? "blue" : direction === "EXPORT" ? "orange" : "gray";
|
||||
|
||||
Reference in New Issue
Block a user