mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 04:48:18 +00:00
Add tareWeightTons property to CreateWagonTypeDto and update related components
This commit is contained in:
@@ -536,6 +536,7 @@ export function WagonTypesCrudPage() {
|
||||
name: '',
|
||||
capacityTons: 0,
|
||||
lengthMeters: 0,
|
||||
tareWeightTons: '',
|
||||
supportedLoadTypes: '',
|
||||
isActive: true,
|
||||
});
|
||||
@@ -586,6 +587,7 @@ export function WagonTypesCrudPage() {
|
||||
name: '',
|
||||
capacityTons: 0,
|
||||
lengthMeters: 0,
|
||||
tareWeightTons: '',
|
||||
supportedLoadTypes: '',
|
||||
isActive: true,
|
||||
});
|
||||
@@ -599,6 +601,7 @@ export function WagonTypesCrudPage() {
|
||||
name: type.name ?? '',
|
||||
capacityTons: type.capacityTons ?? 0,
|
||||
lengthMeters: type.lengthMeters ?? 0,
|
||||
tareWeightTons: type.tareWeightTons ?? '',
|
||||
supportedLoadTypes: type.supportedLoadTypes?.join(', ') ?? '',
|
||||
isActive: type.isActive,
|
||||
});
|
||||
@@ -607,10 +610,17 @@ export function WagonTypesCrudPage() {
|
||||
|
||||
const validateWagonType = () => {
|
||||
const errors: Record<string, string> = {};
|
||||
// normalizePayload strips empty strings, so a blank numeric field would be
|
||||
// dropped from the payload rather than rejected. Each must be a positive
|
||||
// number here — the API's @Min(0.001) agrees.
|
||||
const positive = (value: FormValue) => Number.isFinite(Number(value)) && Number(value) > 0;
|
||||
|
||||
if (!String(form.code ?? '').trim()) errors.code = 'Code is required';
|
||||
if (!String(form.name ?? '').trim()) errors.name = 'Name is required';
|
||||
if (!Number.isFinite(Number(form.capacityTons))) errors.capacityTons = 'Capacity must be a valid number';
|
||||
if (!Number.isFinite(Number(form.lengthMeters))) errors.lengthMeters = 'Length must be a valid number';
|
||||
if (!positive(form.capacityTons)) errors.capacityTons = 'Capacity must be greater than 0';
|
||||
if (!positive(form.lengthMeters)) errors.lengthMeters = 'Length must be greater than 0';
|
||||
if (!positive(form.tareWeightTons))
|
||||
errors.tareWeightTons = 'Tare weight is required and must be greater than 0';
|
||||
return errors;
|
||||
};
|
||||
|
||||
@@ -707,6 +717,7 @@ export function WagonTypesCrudPage() {
|
||||
</MantineButton>
|
||||
</MantineTable.Th>
|
||||
<MantineTable.Th>Length (m)</MantineTable.Th>
|
||||
<MantineTable.Th>Tare weight (tons)</MantineTable.Th>
|
||||
<MantineTable.Th>Load types</MantineTable.Th>
|
||||
<MantineTable.Th>Status</MantineTable.Th>
|
||||
<MantineTable.Th ta="right">Actions</MantineTable.Th>
|
||||
@@ -719,6 +730,7 @@ export function WagonTypesCrudPage() {
|
||||
<MantineTable.Td>{type.name}</MantineTable.Td>
|
||||
<MantineTable.Td>{type.capacityTons}</MantineTable.Td>
|
||||
<MantineTable.Td>{type.lengthMeters}</MantineTable.Td>
|
||||
<MantineTable.Td>{type.tareWeightTons ?? '-'}</MantineTable.Td>
|
||||
<MantineTable.Td>{type.supportedLoadTypes?.join(', ') || '-'}</MantineTable.Td>
|
||||
<MantineTable.Td>
|
||||
<MantineBadge color={type.isActive === false ? 'gray' : 'edr-green'} variant="light">
|
||||
@@ -747,7 +759,7 @@ export function WagonTypesCrudPage() {
|
||||
))}
|
||||
{!query.isLoading && filtered.length === 0 ? (
|
||||
<MantineTable.Tr>
|
||||
<MantineTable.Td colSpan={7}>
|
||||
<MantineTable.Td colSpan={8}>
|
||||
<Text ta="center" c="dimmed" py="xl">
|
||||
No wagon types found.
|
||||
</Text>
|
||||
@@ -756,7 +768,7 @@ export function WagonTypesCrudPage() {
|
||||
) : null}
|
||||
{query.isLoading ? (
|
||||
<MantineTable.Tr>
|
||||
<MantineTable.Td colSpan={7}>
|
||||
<MantineTable.Td colSpan={8}>
|
||||
<Text ta="center" c="dimmed" py="xl">
|
||||
Loading...
|
||||
</Text>
|
||||
@@ -811,6 +823,15 @@ export function WagonTypesCrudPage() {
|
||||
error={fieldErrors.lengthMeters}
|
||||
onChange={(value) => setForm((current) => ({ ...current, lengthMeters: value }))}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Tare weight (tons)"
|
||||
description="Empty wagon weight — counts against the locomotive's pull limit alongside the cargo"
|
||||
required
|
||||
min={0}
|
||||
value={form.tareWeightTons === '' || form.tareWeightTons == null ? '' : Number(form.tareWeightTons)}
|
||||
error={fieldErrors.tareWeightTons}
|
||||
onChange={(value) => setForm((current) => ({ ...current, tareWeightTons: value }))}
|
||||
/>
|
||||
<MantineSelect
|
||||
label="Status"
|
||||
value={form.isActive ? 'true' : 'false'}
|
||||
@@ -1109,6 +1130,16 @@ export function LocomotivesCrudPage() {
|
||||
{ key: 'status', label: 'Status', render: (locomotive) => statusBadge(locomotive.status) },
|
||||
{ key: 'maxPullWeightTons', label: 'Max pull (tons)' },
|
||||
{ key: 'maxTrainLengthMeters', label: 'Max length (m)' },
|
||||
{
|
||||
key: 'overageToleranceTons',
|
||||
label: 'Weight tolerance (t)',
|
||||
render: (locomotive) => locomotive.overageToleranceTons ?? '-',
|
||||
},
|
||||
{
|
||||
key: 'overageToleranceMeters',
|
||||
label: 'Length tolerance (m)',
|
||||
render: (locomotive) => locomotive.overageToleranceMeters ?? '-',
|
||||
},
|
||||
]}
|
||||
fields={[
|
||||
{ key: 'code', label: 'Code', required: true },
|
||||
@@ -1137,6 +1168,11 @@ export function LocomotivesCrudPage() {
|
||||
},
|
||||
{ key: 'maxPullWeightTons', label: 'Max pulling weight (tons)', type: 'number', required: true },
|
||||
{ key: 'maxTrainLengthMeters', label: 'Max train length (meters)', type: 'number', required: true },
|
||||
// Scheduling accepts a consist up to (max + tolerance) on each axis: 37 PW2
|
||||
// wagons gross 3,522.4T against a 3,500T pull limit and only board because
|
||||
// of the weight tolerance.
|
||||
{ key: 'overageToleranceTons', label: 'Weight tolerance (tons over max pull)', type: 'number' },
|
||||
{ key: 'overageToleranceMeters', label: 'Length tolerance (meters over max length)', type: 'number' },
|
||||
{ key: 'powerKw', label: 'Power (kW)', type: 'number' },
|
||||
{ key: 'tractionForceKn', label: 'Traction force (kN)', type: 'number' },
|
||||
{ key: 'maxSpeedKmh', label: 'Max speed (km/h)', type: 'number' },
|
||||
@@ -1148,6 +1184,8 @@ export function LocomotivesCrudPage() {
|
||||
status: 'AVAILABLE',
|
||||
maxPullWeightTons: 0,
|
||||
maxTrainLengthMeters: 760,
|
||||
overageToleranceTons: '',
|
||||
overageToleranceMeters: '',
|
||||
powerKw: '',
|
||||
tractionForceKn: '',
|
||||
maxSpeedKmh: '',
|
||||
|
||||
@@ -158,6 +158,8 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
|
||||
{ id: "status", header: "Status", accessorKey: "status", format: "statusBadge" },
|
||||
{ id: "maxPullWeightTons", header: "Max pull (tons)", accessorKey: "maxPullWeightTons", format: "number" },
|
||||
{ id: "maxTrainLengthMeters", header: "Max length (m)", accessorKey: "maxTrainLengthMeters", format: "number" },
|
||||
{ id: "overageToleranceTons", header: "Weight tolerance (t)", accessorKey: "overageToleranceTons", format: "number" },
|
||||
{ id: "overageToleranceMeters", header: "Length tolerance (m)", accessorKey: "overageToleranceMeters", format: "number" },
|
||||
],
|
||||
// Code is auto-generated server-side (LOCO-NNN) — omitted from the form.
|
||||
formFields: [
|
||||
@@ -167,6 +169,11 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
|
||||
{ name: "currentYardId", label: "Current Yard", type: "select", dynamicOptions: "yards" },
|
||||
{ name: "maxPullWeightTons", label: "Max pulling weight (tons)", type: "number", required: true },
|
||||
{ name: "maxTrainLengthMeters", label: "Max train length (meters)", type: "number", required: true },
|
||||
// Scheduling accepts a consist up to (max + tolerance) on each axis: 37 PW2
|
||||
// wagons gross 3,522.4T against a 3,500T pull limit and only board because
|
||||
// of the weight tolerance.
|
||||
{ name: "overageToleranceTons", label: "Weight tolerance (tons over max pull)", type: "number" },
|
||||
{ name: "overageToleranceMeters", label: "Length tolerance (meters over max length)", type: "number" },
|
||||
{ name: "powerKw", label: "Power (kW)", type: "number" },
|
||||
{ name: "tractionForceKn", label: "Traction force (kN)", type: "number" },
|
||||
{ name: "maxSpeedKmh", label: "Max speed (km/h)", type: "number" },
|
||||
@@ -178,6 +185,8 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
|
||||
currentYardId: "",
|
||||
maxPullWeightTons: 2500,
|
||||
maxTrainLengthMeters: 760,
|
||||
overageToleranceTons: "",
|
||||
overageToleranceMeters: "",
|
||||
powerKw: "",
|
||||
tractionForceKn: "",
|
||||
maxSpeedKmh: "",
|
||||
|
||||
@@ -175,6 +175,7 @@ function CapacityChip({
|
||||
);
|
||||
}
|
||||
|
||||
/** Gross weight (wagon tare + cargo) against the locomotive's pull limit. */
|
||||
function weightPctOf(s: BatchBoardSchedule) {
|
||||
return s.capacity.maxWeightTons && s.capacity.maxWeightTons > 0
|
||||
? (s.capacity.usedWeightTons / s.capacity.maxWeightTons) * 100
|
||||
@@ -185,6 +186,11 @@ function lengthPctOf(s: BatchBoardSchedule) {
|
||||
? (s.capacity.allocatedLengthMeters / s.capacity.maxLengthMeters) * 100
|
||||
: null;
|
||||
}
|
||||
function wagonPctOf(s: BatchBoardSchedule) {
|
||||
return s.capacity.maxWagons && s.capacity.maxWagons > 0
|
||||
? (s.capacity.allocatedWagons / s.capacity.maxWagons) * 100
|
||||
: null;
|
||||
}
|
||||
|
||||
function ScheduleCard({ schedule }: { schedule: BatchBoardSchedule }) {
|
||||
const navigate = useNavigate();
|
||||
@@ -192,6 +198,7 @@ function ScheduleCard({ schedule }: { schedule: BatchBoardSchedule }) {
|
||||
|
||||
const lengthPct = lengthPctOf(schedule);
|
||||
const weightPct = weightPctOf(schedule);
|
||||
const wagonPct = wagonPctOf(schedule);
|
||||
|
||||
const totalBookings = totalBookingCount(counts);
|
||||
|
||||
@@ -275,7 +282,7 @@ function ScheduleCard({ schedule }: { schedule: BatchBoardSchedule }) {
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{/* capacity: weight + length rings + wagons (numbers preserved) */}
|
||||
{/* capacity: the three axes a train is limited by — gross weight, wagon slots, length */}
|
||||
<Box
|
||||
py="sm"
|
||||
px="xs"
|
||||
@@ -289,26 +296,35 @@ function ScheduleCard({ schedule }: { schedule: BatchBoardSchedule }) {
|
||||
{weightPct != null ? (
|
||||
<CapacityRing
|
||||
pct={weightPct}
|
||||
label="WEIGHT"
|
||||
label="GROSS WT"
|
||||
current={fmtTons(capacity.usedWeightTons)}
|
||||
max={fmtTons(capacity.maxWeightTons ?? 0)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Stack gap={0} align="center" style={{ flex: 1 }}>
|
||||
<ThemeIcon size={34} radius="md" variant="light" color="gray">
|
||||
<Package size={17} />
|
||||
</ThemeIcon>
|
||||
<Text fw={800} size="26px" c="dark.5" lh={1.1} mt={6}>
|
||||
{capacity.allocatedWagons}
|
||||
</Text>
|
||||
<Text size="9px" fw={700} c="gray.6" tt="uppercase" style={{ letterSpacing: 0.5 }}>
|
||||
Wagons
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
allocated
|
||||
</Text>
|
||||
</Stack>
|
||||
{wagonPct != null ? (
|
||||
<CapacityRing
|
||||
pct={wagonPct}
|
||||
label="WAGONS"
|
||||
current={String(capacity.allocatedWagons)}
|
||||
max={String(capacity.maxWagons ?? 0)}
|
||||
/>
|
||||
) : (
|
||||
<Stack gap={0} align="center" style={{ flex: 1 }}>
|
||||
<ThemeIcon size={34} radius="md" variant="light" color="gray">
|
||||
<Package size={17} />
|
||||
</ThemeIcon>
|
||||
<Text fw={800} size="26px" c="dark.5" lh={1.1} mt={6}>
|
||||
{capacity.allocatedWagons}
|
||||
</Text>
|
||||
<Text size="9px" fw={700} c="gray.6" tt="uppercase" style={{ letterSpacing: 0.5 }}>
|
||||
Wagons
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
allocated
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{lengthPct != null ? (
|
||||
<CapacityRing
|
||||
@@ -519,30 +535,42 @@ export default function BatchBoardPage() {
|
||||
id: "capacity",
|
||||
header: "Capacity",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => (
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<CapacityChip icon={Weight} pct={weightPctOf(row.original)} text="wt" />
|
||||
<CapacityChip icon={Ruler} pct={lengthPctOf(row.original)} text="len" />
|
||||
<Group
|
||||
gap={4}
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
padding: "2px 8px",
|
||||
borderRadius: 8,
|
||||
background: "var(--mantine-color-edr-green-0)",
|
||||
border: "1px solid var(--mantine-color-edr-green-1)",
|
||||
}}
|
||||
>
|
||||
<Package size={12} color="var(--mantine-color-edr-green-7)" />
|
||||
<Text size="xs" fw={700} c="edr-green.8" lh={1.2}>
|
||||
{row.original.capacity.allocatedWagons}
|
||||
</Text>
|
||||
<Text size="10px" c="dimmed" lh={1.2}>
|
||||
wgn
|
||||
</Text>
|
||||
cell: ({ row }) => {
|
||||
const { allocatedWagons, maxWagons } = row.original.capacity;
|
||||
const wagonPct = wagonPctOf(row.original);
|
||||
return (
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<CapacityChip icon={Weight} pct={weightPctOf(row.original)} text="gross" />
|
||||
<CapacityChip icon={Ruler} pct={lengthPctOf(row.original)} text="len" />
|
||||
{wagonPct != null ? (
|
||||
<CapacityChip
|
||||
icon={Package}
|
||||
pct={wagonPct}
|
||||
text={`${allocatedWagons}/${maxWagons} wgn`}
|
||||
/>
|
||||
) : (
|
||||
<Group
|
||||
gap={4}
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
padding: "2px 8px",
|
||||
borderRadius: 8,
|
||||
background: "var(--mantine-color-edr-green-0)",
|
||||
border: "1px solid var(--mantine-color-edr-green-1)",
|
||||
}}
|
||||
>
|
||||
<Package size={12} color="var(--mantine-color-edr-green-7)" />
|
||||
<Text size="xs" fw={700} c="edr-green.8" lh={1.2}>
|
||||
{allocatedWagons}
|
||||
</Text>
|
||||
<Text size="10px" c="dimmed" lh={1.2}>
|
||||
wgn
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "bookings",
|
||||
|
||||
@@ -892,7 +892,9 @@ export default function BatchScheduleDetailPage() {
|
||||
items={[
|
||||
{
|
||||
label: "Allocated wagons",
|
||||
value: data.capacity.allocatedWagons,
|
||||
value: data.capacity.maxWagons
|
||||
? `${data.capacity.allocatedWagons} / ${data.capacity.maxWagons}`
|
||||
: data.capacity.allocatedWagons,
|
||||
hint: "on this train",
|
||||
icon: Boxes,
|
||||
},
|
||||
@@ -904,10 +906,11 @@ export default function BatchScheduleDetailPage() {
|
||||
icon: Ruler,
|
||||
},
|
||||
{
|
||||
label: "Weight",
|
||||
label: "Gross weight",
|
||||
value: data.capacity.maxWeightTons
|
||||
? `${fmtTons(data.capacity.usedWeightTons)} / ${fmtTons(data.capacity.maxWeightTons)}`
|
||||
: fmtTons(data.capacity.usedWeightTons),
|
||||
hint: "wagon tare + cargo",
|
||||
icon: Weight,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -27,6 +27,10 @@ export interface Locomotive {
|
||||
currentYard?: { id: string; label?: string; code?: string } | null;
|
||||
maxPullWeightTons: number;
|
||||
maxTrainLengthMeters: number;
|
||||
/** Tons a train may exceed maxPullWeightTons by before scheduling blocks it. */
|
||||
overageToleranceTons?: number | null;
|
||||
/** Metres a train may exceed maxTrainLengthMeters by before scheduling blocks it. */
|
||||
overageToleranceMeters?: number | null;
|
||||
powerKw?: number | null;
|
||||
tractionForceKn?: number | null;
|
||||
maxSpeedKmh?: number | null;
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface WagonType {
|
||||
name: string;
|
||||
capacityTons: number;
|
||||
lengthMeters: number;
|
||||
/** Empty wagon weight; required, since pull limits apply to tare + cargo. */
|
||||
tareWeightTons: number;
|
||||
supportedLoadTypes: string[];
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
@@ -280,10 +280,13 @@ export interface BatchBoardSchedule {
|
||||
capacity: {
|
||||
allocatedWagons: number;
|
||||
allocatedLengthMeters: number;
|
||||
/** Train-length cap: locomotive floored by global rules, plus overage tolerance. */
|
||||
maxLengthMeters: number | null;
|
||||
/** GROSS tons on the train — wagon tare + cargo, since the pull limit hauls both. */
|
||||
usedWeightTons: number;
|
||||
/** Pull-weight cap: locomotive floored by global rules, plus overage tolerance. */
|
||||
maxWeightTons: number | null;
|
||||
/** Wagon-slot cap for the train (locomotive/wagon-type derived). */
|
||||
/** Wagon-slot cap for the train, derived from train length and the shortest wagon type. */
|
||||
maxWagons: number | null;
|
||||
};
|
||||
counts: {
|
||||
|
||||
Reference in New Issue
Block a user