Merge pull request #1443 from Tria-plc/credit-invoice

feat(container-returns): show booking ref, company and return time
This commit is contained in:
Hagernesh Tadesse
2026-08-28 15:27:19 +03:00
committed by GitHub
6 changed files with 104 additions and 32 deletions

View File

@@ -84,3 +84,14 @@ export class EmptyContainerReturn extends BaseEntity {
performedBy: string | null; 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 { assertWagonLoad } from './empty-container-wagon.util';
import { import {
EmptyContainerReturn, EmptyContainerReturn,
type EmptyContainerReturnListItem,
type EmptyContainerReturnStatus, type EmptyContainerReturnStatus,
} from './entities/empty-container-return.entity'; } from './entities/empty-container-return.entity';
import { import {
@@ -163,8 +164,43 @@ export class ImportOperationsService {
return this.getCustoms(bookingId); 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) { listEmptyReturnsForBooking(bookingId: string) {

View File

@@ -18,6 +18,7 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { Upload } from "lucide-react"; import { Upload } from "lucide-react";
import { localNowForInput } from "@/lib/format";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useWarehouseYards, useWarehouseZones } from "@/hooks/useWarehouses"; import { useWarehouseYards, useWarehouseZones } from "@/hooks/useWarehouses";
import { importOperationsService } from "@/services/importOperations.service"; import { importOperationsService } from "@/services/importOperations.service";
@@ -61,7 +62,7 @@ export default function BulkContainerReturnModal({
const [warehouseId, setWarehouseId] = useState<string | null>(null); const [warehouseId, setWarehouseId] = useState<string | null>(null);
const [yardId, setYardId] = useState<string | null>(null); const [yardId, setYardId] = useState<string | null>(null);
const [zoneId, setZoneId] = 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({ const { data: warehousesResponse } = useQuery({
queryKey: ["warehouses-list"], queryKey: ["warehouses-list"],
@@ -239,10 +240,10 @@ export default function BulkContainerReturnModal({
/> />
<div> <div>
<Text size="sm" fw={500} mb={4}> <Text size="sm" fw={500} mb={4}>
Returned Date (default) Returned Date &amp; Time (default)
</Text> </Text>
<input <input
type="date" type="datetime-local"
value={returnDate} value={returnDate}
onChange={(e) => setReturnDate(e.target.value)} onChange={(e) => setReturnDate(e.target.value)}
style={{ padding: "8px", borderRadius: "4px", border: "1px solid #ced4da", width: "100%" }} 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); const value = bytes / Math.pow(1024, i);
return `${value.toFixed(i === 0 ? 0 : 1)} ${units[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, Select,
Checkbox, Checkbox,
Autocomplete, Autocomplete,
Input,
} from "@mantine/core"; } from "@mantine/core";
import { ChevronDown, ChevronRight, FileText, History, Upload } from "lucide-react"; import { ChevronDown, ChevronRight, FileText, History, Upload } from "lucide-react";
import { DataTable, type ColumnDef } from "@edr/ui-common"; import { DataTable, type ColumnDef } from "@edr/ui-common";
@@ -44,7 +45,7 @@ import type {
EmptyContainerSize, EmptyContainerSize,
} from "@/types/importOperations"; } from "@/types/importOperations";
import type { TrainScheduleListItem } from "@/types/trainScheduling"; import type { TrainScheduleListItem } from "@/types/trainScheduling";
import { formatDateTime } from "@/lib/format"; import { formatDateTime, localNowForInput } from "@/lib/format";
type ReturnType = "all" | "edr" | "customer"; type ReturnType = "all" | "edr" | "customer";
@@ -383,15 +384,16 @@ export default function ContainerReturnsPage() {
</Text> </Text>
), ),
}, },
{
id: "bookingRef",
header: "Booking Ref",
cell: ({ row }) => (row.original.bookingId ? "Associated" : "—"),
},
{ {
id: "company", id: "company",
header: "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", id: "returnedBy",
@@ -407,9 +409,8 @@ export default function ContainerReturnsPage() {
}, },
{ {
id: "returnDate", id: "returnDate",
header: "Returned Date", header: "Returned Date & Time",
cell: ({ row }) => cell: ({ row }) => formatDateTime(row.original.returnDate),
row.original.returnDate ? new Date(row.original.returnDate).toLocaleDateString() : "—",
}, },
{ {
id: "facility", id: "facility",
@@ -878,7 +879,7 @@ interface ContainerReturnModalProps {
function ContainerReturnModal({ opened, onClose, group, onSubmit, loading }: ContainerReturnModalProps) { function ContainerReturnModal({ opened, onClose, group, onSubmit, loading }: ContainerReturnModalProps) {
const [selectedContainers, setSelectedContainers] = useState<string[]>([]); 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 [warehouse, setWarehouse] = useState<string | null>(null);
const [condition, setCondition] = useState<string>(""); const [condition, setCondition] = useState<string>("");
const [handoverNote, setHandoverNote] = useState<string>(""); const [handoverNote, setHandoverNote] = useState<string>("");
@@ -970,13 +971,20 @@ function ContainerReturnModal({ opened, onClose, group, onSubmit, loading }: Con
searchable searchable
/> />
<input <Input.Wrapper label="Returned Date & Time" required>
type="date" <input
value={returnDate} type="datetime-local"
onChange={(e) => setReturnDate(e.target.value)} value={returnDate}
style={{ padding: "8px", borderRadius: "4px", border: "1px solid #ccc" }} onChange={(e) => setReturnDate(e.target.value)}
required style={{
/> padding: "8px",
borderRadius: "4px",
border: "1px solid #ced4da",
width: "100%",
}}
required
/>
</Input.Wrapper>
<Textarea <Textarea
label="Condition" label="Condition"
@@ -1024,7 +1032,7 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
const [company, setCompany] = useState<string>(""); const [company, setCompany] = useState<string>("");
const [containerSize, setContainerSize] = useState<EmptyContainerSize | null>(null); const [containerSize, setContainerSize] = useState<EmptyContainerSize | null>(null);
const [returnedBy, setReturnedBy] = useState<"EDR" | "CUSTOMER" | 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 [warehouse, setWarehouse] = useState<string | null>(null);
const [yardId, setYardId] = useState<string | null>(null); const [yardId, setYardId] = useState<string | null>(null);
const [zoneId, setZoneId] = useState<string | null>(null); const [zoneId, setZoneId] = useState<string | null>(null);
@@ -1102,7 +1110,7 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
setCompany(""); setCompany("");
setContainerSize(null); setContainerSize(null);
setReturnedBy(null); setReturnedBy(null);
setReturnDate(new Date().toISOString().split("T")[0]); setReturnDate(localNowForInput());
setWarehouse(null); setWarehouse(null);
setYardId(null); setYardId(null);
setZoneId(null); setZoneId(null);
@@ -1189,13 +1197,20 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
searchable searchable
/> />
<input <Input.Wrapper label="Returned Date & Time" required>
type="date" <input
value={returnDate} type="datetime-local"
onChange={(e) => setReturnDate(e.target.value)} value={returnDate}
style={{ padding: "8px", borderRadius: "4px", border: "1px solid #ccc" }} onChange={(e) => setReturnDate(e.target.value)}
required style={{
/> padding: "8px",
borderRadius: "4px",
border: "1px solid #ced4da",
width: "100%",
}}
required
/>
</Input.Wrapper>
<Textarea <Textarea
label="Condition" label="Condition"

View File

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