fix issue

This commit is contained in:
Marshal
2026-07-31 13:05:04 +00:00
parent 950d539e37
commit 66062ac1a2
4 changed files with 142 additions and 328 deletions

View File

@@ -4,7 +4,6 @@ import {
Box,
Button,
Card,
Divider,
Grid,
Group,
Loader,
@@ -15,17 +14,20 @@ import {
Slider,
Stack,
Text,
Textarea,
ThemeIcon,
} from "@mantine/core";
import { useMutation, useQuery } from "@tanstack/react-query";
import { ArrowRight, ArrowRightLeft, CheckCircle2, CircleSlash, Layers, Warehouse } from "lucide-react";
import { ArrowRight, ArrowRightLeft, Layers, Warehouse } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import { api } from "@/services/api";
import { useToast } from "@/hooks/use-toast";
import type { Wagon } from "@/services/wagon.service";
const stripHtml = (html: string) => html.replace(/<[^>]*>/g, "").trim();
export interface WagonYardWorkspaceModalProps {
opened: boolean;
onClose: () => void;
@@ -139,13 +141,10 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
const [transferYardId, setTransferYardId] = useState<string | null>(null);
const [transferQty, setTransferQty] = useState(0);
const [transferReason, setTransferReason] = useState("");
const [toAssignedQty, setToAssignedQty] = useState(0);
const [toAvailableQty, setToAvailableQty] = useState(0);
const createRequest = useMutation(
api.wagonTransferRequests.create.mutationOptions(),
);
const setStatus = useMutation(api.wagons.bulkSetStatus.mutationOptions());
const yardName = useMemo(() => {
const byId = new Map(yards.map((y) => [y.id, y.label || y.code || y.id]));
@@ -242,8 +241,6 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
setTransferYardId(null);
setTransferQty(0);
setTransferReason("");
setToAssignedQty(0);
setToAvailableQty(0);
}, [yardId, typeId]);
// Reset the whole workspace when closed.
@@ -254,13 +251,6 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
}
}, [opened]);
// Keep quantities within bounds as counts shift after each action. A TRANSFER
// REQUEST is deliberately uncapped: OCC delivers in instalments, so asking for
// 50 where 20 sit today is normal — only the status flips below are bounded by
// what is physically in the yard.
useEffect(() => setToAssignedQty((q) => Math.min(q, availableCount)), [availableCount]);
useEffect(() => setToAvailableQty((q) => Math.min(q, assignedCount)), [assignedCount]);
const showError = (err: unknown, fallback: string) => {
const message =
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ?? fallback;
@@ -275,7 +265,7 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
!typeId ||
!transferYardId ||
transferQty < 1 ||
!transferReason.trim()
!stripHtml(transferReason)
)
return;
try {
@@ -284,7 +274,7 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
toYardId: transferYardId,
wagonTypeId: typeId,
quantity: transferQty,
reason: transferReason.trim(),
reason: transferReason,
});
toast({
title: `Requested ${transferQty} ${typeInfo.code(typeId)} wagon(s) · ${yardName(
@@ -300,26 +290,7 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
}
};
const handleFlip = async (
pool: Wagon[],
qty: number,
status: Freight.WagonStatus,
label: string,
reset: () => void,
) => {
if (qty < 1) return;
const ids = pool.slice(0, qty).map((w) => w.id);
if (!ids.length) return;
try {
const res = await setStatus.mutateAsync({ wagonIds: ids, status });
toast({ title: `${res.updated} wagon(s) set to ${label}` });
reset();
} catch (err) {
showError(err, "Status update failed");
}
};
const busy = createRequest.isPending || setStatus.isPending;
const busy = createRequest.isPending;
const pct = (n: number) => (total > 0 ? (n / total) * 100 : 0);
return (
@@ -443,11 +414,8 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
</Card>
{/* ---- Actions ---- */}
<Grid gap="lg">
{/* Transfer */}
<Grid.Col span={{ base: 12, md: 6 }}>
<Card withBorder radius="md" h="100%" padding="lg">
<Group gap="xs" mb={4}>
<Card withBorder radius="md" padding="lg">
<Group gap="xs" mb={4}>
<ThemeIcon variant="light" color="grape" radius="md" size="md">
<ArrowRightLeft size={16} />
</ThemeIcon>
@@ -479,16 +447,17 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
searchable
radius="md"
/>
<Textarea
label="Reason"
placeholder="Why are these wagons needed?"
value={transferReason}
onChange={(e) => setTransferReason(e.currentTarget.value)}
required
autosize
minRows={2}
radius="md"
/>
<div>
<Text size="sm" fw={500} mb={4}>
Reason <Text span c="red">*</Text>
</Text>
<ReactQuill
theme="snow"
value={transferReason}
onChange={setTransferReason}
placeholder="Why are these wagons needed?"
/>
</div>
{transferYardId && transferQty > 0 ? (
<Card bg="var(--mantine-color-gray-0)" radius="md" padding="sm" withBorder>
<Group gap={8} wrap="nowrap">
@@ -518,106 +487,15 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
busy ||
!transferYardId ||
transferQty < 1 ||
!transferReason.trim()
!stripHtml(transferReason)
}
color="edr-green"
>
Request {transferQty > 0 ? `${transferQty} ` : ""}wagon
{transferQty === 1 ? "" : "s"}
</Button>
</Stack>
</Card>
</Grid.Col>
{/* Re-status */}
<Grid.Col span={{ base: 12, md: 6 }}>
<Card withBorder radius="md" h="100%" padding="lg">
<Group gap="xs" mb="md">
<ThemeIcon variant="light" color="orange" radius="md" size="md">
<ArrowRightLeft size={16} />
</ThemeIcon>
<Text fw={700}>Change availability</Text>
</Group>
<Stack gap="lg">
<Box>
<Group justify="space-between" mb={6}>
<Group gap={6}>
<ThemeIcon variant="light" color="blue" radius="sm" size="sm">
<CircleSlash size={12} />
</ThemeIcon>
<Text size="sm" fw={600}>
Available Assigned
</Text>
</Group>
<Badge color="teal" variant="light">
{availableCount} free
</Badge>
</Group>
<QuantityField
value={toAssignedQty}
onChange={setToAssignedQty}
max={availableCount}
/>
<Button
mt="sm"
fullWidth
variant="light"
color="blue"
disabled={busy || toAssignedQty < 1}
loading={setStatus.isPending}
onClick={() =>
handleFlip(availableWagons, toAssignedQty, ASSIGNED, "Assigned", () =>
setToAssignedQty(0),
)
}
>
Assign {toAssignedQty > 0 ? `${toAssignedQty} ` : ""}wagon
{toAssignedQty === 1 ? "" : "s"}
</Button>
</Box>
<Divider variant="dashed" />
<Box>
<Group justify="space-between" mb={6}>
<Group gap={6}>
<ThemeIcon variant="light" color="teal" radius="sm" size="sm">
<CheckCircle2 size={12} />
</ThemeIcon>
<Text size="sm" fw={600}>
Assigned Available
</Text>
</Group>
<Badge color="blue" variant="light">
{assignedCount} assigned
</Badge>
</Group>
<QuantityField
value={toAvailableQty}
onChange={setToAvailableQty}
max={assignedCount}
/>
<Button
mt="sm"
fullWidth
variant="light"
color="teal"
disabled={busy || toAvailableQty < 1}
loading={setStatus.isPending}
onClick={() =>
handleFlip(assignedWagons, toAvailableQty, AVAILABLE, "Available", () =>
setToAvailableQty(0),
)
}
>
Free up {toAvailableQty > 0 ? `${toAvailableQty} ` : ""}wagon
{toAvailableQty === 1 ? "" : "s"}
</Button>
</Box>
</Stack>
</Card>
</Grid.Col>
</Grid>
</Stack>
</Card>
</>
)}
</Stack>