mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Merge pull request #1029 from Tria-plc/edrmiles
Per-booking returns (existing) — click "Record Return" on booking row → select containers + warehouse Standalone returns (new) — top of page button "Record Return" → single container, no booking required Container number input Warehouse dropdown Return date, condition, handover notes Submit Commit: feat: add standalone container return modal
This commit is contained in:
@@ -162,25 +162,36 @@ describe('applyWagonOrderReversal', () => {
|
||||
expect(applyWagonOrderReversal(plan, null)).toBe(plan);
|
||||
});
|
||||
|
||||
it('flips the order and renumbers sequenceNo 1..N when the flag is true', () => {
|
||||
it('flips the position numbers when the flag is true', () => {
|
||||
const reversed = applyWagonOrderReversal(plan, true);
|
||||
// Physically-last wagon (was seq 3, wt-c) is now position 1.
|
||||
expect(reversed.map((s) => s.wagonTypeId)).toEqual(['wt-c', 'wt-b', 'wt-a']);
|
||||
expect(reversed.map((s) => s.sequenceNo)).toEqual([1, 2, 3]);
|
||||
// Physically-last wagon (wt-c) is now position 1.
|
||||
expect(reversed.map((s) => s.sequenceNo)).toEqual([3, 2, 1]);
|
||||
});
|
||||
|
||||
it('keeps each booking with its own wagon — only the position changes', () => {
|
||||
const reversed = applyWagonOrderReversal(plan, true);
|
||||
// The booking that was in the last wagon now sits at sequenceNo 1.
|
||||
expect(reversed[0].sequenceNo).toBe(1);
|
||||
const atPosition1 = reversed.find((s) => s.sequenceNo === 1);
|
||||
expect(
|
||||
(reversed[0].allocations as { bookingId: string }[])[0].bookingId,
|
||||
(atPosition1?.allocations as { bookingId: string }[])[0].bookingId,
|
||||
).toBe('BKG-C');
|
||||
const atPosition3 = reversed.find((s) => s.sequenceNo === 3);
|
||||
expect(
|
||||
(reversed[2].allocations as { bookingId: string }[])[0].bookingId,
|
||||
(atPosition3?.allocations as { bookingId: string }[])[0].bookingId,
|
||||
).toBe('BKG-A');
|
||||
});
|
||||
|
||||
// The regression that emptied every reversed train's container items: the
|
||||
// placement generators pair unit k (booking order) with slot k of this array,
|
||||
// and persistAllocationsAndLoads matches that sequenceNo against the
|
||||
// allocation's booking. Array order must stay packing order.
|
||||
it('keeps array order aligned with booking order so placements still match', () => {
|
||||
const reversed = applyWagonOrderReversal(plan, true);
|
||||
expect(
|
||||
reversed.map((s) => (s.allocations as { bookingId: string }[])[0].bookingId),
|
||||
).toEqual(['BKG-A', 'BKG-B', 'BKG-C']);
|
||||
});
|
||||
|
||||
it('does not mutate the input plan', () => {
|
||||
applyWagonOrderReversal(plan, true);
|
||||
expect(plan.map((s) => s.sequenceNo)).toEqual([1, 2, 3]);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
TextInput,
|
||||
Textarea,
|
||||
Select,
|
||||
Checkbox,
|
||||
@@ -57,6 +58,7 @@ export default function ContainerReturnsPage() {
|
||||
const [expanded, setExpanded] = useState<string | null>(null);
|
||||
const [filterType, setFilterType] = useState<ReturnType>("all");
|
||||
const [returnModalOpen, setReturnModalOpen] = useState(false);
|
||||
const [standaloneModalOpen, setStandaloneModalOpen] = useState(false);
|
||||
const [activeKey, setActiveKey] = useState<string | null>(null);
|
||||
|
||||
const { data: unloadedQueue = [], isLoading: queueLoading } = useQuery({
|
||||
@@ -76,7 +78,7 @@ export default function ContainerReturnsPage() {
|
||||
for (const item of unloadedQueue) {
|
||||
if (!item.bookingId) continue;
|
||||
|
||||
// EDR returns
|
||||
// EDR last-mile returns: same EDR truck that delivered will return with empty containers
|
||||
const edrTrucks = await warehouseService.getLastMileTrucks(item.bookingId).catch(() => []);
|
||||
if (edrTrucks.length > 0) {
|
||||
const inventory = await api.warehouses.listInventory
|
||||
@@ -112,7 +114,7 @@ export default function ContainerReturnsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Customer returns
|
||||
// Customer self-haul last-mile returns: same customer truck that delivered will return with empty containers
|
||||
const customerTrucks = await warehouseService.getCustomerTrucks(item.bookingId).catch(() => []);
|
||||
if (customerTrucks.length > 0) {
|
||||
const inventory = await api.warehouses.listInventory
|
||||
@@ -229,19 +231,22 @@ export default function ContainerReturnsPage() {
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Container Returns"
|
||||
subtitle="Track empty container returns by EDR or customer"
|
||||
subtitle="Empty containers returned by last-mile trucks (EDR or customer self-haul)"
|
||||
/>
|
||||
|
||||
<Group mb="lg">
|
||||
<Group mb="lg" justify="space-between">
|
||||
<SegmentedControl
|
||||
value={filterType}
|
||||
onChange={(val) => setFilterType(val as ReturnType)}
|
||||
data={[
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "EDR Returns", value: "edr" },
|
||||
{ label: "Customer Returns", value: "customer" },
|
||||
{ label: "EDR Last Mile", value: "edr" },
|
||||
{ label: "Customer Self-Haul", value: "customer" },
|
||||
]}
|
||||
/>
|
||||
<Button onClick={() => setStandaloneModalOpen(true)}>
|
||||
Record Return
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{filteredGroups.length === 0 ? (
|
||||
@@ -282,7 +287,7 @@ export default function ContainerReturnsPage() {
|
||||
<Table.Td>{group.companyName ?? "—"}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={group.returnType === "EDR" ? "edr-green" : "blue"}>
|
||||
{group.returnType}
|
||||
{group.returnType === "EDR" ? "EDR Last Mile" : "Customer Self-Haul"}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
@@ -348,6 +353,13 @@ export default function ContainerReturnsPage() {
|
||||
onSubmit={(payload) => createReturnsMutation.mutate(payload)}
|
||||
loading={createReturnsMutation.isPending}
|
||||
/>
|
||||
|
||||
<StandaloneReturnModal
|
||||
opened={standaloneModalOpen}
|
||||
onClose={() => setStandaloneModalOpen(false)}
|
||||
onSubmit={(payload) => createReturnsMutation.mutate(payload)}
|
||||
loading={createReturnsMutation.isPending}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -416,7 +428,7 @@ function ContainerReturnModal({ opened, onClose, group, onSubmit, loading }: Con
|
||||
<Group>
|
||||
<Text fw={600}>{group.bookingRef}</Text>
|
||||
<Badge color={group.returnType === "EDR" ? "edr-green" : "blue"}>
|
||||
{group.returnType}
|
||||
{group.returnType === "EDR" ? "EDR Last Mile" : "Customer Self-Haul"}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
@@ -493,3 +505,130 @@ function ContainerReturnModal({ opened, onClose, group, onSubmit, loading }: Con
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
interface StandaloneReturnModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (payload: any) => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: StandaloneReturnModalProps) {
|
||||
const [containerNumber, setContainerNumber] = useState<string>("");
|
||||
const [returnDate, setReturnDate] = useState<string>(new Date().toISOString().split("T")[0]);
|
||||
const [warehouse, setWarehouse] = useState<string | null>(null);
|
||||
const [condition, setCondition] = useState<string>("");
|
||||
const [handoverNote, setHandoverNote] = useState<string>("");
|
||||
|
||||
const { data: warehousesResponse } = useQuery({
|
||||
queryKey: ["warehouses-list"],
|
||||
queryFn: async () => {
|
||||
return await warehouseService.list({});
|
||||
},
|
||||
});
|
||||
|
||||
const warehouses = (warehousesResponse as any)?.data ?? warehousesResponse ?? [];
|
||||
const warehouseOptions = Array.isArray(warehouses)
|
||||
? warehouses.map((wh: any) => ({
|
||||
value: wh.id,
|
||||
label: `${wh.name} ${wh.code ? `(${wh.code})` : ""}`,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!containerNumber || !warehouse) return;
|
||||
|
||||
const selectedWarehouse = Array.isArray(warehouses) ? warehouses.find((wh: any) => wh.id === warehouse) : null;
|
||||
|
||||
onSubmit({
|
||||
trucks: [
|
||||
{
|
||||
bookingId: null,
|
||||
customerId: null,
|
||||
returnType: "CUSTOMER",
|
||||
containers: [
|
||||
{
|
||||
containerNumber,
|
||||
returnDate,
|
||||
warehouse: selectedWarehouse?.name || warehouse,
|
||||
condition: condition || undefined,
|
||||
handoverNote: handoverNote || undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
setContainerNumber("");
|
||||
setReturnDate(new Date().toISOString().split("T")[0]);
|
||||
setWarehouse(null);
|
||||
setCondition("");
|
||||
setHandoverNote("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title="Record Container Return (Standalone)" size="lg">
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
Record container return without booking association
|
||||
</Text>
|
||||
|
||||
<TextInput
|
||||
label="Container Number"
|
||||
placeholder="e.g., TEMU1234567"
|
||||
value={containerNumber}
|
||||
onChange={(e) => setContainerNumber(e.currentTarget.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Return Warehouse"
|
||||
placeholder="Select warehouse for container return"
|
||||
value={warehouse}
|
||||
onChange={setWarehouse}
|
||||
data={warehouseOptions}
|
||||
required
|
||||
searchable
|
||||
/>
|
||||
|
||||
<input
|
||||
type="date"
|
||||
value={returnDate}
|
||||
onChange={(e) => setReturnDate(e.target.value)}
|
||||
style={{ padding: "8px", borderRadius: "4px", border: "1px solid #ccc" }}
|
||||
required
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Condition"
|
||||
placeholder="Damage, residue, or cleanliness notes"
|
||||
value={condition}
|
||||
onChange={(e) => setCondition(e.currentTarget.value)}
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Handover Note"
|
||||
placeholder="Consignee, trucker, or authorization notes"
|
||||
value={handoverNote}
|
||||
onChange={(e) => setHandoverNote(e.currentTarget.value)}
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={onClose} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!containerNumber || !warehouse}
|
||||
loading={loading}
|
||||
>
|
||||
Record Return
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user