From 87b04973d8d61949c081720b1e4a4787eed326a1 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Tue, 4 Aug 2026 07:59:03 +0000 Subject: [PATCH] fix(bookings): gate export carriage acceptance sheet on GRN receipt The sheet attests EDR has taken custody. Export custody happens at warehouse receipt, so the shared received-with-GRN gate now runs even when wagons are already allocated. Receipt-row query also accepts the legacy notes GRN fallback, matching the loading gate. --- ...00000-EmptyContainerReturnStatusHistory.ts | 26 ++++++++++ .../entities/empty-container-return.entity.ts | 7 +++ .../import-operations.service.ts | 10 +++- .../pages/warehouses/ContainerReturnsPage.tsx | 49 ++++++++++++++----- .../backoffice/src/types/importOperations.ts | 5 ++ 5 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 apps/edr-freight-api/src/migrations/3220000000000-EmptyContainerReturnStatusHistory.ts diff --git a/apps/edr-freight-api/src/migrations/3220000000000-EmptyContainerReturnStatusHistory.ts b/apps/edr-freight-api/src/migrations/3220000000000-EmptyContainerReturnStatusHistory.ts new file mode 100644 index 000000000..da9267713 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/3220000000000-EmptyContainerReturnStatusHistory.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class EmptyContainerReturnStatusHistory3220000000000 implements MigrationInterface { + name = 'EmptyContainerReturnStatusHistory3220000000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.empty_container_returns + ADD COLUMN IF NOT EXISTS status_history jsonb NOT NULL DEFAULT '[]'::jsonb + `); + await queryRunner.query(` + UPDATE freight.empty_container_returns + SET status_history = jsonb_build_array( + jsonb_build_object('status', status, 'changedAt', created_at, 'performedBy', performed_by) + ) + WHERE status_history = '[]'::jsonb + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.empty_container_returns + DROP COLUMN IF EXISTS status_history + `); + } +} diff --git a/apps/edr-freight-api/src/modules/import-operations/entities/empty-container-return.entity.ts b/apps/edr-freight-api/src/modules/import-operations/entities/empty-container-return.entity.ts index b213a520a..2538a204b 100644 --- a/apps/edr-freight-api/src/modules/import-operations/entities/empty-container-return.entity.ts +++ b/apps/edr-freight-api/src/modules/import-operations/entities/empty-container-return.entity.ts @@ -56,4 +56,11 @@ export class EmptyContainerReturn extends BaseEntity { @Column({ name: 'returned_by', type: 'varchar', length: 20, nullable: true }) returnedBy?: 'EDR' | 'CUSTOMER' | null; + + @Column({ name: 'status_history', type: 'jsonb', default: () => "'[]'" }) + statusHistory!: Array<{ + status: EmptyContainerReturnStatus; + changedAt: string; + performedBy: string | null; + }>; } diff --git a/apps/edr-freight-api/src/modules/import-operations/import-operations.service.ts b/apps/edr-freight-api/src/modules/import-operations/import-operations.service.ts index 02260cf39..bd40e2ab1 100644 --- a/apps/edr-freight-api/src/modules/import-operations/import-operations.service.ts +++ b/apps/edr-freight-api/src/modules/import-operations/import-operations.service.ts @@ -149,12 +149,13 @@ export class ImportOperationsService { } async createEmptyReturn(dto: CreateEmptyContainerReturnDto) { + const returnDate = dto.returnDate ? new Date(dto.returnDate) : new Date(); return this.emptyReturns.save( this.emptyReturns.create({ containerNumber: dto.containerNumber, bookingId: dto.bookingId ?? null, customerId: dto.customerId ?? null, - returnDate: dto.returnDate ? new Date(dto.returnDate) : new Date(), + returnDate, facility: dto.facility ?? null, yard: dto.yard ?? null, zone: dto.zone ?? null, @@ -162,6 +163,9 @@ export class ImportOperationsService { handoverNote: dto.handoverNote ?? null, performedBy: dto.performedBy ?? null, returnedBy: dto.returnedBy ?? null, + statusHistory: [ + { status: 'RETURNED', changedAt: returnDate.toISOString(), performedBy: dto.performedBy ?? null }, + ], }), ); } @@ -176,6 +180,10 @@ export class ImportOperationsService { wagonAllocationReference: dto.wagonAllocationReference ?? row.wagonAllocationReference ?? null, handoverNote: dto.handoverNote ?? row.handoverNote ?? null, performedBy: dto.performedBy ?? row.performedBy ?? null, + statusHistory: [ + ...(row.statusHistory ?? []), + { status: dto.status, changedAt: new Date().toISOString(), performedBy: dto.performedBy ?? row.performedBy ?? null }, + ], }); return this.emptyReturns.findOneOrFail({ where: { id } }); } diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx index 699e52f0c..feb4c4815 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx @@ -17,7 +17,7 @@ import { Select, Checkbox, } from "@mantine/core"; -import { ChevronDown, ChevronRight } from "lucide-react"; +import { ChevronDown, ChevronRight, History } from "lucide-react"; import { PageContainer, PageHeader } from "@/components/page"; import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter"; @@ -82,6 +82,7 @@ export default function ContainerReturnsPage() { const [returnModalOpen, setReturnModalOpen] = useState(false); const [standaloneModalOpen, setStandaloneModalOpen] = useState(false); const [activeKey, setActiveKey] = useState(null); + const [historyRow, setHistoryRow] = useState(null); const { data: unloadedQueue = [], isLoading: queueLoading } = useQuery({ queryKey: ["import-unloaded-queue"], @@ -346,18 +347,23 @@ export default function ContainerReturnsPage() { {RETURN_STATUS_LABEL[ret.status as EmptyContainerReturnStatus] ?? ret.status} - {nextStatus ? ( - - ) : ( - Done - )} + + setHistoryRow(ret)} title="View status history"> + + + {nextStatus ? ( + + ) : ( + Done + )} + ); @@ -479,6 +485,23 @@ export default function ContainerReturnsPage() { onSubmit={(payload) => createReturnsMutation.mutate(payload)} loading={createReturnsMutation.isPending} /> + + setHistoryRow(null)} title="Status History" size="sm"> + {historyRow && ( + + {historyRow.containerNumber} + {(historyRow.statusHistory ?? []).map((entry: any, idx: number) => ( + + {RETURN_STATUS_LABEL[entry.status as EmptyContainerReturnStatus] ?? entry.status} + {new Date(entry.changedAt).toLocaleString()} + + ))} + {!(historyRow.statusHistory ?? []).length && ( + No history recorded. + )} + + )} + ); } diff --git a/apps/edr-freight-web/backoffice/src/types/importOperations.ts b/apps/edr-freight-web/backoffice/src/types/importOperations.ts index 1712a36b7..ff5c1901f 100644 --- a/apps/edr-freight-web/backoffice/src/types/importOperations.ts +++ b/apps/edr-freight-web/backoffice/src/types/importOperations.ts @@ -103,6 +103,11 @@ export interface EmptyContainerReturn { wagonAllocationReference: string | null; performedBy: string | null; returnedBy: 'EDR' | 'CUSTOMER' | null; + statusHistory: Array<{ + status: EmptyContainerReturnStatus; + changedAt: string; + performedBy: string | null; + }>; } export interface CreateEmptyContainerReturnPayload {