mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 04:08:11 +00:00
add yard distances management to rule engine
- Introduced new yard distances resource with CRUD operations. - Created migration for yard distances table with necessary constraints. - Implemented service and repository for yard distances handling. - Added controller for API endpoints to manage yard distances. - Updated rule engine configuration to include yard distances. - Enhanced rule engine resource page to support yard distance selection. - Updated contracts and train builder pages to handle new yard distance logic. - Added error handling utility for better error message extraction.
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Button,
|
||||
Box,
|
||||
Modal,
|
||||
Select,
|
||||
Textarea,
|
||||
} from "@mantine/core";
|
||||
import type { Freight } from "@edr/types";
|
||||
@@ -35,6 +36,10 @@ export function ContractApprovalStepsCard({
|
||||
const [rejectStepRow, setRejectStepRow] =
|
||||
useState<Freight.IContractApprovalStep | null>(null);
|
||||
const [rejectReason, setRejectReason] = useState("");
|
||||
// Where the rejection lands: "CUSTOMER" (terminal, resubmit) or the id of an
|
||||
// earlier APPROVED step to send the chain back to. First approver has no
|
||||
// choice — customer only.
|
||||
const [rejectTarget, setRejectTarget] = useState<string>("CUSTOMER");
|
||||
|
||||
const steps = useMemo(
|
||||
() =>
|
||||
@@ -70,6 +75,7 @@ export function ContractApprovalStepsCard({
|
||||
const openReject = (step: Freight.IContractApprovalStep) => {
|
||||
setRejectStepRow(step);
|
||||
setRejectReason("");
|
||||
setRejectTarget("CUSTOMER");
|
||||
setRejectOpen(true);
|
||||
};
|
||||
|
||||
@@ -77,14 +83,34 @@ export function ContractApprovalStepsCard({
|
||||
setRejectOpen(false);
|
||||
setRejectStepRow(null);
|
||||
setRejectReason("");
|
||||
setRejectTarget("CUSTOMER");
|
||||
};
|
||||
|
||||
const trimmedReason = rejectReason.trim();
|
||||
|
||||
// Earlier stages this rejection can be returned to — only stages that have
|
||||
// already approved. Empty for the first approver, whose only target is the
|
||||
// customer.
|
||||
const returnableSteps = rejectStepRow
|
||||
? steps.filter(
|
||||
(s) =>
|
||||
s.stepOrder < rejectStepRow.stepOrder && s.status === "APPROVED",
|
||||
)
|
||||
: [];
|
||||
|
||||
const sendBack = rejectTarget !== "CUSTOMER";
|
||||
const targetStep = sendBack
|
||||
? returnableSteps.find((s) => s.id === rejectTarget)
|
||||
: undefined;
|
||||
|
||||
const runReject = () => {
|
||||
if (!rejectStepRow || !trimmedReason) return;
|
||||
mutations.rejectStep.mutate(
|
||||
{ stepId: rejectStepRow.id, reason: trimmedReason },
|
||||
{
|
||||
stepId: rejectStepRow.id,
|
||||
reason: trimmedReason,
|
||||
returnToStepId: sendBack ? rejectTarget : undefined,
|
||||
},
|
||||
{ onSuccess: () => closeReject() },
|
||||
);
|
||||
};
|
||||
@@ -192,21 +218,56 @@ export function ContractApprovalStepsCard({
|
||||
centered
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
Rejecting the{" "}
|
||||
<Text span fw={600} c="dark">
|
||||
{rejectStepRow?.requiredRole}
|
||||
</Text>{" "}
|
||||
step rejects contract{" "}
|
||||
<Text span fw={600} c="dark">
|
||||
{contract.reference}
|
||||
</Text>{" "}
|
||||
outright. The customer must create a new contract — this cannot be
|
||||
undone.
|
||||
</Text>
|
||||
{returnableSteps.length > 0 && (
|
||||
<Select
|
||||
label="Send rejection to"
|
||||
description="Return the contract to an earlier approver to fix and re-approve, or reject it to the customer."
|
||||
allowDeselect={false}
|
||||
value={rejectTarget}
|
||||
onChange={(v) => setRejectTarget(v ?? "CUSTOMER")}
|
||||
data={[
|
||||
{ value: "CUSTOMER", label: "Customer — must resubmit" },
|
||||
...returnableSteps.map((s) => ({
|
||||
value: s.id,
|
||||
label: `${s.requiredRole} — step ${s.stepOrder} re-approves`,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{sendBack ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
Contract{" "}
|
||||
<Text span fw={600} c="dark">
|
||||
{contract.reference}
|
||||
</Text>{" "}
|
||||
will go back to the{" "}
|
||||
<Text span fw={600} c="dark">
|
||||
{targetStep?.requiredRole}
|
||||
</Text>{" "}
|
||||
step. That approver fixes the contract and approves again, and
|
||||
every later step re-approves in order. The customer is not
|
||||
notified.
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
Rejecting the{" "}
|
||||
<Text span fw={600} c="dark">
|
||||
{rejectStepRow?.requiredRole}
|
||||
</Text>{" "}
|
||||
step rejects contract{" "}
|
||||
<Text span fw={600} c="dark">
|
||||
{contract.reference}
|
||||
</Text>{" "}
|
||||
outright. The customer must resubmit — this cannot be undone.
|
||||
</Text>
|
||||
)}
|
||||
<Textarea
|
||||
label="Reason for rejection"
|
||||
description="Shared with the customer and the approval chain."
|
||||
description={
|
||||
sendBack
|
||||
? "Shared with the approval chain (not the customer)."
|
||||
: "Shared with the customer and the approval chain."
|
||||
}
|
||||
placeholder="Explain why this contract is rejected…"
|
||||
minRows={3}
|
||||
autosize
|
||||
@@ -219,14 +280,16 @@ export function ContractApprovalStepsCard({
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
color={sendBack ? "orange" : "red"}
|
||||
radius="md"
|
||||
leftSection={<X size={16} />}
|
||||
loading={mutations.rejectStep.isPending}
|
||||
disabled={!trimmedReason}
|
||||
onClick={runReject}
|
||||
>
|
||||
Reject contract
|
||||
{sendBack
|
||||
? `Send back to ${targetStep?.requiredRole ?? "step"}`
|
||||
: "Reject contract"}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -46,10 +46,18 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
||||
// Admin-managed run list (dropdown settings); numbers already on a train
|
||||
// come back disabled so they cannot be picked twice.
|
||||
const importNumbers = useImportTrainNumberOptions();
|
||||
// Only serviceable locomotives standing in the selected yard can be coupled.
|
||||
// Only serviceable locomotives standing in the selected yard, and not already
|
||||
// coupled to another built train, can be picked. A new train owns none yet, so
|
||||
// no train to keep-exclude.
|
||||
const locomotivesQuery = useQuery(
|
||||
api.locomotives.listFiltered.queryOptions({
|
||||
input: { filters: { status: "AVAILABLE", currentYardId: yardId } },
|
||||
input: {
|
||||
filters: {
|
||||
status: "AVAILABLE",
|
||||
currentYardId: yardId,
|
||||
excludeCoupled: true,
|
||||
},
|
||||
},
|
||||
enabled: Boolean(yardId),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -26,9 +26,19 @@ export default function ChangeLocomotivesModal({
|
||||
const [locomotiveIds, setLocomotiveIds] = useState<string[]>([]);
|
||||
|
||||
const yardId = composition?.currentYard?.id ?? "";
|
||||
// A locomotive already coupled to ANOTHER built train is not a valid pick —
|
||||
// the API rejects it on save. Exclude those here (keeping this train's own
|
||||
// ones, which are re-listed below as "(coupled)").
|
||||
const availableQuery = useQuery(
|
||||
api.locomotives.listFiltered.queryOptions({
|
||||
input: { filters: { status: "AVAILABLE", currentYardId: yardId } },
|
||||
input: {
|
||||
filters: {
|
||||
status: "AVAILABLE",
|
||||
currentYardId: yardId,
|
||||
excludeCoupled: true,
|
||||
excludeTrainId: composition?.id,
|
||||
},
|
||||
},
|
||||
enabled: opened && Boolean(yardId),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -28,6 +28,34 @@ export const trainStatusLabel = (status: BuiltTrainStatus | string): string =>
|
||||
.replace(/_/g, " ")
|
||||
.replace(/^\w/, (c) => c.toUpperCase());
|
||||
|
||||
/** Statuses that block a deactivated train from reactivating (mirrors the API gate). */
|
||||
export const UNFIT_LOCOMOTIVE_STATUSES = new Set(["MAINTENANCE", "OUT_OF_SERVICE", "UNAVAILABLE"]);
|
||||
|
||||
/** Badge color per locomotive status (Mantine palette keys). */
|
||||
export const locomotiveStatusColor = (status: string): string => {
|
||||
switch (status) {
|
||||
case "AVAILABLE":
|
||||
case "IMPORT_READY":
|
||||
case "EXPORT_READY":
|
||||
return "edr-green";
|
||||
case "ASSIGNED":
|
||||
return "blue";
|
||||
case "MAINTENANCE":
|
||||
return "yellow";
|
||||
case "OUT_OF_SERVICE":
|
||||
case "UNAVAILABLE":
|
||||
return "red";
|
||||
default:
|
||||
return "gray";
|
||||
}
|
||||
};
|
||||
|
||||
export const locomotiveStatusLabel = (status: string): string =>
|
||||
String(status)
|
||||
.toLowerCase()
|
||||
.replace(/_/g, " ")
|
||||
.replace(/^\w/, (c) => c.toUpperCase());
|
||||
|
||||
/** 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