mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 10:58:14 +00:00
fix issue
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user