Merge pull request #452 from Tria-plc/freight_feature/usermanagement

Freight feature/usermanagement
This commit is contained in:
marshal
2026-07-04 07:25:38 +03:00
committed by GitHub
4 changed files with 218 additions and 86 deletions

View File

@@ -7,6 +7,7 @@ import {
import { useMutation, useQuery } from "@tanstack/react-query";
import {
Alert,
Badge,
Box,
Button,
Center,
@@ -18,6 +19,7 @@ import {
Paper,
Select,
Stack,
Switch,
Text,
Textarea,
TextInput,
@@ -82,12 +84,13 @@ interface UnitDraft {
containerNumber: string;
sealNumber: string;
vgmTons: number | string;
/** Per-unit flags — the line's hazardous/reefer counts are derived from these. */
hazardous: boolean;
reefer: boolean;
}
interface ContainerLineDraft {
containerSize: string;
hazardousQuantity: number | string;
reeferQuantity: number | string;
units: UnitDraft[];
}
@@ -100,7 +103,13 @@ interface BulkLineDraft {
}
function emptyUnit(): UnitDraft {
return { containerNumber: "", sealNumber: "", vgmTons: "" };
return {
containerNumber: "",
sealNumber: "",
vgmTons: "",
hazardous: false,
reefer: false,
};
}
function bulkUnitOfMeasure(
@@ -199,11 +208,13 @@ export default function GlCreateBookingForm() {
setContainerLines(
lines.containers.map((c) => ({
containerSize: c.containerSize,
hazardousQuantity: c.hazardousQuantity ?? "0",
reeferQuantity: c.reeferQuantity ?? "",
units: Array.from({ length: Math.max(1, c.quantity) }, () =>
emptyUnit(),
),
// The request carries counts; pre-toggle the first N units so GL sees
// the customer's declared hazardous/reefer split and can adjust it.
units: Array.from({ length: Math.max(1, c.quantity) }, (_, i) => ({
...emptyUnit(),
hazardous: i < Number(c.hazardousQuantity ?? 0),
reefer: i < Number(c.reeferQuantity ?? 0),
})),
})),
);
} else if (lines.bulk) {
@@ -229,8 +240,6 @@ export default function GlCreateBookingForm() {
setContainerLines(
containerSizes.map((size) => ({
containerSize: size,
hazardousQuantity: "0",
reeferQuantity: "0",
units: [emptyUnit()],
})),
);
@@ -261,8 +270,8 @@ export default function GlCreateBookingForm() {
containers: containerLines.map((l) => ({
containerSize: l.containerSize,
quantity: l.units.length,
hazardousQuantity: Number(l.hazardousQuantity || 0),
reeferQuantity: Number(l.reeferQuantity || 0),
hazardousQuantity: l.units.filter((u) => u.hazardous).length,
reeferQuantity: l.units.filter((u) => u.reefer).length,
})),
bulkQuantity: bulkLines.reduce(
(s, l) => s + Number(l.cargoWeightTons || l.itemCount || 0),
@@ -384,12 +393,10 @@ export default function GlCreateBookingForm() {
.map((l) => ({
containerSize: l.containerSize,
quantity: l.units.length,
...(l.hazardousQuantity !== ""
? { hazardousQuantity: Number(l.hazardousQuantity) }
: {}),
...(l.reeferQuantity !== ""
? { reeferQuantity: Number(l.reeferQuantity) }
: {}),
// Counts are derived from the per-unit toggles — they can never
// exceed the line quantity.
hazardousQuantity: l.units.filter((u) => u.hazardous).length,
reeferQuantity: l.units.filter((u) => u.reefer).length,
units: l.units.map((u) => ({
containerNumber: u.containerNumber,
...(u.sealNumber ? { sealNumber: u.sealNumber } : {}),
@@ -652,7 +659,7 @@ export default function GlCreateBookingForm() {
<Text fz={14} fw={700} mb={10}>
{line.containerSize} containers
</Text>
<Group gap={12} grow mb={12} align="flex-start">
<Group gap={12} mb={12} align="flex-start">
<NumberInput
label="Quantity *"
min={1}
@@ -660,37 +667,24 @@ export default function GlCreateBookingForm() {
onChange={(v) => syncUnits(lineIdx, Number(v) || 0)}
radius={10}
styles={fieldStyles}
w={160}
/>
{contract.isHazardous ? (
<NumberInput
label="Hazardous qty"
min={0}
value={line.hazardousQuantity}
onChange={(v) =>
patchLine(lineIdx, { hazardousQuantity: v })
}
radius={10}
styles={fieldStyles}
/>
<Badge variant="light" color="red" radius="sm" mt={30}>
{line.units.filter((u) => u.hazardous).length} hazardous
</Badge>
) : null}
{contract.isReefer ? (
<NumberInput
label="Reefer qty"
min={0}
value={line.reeferQuantity}
onChange={(v) =>
patchLine(lineIdx, { reeferQuantity: v })
}
radius={10}
styles={fieldStyles}
/>
<Badge variant="light" color="blue" radius="sm" mt={30}>
{line.units.filter((u) => u.reefer).length} refrigerated
</Badge>
) : null}
</Group>
<StepLabel>Per-container details</StepLabel>
<Stack gap={10} mt={8}>
{line.units.map((unit, unitIdx) => (
<Group key={unitIdx} gap={10} grow align="flex-start">
<Group key={unitIdx} gap={10} align="flex-start" wrap="nowrap">
<TextInput
label={unitIdx === 0 ? "Container number *" : undefined}
placeholder="e.g. MSCU1234567"
@@ -702,6 +696,7 @@ export default function GlCreateBookingForm() {
}
radius={10}
styles={fieldStyles}
style={{ flex: 1 }}
/>
<TextInput
label={unitIdx === 0 ? "Seal number" : undefined}
@@ -714,6 +709,7 @@ export default function GlCreateBookingForm() {
}
radius={10}
styles={fieldStyles}
style={{ flex: 1 }}
/>
<NumberInput
label={unitIdx === 0 ? "VGM (tons) *" : undefined}
@@ -726,7 +722,52 @@ export default function GlCreateBookingForm() {
}
radius={10}
styles={fieldStyles}
style={{ flex: 1 }}
/>
{/* Per-unit flags: toggle exactly the containers that are
hazardous / refrigerated; line counts derive from these. */}
{contract.isHazardous ? (
<Stack gap={6} align="center" style={{ flexShrink: 0 }}>
{unitIdx === 0 ? (
<Text fz={12} fw={600} c="#4A5A68">
Hazardous
</Text>
) : null}
<Switch
color="red"
size="sm"
mt={unitIdx === 0 ? 0 : 8}
aria-label={`Container ${unitIdx + 1} hazardous`}
checked={unit.hazardous}
onChange={(e) =>
patchUnit(lineIdx, unitIdx, {
hazardous: e.currentTarget.checked,
})
}
/>
</Stack>
) : null}
{contract.isReefer ? (
<Stack gap={6} align="center" style={{ flexShrink: 0 }}>
{unitIdx === 0 ? (
<Text fz={12} fw={600} c="#4A5A68">
Reefer
</Text>
) : null}
<Switch
color="blue"
size="sm"
mt={unitIdx === 0 ? 0 : 8}
aria-label={`Container ${unitIdx + 1} refrigerated`}
checked={unit.reefer}
onChange={(e) =>
patchUnit(lineIdx, unitIdx, {
reefer: e.currentTarget.checked,
})
}
/>
</Stack>
) : null}
</Group>
))}
</Stack>