mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 06:35:42 +00:00
Merge pull request #1475 from Tria-plc/Emty-container-return
feat(import-operations): record empty container return per booking
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
import {
|
||||||
|
assembleEmptyReturnBookings,
|
||||||
|
type EmptyReturnBookingUnitRow,
|
||||||
|
} from './empty-return-bookings.util';
|
||||||
|
|
||||||
|
const booking = {
|
||||||
|
bookingId: 'b1',
|
||||||
|
bookingReference: 'BK-2026-000263',
|
||||||
|
bookingStatus: 'IN_TRANSIT',
|
||||||
|
equipmentReturn: 'WITH_RETURN',
|
||||||
|
customerId: 'c1',
|
||||||
|
companyName: 'Afri Software Solutions',
|
||||||
|
};
|
||||||
|
|
||||||
|
const unit = (
|
||||||
|
overrides: Partial<EmptyReturnBookingUnitRow> & { unitId: string; containerNumber: string },
|
||||||
|
): EmptyReturnBookingUnitRow => ({
|
||||||
|
...booking,
|
||||||
|
containerSize: '40ft',
|
||||||
|
containerType: '40FT',
|
||||||
|
returnId: null,
|
||||||
|
returnStatus: null,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('assembleEmptyReturnBookings', () => {
|
||||||
|
it('groups a booking’s flagged containers onto one row, all pending', () => {
|
||||||
|
const rows = assembleEmptyReturnBookings([
|
||||||
|
unit({ unitId: 'u1', containerNumber: 'MSFH8596324' }),
|
||||||
|
unit({ unitId: 'u2', containerNumber: 'SDJU8596324' }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(rows).toHaveLength(1);
|
||||||
|
expect(rows[0].bookingReference).toBe('BK-2026-000263');
|
||||||
|
expect(rows[0].companyName).toBe('Afri Software Solutions');
|
||||||
|
expect(rows[0].containers.map((c) => c.containerNumber)).toEqual([
|
||||||
|
'MSFH8596324',
|
||||||
|
'SDJU8596324',
|
||||||
|
]);
|
||||||
|
expect(rows[0]).toMatchObject({ expectedCount: 2, recordedCount: 0, pendingCount: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps an already-recorded container visible but out of the pending count', () => {
|
||||||
|
const rows = assembleEmptyReturnBookings([
|
||||||
|
unit({
|
||||||
|
unitId: 'u1',
|
||||||
|
containerNumber: 'MSFH8596324',
|
||||||
|
returnId: 'r1',
|
||||||
|
returnStatus: 'ASSIGNED_STORAGE',
|
||||||
|
}),
|
||||||
|
unit({ unitId: 'u2', containerNumber: 'SDJU8596324' }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(rows[0]).toMatchObject({ expectedCount: 2, recordedCount: 1, pendingCount: 1 });
|
||||||
|
expect(rows[0].containers[0].returnStatus).toBe('ASSIGNED_STORAGE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops a booking once every container is recorded', () => {
|
||||||
|
const rows = assembleEmptyReturnBookings([
|
||||||
|
unit({
|
||||||
|
unitId: 'u1',
|
||||||
|
containerNumber: 'MSFH8596324',
|
||||||
|
returnId: 'r1',
|
||||||
|
returnStatus: 'RETURNED',
|
||||||
|
}),
|
||||||
|
unit({
|
||||||
|
unitId: 'u2',
|
||||||
|
containerNumber: 'SDJU8596324',
|
||||||
|
returnId: 'r2',
|
||||||
|
returnStatus: 'COMPLETED',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(rows).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps each booking on its own row, in query order', () => {
|
||||||
|
const other = {
|
||||||
|
...booking,
|
||||||
|
bookingId: 'b2',
|
||||||
|
bookingReference: 'BK-2026-000286',
|
||||||
|
companyName: 'DE BE KE',
|
||||||
|
};
|
||||||
|
const rows = assembleEmptyReturnBookings([
|
||||||
|
unit({ unitId: 'u1', containerNumber: 'MSFH8596324' }),
|
||||||
|
{ ...unit({ unitId: 'u2', containerNumber: 'ASDS1234567' }), ...other },
|
||||||
|
unit({ unitId: 'u3', containerNumber: 'SDJU8596324' }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(rows.map((r) => r.bookingReference)).toEqual(['BK-2026-000263', 'BK-2026-000286']);
|
||||||
|
expect(rows[0].containers).toHaveLength(2);
|
||||||
|
expect(rows[1].containers).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns nothing when no booking owes an empty', () => {
|
||||||
|
expect(assembleEmptyReturnBookings([])).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import type { EmptyContainerReturnStatus } from './entities/empty-container-return.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `WITH_RETURN` is the current value; `RETURN` is what older bookings were
|
||||||
|
* written with. Both mean the same thing — the booking owes empties back.
|
||||||
|
*/
|
||||||
|
export const WITH_RETURN_EQUIPMENT_VALUES = ['WITH_RETURN', 'RETURN'];
|
||||||
|
|
||||||
|
/** Bookings in these statuses never ship, so they never owe an empty back. */
|
||||||
|
export const EMPTY_RETURN_CLOSED_BOOKING_STATUSES = ['DRAFT', 'CANCELLED', 'REJECTED', 'EXPIRED'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One flagged return container of a booking, as the query hands it over: the
|
||||||
|
* booking columns repeat on every row, and `returnId` is set when this exact
|
||||||
|
* container already has an empty return recorded against the booking.
|
||||||
|
*/
|
||||||
|
export interface EmptyReturnBookingUnitRow {
|
||||||
|
bookingId: string;
|
||||||
|
bookingReference: string;
|
||||||
|
bookingStatus: string;
|
||||||
|
equipmentReturn: string;
|
||||||
|
customerId: string | null;
|
||||||
|
companyName: string | null;
|
||||||
|
unitId: string;
|
||||||
|
containerNumber: string;
|
||||||
|
containerSize: string | null;
|
||||||
|
containerType: string | null;
|
||||||
|
returnId: string | null;
|
||||||
|
returnStatus: EmptyContainerReturnStatus | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One container a booking owes back empty. */
|
||||||
|
export interface EmptyReturnBookingContainer {
|
||||||
|
/** Stable row key — the booking container unit id. */
|
||||||
|
key: string;
|
||||||
|
unitId: string;
|
||||||
|
containerNumber: string;
|
||||||
|
containerSize: string | null;
|
||||||
|
containerType: string | null;
|
||||||
|
/** Set once the empty return for this container has been recorded. */
|
||||||
|
returnId: string | null;
|
||||||
|
returnStatus: EmptyContainerReturnStatus | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A booking that ships with empty-container return and still owes empties. */
|
||||||
|
export interface EmptyReturnBookingRow {
|
||||||
|
bookingId: string;
|
||||||
|
bookingReference: string;
|
||||||
|
bookingStatus: string;
|
||||||
|
equipmentReturn: string;
|
||||||
|
customerId: string | null;
|
||||||
|
companyName: string | null;
|
||||||
|
containers: EmptyReturnBookingContainer[];
|
||||||
|
expectedCount: number;
|
||||||
|
recordedCount: number;
|
||||||
|
pendingCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups a booking's flagged return containers onto one row per booking.
|
||||||
|
*
|
||||||
|
* A container whose empty return is already recorded keeps its row — the
|
||||||
|
* screen shows what has been done — but stops counting as pending, and a
|
||||||
|
* booking with nothing left pending drops off the list entirely.
|
||||||
|
*
|
||||||
|
* Row order follows the query (newest booking first, containers in booking
|
||||||
|
* order), so the caller decides the ordering, not this function.
|
||||||
|
*/
|
||||||
|
export function assembleEmptyReturnBookings(
|
||||||
|
units: EmptyReturnBookingUnitRow[],
|
||||||
|
): EmptyReturnBookingRow[] {
|
||||||
|
const rows = new Map<string, EmptyReturnBookingRow>();
|
||||||
|
|
||||||
|
for (const unit of units) {
|
||||||
|
const row = rows.get(unit.bookingId) ?? {
|
||||||
|
bookingId: unit.bookingId,
|
||||||
|
bookingReference: unit.bookingReference,
|
||||||
|
bookingStatus: unit.bookingStatus,
|
||||||
|
equipmentReturn: unit.equipmentReturn,
|
||||||
|
customerId: unit.customerId,
|
||||||
|
companyName: unit.companyName,
|
||||||
|
containers: [],
|
||||||
|
expectedCount: 0,
|
||||||
|
recordedCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
};
|
||||||
|
row.containers.push({
|
||||||
|
key: unit.unitId,
|
||||||
|
unitId: unit.unitId,
|
||||||
|
containerNumber: unit.containerNumber,
|
||||||
|
containerSize: unit.containerSize,
|
||||||
|
containerType: unit.containerType,
|
||||||
|
returnId: unit.returnId,
|
||||||
|
returnStatus: unit.returnStatus,
|
||||||
|
});
|
||||||
|
rows.set(unit.bookingId, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...rows.values()]
|
||||||
|
.map((row) => ({
|
||||||
|
...row,
|
||||||
|
expectedCount: row.containers.length,
|
||||||
|
recordedCount: row.containers.filter((container) => container.returnId).length,
|
||||||
|
pendingCount: row.containers.filter((container) => !container.returnId).length,
|
||||||
|
}))
|
||||||
|
.filter((row) => row.pendingCount > 0);
|
||||||
|
}
|
||||||
@@ -119,6 +119,15 @@ export class ImportOperationsController {
|
|||||||
return this.service.listEmptyReturns();
|
return this.service.listEmptyReturns();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('empty-return-bookings')
|
||||||
|
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Bookings shipping with empty-container return that still owe empties, with their containers',
|
||||||
|
})
|
||||||
|
listEmptyReturnBookings() {
|
||||||
|
return this.service.listEmptyReturnBookings();
|
||||||
|
}
|
||||||
|
|
||||||
@Post('empty-container-returns')
|
@Post('empty-container-returns')
|
||||||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||||||
@ApiOperation({ summary: 'Batch 16: create an empty container return record' })
|
@ApiOperation({ summary: 'Batch 16: create an empty container return record' })
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ import {
|
|||||||
type DjiboutiIncidentType,
|
type DjiboutiIncidentType,
|
||||||
} from './entities/djibouti-incident.entity';
|
} from './entities/djibouti-incident.entity';
|
||||||
import { assertWagonLoad } from './empty-container-wagon.util';
|
import { assertWagonLoad } from './empty-container-wagon.util';
|
||||||
|
import {
|
||||||
|
assembleEmptyReturnBookings,
|
||||||
|
EMPTY_RETURN_CLOSED_BOOKING_STATUSES,
|
||||||
|
WITH_RETURN_EQUIPMENT_VALUES,
|
||||||
|
type EmptyReturnBookingRow,
|
||||||
|
type EmptyReturnBookingUnitRow,
|
||||||
|
} from './empty-return-bookings.util';
|
||||||
import {
|
import {
|
||||||
EmptyContainerReturn,
|
EmptyContainerReturn,
|
||||||
type EmptyContainerReturnListItem,
|
type EmptyContainerReturnListItem,
|
||||||
@@ -207,6 +214,51 @@ export class ImportOperationsService {
|
|||||||
return this.emptyReturns.find({ where: { bookingId }, order: { createdAt: 'DESC' } as never });
|
return this.emptyReturns.find({ where: { bookingId }, order: { createdAt: 'DESC' } as never });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bookings that ship WITH empty-container return and still owe empties, each
|
||||||
|
* with the containers that are to be returned — the ones the booking flagged
|
||||||
|
* `is_return`, carrying the empty return already recorded against each, if
|
||||||
|
* any.
|
||||||
|
*/
|
||||||
|
async listEmptyReturnBookings(): Promise<EmptyReturnBookingRow[]> {
|
||||||
|
const units: EmptyReturnBookingUnitRow[] = await this.emptyReturns.manager.query(
|
||||||
|
`SELECT b.id AS "bookingId",
|
||||||
|
b.reference AS "bookingReference",
|
||||||
|
b.status AS "bookingStatus",
|
||||||
|
b.equipment_return AS "equipmentReturn",
|
||||||
|
b.company_id AS "customerId",
|
||||||
|
c.name AS "companyName",
|
||||||
|
u.id AS "unitId",
|
||||||
|
u.container_number AS "containerNumber",
|
||||||
|
COALESCE(bc.container_size, ct.code) AS "containerSize",
|
||||||
|
ct.label AS "containerType",
|
||||||
|
r.id AS "returnId",
|
||||||
|
r.status AS "returnStatus"
|
||||||
|
FROM freight.booking_container_units u
|
||||||
|
JOIN freight.booking_container bc ON bc.id = u.booking_container_id AND bc.deleted_at IS NULL
|
||||||
|
JOIN freight.bookings b ON b.id = bc.booking_id AND b.deleted_at IS NULL
|
||||||
|
LEFT JOIN freight.companies c ON c.id = b.company_id
|
||||||
|
LEFT JOIN freight.container_types ct ON ct.id = bc.container_type_id
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT er.id, er.status
|
||||||
|
FROM freight.empty_container_returns er
|
||||||
|
WHERE er.deleted_at IS NULL
|
||||||
|
AND er.booking_id = b.id
|
||||||
|
AND upper(er.container_number) = upper(u.container_number)
|
||||||
|
ORDER BY er.created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
) r ON TRUE
|
||||||
|
WHERE u.deleted_at IS NULL
|
||||||
|
AND u.is_return = true
|
||||||
|
AND b.equipment_return = ANY($1)
|
||||||
|
AND b.status <> ALL($2)
|
||||||
|
ORDER BY b.created_at DESC, u.sort_order ASC`,
|
||||||
|
[WITH_RETURN_EQUIPMENT_VALUES, EMPTY_RETURN_CLOSED_BOOKING_STATUSES],
|
||||||
|
);
|
||||||
|
|
||||||
|
return assembleEmptyReturnBookings(units);
|
||||||
|
}
|
||||||
|
|
||||||
async createEmptyReturn(dto: CreateEmptyContainerReturnDto) {
|
async createEmptyReturn(dto: CreateEmptyContainerReturnDto) {
|
||||||
const returnDate = dto.returnDate ? new Date(dto.returnDate) : new Date();
|
const returnDate = dto.returnDate ? new Date(dto.returnDate) : new Date();
|
||||||
const saved = await this.emptyReturns.save(
|
const saved = await this.emptyReturns.save(
|
||||||
|
|||||||
@@ -816,6 +816,7 @@ export const URL_CONSTANTS = {
|
|||||||
CUSTOMS_RELEASE_PERMITTED: (bookingId: string) =>
|
CUSTOMS_RELEASE_PERMITTED: (bookingId: string) =>
|
||||||
`/import-operations/customs/${bookingId}/release-permitted`,
|
`/import-operations/customs/${bookingId}/release-permitted`,
|
||||||
EMPTY_CONTAINER_RETURNS: "/import-operations/empty-container-returns",
|
EMPTY_CONTAINER_RETURNS: "/import-operations/empty-container-returns",
|
||||||
|
EMPTY_RETURN_BOOKINGS: "/import-operations/empty-return-bookings",
|
||||||
EMPTY_CONTAINER_RETURNS_BULK: "/import-operations/empty-container-returns/bulk",
|
EMPTY_CONTAINER_RETURNS_BULK: "/import-operations/empty-container-returns/bulk",
|
||||||
EMPTY_CONTAINER_RETURN_STATUS: (id: string) =>
|
EMPTY_CONTAINER_RETURN_STATUS: (id: string) =>
|
||||||
`/import-operations/empty-container-returns/${id}/status`,
|
`/import-operations/empty-container-returns/${id}/status`,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { DataTable, type ColumnDef } from "@edr/ui-common";
|
|||||||
import { PageContainer, PageHeader } from "@/components/page";
|
import { PageContainer, PageHeader } from "@/components/page";
|
||||||
import ListControls from "@/components/common/ListControls";
|
import ListControls from "@/components/common/ListControls";
|
||||||
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
||||||
import { extractDownloadErrorMessage } from "@/components/warehouses/options";
|
import { extractDownloadErrorMessage, extractErrorMessage } from "@/components/warehouses/options";
|
||||||
import { openPdfBlob } from "@/components/warehouses/pdf";
|
import { openPdfBlob } from "@/components/warehouses/pdf";
|
||||||
import BulkContainerReturnModal from "@/components/warehouses/BulkContainerReturnModal";
|
import BulkContainerReturnModal from "@/components/warehouses/BulkContainerReturnModal";
|
||||||
import { downloadContainerReturnTemplate } from "@/components/warehouses/container-return-excel";
|
import { downloadContainerReturnTemplate } from "@/components/warehouses/container-return-excel";
|
||||||
@@ -44,6 +44,7 @@ import type {
|
|||||||
EmptyContainerReturn,
|
EmptyContainerReturn,
|
||||||
EmptyContainerReturnStatus,
|
EmptyContainerReturnStatus,
|
||||||
EmptyContainerSize,
|
EmptyContainerSize,
|
||||||
|
EmptyReturnBooking,
|
||||||
} from "@/types/importOperations";
|
} from "@/types/importOperations";
|
||||||
import type { TrainScheduleListItem } from "@/types/trainScheduling";
|
import type { TrainScheduleListItem } from "@/types/trainScheduling";
|
||||||
import { formatDateTime, localNowForInput } from "@/lib/format";
|
import { formatDateTime, localNowForInput } from "@/lib/format";
|
||||||
@@ -111,6 +112,8 @@ export default function ContainerReturnsPage() {
|
|||||||
const [activeKey, setActiveKey] = useState<string | null>(null);
|
const [activeKey, setActiveKey] = useState<string | null>(null);
|
||||||
const [historyRow, setHistoryRow] = useState<any | null>(null);
|
const [historyRow, setHistoryRow] = useState<any | null>(null);
|
||||||
const [allocateRow, setAllocateRow] = useState<EmptyContainerReturn | null>(null);
|
const [allocateRow, setAllocateRow] = useState<EmptyContainerReturn | null>(null);
|
||||||
|
const [emptyReturnBooking, setEmptyReturnBooking] = useState<EmptyReturnBooking | null>(null);
|
||||||
|
const [expandedBooking, setExpandedBooking] = useState<string | null>(null);
|
||||||
const [documentBusyId, setDocumentBusyId] = useState<string | null>(null);
|
const [documentBusyId, setDocumentBusyId] = useState<string | null>(null);
|
||||||
|
|
||||||
const viewInterchangeDocument = async (ret: EmptyContainerReturn) => {
|
const viewInterchangeDocument = async (ret: EmptyContainerReturn) => {
|
||||||
@@ -146,6 +149,16 @@ export default function ContainerReturnsPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bookings that ship WITH empty-container return and still owe empties. This
|
||||||
|
// list stands on the booking's own return flags, so it does not wait for the
|
||||||
|
// box to reach a warehouse or for a last-mile truck to be assigned — the
|
||||||
|
// queue below still covers that path.
|
||||||
|
const emptyReturnBookingsQuery = useQuery({
|
||||||
|
queryKey: ["empty-return-bookings"],
|
||||||
|
queryFn: () => importOperationsService.listEmptyReturnBookings(),
|
||||||
|
});
|
||||||
|
const emptyReturnBookings = emptyReturnBookingsQuery.data ?? [];
|
||||||
|
|
||||||
const bookingIds = unloadedQueue.map((item) => item.bookingId).filter(Boolean) as string[];
|
const bookingIds = unloadedQueue.map((item) => item.bookingId).filter(Boolean) as string[];
|
||||||
const containerReturnsQuery = useQuery({
|
const containerReturnsQuery = useQuery({
|
||||||
queryKey: ["container-returns", bookingIds],
|
queryKey: ["container-returns", bookingIds],
|
||||||
@@ -287,6 +300,10 @@ export default function ContainerReturnsPage() {
|
|||||||
searchKeys: ["bookingRef", "companyName"],
|
searchKeys: ["bookingRef", "companyName"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const bookingReturnControls = useListControls(emptyReturnBookings, {
|
||||||
|
searchKeys: ["bookingReference", "companyName", "bookingStatus"],
|
||||||
|
});
|
||||||
|
|
||||||
const createReturnsMutation = useMutation({
|
const createReturnsMutation = useMutation({
|
||||||
mutationFn: async (payload: {
|
mutationFn: async (payload: {
|
||||||
trucks: Array<{
|
trucks: Array<{
|
||||||
@@ -332,9 +349,11 @@ export default function ContainerReturnsPage() {
|
|||||||
toast({ title: "Container returns recorded" });
|
toast({ title: "Container returns recorded" });
|
||||||
qc.invalidateQueries({ queryKey: ["container-returns", bookingIds] });
|
qc.invalidateQueries({ queryKey: ["container-returns", bookingIds] });
|
||||||
qc.invalidateQueries({ queryKey: ["empty-container-returns"] });
|
qc.invalidateQueries({ queryKey: ["empty-container-returns"] });
|
||||||
|
qc.invalidateQueries({ queryKey: ["empty-return-bookings"] });
|
||||||
setReturnModalOpen(false);
|
setReturnModalOpen(false);
|
||||||
setStandaloneModalOpen(false);
|
setStandaloneModalOpen(false);
|
||||||
setActiveKey(null);
|
setActiveKey(null);
|
||||||
|
setEmptyReturnBooking(null);
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
toast({
|
toast({
|
||||||
@@ -548,6 +567,163 @@ export default function ContainerReturnsPage() {
|
|||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
<Card withBorder radius="lg" p="md" mb="lg">
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group justify="space-between" align="flex-start">
|
||||||
|
<div>
|
||||||
|
<Text fw={600}>Bookings With Empty Container Return</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Bookings that ship with equipment return and still owe empties. Open one to pick
|
||||||
|
the containers coming back, then record the return for that booking.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Badge variant="light" size="lg">
|
||||||
|
{emptyReturnBookings.length} booking{emptyReturnBookings.length !== 1 ? "s" : ""}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<ListControls
|
||||||
|
search={bookingReturnControls.search}
|
||||||
|
onSearchChange={bookingReturnControls.setSearch}
|
||||||
|
searchPlaceholder="Search booking, company, status…"
|
||||||
|
dateFrom={bookingReturnControls.dateFrom}
|
||||||
|
onDateFromChange={bookingReturnControls.setDateFrom}
|
||||||
|
dateTo={bookingReturnControls.dateTo}
|
||||||
|
onDateToChange={bookingReturnControls.setDateTo}
|
||||||
|
showDateRange={false}
|
||||||
|
hasFilters={bookingReturnControls.hasFilters}
|
||||||
|
onReset={bookingReturnControls.reset}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{emptyReturnBookingsQuery.isLoading ? (
|
||||||
|
<Group justify="center" py="md">
|
||||||
|
<Loader size="sm" />
|
||||||
|
</Group>
|
||||||
|
) : emptyReturnBookingsQuery.isError ? (
|
||||||
|
<Alert color="red">
|
||||||
|
Could not load bookings with empty container return.{" "}
|
||||||
|
{extractErrorMessage(emptyReturnBookingsQuery.error)}
|
||||||
|
</Alert>
|
||||||
|
) : bookingReturnControls.pagedRows.length === 0 ? (
|
||||||
|
<Alert color="gray">
|
||||||
|
No booking is waiting on an empty container return.
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Table.ScrollContainer minWidth={900}>
|
||||||
|
<Table highlightOnHover verticalSpacing="xs">
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th w={40} />
|
||||||
|
<Table.Th>Booking Ref</Table.Th>
|
||||||
|
<Table.Th>Company</Table.Th>
|
||||||
|
<Table.Th>Booking Status</Table.Th>
|
||||||
|
<Table.Th>Containers To Return</Table.Th>
|
||||||
|
<Table.Th ta="right">Action</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{bookingReturnControls.pagedRows.map((booking) => {
|
||||||
|
const isOpen = expandedBooking === booking.bookingId;
|
||||||
|
return (
|
||||||
|
<Fragment key={booking.bookingId}>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
onClick={() =>
|
||||||
|
setExpandedBooking(isOpen ? null : booking.bookingId)
|
||||||
|
}
|
||||||
|
title={isOpen ? "Hide containers" : "Show containers"}
|
||||||
|
>
|
||||||
|
{isOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fw={600}>{booking.bookingReference}</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{booking.companyName ?? "—"}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge size="sm" variant="light">
|
||||||
|
{booking.bookingStatus.replaceAll("_", " ")}
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Badge color="orange">{booking.pendingCount} pending</Badge>
|
||||||
|
{booking.recordedCount > 0 && (
|
||||||
|
<Badge color="edr-green" variant="light">
|
||||||
|
{booking.recordedCount} recorded
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td ta="right">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => setEmptyReturnBooking(booking)}
|
||||||
|
>
|
||||||
|
Empty Container Return
|
||||||
|
</Button>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
{isOpen && (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={6}>
|
||||||
|
<Table striped>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Container</Table.Th>
|
||||||
|
<Table.Th>Size</Table.Th>
|
||||||
|
<Table.Th>Type</Table.Th>
|
||||||
|
<Table.Th>Return Status</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{booking.containers.map((container) => (
|
||||||
|
<Table.Tr key={container.key}>
|
||||||
|
<Table.Td>{container.containerNumber}</Table.Td>
|
||||||
|
<Table.Td>{container.containerSize ?? "—"}</Table.Td>
|
||||||
|
<Table.Td>{container.containerType ?? "—"}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{container.returnStatus ? (
|
||||||
|
<Badge size="sm" color="edr-green" variant="light">
|
||||||
|
{RETURN_STATUS_LABEL[container.returnStatus] ??
|
||||||
|
container.returnStatus}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge size="sm" color="orange" variant="light">
|
||||||
|
Awaiting return
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
|
<RuleEngineListFooter
|
||||||
|
pagination={bookingReturnControls.pagination}
|
||||||
|
pageCount={bookingReturnControls.pageCount}
|
||||||
|
totalCount={bookingReturnControls.totalCount}
|
||||||
|
itemLabel="bookings"
|
||||||
|
onPaginationChange={bookingReturnControls.setPagination}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{returnedContainers.length > 0 && (
|
{returnedContainers.length > 0 && (
|
||||||
<Card withBorder radius="lg" p="md" mb="lg">
|
<Card withBorder radius="lg" p="md" mb="lg">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
@@ -721,6 +897,13 @@ export default function ContainerReturnsPage() {
|
|||||||
loading={createReturnsMutation.isPending}
|
loading={createReturnsMutation.isPending}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<BookingEmptyReturnModal
|
||||||
|
booking={emptyReturnBooking}
|
||||||
|
onClose={() => setEmptyReturnBooking(null)}
|
||||||
|
onSubmit={(payload) => createReturnsMutation.mutate(payload)}
|
||||||
|
loading={createReturnsMutation.isPending}
|
||||||
|
/>
|
||||||
|
|
||||||
<BulkContainerReturnModal
|
<BulkContainerReturnModal
|
||||||
opened={bulkModalOpen}
|
opened={bulkModalOpen}
|
||||||
onClose={() => setBulkModalOpen(false)}
|
onClose={() => setBulkModalOpen(false)}
|
||||||
@@ -1028,6 +1211,297 @@ function ContainerReturnModal({ opened, onClose, group, onSubmit, loading }: Con
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface BookingEmptyReturnModalProps {
|
||||||
|
booking: EmptyReturnBooking | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (payload: any) => void;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the empty return for ONE booking: tick the containers coming back,
|
||||||
|
* say where they landed, and every tick becomes an empty container return on
|
||||||
|
* that booking. Containers whose return is already recorded stay visible but
|
||||||
|
* cannot be ticked again. A legacy booking that never captured container
|
||||||
|
* numbers shows numberless slots — the number is typed here instead.
|
||||||
|
*/
|
||||||
|
function BookingEmptyReturnModal({ booking, onClose, onSubmit, loading }: BookingEmptyReturnModalProps) {
|
||||||
|
const [selected, setSelected] = useState<string[]>([]);
|
||||||
|
const [returnedBy, setReturnedBy] = useState<"EDR" | "CUSTOMER" | null>(null);
|
||||||
|
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);
|
||||||
|
const [condition, setCondition] = useState<string>("");
|
||||||
|
const [handoverNote, setHandoverNote] = useState<string>("");
|
||||||
|
|
||||||
|
const bookingId = booking?.bookingId ?? null;
|
||||||
|
|
||||||
|
// A fresh booking starts from a clean form — never inherit the last one's
|
||||||
|
// ticks, typed numbers, or placement.
|
||||||
|
useEffect(() => {
|
||||||
|
setSelected([]);
|
||||||
|
setReturnedBy(null);
|
||||||
|
setReturnDate(localNowForInput());
|
||||||
|
setWarehouse(null);
|
||||||
|
setYardId(null);
|
||||||
|
setZoneId(null);
|
||||||
|
setCondition("");
|
||||||
|
setHandoverNote("");
|
||||||
|
}, [bookingId]);
|
||||||
|
|
||||||
|
const { data: warehousesResponse } = useQuery({
|
||||||
|
queryKey: ["warehouses-list"],
|
||||||
|
queryFn: async () => {
|
||||||
|
return await warehouseService.list({});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const warehouses = (warehousesResponse as any)?.data ?? warehousesResponse ?? [];
|
||||||
|
const { data: yards } = useWarehouseYards(warehouse ?? undefined);
|
||||||
|
const { data: zones } = useWarehouseZones(yardId ?? undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setYardId(null);
|
||||||
|
setZoneId(null);
|
||||||
|
}, [warehouse]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setZoneId(null);
|
||||||
|
}, [yardId]);
|
||||||
|
|
||||||
|
const warehouseOptions = Array.isArray(warehouses)
|
||||||
|
? warehouses.map((wh: any) => ({
|
||||||
|
value: wh.id,
|
||||||
|
label: `${wh.name} ${wh.code ? `(${wh.code})` : ""}`,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const yardOptions = (yards ?? [])
|
||||||
|
.filter((y) => y.status === "ACTIVE")
|
||||||
|
.map((y) => ({ value: y.id, label: `${y.name} (${y.code})` }));
|
||||||
|
|
||||||
|
const zoneOptions = (zones ?? [])
|
||||||
|
.filter((z) => z.status === "ACTIVE")
|
||||||
|
.map((z) => ({ value: z.id, label: `${z.name} (${z.code})` }));
|
||||||
|
|
||||||
|
const pending = (booking?.containers ?? []).filter((container) => !container.returnId);
|
||||||
|
|
||||||
|
const toggle = (key: string, checked: boolean) =>
|
||||||
|
setSelected((current) => (checked ? [...current, key] : current.filter((k) => k !== key)));
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!booking || !selected.length || !warehouse || !returnedBy) return;
|
||||||
|
|
||||||
|
const selectedWarehouse = Array.isArray(warehouses)
|
||||||
|
? warehouses.find((wh: any) => wh.id === warehouse)
|
||||||
|
: null;
|
||||||
|
const selectedYard = yards?.find((y) => y.id === yardId);
|
||||||
|
const selectedZone = zones?.find((z) => z.id === zoneId);
|
||||||
|
|
||||||
|
const containers = pending
|
||||||
|
.filter((container) => selected.includes(container.key))
|
||||||
|
.map((container) => ({
|
||||||
|
containerNumber: container.containerNumber,
|
||||||
|
// The booking records "20ft"/"40ft"; the wagon rule only needs the number.
|
||||||
|
containerSize: container.containerSize?.includes("40")
|
||||||
|
? ("40" as const)
|
||||||
|
: container.containerSize?.includes("20")
|
||||||
|
? ("20" as const)
|
||||||
|
: undefined,
|
||||||
|
returnDate,
|
||||||
|
warehouse: selectedWarehouse?.name || warehouse,
|
||||||
|
yard: selectedYard?.name,
|
||||||
|
zone: selectedZone?.name,
|
||||||
|
condition: condition || undefined,
|
||||||
|
handoverNote: handoverNote || undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
onSubmit({
|
||||||
|
trucks: [
|
||||||
|
{
|
||||||
|
bookingId: booking.bookingId,
|
||||||
|
customerId: booking.customerId,
|
||||||
|
companyName: booking.companyName ?? undefined,
|
||||||
|
returnType: returnedBy,
|
||||||
|
containers,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={!!booking}
|
||||||
|
onClose={onClose}
|
||||||
|
title="Empty Container Return"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{booking && (
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group gap="sm">
|
||||||
|
<Text fw={600}>{booking.bookingReference}</Text>
|
||||||
|
{booking.companyName && <Text c="dimmed">{booking.companyName}</Text>}
|
||||||
|
<Badge size="sm" variant="light">
|
||||||
|
{booking.bookingStatus.replaceAll("_", " ")}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Group justify="space-between" mb="xs">
|
||||||
|
<Text size="sm" fw={600}>
|
||||||
|
Containers to return
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
size="compact-xs"
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() =>
|
||||||
|
setSelected(
|
||||||
|
selected.length === pending.length ? [] : pending.map((c) => c.key),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{selected.length === pending.length ? "Clear all" : "Select all"}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th w={40} />
|
||||||
|
<Table.Th>Container</Table.Th>
|
||||||
|
<Table.Th w={90}>Size</Table.Th>
|
||||||
|
<Table.Th w={150}>Status</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{booking.containers.map((container) => {
|
||||||
|
const recorded = Boolean(container.returnId);
|
||||||
|
return (
|
||||||
|
<Table.Tr key={container.key}>
|
||||||
|
<Table.Td>
|
||||||
|
<Checkbox
|
||||||
|
checked={selected.includes(container.key)}
|
||||||
|
disabled={recorded}
|
||||||
|
onChange={(e) => toggle(container.key, e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm">{container.containerNumber}</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{container.containerSize ?? "—"}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{recorded ? (
|
||||||
|
<Badge size="sm" color="edr-green" variant="light">
|
||||||
|
{(container.returnStatus &&
|
||||||
|
RETURN_STATUS_LABEL[container.returnStatus]) ??
|
||||||
|
"Recorded"}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge size="sm" color="orange" variant="light">
|
||||||
|
Awaiting return
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Returned By"
|
||||||
|
placeholder="Select truck type"
|
||||||
|
value={returnedBy}
|
||||||
|
onChange={(val) => setReturnedBy(val as "EDR" | "CUSTOMER" | null)}
|
||||||
|
data={[
|
||||||
|
{ value: "EDR", label: "EDR Last Mile" },
|
||||||
|
{ value: "CUSTOMER", label: "Customer Self-Haul" },
|
||||||
|
]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Return Warehouse"
|
||||||
|
placeholder="Select warehouse for container return"
|
||||||
|
value={warehouse}
|
||||||
|
onChange={setWarehouse}
|
||||||
|
data={warehouseOptions}
|
||||||
|
required
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Yard"
|
||||||
|
placeholder={warehouse ? "Select yard" : "Select warehouse first"}
|
||||||
|
value={yardId}
|
||||||
|
onChange={setYardId}
|
||||||
|
data={yardOptions}
|
||||||
|
disabled={!warehouse}
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Zone"
|
||||||
|
placeholder={yardId ? "Select zone" : "Select yard first"}
|
||||||
|
value={zoneId}
|
||||||
|
onChange={setZoneId}
|
||||||
|
data={zoneOptions}
|
||||||
|
disabled={!yardId}
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
placeholder="Damage, residue, or cleanliness notes"
|
||||||
|
value={condition}
|
||||||
|
onChange={(e) => setCondition(e.currentTarget.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Handover Note"
|
||||||
|
placeholder="Consignee, trucker, or authorization notes"
|
||||||
|
value={handoverNote}
|
||||||
|
onChange={(e) => setHandoverNote(e.currentTarget.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="flex-end" gap="sm">
|
||||||
|
<Button variant="default" onClick={onClose} disabled={loading}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!selected.length || !warehouse || !returnedBy}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
Record Empty Return ({selected.length})
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface StandaloneReturnModalProps {
|
interface StandaloneReturnModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
LoadEmptyContainersOnTrainPayload,
|
LoadEmptyContainersOnTrainPayload,
|
||||||
DjiboutiIncident,
|
DjiboutiIncident,
|
||||||
EmptyContainerReturn,
|
EmptyContainerReturn,
|
||||||
|
EmptyReturnBooking,
|
||||||
ImportCustomsFinalization,
|
ImportCustomsFinalization,
|
||||||
ImportOperationActionPayload,
|
ImportOperationActionPayload,
|
||||||
RecordDeclarationPayload,
|
RecordDeclarationPayload,
|
||||||
@@ -114,6 +115,14 @@ export const importOperationsService = {
|
|||||||
return unwrap(response.data);
|
return unwrap(response.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Bookings that ship with empty-container return and still owe empties back. */
|
||||||
|
listEmptyReturnBookings: async (): Promise<EmptyReturnBooking[]> => {
|
||||||
|
const response = await client.get<EmptyReturnBooking[]>(
|
||||||
|
URL_CONSTANTS.IMPORT_OPERATIONS.EMPTY_RETURN_BOOKINGS,
|
||||||
|
);
|
||||||
|
return unwrap(response.data);
|
||||||
|
},
|
||||||
|
|
||||||
createEmptyReturn: async (
|
createEmptyReturn: async (
|
||||||
payload: CreateEmptyContainerReturnPayload,
|
payload: CreateEmptyContainerReturnPayload,
|
||||||
): Promise<EmptyContainerReturn> => {
|
): Promise<EmptyContainerReturn> => {
|
||||||
|
|||||||
@@ -121,6 +121,33 @@ export interface EmptyContainerReturn {
|
|||||||
|
|
||||||
export type EmptyContainerSize = '20' | '40';
|
export type EmptyContainerSize = '20' | '40';
|
||||||
|
|
||||||
|
/** One container a with-return booking owes back empty. */
|
||||||
|
export interface EmptyReturnBookingContainer {
|
||||||
|
/** Stable row key — the booking container unit id. */
|
||||||
|
key: string;
|
||||||
|
unitId: string;
|
||||||
|
containerNumber: string;
|
||||||
|
containerSize: string | null;
|
||||||
|
containerType: string | null;
|
||||||
|
/** Set once this container's empty return has been recorded. */
|
||||||
|
returnId: string | null;
|
||||||
|
returnStatus: EmptyContainerReturnStatus | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A booking that ships with empty-container return and still owes empties. */
|
||||||
|
export interface EmptyReturnBooking {
|
||||||
|
bookingId: string;
|
||||||
|
bookingReference: string;
|
||||||
|
bookingStatus: string;
|
||||||
|
equipmentReturn: string;
|
||||||
|
customerId: string | null;
|
||||||
|
companyName: string | null;
|
||||||
|
containers: EmptyReturnBookingContainer[];
|
||||||
|
expectedCount: number;
|
||||||
|
recordedCount: number;
|
||||||
|
pendingCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateEmptyContainerReturnPayload {
|
export interface CreateEmptyContainerReturnPayload {
|
||||||
containerNumber: string;
|
containerNumber: string;
|
||||||
containerSize?: EmptyContainerSize;
|
containerSize?: EmptyContainerSize;
|
||||||
|
|||||||
Reference in New Issue
Block a user