feat(container-returns): show booking ref, company and return time

The returns table renders one list holding both booking-linked and standalone
empty returns, but a booking-linked row showed the literal string Associated
in place of its reference, and nothing showed the owning company.

listEmptyReturns becomes a raw projection joining freight.bookings and
freight.companies, so each row carries bookingReference and a companyName that
falls back to the booking's company when none was typed on the return itself.

Return date was collected as a bare date input, storing every return at 00:00.
All three entry points — booking-linked, standalone and the bulk default — now
use datetime-local seeded from local time rather than UTC, and the column
renders date and time.
This commit is contained in:
Hagernesh
2026-08-28 12:24:17 +00:00
parent 2d8b022f6b
commit 3a640de45a
6 changed files with 104 additions and 32 deletions

View File

@@ -84,3 +84,14 @@ export class EmptyContainerReturn extends BaseEntity {
performedBy: string | null;
}>;
}
/**
* A row of the returns list: the entity's own columns plus the booking
* reference and owning company joined in. Standalone returns leave
* `bookingId`/`bookingReference` null.
*/
export interface EmptyContainerReturnListItem
extends Omit<EmptyContainerReturn, 'createdAt' | 'updatedAt' | 'deletedAt'> {
bookingReference: string | null;
createdAt: Date;
}

View File

@@ -27,6 +27,7 @@ import {
import { assertWagonLoad } from './empty-container-wagon.util';
import {
EmptyContainerReturn,
type EmptyContainerReturnListItem,
type EmptyContainerReturnStatus,
} from './entities/empty-container-return.entity';
import {
@@ -163,8 +164,43 @@ export class ImportOperationsService {
return this.getCustoms(bookingId);
}
listEmptyReturns() {
return this.emptyReturns.find({ order: { createdAt: 'DESC' } as never });
/**
* Every empty return, booking-linked and standalone alike, in one list. The
* booking reference and the owning company are joined in so the table can
* show which booking a box came back on without a second round trip — a
* standalone row simply has neither, and falls back to the typed
* `company_name`.
*/
listEmptyReturns(): Promise<EmptyContainerReturnListItem[]> {
return this.emptyReturns.manager.query(`
SELECT
r.id,
r.container_number AS "containerNumber",
r.booking_id AS "bookingId",
b.reference AS "bookingReference",
r.customer_id AS "customerId",
COALESCE(r.company_name, c.name) AS "companyName",
r.return_date AS "returnDate",
r.facility,
r.yard,
r.zone,
r.condition,
r.handover_note AS "handoverNote",
r.status,
r.wagon_allocation_reference AS "wagonAllocationReference",
r.container_size AS "containerSize",
r.train_schedule_id AS "trainScheduleId",
r.wagon_sequence_no AS "wagonSequenceNo",
r.performed_by AS "performedBy",
r.returned_by AS "returnedBy",
r.status_history AS "statusHistory",
r.created_at AS "createdAt"
FROM freight.empty_container_returns r
LEFT JOIN freight.bookings b ON b.id = r.booking_id
LEFT JOIN freight.companies c ON c.id = b.company_id
WHERE r.deleted_at IS NULL
ORDER BY r.created_at DESC
`);
}
listEmptyReturnsForBooking(bookingId: string) {

View File

@@ -18,6 +18,7 @@ import {
} from "@mantine/core";
import { Upload } from "lucide-react";
import { localNowForInput } from "@/lib/format";
import { useToast } from "@/hooks/use-toast";
import { useWarehouseYards, useWarehouseZones } from "@/hooks/useWarehouses";
import { importOperationsService } from "@/services/importOperations.service";
@@ -61,7 +62,7 @@ export default function BulkContainerReturnModal({
const [warehouseId, setWarehouseId] = useState<string | null>(null);
const [yardId, setYardId] = useState<string | null>(null);
const [zoneId, setZoneId] = useState<string | null>(null);
const [returnDate, setReturnDate] = useState(new Date().toISOString().split("T")[0]);
const [returnDate, setReturnDate] = useState(localNowForInput());
const { data: warehousesResponse } = useQuery({
queryKey: ["warehouses-list"],
@@ -239,10 +240,10 @@ export default function BulkContainerReturnModal({
/>
<div>
<Text size="sm" fw={500} mb={4}>
Returned Date (default)
Returned Date &amp; Time (default)
</Text>
<input
type="date"
type="datetime-local"
value={returnDate}
onChange={(e) => setReturnDate(e.target.value)}
style={{ padding: "8px", borderRadius: "4px", border: "1px solid #ced4da", width: "100%" }}

View File

@@ -60,3 +60,10 @@ export function formatBytes(bytes: number): string {
const value = bytes / Math.pow(1024, i);
return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
}
/** `YYYY-MM-DDTHH:mm` for now, in local time — what `datetime-local` expects. */
export function localNowForInput(): string {
const d = new Date();
d.setMinutes(d.getMinutes() - d.getTimezoneOffset());
return d.toISOString().slice(0, 16);
}

View File

@@ -19,6 +19,7 @@ import {
Select,
Checkbox,
Autocomplete,
Input,
} from "@mantine/core";
import { ChevronDown, ChevronRight, FileText, History, Upload } from "lucide-react";
import { DataTable, type ColumnDef } from "@edr/ui-common";
@@ -44,7 +45,7 @@ import type {
EmptyContainerSize,
} from "@/types/importOperations";
import type { TrainScheduleListItem } from "@/types/trainScheduling";
import { formatDateTime } from "@/lib/format";
import { formatDateTime, localNowForInput } from "@/lib/format";
type ReturnType = "all" | "edr" | "customer";
@@ -383,15 +384,16 @@ export default function ContainerReturnsPage() {
</Text>
),
},
{
id: "bookingRef",
header: "Booking Ref",
cell: ({ row }) => (row.original.bookingId ? "Associated" : "—"),
},
{
id: "company",
header: "Company",
cell: ({ row }) => row.original.companyName || "—",
// One identity column: who the box belongs to, and the booking it came
// back on. A standalone return has no booking, so only the name shows.
cell: ({ row }) => {
const { companyName, bookingReference } = row.original;
if (!companyName) return bookingReference || "—";
return bookingReference ? `${companyName} (${bookingReference})` : companyName;
},
},
{
id: "returnedBy",
@@ -407,9 +409,8 @@ export default function ContainerReturnsPage() {
},
{
id: "returnDate",
header: "Returned Date",
cell: ({ row }) =>
row.original.returnDate ? new Date(row.original.returnDate).toLocaleDateString() : "—",
header: "Returned Date & Time",
cell: ({ row }) => formatDateTime(row.original.returnDate),
},
{
id: "facility",
@@ -878,7 +879,7 @@ interface ContainerReturnModalProps {
function ContainerReturnModal({ opened, onClose, group, onSubmit, loading }: ContainerReturnModalProps) {
const [selectedContainers, setSelectedContainers] = useState<string[]>([]);
const [returnDate, setReturnDate] = useState<string>(new Date().toISOString().split("T")[0]);
const [returnDate, setReturnDate] = useState<string>(localNowForInput());
const [warehouse, setWarehouse] = useState<string | null>(null);
const [condition, setCondition] = useState<string>("");
const [handoverNote, setHandoverNote] = useState<string>("");
@@ -970,13 +971,20 @@ function ContainerReturnModal({ opened, onClose, group, onSubmit, loading }: Con
searchable
/>
<input
type="date"
value={returnDate}
onChange={(e) => setReturnDate(e.target.value)}
style={{ padding: "8px", borderRadius: "4px", border: "1px solid #ccc" }}
required
/>
<Input.Wrapper label="Returned Date & Time" required>
<input
type="datetime-local"
value={returnDate}
onChange={(e) => setReturnDate(e.target.value)}
style={{
padding: "8px",
borderRadius: "4px",
border: "1px solid #ced4da",
width: "100%",
}}
required
/>
</Input.Wrapper>
<Textarea
label="Condition"
@@ -1024,7 +1032,7 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
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]);
const [returnDate, setReturnDate] = useState<string>(localNowForInput());
const [warehouse, setWarehouse] = useState<string | null>(null);
const [yardId, setYardId] = useState<string | null>(null);
const [zoneId, setZoneId] = useState<string | null>(null);
@@ -1102,7 +1110,7 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
setCompany("");
setContainerSize(null);
setReturnedBy(null);
setReturnDate(new Date().toISOString().split("T")[0]);
setReturnDate(localNowForInput());
setWarehouse(null);
setYardId(null);
setZoneId(null);
@@ -1189,13 +1197,20 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
searchable
/>
<input
type="date"
value={returnDate}
onChange={(e) => setReturnDate(e.target.value)}
style={{ padding: "8px", borderRadius: "4px", border: "1px solid #ccc" }}
required
/>
<Input.Wrapper label="Returned Date & Time" required>
<input
type="datetime-local"
value={returnDate}
onChange={(e) => setReturnDate(e.target.value)}
style={{
padding: "8px",
borderRadius: "4px",
border: "1px solid #ced4da",
width: "100%",
}}
required
/>
</Input.Wrapper>
<Textarea
label="Condition"

View File

@@ -92,6 +92,8 @@ export interface EmptyContainerReturn {
id: string;
containerNumber: string;
bookingId: string | null;
/** Reference of the booking this empty came back on; null for standalone returns. */
bookingReference: string | null;
customerId: string | null;
/** Owning company as text — set for backfilled boxes whose company is unregistered. */
companyName: string | null;