mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Add ScheduleWorkspacePanel and integrate it into TrainScheduleV2DetailPage with tabs
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Modal,
|
||||
Paper,
|
||||
Progress,
|
||||
ScrollArea,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeftRight,
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
Inbox,
|
||||
PackageCheck,
|
||||
Repeat,
|
||||
Train,
|
||||
Weight,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
|
||||
import { api } from "@/services/api";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import type {
|
||||
EligibleContainerBooking,
|
||||
FreightType,
|
||||
TrainScheduleDetail,
|
||||
} from "@/types/trainScheduling";
|
||||
|
||||
interface ScheduleWorkspacePanelProps {
|
||||
schedule: TrainScheduleDetail;
|
||||
/** Refetch the schedule detail after a mutation so both panels refresh. */
|
||||
onChanged: () => void;
|
||||
}
|
||||
|
||||
const GREEN = "var(--mantine-color-edr-green-6)";
|
||||
|
||||
/** Cargo weight already allocated to this train (sum of on-train bookings). */
|
||||
function usedWeight(schedule: TrainScheduleDetail): number {
|
||||
return (schedule.bookings ?? []).reduce(
|
||||
(sum, b) => sum + (Number(b.weightTons) || 0),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
/** Max pull weight across all locomotives on the set (0 when unknown). */
|
||||
function pullCapacity(schedule: TrainScheduleDetail): number {
|
||||
const set = schedule.trainSet;
|
||||
if (!set) return 0;
|
||||
const locos =
|
||||
set.locomotives && set.locomotives.length > 0
|
||||
? set.locomotives
|
||||
: set.locomotive
|
||||
? [set.locomotive]
|
||||
: [];
|
||||
return locos.reduce((sum, l) => sum + (Number(l.maxPullWeightTons) || 0), 0);
|
||||
}
|
||||
|
||||
export function ScheduleWorkspacePanel({
|
||||
schedule,
|
||||
onChanged,
|
||||
}: ScheduleWorkspacePanelProps) {
|
||||
const { toast } = useToast();
|
||||
|
||||
const freightType: FreightType | undefined =
|
||||
schedule.freightType === "CONTAINER" || schedule.freightType === "BULK"
|
||||
? schedule.freightType
|
||||
: undefined;
|
||||
|
||||
const locked = ["DISPATCHED", "ARRIVED"].includes(schedule.status);
|
||||
const canManage = ["DRAFT", "SCHEDULED"].includes(schedule.status);
|
||||
|
||||
// Pool = accepted, ready-to-pay bookings on THIS train's route+day that are not
|
||||
// yet linked to any schedule (same filter the auto-batch uses).
|
||||
const poolQuery = useQuery(
|
||||
api.trainScheduling.eligibleBookings.queryOptions({
|
||||
input: {
|
||||
filters: {
|
||||
originStationId: schedule.originStation?.id,
|
||||
destinationStationId: schedule.destinationStation?.id,
|
||||
trainScheduleId: schedule.id,
|
||||
},
|
||||
freightType,
|
||||
},
|
||||
enabled: Boolean(schedule.originStation?.id && schedule.destinationStation?.id),
|
||||
}),
|
||||
);
|
||||
|
||||
const onTrainIds = useMemo(
|
||||
() => new Set((schedule.bookings ?? []).map((b) => b.id)),
|
||||
[schedule.bookings],
|
||||
);
|
||||
|
||||
const pool: EligibleContainerBooking[] = useMemo(
|
||||
() => (poolQuery.data?.items ?? []).filter((b) => !onTrainIds.has(b.id)),
|
||||
[poolQuery.data, onTrainIds],
|
||||
);
|
||||
|
||||
const onTrain = schedule.bookings ?? [];
|
||||
|
||||
// ── Mutations (reuse the existing endpoints) ───────────────────────────────
|
||||
const assign = useMutation(api.trainScheduling.assignBookings.mutationOptions());
|
||||
const unassign = useMutation(api.trainScheduling.unassignBooking.mutationOptions());
|
||||
const moveSchedule = useMutation(
|
||||
api.trainScheduling.moveBookingSchedule.mutationOptions(),
|
||||
);
|
||||
|
||||
const [moveBookingId, setMoveBookingId] = useState<string | null>(null);
|
||||
const [moveTarget, setMoveTarget] = useState<string | null>(null);
|
||||
|
||||
const { data: targets } = useQuery(
|
||||
api.trainScheduling.bookableSchedules.queryOptions({
|
||||
input: {
|
||||
originYardId: schedule.originStation?.id,
|
||||
destinationYardId: schedule.destinationStation?.id,
|
||||
},
|
||||
enabled: Boolean(
|
||||
schedule.originStation?.id && schedule.destinationStation?.id,
|
||||
),
|
||||
}),
|
||||
);
|
||||
const moveOptions = useMemo(
|
||||
() =>
|
||||
(targets ?? [])
|
||||
.filter((s) => s.id !== schedule.id)
|
||||
.map((s) => ({
|
||||
value: s.id,
|
||||
label: `${s.routeName ?? `${s.origin} → ${s.destination}`} · ${new Date(
|
||||
s.scheduleDate,
|
||||
).toLocaleString()} · ${s.remainingWagons}/${s.maxWagons} free`,
|
||||
})),
|
||||
[targets, schedule.id],
|
||||
);
|
||||
|
||||
// ── Capacity meter (by cargo weight vs locomotive pull) ────────────────────
|
||||
const used = usedWeight(schedule);
|
||||
const capacity = pullCapacity(schedule);
|
||||
const pct = capacity > 0 ? Math.min(100, Math.round((used / capacity) * 100)) : 0;
|
||||
const over = capacity > 0 && used > capacity;
|
||||
|
||||
const forceAdd = (bookingId: string, ref: string, weightTons: number) => {
|
||||
const wouldOverfill = capacity > 0 && used + (weightTons || 0) > capacity;
|
||||
assign
|
||||
.mutateAsync({
|
||||
id: schedule.id,
|
||||
freightType,
|
||||
payload: {
|
||||
bookingIds: [...onTrainIds, bookingId],
|
||||
forceAssign: true,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
toast({
|
||||
title: `${ref} added to train`,
|
||||
description: wouldOverfill
|
||||
? "Force-added past the pull-weight limit — review capacity."
|
||||
: "Wagons auto-pinned.",
|
||||
variant: wouldOverfill ? "destructive" : undefined,
|
||||
});
|
||||
onChanged();
|
||||
void poolQuery.refetch();
|
||||
})
|
||||
.catch(() =>
|
||||
toast({ title: "Could not add booking", variant: "destructive" }),
|
||||
);
|
||||
};
|
||||
|
||||
const removeFromTrain = (bookingId: string, ref: string) => {
|
||||
unassign
|
||||
.mutateAsync({ id: schedule.id, bookingId })
|
||||
.then(() => {
|
||||
toast({ title: `${ref} removed from train` });
|
||||
onChanged();
|
||||
void poolQuery.refetch();
|
||||
})
|
||||
.catch(() =>
|
||||
toast({ title: "Could not remove booking", variant: "destructive" }),
|
||||
);
|
||||
};
|
||||
|
||||
const doMove = () => {
|
||||
if (!moveBookingId || !moveTarget) return;
|
||||
moveSchedule
|
||||
.mutateAsync({ bookingId: moveBookingId, trainScheduleId: moveTarget })
|
||||
.then(() => {
|
||||
toast({ title: "Booking reassigned to another train" });
|
||||
setMoveBookingId(null);
|
||||
onChanged();
|
||||
void poolQuery.refetch();
|
||||
})
|
||||
.catch(() =>
|
||||
toast({ title: "Could not reassign booking", variant: "destructive" }),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper radius="xl" p="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
||||
<Stack gap="lg">
|
||||
{/* Header + capacity meter */}
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
||||
<Group gap="sm" align="center" wrap="nowrap">
|
||||
<ThemeIcon size={40} radius="md" variant="light" color="edr-green">
|
||||
<PackageCheck size={20} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700}>Allocation workspace</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Manually add ready-to-pay bookings, remove, or reassign them
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<Box miw={240} style={{ flex: "0 1 320px" }}>
|
||||
<Group justify="space-between" mb={4} gap={4}>
|
||||
<Group gap={6} align="center">
|
||||
<Weight size={14} color={over ? "#B42318" : undefined} />
|
||||
<Text size="xs" fw={600} c={over ? "red" : "dimmed"}>
|
||||
Load {used.toFixed(1)}T
|
||||
{capacity > 0 ? ` / ${capacity.toFixed(0)}T pull` : ""}
|
||||
</Text>
|
||||
</Group>
|
||||
{over ? (
|
||||
<Badge color="red" variant="light" size="sm" radius="sm">
|
||||
Over capacity
|
||||
</Badge>
|
||||
) : (
|
||||
<Text size="xs" c="dimmed">
|
||||
{capacity > 0 ? `${pct}%` : "—"}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
<Progress
|
||||
value={capacity > 0 ? pct : 0}
|
||||
color={over ? "red" : pct > 85 ? "orange" : "edr-green"}
|
||||
radius="xl"
|
||||
size="md"
|
||||
/>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
{over ? (
|
||||
<Group
|
||||
gap={8}
|
||||
p="xs"
|
||||
wrap="nowrap"
|
||||
align="center"
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
background: "var(--mantine-color-red-0)",
|
||||
border: "1px solid var(--mantine-color-red-2)",
|
||||
}}
|
||||
>
|
||||
<AlertTriangle size={16} color="#B42318" />
|
||||
<Text size="xs" c="red.8" fw={500}>
|
||||
This train is loaded beyond its locomotive pull weight. Force-adds are
|
||||
allowed, but review before dispatch.
|
||||
</Text>
|
||||
</Group>
|
||||
) : null}
|
||||
|
||||
{locked ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
This train is {schedule.status.toLowerCase()} — bookings can no longer be
|
||||
changed.
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{/* Two-panel board */}
|
||||
<Group align="stretch" gap="lg" grow wrap="wrap">
|
||||
{/* Pool */}
|
||||
<PanelColumn
|
||||
title="Ready to pay"
|
||||
hint="Accepted · this route & day"
|
||||
count={pool.length}
|
||||
accent="#F2A516"
|
||||
loading={poolQuery.isLoading}
|
||||
emptyIcon={Inbox}
|
||||
emptyText="No ready-to-pay bookings waiting for this train."
|
||||
>
|
||||
{pool.map((b) => (
|
||||
<BookingCard
|
||||
key={b.id}
|
||||
reference={b.reference}
|
||||
customer={b.customer}
|
||||
weightTons={b.weightTons}
|
||||
status={b.status}
|
||||
right={
|
||||
canManage ? (
|
||||
<Tooltip label="Force-add to this train" withArrow>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
rightSection={<ArrowRight size={14} />}
|
||||
loading={assign.isPending}
|
||||
onClick={() => forceAdd(b.id, b.reference, b.weightTons)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</PanelColumn>
|
||||
|
||||
{/* On train */}
|
||||
<PanelColumn
|
||||
title="On this train"
|
||||
hint="Allocated bookings"
|
||||
count={onTrain.length}
|
||||
accent="#0EA371"
|
||||
emptyIcon={Train}
|
||||
emptyText="No bookings allocated yet. Add one from the pool."
|
||||
>
|
||||
{onTrain.map((b) => (
|
||||
<BookingCard
|
||||
key={b.id}
|
||||
reference={b.reference ?? b.id.slice(0, 8)}
|
||||
customer={b.customer}
|
||||
weightTons={b.weightTons}
|
||||
status={b.status}
|
||||
right={
|
||||
canManage ? (
|
||||
<Group gap={6} wrap="nowrap" justify="flex-end">
|
||||
<Tooltip label="Reassign to another train" withArrow>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
variant="subtle"
|
||||
color="orange"
|
||||
radius="md"
|
||||
leftSection={<Repeat size={13} />}
|
||||
onClick={() => {
|
||||
setMoveBookingId(b.id);
|
||||
setMoveTarget(null);
|
||||
}}
|
||||
>
|
||||
Move
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label="Remove from this train" withArrow>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
variant="light"
|
||||
color="red"
|
||||
radius="md"
|
||||
leftSection={<X size={13} />}
|
||||
loading={unassign.isPending}
|
||||
onClick={() =>
|
||||
removeFromTrain(b.id, b.reference ?? b.id.slice(0, 8))
|
||||
}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</PanelColumn>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
{/* Reassign modal */}
|
||||
<Modal
|
||||
opened={Boolean(moveBookingId)}
|
||||
onClose={() => setMoveBookingId(null)}
|
||||
title={
|
||||
<Group gap={8}>
|
||||
<ArrowLeftRight size={18} />
|
||||
<Text fw={700}>Reassign booking to another train</Text>
|
||||
</Group>
|
||||
}
|
||||
centered
|
||||
radius="lg"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Target train (same route, open window)"
|
||||
placeholder="Select an open schedule"
|
||||
data={moveOptions}
|
||||
value={moveTarget}
|
||||
onChange={setMoveTarget}
|
||||
searchable
|
||||
nothingFoundMessage="No other open schedules on this route"
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={() => setMoveBookingId(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="edr-green"
|
||||
disabled={!moveTarget}
|
||||
loading={moveSchedule.isPending}
|
||||
leftSection={<CheckCircle2 size={16} />}
|
||||
onClick={doMove}
|
||||
>
|
||||
Reassign
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Sub-components ───────────────────────────────────────────────────────────
|
||||
|
||||
function PanelColumn({
|
||||
title,
|
||||
hint,
|
||||
count,
|
||||
accent,
|
||||
loading,
|
||||
emptyIcon: EmptyIcon,
|
||||
emptyText,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
hint: string;
|
||||
count: number;
|
||||
accent: string;
|
||||
loading?: boolean;
|
||||
emptyIcon: typeof Inbox;
|
||||
emptyText: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const isEmpty = !loading && count === 0;
|
||||
return (
|
||||
<Paper
|
||||
radius="lg"
|
||||
withBorder
|
||||
p="md"
|
||||
miw={280}
|
||||
style={{
|
||||
flex: 1,
|
||||
borderColor: "var(--mantine-color-gray-2)",
|
||||
background: `linear-gradient(180deg, ${accent}0A 0%, transparent 90px)`,
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" align="center" mb="sm">
|
||||
<Group gap={8} align="center">
|
||||
<Box w={8} h={8} style={{ borderRadius: 999, background: accent }} />
|
||||
<Text fw={700} size="sm">
|
||||
{title}
|
||||
</Text>
|
||||
<Badge variant="light" color="gray" radius="sm" size="sm">
|
||||
{count}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
{hint}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{isEmpty ? (
|
||||
<Stack align="center" gap={6} py={32}>
|
||||
<EmptyIcon size={24} color="var(--mantine-color-gray-4)" />
|
||||
<Text size="xs" c="dimmed" ta="center" maw={220}>
|
||||
{emptyText}
|
||||
</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<ScrollArea.Autosize mah={420} type="hover">
|
||||
<Stack gap={8} pr={4}>
|
||||
{loading ? (
|
||||
<Text size="xs" c="dimmed" py="md" ta="center">
|
||||
Loading…
|
||||
</Text>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Stack>
|
||||
</ScrollArea.Autosize>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function BookingCard({
|
||||
reference,
|
||||
customer,
|
||||
weightTons,
|
||||
status,
|
||||
right,
|
||||
}: {
|
||||
reference: string;
|
||||
customer?: string | null;
|
||||
weightTons?: number | null;
|
||||
status?: string | null;
|
||||
right?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Paper
|
||||
radius="md"
|
||||
withBorder
|
||||
p="sm"
|
||||
style={{
|
||||
borderColor: "var(--mantine-color-gray-2)",
|
||||
transition: "border-color 120ms ease, box-shadow 120ms ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = GREEN;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = "var(--mantine-color-gray-2)";
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" align="center" wrap="nowrap" gap="sm">
|
||||
<Stack gap={3} style={{ minWidth: 0 }}>
|
||||
<Group gap={8} align="center" wrap="nowrap">
|
||||
<Text size="sm" fw={700} truncate>
|
||||
{reference}
|
||||
</Text>
|
||||
{status ? <BookingStatusBadge status={status} /> : null}
|
||||
</Group>
|
||||
<Group gap={10} align="center" wrap="nowrap">
|
||||
<Text size="xs" c="dimmed" truncate>
|
||||
{customer ?? "—"}
|
||||
</Text>
|
||||
{weightTons != null ? (
|
||||
<Group gap={3} align="center" wrap="nowrap">
|
||||
<Weight size={11} color="var(--mantine-color-gray-5)" />
|
||||
<Text size="xs" c="dimmed">
|
||||
{Number(weightTons).toFixed(1)}T
|
||||
</Text>
|
||||
</Group>
|
||||
) : null}
|
||||
</Group>
|
||||
</Stack>
|
||||
{right ? <Box style={{ flexShrink: 0 }}>{right}</Box> : null}
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Paper,
|
||||
RingProgress,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
@@ -25,10 +26,12 @@ import {
|
||||
LayoutGrid,
|
||||
Navigation,
|
||||
Package,
|
||||
PackageCheck,
|
||||
Route as RouteIcon,
|
||||
Send,
|
||||
Train,
|
||||
Weight,
|
||||
Workflow as WorkflowIcon,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
@@ -45,6 +48,7 @@ import { FleetAvailabilitySummary } from "@/components/trainScheduling/FleetAvai
|
||||
import { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTrainDialog";
|
||||
import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel";
|
||||
import { ScheduleBookingsStep } from "@/components/trainScheduling/ScheduleBookingsStep";
|
||||
import { ScheduleWorkspacePanel } from "@/components/trainScheduling/ScheduleWorkspacePanel";
|
||||
import { FreightTypeBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
|
||||
import {
|
||||
RouteCorridor,
|
||||
@@ -1028,6 +1032,18 @@ export default function TrainScheduleV2DetailPage() {
|
||||
</Paper>
|
||||
) : null}
|
||||
|
||||
<Tabs defaultValue="workflow" radius="md" color="edr-green" keepMounted={false}>
|
||||
<Tabs.List mb="md">
|
||||
<Tabs.Tab value="workflow" leftSection={<WorkflowIcon size={16} />}>
|
||||
Workflow
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="workspace" leftSection={<PackageCheck size={16} />}>
|
||||
Workspace
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="workflow">
|
||||
<Stack gap="lg">
|
||||
<Paper radius="xl" p="lg">
|
||||
<Stack gap="lg">
|
||||
{/* Workflow header with ring progress */}
|
||||
@@ -1085,7 +1101,20 @@ export default function TrainScheduleV2DetailPage() {
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<ScheduleBatchPanel schedule={schedule} />
|
||||
<ScheduleBatchPanel schedule={schedule} />
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="workspace">
|
||||
<ScheduleWorkspacePanel
|
||||
schedule={schedule}
|
||||
onChanged={() => {
|
||||
autoPreviewedRef.current = false;
|
||||
void detailQuery.refetch();
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
{scheduleId ? (
|
||||
<RescheduleTrainDialog
|
||||
|
||||
@@ -89,7 +89,6 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, maxTrainLengthMeters: value }))
|
||||
}
|
||||
min={1}
|
||||
clampBehavior="strict"
|
||||
disabled={loading}
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -100,7 +99,6 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, maxTrainWeightTons: value }))
|
||||
}
|
||||
min={1}
|
||||
clampBehavior="strict"
|
||||
disabled={loading}
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -110,7 +108,6 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, maxWagonsPerTrain: value }))
|
||||
}
|
||||
min={1}
|
||||
clampBehavior="strict"
|
||||
disabled={loading}
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -124,7 +121,6 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
}))
|
||||
}
|
||||
min={0.001}
|
||||
clampBehavior="strict"
|
||||
disabled={loading}
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -138,7 +134,6 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
}))
|
||||
}
|
||||
min={0}
|
||||
clampBehavior="strict"
|
||||
disabled={loading}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -158,7 +153,6 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, importWindowLeadDays: value }))
|
||||
}
|
||||
min={0}
|
||||
clampBehavior="strict"
|
||||
disabled={loading}
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -169,7 +163,6 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, exportBookingLeadHours: value }))
|
||||
}
|
||||
min={1}
|
||||
clampBehavior="strict"
|
||||
disabled={loading}
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -181,7 +174,6 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
}
|
||||
min={0}
|
||||
max={23}
|
||||
clampBehavior="strict"
|
||||
disabled={loading}
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -193,7 +185,6 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
min={0.25}
|
||||
max={12}
|
||||
step={0.25}
|
||||
clampBehavior="strict"
|
||||
disabled={loading}
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -204,7 +195,6 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, docReviewMinutes: value }))
|
||||
}
|
||||
min={0}
|
||||
clampBehavior="strict"
|
||||
disabled={loading}
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -215,7 +205,6 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, paymentWindowMinutes: value }))
|
||||
}
|
||||
min={1}
|
||||
clampBehavior="strict"
|
||||
disabled={loading}
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -226,7 +215,6 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
setForm((current) => ({ ...current, reopenDelayMinutes: value }))
|
||||
}
|
||||
min={1}
|
||||
clampBehavior="strict"
|
||||
disabled={loading}
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
|
||||
Reference in New Issue
Block a user