feat: enhance locomotive creation and management with optional code generation and default max pull weight

This commit is contained in:
Marshal
2026-06-28 23:02:40 +00:00
parent d6ae1ac18d
commit 47c91cad03
9 changed files with 66 additions and 80 deletions

View File

@@ -1,15 +1,10 @@
import { useState } from "react";
import { Download, Zap, FileText, Clock } from "lucide-react";
import { Stack, Text, Button } from "@mantine/core";
import { AllocateBookingWizard } from "@/components/trainScheduling/AllocateBookingWizard";
import type { BookingDetail } from "@/types/booking";
import { BookingActionsMenu } from "./BookingActionsMenu";
import { SectionCard } from "./detail/SectionCard";
import { toBookingListRow } from "@/features/bookings/mapBookingListRow";
import { canAllocateBooking } from "@/features/bookings/booking-actions.config";
import { useAuth } from "@/auth/useAuth";
import { canManageScheduling } from "@/lib/permissions";
import type { useBookingMutations } from "@/hooks/bookings/useBookings";
type Mutations = ReturnType<typeof useBookingMutations>;
@@ -21,11 +16,8 @@ interface BookingActionsToolbarProps {
/** Detail-page actions: primary toolbar + downloads. */
export function BookingActionsToolbar({ booking, mutations }: BookingActionsToolbarProps) {
const { user } = useAuth();
const row = toBookingListRow(booking);
const { status } = booking;
const [allocateOpen, setAllocateOpen] = useState(false);
const canAllocate = canManageScheduling(user);
const downloadBlob = async (fn: () => Promise<Blob>, filename: string) => {
const blob = await fn();
@@ -106,11 +98,7 @@ export function BookingActionsToolbar({ booking, mutations }: BookingActionsTool
<Text size="xs" c="dimmed">
Confirm each step before it is applied.
</Text>
<BookingActionsMenu
row={row}
variant="toolbar"
onAllocateBooking={() => setAllocateOpen(true)}
/>
<BookingActionsMenu row={row} variant="toolbar" />
</Stack>
</SectionCard>
@@ -130,14 +118,6 @@ export function BookingActionsToolbar({ booking, mutations }: BookingActionsTool
</Button>
</SectionCard>
)}
{canAllocate && canAllocateBooking(booking) ? (
<AllocateBookingWizard
booking={booking}
opened={allocateOpen}
onClose={() => setAllocateOpen(false)}
/>
) : null}
</Stack>
);
}

View File

@@ -66,12 +66,20 @@ const FleetFormDialog = ({
const [values, setValues] = useState<Record<string, unknown>>({});
const [errors, setErrors] = useState<Record<string, string>>({});
// Seed the form ONLY when the dialog opens or the edited record changes — NOT
// when `fields`/`emptyValues` get new object refs (they're rebuilt whenever the
// dynamic select options finish loading). Re-seeding on those would wipe the
// user's in-progress edits (e.g. a changed Current Yard / status) the moment
// the yard or wagon-type options resolve.
const recordId =
initialRecord && "id" in initialRecord ? String(initialRecord.id) : null;
useEffect(() => {
if (open) {
setValues(buildInitialValues(fields, emptyValues, initialRecord));
setErrors({});
}
}, [open, fields, emptyValues, initialRecord]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, recordId]);
const shortFields = useMemo(
() => fields.filter((f) => f.type !== "textarea"),

View File

@@ -7,8 +7,6 @@ import {
MessageSquareWarning,
Play,
ShieldCheck,
TrainTrack,
Truck,
XCircle,
} from "lucide-react";
@@ -388,47 +386,10 @@ export function getBookingActions(
actions = withCancel(OPERATION_REVIEW_ACTIONS);
break;
case "PAID":
if (
canAllocateBooking({ status, schedulingStatus: ctx.schedulingStatus })
) {
actions = [
{
id: "allocateBooking",
label: "Allocate booking",
shortLabel: "Allocate",
description: "Assign to train, wagons, and finalize schedule",
confirmTitle: "Allocate booking?",
confirmDescription: "Opens the train allocation wizard.",
variant: "default",
icon: TrainTrack,
primary: true,
},
{
id: "startTransit",
label: "Start transit",
shortLabel: "Transit",
description: "Begin rail movement",
confirmTitle: "Start transit?",
confirmDescription: "The booking will move to in transit status.",
variant: "default",
icon: Truck,
},
];
} else {
actions = [
{
id: "startTransit",
label: "Start transit",
shortLabel: "Transit",
description: "Begin rail movement",
confirmTitle: "Start transit?",
confirmDescription: "The booking will move to in transit status.",
variant: "default",
icon: Truck,
primary: true,
},
];
}
// Allocate is handled by the Operations "Ready to allocate" queue, not the
// per-booking action menu. Start transit was removed entirely. No per-row
// action remains in the PAID state.
actions = [];
break;
case "IN_TRANSIT":
actions = [

View File

@@ -4,6 +4,7 @@ import {
FileSignature,
Layers,
LayoutGrid,
Milestone,
Package,
ShieldCheck,
} from "lucide-react";
@@ -288,6 +289,16 @@ export default function BookingRequestDetailPage() {
booking={booking}
mutations={mutations}
/>
<Button
fullWidth
variant="default"
leftSection={<Milestone size={16} />}
onClick={() =>
navigate(`/dashboard/bookings/${booking.id}/milestones`)
}
>
View clearance milestones
</Button>
{showContractButton && (
<Button
fullWidth

View File

@@ -246,7 +246,6 @@ const FleetResourcePage = () => {
meta: { headerClassName, cellClassName },
cell: ({ row }) => {
const value = (row.original as unknown as Record<string, unknown>)[col.accessorKey];
console.log(`${col.accessorKey}:`, value, 'format:', col.format);
return formatFleetCell(value, col.format, col.accessorKey);
},
}));

View File

@@ -147,8 +147,8 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
{ id: "maxPullWeightTons", header: "Max pull (tons)", accessorKey: "maxPullWeightTons", format: "number" },
{ id: "maxTrainLengthMeters", header: "Max length (m)", accessorKey: "maxTrainLengthMeters", format: "number" },
],
// Code is auto-generated server-side (LOCO-NNN) — omitted from the form.
formFields: [
{ name: "code", label: "Code", type: "text", required: true },
{ name: "name", label: "Name", type: "text" },
{ name: "locomotiveType", label: "Locomotive type", type: "select", required: true, options: LOCOMOTIVE_TYPE_OPTIONS },
{ name: "status", label: "Status", type: "select", required: true, options: LOCOMOTIVE_STATUS_OPTIONS },
@@ -160,12 +160,11 @@ export const FLEET_RESOURCES: FleetResourceConfig[] = [
{ name: "maxSpeedKmh", label: "Max speed (km/h)", type: "number" },
],
emptyValues: {
code: "",
name: "",
locomotiveType: "DIESEL",
status: "AVAILABLE",
currentYardId: "",
maxPullWeightTons: 0,
maxPullWeightTons: 2500,
maxTrainLengthMeters: 760,
powerKw: "",
tractionForceKn: "",