mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 19:30:57 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 & 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%" }}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user