feat(import-operations): bulk Excel upload for yard-resident empty containers

Empties already sitting in an EDR yard but never entered in the system had
to be typed one at a time. Adds a bulk path: parse the sheet in the browser
(all-or-nothing, row-numbered errors), preview it, then POST one batch.

The server rejects the batch if any container already has a non-COMPLETED
return, so re-uploading the same sheet cannot duplicate boxes. No interchange
notification fires — these are historical rows, not a live handover.

Company is an Autocomplete over registered customers that also accepts a
typed name, since a backfilled box may belong to a company that is not a
customer yet. Exact name match sets customer_id; the name always lands in the
new empty_container_returns.company_name.

Also fixes the single Record Return modal, which collected Yard and Zone and
then dropped them before the API call, and did not invalidate the returns
list after a standalone return.
This commit is contained in:
Hagernesh
2026-08-28 11:31:50 +00:00
parent 132374ed03
commit 3f4fd8648a
16 changed files with 1075 additions and 44 deletions

View File

@@ -18,8 +18,9 @@ import {
Textarea,
Select,
Checkbox,
Autocomplete,
} from "@mantine/core";
import { ChevronDown, ChevronRight, FileText, History } from "lucide-react";
import { ChevronDown, ChevronRight, FileText, History, Upload } from "lucide-react";
import { DataTable, type ColumnDef } from "@edr/ui-common";
import { PageContainer, PageHeader } from "@/components/page";
@@ -27,6 +28,8 @@ import ListControls from "@/components/common/ListControls";
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
import { extractDownloadErrorMessage } from "@/components/warehouses/options";
import { openPdfBlob } from "@/components/warehouses/pdf";
import BulkContainerReturnModal from "@/components/warehouses/BulkContainerReturnModal";
import { useCompanyOptions } from "@/components/warehouses/useCompanyOptions";
import { OverviewHorizontalBarChart } from "@/components/overview/OverviewHorizontalBarChart";
import { OverviewStackedBarChart } from "@/components/overview/OverviewStackedBarChart";
import { useListControls, toDayString } from "@/hooks/useListControls";
@@ -102,6 +105,7 @@ export default function ContainerReturnsPage() {
const [filterType, setFilterType] = useState<ReturnType>("all");
const [returnModalOpen, setReturnModalOpen] = useState(false);
const [standaloneModalOpen, setStandaloneModalOpen] = useState(false);
const [bulkModalOpen, setBulkModalOpen] = useState(false);
const [activeKey, setActiveKey] = useState<string | null>(null);
const [historyRow, setHistoryRow] = useState<any | null>(null);
const [allocateRow, setAllocateRow] = useState<EmptyContainerReturn | null>(null);
@@ -287,11 +291,14 @@ export default function ContainerReturnsPage() {
bookingId: string;
customerId: string | null;
returnType: "EDR" | "CUSTOMER";
companyName?: string;
containers: Array<{
containerNumber: string;
containerSize?: EmptyContainerSize;
returnDate: string;
warehouse: string;
yard?: string;
zone?: string;
condition?: string;
handoverNote?: string;
}>;
@@ -306,7 +313,10 @@ export default function ContainerReturnsPage() {
returnDate: new Date(container.returnDate).toISOString(),
bookingId: truck.bookingId,
customerId: truck.customerId ?? undefined,
companyName: truck.companyName,
facility: container.warehouse,
yard: container.yard,
zone: container.zone,
condition: container.condition,
handoverNote: container.handoverNote,
returnedBy: truck.returnType,
@@ -319,7 +329,9 @@ export default function ContainerReturnsPage() {
onSuccess: () => {
toast({ title: "Container returns recorded" });
qc.invalidateQueries({ queryKey: ["container-returns", bookingIds] });
qc.invalidateQueries({ queryKey: ["empty-container-returns"] });
setReturnModalOpen(false);
setStandaloneModalOpen(false);
setActiveKey(null);
},
onError: (error: any) => {
@@ -376,6 +388,11 @@ export default function ContainerReturnsPage() {
header: "Booking Ref",
cell: ({ row }) => (row.original.bookingId ? "Associated" : "—"),
},
{
id: "company",
header: "Company",
cell: ({ row }) => row.original.companyName || "—",
},
{
id: "returnedBy",
header: "Returned By",
@@ -510,9 +527,16 @@ export default function ContainerReturnsPage() {
{ label: "Customer Self-Haul", value: "customer" },
]}
/>
<Button onClick={() => setStandaloneModalOpen(true)}>
Record Return
</Button>
<Group gap="sm">
<Button
variant="default"
leftSection={<Upload size={16} />}
onClick={() => setBulkModalOpen(true)}
>
Bulk Upload
</Button>
<Button onClick={() => setStandaloneModalOpen(true)}>Record Return</Button>
</Group>
</Group>
{returnedContainers.length > 0 && (
@@ -688,6 +712,12 @@ export default function ContainerReturnsPage() {
loading={createReturnsMutation.isPending}
/>
<BulkContainerReturnModal
opened={bulkModalOpen}
onClose={() => setBulkModalOpen(false)}
onUploaded={() => qc.invalidateQueries({ queryKey: ["empty-container-returns"] })}
/>
<ExportTrainAllocationModal
row={allocateRow}
onClose={() => setAllocateRow(null)}
@@ -991,6 +1021,7 @@ interface StandaloneReturnModalProps {
function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: StandaloneReturnModalProps) {
const [containerNumber, setContainerNumber] = useState<string>("");
const [company, setCompany] = useState<string>("");
const [containerSize, setContainerSize] = useState<EmptyContainerSize | null>(null);
const [returnedBy, setReturnedBy] = useState<"EDR" | "CUSTOMER" | null>(null);
const [returnDate, setReturnDate] = useState<string>(new Date().toISOString().split("T")[0]);
@@ -1009,6 +1040,7 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
const warehouses = (warehousesResponse as any)?.data ?? warehousesResponse ?? [];
const companies = useCompanyOptions();
const { data: yards } = useWarehouseYards(warehouse ?? undefined);
const { data: zones } = useWarehouseZones(yardId ?? undefined);
@@ -1047,7 +1079,8 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
trucks: [
{
bookingId: null,
customerId: null,
customerId: company ? (companies.resolveId(company) ?? null) : null,
companyName: company || undefined,
returnType: returnedBy,
containers: [
{
@@ -1066,6 +1099,7 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
});
setContainerNumber("");
setCompany("");
setContainerSize(null);
setReturnedBy(null);
setReturnDate(new Date().toISOString().split("T")[0]);
@@ -1092,6 +1126,16 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
required
/>
<Autocomplete
label="Company"
description="Pick a registered customer, or type a company that is not on the system yet"
placeholder={companies.loading ? "Loading companies…" : "Search or type a company"}
data={companies.names}
value={company}
onChange={setCompany}
limit={20}
/>
<Select
label="Returned By"
placeholder="Select truck type"