mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 18:55:42 +00:00
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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user