merge conflict resolved

This commit is contained in:
marshal
2026-09-02 22:35:18 +00:00
376 changed files with 26459 additions and 2649 deletions

View File

@@ -13,6 +13,7 @@ import type {
CustomerBooking,
CustomerDocument,
CustomerPayment,
CustomerAccount,
CustomerResetTarget,
PaginatedCompanies,
ProfileStatus,
@@ -84,6 +85,7 @@ import type {
UpdateCheckpointPayload,
DispatchSchedulePayload,
StaffBookingWindow,
MarshallingStop,
ScheduleMergePreview,
TrainScheduleDetail,
TrainScheduleFilters,
@@ -151,6 +153,15 @@ import type {
WarehouseLoading,
WarehouseYard,
WarehouseZone,
ZoneContentItem,
ZoneLayout,
ZoneSlotSummary,
WarehouseZoneStack,
WarehouseZoneSlot,
SaveStackPayload,
AvailableSlot,
ContainerAccessibility,
FindSlotPayload,
} from "@/types/warehouse";
import { endpoint } from "@/utils/endpoint";
import {
@@ -285,6 +296,11 @@ const INVENTORY_INVALIDATIONS: ReadonlyArray<readonly unknown[]> = [
const TRAIN_SCHEDULING_INVALIDATIONS: ReadonlyArray<readonly unknown[]> = [
QUERY_KEYS.TRAIN_SCHEDULING.ROOT,
QUERY_KEYS.BOOKINGS.ROOT,
// A station's loading window gates the warehouse loading queues too — they
// render the same Start/End controls, so they must refresh on the same click.
["loadable-trains"],
["train-loadable-items"],
["warehouse-inventory"],
];
/**
@@ -595,6 +611,13 @@ export const api = {
({ id }) => QUERY_KEYS.TRAIN_SCHEDULING.track(id),
),
marshallingStops: endpoint<{ id: string }, MarshallingStop[]>(
"train-scheduling",
"marshalling-stops",
({ id }) => trainSchedulingService.getMarshallingStops(id),
({ id }) => QUERY_KEYS.TRAIN_SCHEDULING.marshallingStops(id),
),
unassignedBookings: endpoint<
{ scheduleId: string },
UnassignedBookingsResponse
@@ -1215,6 +1238,14 @@ export const api = {
() => [["warehouses"], ["warehouse-yards"]],
),
deleteYard: endpoint<{ id: string }, unknown>(
"warehouses",
"deleteYard",
({ id }) => warehouseService.removeYard(id).then((r) => r.data),
undefined,
() => [["warehouses"], ["warehouse-yards"]],
),
// ── Zones ──────────────────────────────────────────────────────────────
listZones: endpoint<{ yardId: string }, WarehouseZone[]>(
"warehouses",
@@ -1247,6 +1278,100 @@ export const api = {
() => [["warehouse-yards"]],
),
zoneContents: endpoint<{ zoneId: string }, ZoneContentItem[]>(
"warehouse-zones",
"contents",
({ zoneId }) => warehouseService.zoneContents(zoneId).then((r) => r.data),
({ zoneId }) => ["warehouse-zones", zoneId, "contents"],
),
zoneLayout: endpoint<{ zoneId: string }, ZoneLayout>(
"warehouse-zones",
"layout",
({ zoneId }) => warehouseService.zoneLayout(zoneId).then((r) => r.data),
({ zoneId }) => ["warehouse-zones", zoneId, "layout"],
),
zoneSlotSummary: endpoint<{ zoneId: string }, ZoneSlotSummary>(
"warehouse-zones",
"slot-summary",
({ zoneId }) => warehouseService.zoneSlotSummary(zoneId).then((r) => r.data),
({ zoneId }) => ["warehouse-zones", zoneId, "slot-summary"],
),
// ── Ground stacks and slots ────────────────────────────────────────────
listStacks: endpoint<{ zoneId: string }, WarehouseZoneStack[]>(
"warehouse-zone-stacks",
"list",
({ zoneId }) => warehouseService.listStacks(zoneId).then((r) => r.data),
({ zoneId }) => ["warehouse-zone-stacks", zoneId],
),
createStack: endpoint<
{ zoneId: string; payload: SaveStackPayload },
WarehouseZoneStack
>(
"warehouse-zone-stacks",
"create",
({ zoneId, payload }) =>
warehouseService.createStack(zoneId, payload).then((r) => r.data),
undefined,
({ zoneId }) => [
["warehouse-zone-stacks", zoneId],
["warehouse-zones", zoneId, "layout"],
["warehouse-zones", zoneId, "slot-summary"],
],
),
updateStack: endpoint<
{ id: string; zoneId: string; payload: Partial<SaveStackPayload> },
WarehouseZoneStack
>(
"warehouse-zone-stacks",
"update",
({ id, payload }) => warehouseService.updateStack(id, payload).then((r) => r.data),
undefined,
({ zoneId }) => [
["warehouse-zone-stacks", zoneId],
["warehouse-zones", zoneId, "layout"],
["warehouse-zones", zoneId, "slot-summary"],
],
),
deleteStack: endpoint<{ id: string; zoneId: string }, unknown>(
"warehouse-zone-stacks",
"delete",
({ id }) => warehouseService.removeStack(id).then((r) => r.data),
undefined,
({ zoneId }) => [
["warehouse-zone-stacks", zoneId],
["warehouse-zones", zoneId, "layout"],
["warehouse-zones", zoneId, "slot-summary"],
],
),
updateSlot: endpoint<
{ slotId: string; zoneId: string; payload: { status?: string; isActive?: boolean } },
WarehouseZoneSlot
>(
"warehouse-zone-stacks",
"update-slot",
({ slotId, payload }) => warehouseService.updateSlot(slotId, payload).then((r) => r.data),
undefined,
({ zoneId }) => [
["warehouse-zones", zoneId, "layout"],
["warehouse-zones", zoneId, "slot-summary"],
],
),
deleteZone: endpoint<{ id: string }, unknown>(
"warehouses",
"deleteZone",
({ id }) => warehouseService.removeZone(id).then((r) => r.data),
undefined,
() => [["warehouses"], ["warehouse-yards"]],
),
// ── Inventory (queries) ────────────────────────────────────────────────
listInventory: endpoint<
{ filter?: InventoryFilter },
@@ -1470,7 +1595,7 @@ export const api = {
"store",
({ id, payload }) => warehouseService.store(id, payload).then((r) => r.data),
undefined,
() => INVENTORY_INVALIDATIONS,
() => [...INVENTORY_INVALIDATIONS, ["warehouse-zones"]],
),
reserve: endpoint<ReserveInventoryPayload, WarehouseInventoryItem>(
@@ -1509,6 +1634,36 @@ export const api = {
() => INVENTORY_INVALIDATIONS,
),
findAvailableSlot: endpoint<FindSlotPayload, AvailableSlot | null>(
"warehouse-inventory",
"find-slot",
(payload) => warehouseService.findAvailableSlot(payload).then((r) => r.data),
(payload) => ["warehouse-inventory", "find-slot", payload],
),
containerAccessibility: endpoint<{ id: string }, ContainerAccessibility>(
"warehouse-inventory",
"accessibility",
({ id }) => warehouseService.containerAccessibility(id).then((r) => r.data),
({ id }) => ["warehouse-inventory", id, "accessibility"],
),
assignSlot: endpoint<{ id: string; slotId?: string }, WarehouseInventoryItem>(
"warehouse-inventory",
"assign-slot",
({ id, slotId }) => warehouseService.assignSlot(id, slotId).then((r) => r.data),
undefined,
() => [...INVENTORY_INVALIDATIONS, ["warehouse-zones"]],
),
releaseSlot: endpoint<{ id: string }, WarehouseInventoryItem>(
"warehouse-inventory",
"release-slot",
({ id }) => warehouseService.releaseSlot(id).then((r) => r.data),
undefined,
() => [...INVENTORY_INVALIDATIONS, ["warehouse-zones"]],
),
move: endpoint<
{ id: string; payload: MoveInventoryPayload },
WarehouseInventoryItem
@@ -1518,7 +1673,7 @@ export const api = {
({ id, payload }) =>
warehouseService.move(id, payload).then((r) => r.data),
undefined,
() => INVENTORY_INVALIDATIONS,
() => [...INVENTORY_INVALIDATIONS, ["warehouse-zones"]],
),
markReadyForPickup: endpoint<string, WarehouseInventoryItem>(
@@ -3267,6 +3422,13 @@ export const api = {
({ id }) => QUERY_KEYS.CUSTOMERS.payments(id),
),
accounts: endpoint<{ companyId: string }, CustomerAccount[]>(
"customers",
"accounts",
({ companyId }) => customersService.accounts(companyId),
({ companyId }) => QUERY_KEYS.CUSTOMERS.accounts(companyId),
),
resetTarget: endpoint<{ companyId: string }, CustomerResetTarget>(
"customers",
"resetTarget",

View File

@@ -73,6 +73,18 @@ export interface BookingListFilter {
freightType?: string;
/** Service type (rule-engine service_types.id). */
serviceTypeId?: string;
/** Cargo type OR cargo group — a group matches every commodity beneath it. */
cargoTypeId?: string;
/** Contains-search over content: cargo description, commodity, container types. */
cargoText?: string;
/** Bookings carrying this container type; also scopes containersMin/Max to it. */
containerTypeId?: string;
/** Container count bounds — of containerTypeId when set, else of all types. */
containersMin?: string;
containersMax?: string;
/** Bounds on containers declared on the shipment request behind the booking. */
requestedContainersMin?: string;
requestedContainersMax?: string;
/** ONE_TIME | GENERAL_CONTRACT — the booking-kind tab filter. */
bookingType?: string;
/** 'true' → customs bookings, 'false' → self-clearance (non-customs). */
@@ -199,6 +211,15 @@ export const bookingsService = {
if (filter.companyId) params.companyId = filter.companyId;
if (filter.freightType) params.freightType = filter.freightType;
if (filter.serviceTypeId) params.serviceTypeId = filter.serviceTypeId;
if (filter.cargoTypeId) params.cargoTypeId = filter.cargoTypeId;
if (filter.cargoText) params.cargoText = filter.cargoText;
if (filter.containerTypeId) params.containerTypeId = filter.containerTypeId;
if (filter.containersMin) params.containersMin = filter.containersMin;
if (filter.containersMax) params.containersMax = filter.containersMax;
if (filter.requestedContainersMin)
params.requestedContainersMin = filter.requestedContainersMin;
if (filter.requestedContainersMax)
params.requestedContainersMax = filter.requestedContainersMax;
if (filter.bookingType) params.bookingType = filter.bookingType;
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
if (filter.paymentCurrency)
@@ -238,6 +259,15 @@ export const bookingsService = {
if (filter.contractId) params.contractId = filter.contractId;
if (filter.freightType) params.freightType = filter.freightType;
if (filter.serviceTypeId) params.serviceTypeId = filter.serviceTypeId;
if (filter.cargoTypeId) params.cargoTypeId = filter.cargoTypeId;
if (filter.cargoText) params.cargoText = filter.cargoText;
if (filter.containerTypeId) params.containerTypeId = filter.containerTypeId;
if (filter.containersMin) params.containersMin = filter.containersMin;
if (filter.containersMax) params.containersMax = filter.containersMax;
if (filter.requestedContainersMin)
params.requestedContainersMin = filter.requestedContainersMin;
if (filter.requestedContainersMax)
params.requestedContainersMax = filter.requestedContainersMax;
if (filter.bookingType) params.bookingType = filter.bookingType;
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
if (filter.paymentCurrency)

View File

@@ -10,6 +10,7 @@ import type {
CustomerBooking,
CustomerDocument,
CustomerPayment,
CustomerAccount,
CustomerResetTarget,
PaginatedCompanies,
ProfileStatus,
@@ -90,6 +91,18 @@ export const customersService = {
.then((r) => r.data);
},
/**
* Every portal login belonging to this customer, primary contact first.
*
* Not filtered to active accounts — a suspended or never-activated login is
* exactly what staff are checking when a customer says they cannot sign in.
*/
accounts(companyId: string): Promise<CustomerAccount[]> {
return apiClient
.get<CustomerAccount[]>(URL_CONSTANTS.COMPANIES.ACCOUNTS(companyId))
.then((r) => r.data);
},
/**
* The IAM account a reset link would go to. Read before offering the action
* so staff see the credentials the link actually reaches, not the company's

View File

@@ -0,0 +1,62 @@
import { api as client } from '../auth/http';
import { URL_CONSTANTS } from '@/constants/URLS';
import { unwrap } from '@/utils/endpoint';
import type {
EmptyReturnQuote,
EmptyReturnRequest,
EmptyReturnRequestStatus,
PlannedEmptyReturn,
} from '@/types/importOperations';
/**
* Empty container return requests — the customer-initiated path for a booking
* that was sold WITHOUT the return service. Staff price and approve them here;
* the customer pays and books the truck from the portal.
*/
export const emptyReturnRequestsService = {
list: async (params: {
status?: EmptyReturnRequestStatus;
bookingId?: string;
} = {}): Promise<EmptyReturnRequest[]> => {
const response = await client.get<EmptyReturnRequest[]>(
URL_CONSTANTS.EMPTY_RETURN_REQUESTS.BASE,
{ params },
);
return unwrap(response.data);
},
/** Scheduled returns the warehouse is expecting, with date and truck. */
planned: async (): Promise<PlannedEmptyReturn[]> => {
const response = await client.get<PlannedEmptyReturn[]>(
URL_CONSTANTS.EMPTY_RETURN_REQUESTS.PLANNED,
);
return unwrap(response.data);
},
/** The route price staff see prefilled at approval. */
quote: async (bookingId: string): Promise<EmptyReturnQuote> => {
const response = await client.get<{ quote: EmptyReturnQuote }>(
URL_CONSTANTS.EMPTY_RETURN_REQUESTS.ELIGIBILITY(bookingId),
);
return unwrap(response.data).quote;
},
approve: async (
id: string,
payload: { unitAmount?: number; currency?: string } = {},
): Promise<EmptyReturnRequest> => {
const response = await client.post<EmptyReturnRequest>(
URL_CONSTANTS.EMPTY_RETURN_REQUESTS.APPROVE(id),
payload,
);
return unwrap(response.data);
},
reject: async (id: string, reason: string): Promise<EmptyReturnRequest> => {
const response = await client.post<EmptyReturnRequest>(
URL_CONSTANTS.EMPTY_RETURN_REQUESTS.REJECT(id),
{ reason },
);
return unwrap(response.data);
},
};

View File

@@ -8,6 +8,7 @@ import type {
LoadEmptyContainersOnTrainPayload,
DjiboutiIncident,
EmptyContainerReturn,
EmptyReturnBooking,
ImportCustomsFinalization,
ImportOperationActionPayload,
RecordDeclarationPayload,
@@ -114,6 +115,14 @@ export const importOperationsService = {
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 (
payload: CreateEmptyContainerReturnPayload,
): Promise<EmptyContainerReturn> => {
@@ -124,6 +133,17 @@ export const importOperationsService = {
return unwrap(response.data);
},
/** Backfill empties already in the yard. All-or-nothing on the server. */
bulkCreateEmptyReturns: async (
returns: CreateEmptyContainerReturnPayload[],
): Promise<EmptyContainerReturn[]> => {
const response = await client.post<EmptyContainerReturn[]>(
URL_CONSTANTS.IMPORT_OPERATIONS.EMPTY_CONTAINER_RETURNS_BULK,
{ returns },
);
return unwrap(response.data);
},
loadEmptyContainersOnTrain: async (
payload: LoadEmptyContainersOnTrainPayload,
): Promise<EmptyContainerReturn[]> => {

View File

@@ -33,6 +33,7 @@ import type {
UpdateCheckpointPayload,
DispatchSchedulePayload,
StaffBookingWindow,
MarshallingStop,
ScheduleMergePreview,
TrainScheduleDetail,
UpdateScheduleWindowRulePayload,
@@ -765,6 +766,24 @@ export const trainSchedulingService = {
return response.data;
},
getMarshallingStops: async (scheduleId: string): Promise<MarshallingStop[]> => {
const response = await client.get<MarshallingStop[]>(
URL_CONSTANTS.TRAIN_SCHEDULING.MARSHALLING_STOPS(scheduleId),
);
return unwrap(response.data);
},
downloadMarshallingDocumentAt: async (
scheduleId: string,
stopIndex: number,
): Promise<Blob> => {
const response = await client.get(
URL_CONSTANTS.TRAIN_SCHEDULING.MARSHALLING_DOCUMENT_AT(scheduleId, stopIndex),
{ responseType: "blob" },
);
return response.data;
},
getTrack: async (scheduleId: string): Promise<TrainTrackResponse> => {
const response = await client.get<TrainTrackResponse>(
URL_CONSTANTS.TRAIN_SCHEDULING.CHECKPOINTS(scheduleId),

View File

@@ -5,6 +5,14 @@ import { api as apiClient } from '../auth/http';
import { URL_CONSTANTS } from '@/constants/URLS';
import type {
ZoneOccupancy,
ZoneLayout,
ZoneSlotSummary,
WarehouseZoneStack,
WarehouseZoneSlot,
SaveStackPayload,
AvailableSlot,
ContainerAccessibility,
FindSlotPayload,
TruckOnSite,
WarehouseOpsStats,
WarehouseThroughputPoint,
@@ -41,6 +49,7 @@ import type {
MoveInventoryPayload,
StoreInventoryPayload,
ReceiveInventoryPayload,
RegisterBacklogContainerPayload,
ReleaseOrderPayload,
DeliverInventoryPayload,
EligibleBooking,
@@ -71,7 +80,9 @@ import type {
WarehouseLoading,
WarehouseYard,
WarehouseZone,
ZoneContentItem,
} from '@/types/warehouse';
import type { StationWorkLog } from '@/types/trainScheduling';
export type ContainerItemStage = 'PENDING' | 'RECEIVED' | 'GRN' | 'ASSIGNED' | 'LOADED' | 'LEFT' | 'DELIVERED';
@@ -105,6 +116,10 @@ export interface LoadableTrain {
departureTime: string | null;
readyCount: number;
loadedCount: number;
/** freight.yards.id the train departs from. */
originStationId: string | null;
/** The schedule's per-yard loading/unloading windows — same store the train schedule page writes. */
stationWorkLogs: Record<string, StationWorkLog> | null;
}
/** A container/cargo inventory item assigned to a train, with its allocated wagon. */
@@ -122,6 +137,11 @@ export interface TrainLoadableItem {
wagonId: string | null;
wagonNumber: string | null;
sequenceNo: number | null;
/** The booking's boarding yard — the yard whose loading window gates this item. */
originYardId: string | null;
originYardLabel: string | null;
/** True once "Start loading" was clicked for this item's boarding yard on this train. */
loadingWindowStarted: boolean;
loadable: boolean;
}
@@ -186,7 +206,7 @@ export const warehouseService = {
/** A booking's containers with VGM cargo weight (tonnes) for exit weighing. */
getContainerWeights: async (
bookingId: string,
): Promise<Array<{ containerNumber: string; weightTons: number }>> => {
): Promise<Array<{ containerNumber: string; weightTons: number; containerSize: string | null }>> => {
const { data } = await apiClient.get(
`/warehouse-inventory/bookings/${bookingId}/container-weights`,
);
@@ -257,6 +277,7 @@ export const warehouseService = {
apiClient.post<Warehouse>(URL_CONSTANTS.WAREHOUSES.BASE, payload),
update: (id: string, payload: Partial<SaveWarehousePayload>) =>
apiClient.patch<Warehouse>(URL_CONSTANTS.WAREHOUSES.BY_ID(id), payload),
remove: (id: string) => apiClient.delete(URL_CONSTANTS.WAREHOUSES.BY_ID(id)),
// Yards list now returns the standard paginated envelope ({ items, meta }).
listFacilities: () =>
apiClient.get<{ items: WarehouseFacility[] }>(URL_CONSTANTS.RULE_ENGINE.YARDS, {
@@ -272,6 +293,7 @@ export const warehouseService = {
getYard: (id: string) => apiClient.get<WarehouseYard>(URL_CONSTANTS.WAREHOUSE_YARDS.BY_ID(id)),
updateYard: (id: string, payload: Partial<SaveYardPayload>) =>
apiClient.patch<WarehouseYard>(URL_CONSTANTS.WAREHOUSE_YARDS.BY_ID(id), payload),
removeYard: (id: string) => apiClient.delete(URL_CONSTANTS.WAREHOUSE_YARDS.BY_ID(id)),
// ── Zones ──────────────────────────────────────────────────────────────
listZones: (yardId: string) =>
@@ -282,6 +304,40 @@ export const warehouseService = {
getZone: (id: string) => apiClient.get<WarehouseZone>(URL_CONSTANTS.WAREHOUSE_ZONES.BY_ID(id)),
updateZone: (id: string, payload: Partial<SaveZonePayload>) =>
apiClient.patch<WarehouseZone>(URL_CONSTANTS.WAREHOUSE_ZONES.BY_ID(id), payload),
removeZone: (id: string) => apiClient.delete(URL_CONSTANTS.WAREHOUSE_ZONES.BY_ID(id)),
zoneContents: (zoneId: string) =>
apiClient.get<ZoneContentItem[]>(`${URL_CONSTANTS.WAREHOUSE_ZONES.BY_ID(zoneId)}/contents`),
zoneLayout: (zoneId: string) =>
apiClient.get<ZoneLayout>(URL_CONSTANTS.WAREHOUSE_ZONES.LAYOUT(zoneId)),
zoneSlotSummary: (zoneId: string) =>
apiClient.get<ZoneSlotSummary>(URL_CONSTANTS.WAREHOUSE_ZONES.SLOT_SUMMARY(zoneId)),
// ── Ground stacks and slots ────────────────────────────────────────────
listStacks: (zoneId: string) =>
apiClient.get<WarehouseZoneStack[]>(URL_CONSTANTS.WAREHOUSE_ZONE_STACKS.BY_ZONE(zoneId)),
createStack: (zoneId: string, payload: SaveStackPayload) =>
apiClient.post<WarehouseZoneStack>(URL_CONSTANTS.WAREHOUSE_ZONE_STACKS.BASE, {
...payload,
zoneId,
}),
updateStack: (id: string, payload: Partial<SaveStackPayload>) =>
apiClient.patch<WarehouseZoneStack>(URL_CONSTANTS.WAREHOUSE_ZONE_STACKS.BY_ID(id), payload),
removeStack: (id: string) =>
apiClient.delete(URL_CONSTANTS.WAREHOUSE_ZONE_STACKS.BY_ID(id)),
updateSlot: (slotId: string, payload: { status?: string; isActive?: boolean }) =>
apiClient.patch<WarehouseZoneSlot>(URL_CONSTANTS.WAREHOUSE_ZONE_STACKS.SLOT(slotId), payload),
// ── Physical placement ─────────────────────────────────────────────────
findAvailableSlot: (payload: FindSlotPayload) =>
apiClient.post<AvailableSlot | null>(URL_CONSTANTS.WAREHOUSE_INVENTORY.FIND_SLOT, payload),
assignSlot: (id: string, slotId?: string) =>
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.ASSIGN_SLOT(id), {
slotId,
}),
releaseSlot: (id: string) =>
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.RELEASE_SLOT(id), {}),
containerAccessibility: (id: string) =>
apiClient.get<ContainerAccessibility>(URL_CONSTANTS.WAREHOUSE_INVENTORY.ACCESSIBILITY(id)),
// ── Inventory ──────────────────────────────────────────────────────────
listInventory: (filter?: InventoryFilter) =>
@@ -351,6 +407,20 @@ export const warehouseService = {
// ── Receive (Import/Export bulk) ─────────────────────────────────────────
eligibleBookings: (direction?: 'IMPORT' | 'EXPORT') =>
apiClient.get<EligibleBooking[]>(URL_CONSTANTS.WAREHOUSE_INVENTORY.ELIGIBLE_BOOKINGS(direction)),
/** Register one loaded container already in the yard but never entered. */
registerBacklogContainer: (payload: RegisterBacklogContainerPayload) =>
apiClient.post<WarehouseInventoryItem>(
URL_CONSTANTS.WAREHOUSE_INVENTORY.REGISTER_BACKLOG,
payload,
),
/** Bulk backlog registration — all-or-nothing on the server. */
registerBacklogContainersBulk: (containers: RegisterBacklogContainerPayload[]) =>
apiClient.post<WarehouseInventoryItem[]>(
URL_CONSTANTS.WAREHOUSE_INVENTORY.REGISTER_BACKLOG_BULK,
{ containers },
),
receiveBulk: (payload: BulkReceivePayload) =>
apiClient.post<BulkReceiveResult>(URL_CONSTANTS.WAREHOUSE_INVENTORY.RECEIVE_BULK, payload),
bulkMarkInspected: (payload: BulkInspectPayload) =>