mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 06:35:42 +00:00
Merge pull request #1443 from Tria-plc/credit-invoice
feat(container-returns): show booking ref, company and return time
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 & 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%" }}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user