2026-08-10 15:36:53 +08:00
|
|
|
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
2026-08-13 12:22:41 +08:00
|
|
|
|
|
|
|
|
export interface CustomerContainerTransitSummary {
|
|
|
|
|
customerId: string
|
|
|
|
|
containerCount: number
|
|
|
|
|
totalTeu: number | string
|
|
|
|
|
unknownContainerCount: number
|
|
|
|
|
asOf: string
|
|
|
|
|
definition: string
|
|
|
|
|
}
|
2026-08-10 15:36:53 +08:00
|
|
|
import type {
|
|
|
|
|
ContainerBookingResource,
|
|
|
|
|
ContainerPage,
|
|
|
|
|
ContainerQuery,
|
|
|
|
|
ContainerSaveRequest,
|
2026-08-14 20:59:19 +08:00
|
|
|
ContainerSimulationCreateRequest,
|
|
|
|
|
ContainerOceanBookingLinkRequest,
|
2026-08-10 15:36:53 +08:00
|
|
|
SmartLoadingCandidate,
|
|
|
|
|
SmartLoadingItem,
|
|
|
|
|
SmartLoadingPlan,
|
2026-08-13 11:57:44 +08:00
|
|
|
BusinessDocument,
|
|
|
|
|
ContainerDocumentType,
|
|
|
|
|
ContainerOverview,
|
|
|
|
|
ContainerOverviewDocument,
|
2026-08-10 15:36:53 +08:00
|
|
|
} from '../types'
|
|
|
|
|
|
|
|
|
|
const containerPath = '/platform/v1/containers'
|
|
|
|
|
const smartLoadingPath = '/platform/v1/first-leg/smart-loading'
|
|
|
|
|
|
|
|
|
|
function id(value: unknown): string { return value == null ? '' : String(value) }
|
|
|
|
|
function optionalId(value: unknown): string | null { return value == null ? null : String(value) }
|
|
|
|
|
function numberValue(value: unknown, fallback = 0): number {
|
|
|
|
|
const parsed = Number(value)
|
|
|
|
|
return Number.isFinite(parsed) ? parsed : fallback
|
|
|
|
|
}
|
|
|
|
|
function operationId(prefix: string) { return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}` }
|
2026-08-13 11:57:44 +08:00
|
|
|
function document(raw: BusinessDocument | null | undefined): BusinessDocument | null {
|
|
|
|
|
if (!raw) return null
|
|
|
|
|
return { ...raw, id: raw.id == null ? null : id(raw.id), fileId: id(raw.fileId), uploadedBy: raw.uploadedBy == null ? null : id(raw.uploadedBy), completedBy: raw.completedBy == null ? null : id(raw.completedBy), version: numberValue(raw.version) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function overviewDocument(raw: ContainerOverviewDocument): ContainerOverviewDocument {
|
|
|
|
|
return { ...raw, id: id(raw.id), fileId: id(raw.fileId), uploadedBy: optionalId(raw.uploadedBy), version: numberValue(raw.version) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function overview(raw: ContainerOverview): ContainerOverview {
|
|
|
|
|
return {
|
|
|
|
|
...raw,
|
|
|
|
|
container: { ...raw.container, id: id(raw.container.id), warehouseId: id(raw.container.warehouseId), version: numberValue(raw.container.version) },
|
|
|
|
|
loadingPlanId: optionalId(raw.loadingPlanId),
|
|
|
|
|
pda: raw.pda ? {
|
|
|
|
|
...raw.pda, taskId: id(raw.pda.taskId), loadingPlanId: id(raw.pda.loadingPlanId),
|
|
|
|
|
containerBookingId: id(raw.pda.containerBookingId), taskVersion: numberValue(raw.pda.taskVersion),
|
|
|
|
|
itemCount: numberValue(raw.pda.itemCount), completedItemCount: numberValue(raw.pda.completedItemCount),
|
|
|
|
|
unresolvedItemCount: numberValue(raw.pda.unresolvedItemCount),
|
|
|
|
|
photoCounts: (raw.pda.photoCounts || []).map((item) => ({ ...item, count: numberValue(item.count) })),
|
|
|
|
|
photos: (raw.pda.photos || []).map((item) => ({ ...item, evidenceId: id(item.evidenceId), tenantId: id(item.tenantId), inboundId: id(item.inboundId), fileId: id(item.fileId) })),
|
|
|
|
|
exceptionReasons: raw.pda.exceptionReasons || [],
|
|
|
|
|
} : null,
|
|
|
|
|
documents: (raw.documents || []).map(overviewDocument),
|
|
|
|
|
exception: { ...raw.exception, markedBy: optionalId(raw.exception?.markedBy) },
|
|
|
|
|
exceptionHistory: (raw.exceptionHistory || []).map((entry) => ({ ...entry, id: id(entry.id), operatorId: optionalId(entry.operatorId) })),
|
|
|
|
|
timeline: (raw.timeline || []).map((entry) => ({ ...entry, operatorId: optionalId(entry.operatorId) })),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-10 15:36:53 +08:00
|
|
|
|
|
|
|
|
function container(raw: ContainerBookingResource): ContainerBookingResource {
|
|
|
|
|
return {
|
|
|
|
|
...raw,
|
|
|
|
|
id: id(raw.id),
|
|
|
|
|
planId: optionalId(raw.planId),
|
2026-08-17 19:57:32 +08:00
|
|
|
planNo: raw.planNo || null,
|
2026-08-17 13:24:55 +08:00
|
|
|
mergedIntoContainerBookingId: optionalId(raw.mergedIntoContainerBookingId),
|
2026-08-10 15:36:53 +08:00
|
|
|
warehouseId: id(raw.warehouseId),
|
2026-08-14 20:59:19 +08:00
|
|
|
oceanBookingId: optionalId(raw.oceanBookingId),
|
|
|
|
|
oceanBookingRequirementId: optionalId(raw.oceanBookingRequirementId),
|
|
|
|
|
customerId: optionalId(raw.customerId),
|
2026-08-10 15:36:53 +08:00
|
|
|
shippingCompanyId: optionalId(raw.shippingCompanyId),
|
2026-08-20 18:02:52 +08:00
|
|
|
emptyReportSourceVersion: optionalId(raw.emptyReportSourceVersion),
|
2026-08-10 15:36:53 +08:00
|
|
|
version: numberValue(raw.version),
|
|
|
|
|
capacityCbm: raw.capacityCbm == null ? null : numberValue(raw.capacityCbm),
|
|
|
|
|
maxWeightKg: raw.maxWeightKg == null ? null : numberValue(raw.maxWeightKg),
|
2026-08-17 19:57:32 +08:00
|
|
|
exceptionActive: Boolean(raw.exceptionActive),
|
2026-08-13 11:57:44 +08:00
|
|
|
clearanceDocument: document(raw.clearanceDocument),
|
2026-08-10 15:36:53 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function item(raw: SmartLoadingItem): SmartLoadingItem {
|
|
|
|
|
return {
|
|
|
|
|
...raw,
|
|
|
|
|
id: id(raw.id), loadingPlanId: id(raw.loadingPlanId), accountId: id(raw.accountId),
|
|
|
|
|
inboundId: id(raw.inboundId), inboundDetailId: id(raw.inboundDetailId), orderId: id(raw.orderId),
|
|
|
|
|
destinationWarehouseId: optionalId(raw.destinationWarehouseId), version: numberValue(raw.version),
|
|
|
|
|
packageCount: numberValue(raw.packageCount),
|
2026-08-17 16:45:46 +08:00
|
|
|
pickedPackageCount: numberValue(raw.pickedPackageCount),
|
|
|
|
|
minPackageCount: numberValue(raw.minPackageCount, 1),
|
|
|
|
|
canAdjust: Boolean(raw.canAdjust), canRemove: Boolean(raw.canRemove),
|
2026-08-10 15:36:53 +08:00
|
|
|
productQuantity: raw.productQuantity == null ? null : numberValue(raw.productQuantity),
|
|
|
|
|
grossWeightKg: raw.grossWeightKg == null ? null : numberValue(raw.grossWeightKg),
|
|
|
|
|
volumeCbm: raw.volumeCbm == null ? null : numberValue(raw.volumeCbm),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function plan(raw: SmartLoadingPlan): SmartLoadingPlan {
|
2026-08-17 16:45:46 +08:00
|
|
|
return {
|
|
|
|
|
...raw,
|
|
|
|
|
id: id(raw.id), bookingId: id(raw.bookingId), version: numberValue(raw.version),
|
|
|
|
|
canAssign: Boolean(raw.canAssign), canEdit: Boolean(raw.canEdit),
|
|
|
|
|
blockedReason: raw.blockedReason || null,
|
|
|
|
|
pdaTaskId: optionalId(raw.pdaTaskId),
|
|
|
|
|
pdaTaskVersion: raw.pdaTaskVersion == null ? null : numberValue(raw.pdaTaskVersion),
|
|
|
|
|
items: (raw.items || []).map(item),
|
|
|
|
|
}
|
2026-08-10 15:36:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function listContainers(query: ContainerQuery): Promise<ContainerPage> {
|
|
|
|
|
const response = await http.get<ApiEnvelope<ContainerPage>, ApiEnvelope<ContainerPage>>(containerPath, { params: query })
|
|
|
|
|
const result = unwrapApiResponse(response)
|
|
|
|
|
return { ...result, records: (result.records || []).map(container), page: numberValue(result.page, query.page), size: numberValue(result.size, query.size), total: numberValue(result.total), pages: numberValue(result.pages) }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 12:22:41 +08:00
|
|
|
export async function getCustomerContainerTransitSummary(customerId: string): Promise<CustomerContainerTransitSummary> {
|
|
|
|
|
const response = await http.get<ApiEnvelope<Record<string, unknown>>, ApiEnvelope<Record<string, unknown>>>(
|
|
|
|
|
`/platform/v1/customers/${encodeURIComponent(customerId)}/container-transit-summary`,
|
|
|
|
|
)
|
|
|
|
|
const result = unwrapApiResponse(response)
|
|
|
|
|
return {
|
|
|
|
|
customerId: id(result.customerId), containerCount: numberValue(result.containerCount),
|
|
|
|
|
totalTeu: String(result.totalTeu ?? '0'), unknownContainerCount: numberValue(result.unknownContainerCount),
|
|
|
|
|
asOf: id(result.asOf), definition: id(result.definition),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 15:36:53 +08:00
|
|
|
export async function getContainer(containerId: string) {
|
|
|
|
|
const response = await http.get<ApiEnvelope<ContainerBookingResource>, ApiEnvelope<ContainerBookingResource>>(`${containerPath}/${encodeURIComponent(containerId)}`)
|
|
|
|
|
return container(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 11:57:44 +08:00
|
|
|
export async function getContainerOverview(containerId: string) {
|
|
|
|
|
const response = await http.get<ApiEnvelope<ContainerOverview>, ApiEnvelope<ContainerOverview>>(`${containerPath}/${encodeURIComponent(containerId)}/overview`)
|
|
|
|
|
return overview(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function markContainerException(containerId: string, expectedVersion: number, reason: string) {
|
|
|
|
|
const response = await http.post<ApiEnvelope<null>, ApiEnvelope<null>>(`${containerPath}/${encodeURIComponent(containerId)}/exception`, { expectedVersion, reason }, { headers: { 'Idempotency-Key': operationId('container-exception-mark') } })
|
|
|
|
|
unwrapApiResponse(response)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function cancelContainerException(containerId: string, expectedVersion: number, reason: string) {
|
|
|
|
|
const response = await http.delete<ApiEnvelope<null>, ApiEnvelope<null>>(`${containerPath}/${encodeURIComponent(containerId)}/exception`, { data: { expectedVersion, reason }, headers: { 'Idempotency-Key': operationId('container-exception-cancel') } })
|
|
|
|
|
unwrapApiResponse(response)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function uploadContainerDocument(containerId: string, documentType: Extract<ContainerDocumentType, 'DELIVERY_ORDER' | 'PICKUP_ORDER'>, file: File, expectedVersion: number, operationReason?: string | null) {
|
|
|
|
|
const body = new FormData()
|
|
|
|
|
body.append('file', file)
|
|
|
|
|
body.append('expectedVersion', String(expectedVersion))
|
|
|
|
|
if (operationReason) body.append('operationReason', operationReason)
|
|
|
|
|
const response = await http.post<ApiEnvelope<BusinessDocument>, ApiEnvelope<BusinessDocument>>(`${containerPath}/${encodeURIComponent(containerId)}/documents/${documentType}/upload`, body, { headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': operationId(`container-${documentType.toLowerCase()}-upload`) } })
|
|
|
|
|
return document(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function downloadContainerDocument(containerId: string, documentType: Extract<ContainerDocumentType, 'DELIVERY_ORDER' | 'PICKUP_ORDER'>, fileId: string) {
|
|
|
|
|
return http.get<Blob, Blob>(`${containerPath}/${encodeURIComponent(containerId)}/documents/${documentType}/${encodeURIComponent(fileId)}/download`, { responseType: 'blob' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function downloadContainerPdaPhoto(downloadPath: string) {
|
2026-08-14 14:50:44 +08:00
|
|
|
if (!/^\/api\/platform\/v1\/containers\/[1-9][0-9]*\/pda\/photos\/[1-9][0-9]*\/download$/.test(downloadPath)) throw new Error('invalid container photo path')
|
2026-08-13 11:57:44 +08:00
|
|
|
return http.get<Blob, Blob>(downloadPath, { responseType: 'blob' })
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-14 20:59:19 +08:00
|
|
|
export async function createContainerSimulation(payload: ContainerSimulationCreateRequest) {
|
2026-08-10 15:36:53 +08:00
|
|
|
const response = await http.post<ApiEnvelope<ContainerBookingResource>, ApiEnvelope<ContainerBookingResource>>(containerPath, payload)
|
|
|
|
|
return container(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-14 20:59:19 +08:00
|
|
|
export async function linkContainerOceanBooking(containerId: string, payload: ContainerOceanBookingLinkRequest) {
|
|
|
|
|
const response = await http.post<ApiEnvelope<ContainerBookingResource>, ApiEnvelope<ContainerBookingResource>>(`${containerPath}/${encodeURIComponent(containerId)}/ocean-booking-link`, payload)
|
|
|
|
|
return container(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function unlinkContainerOceanBooking(containerId: string, expectedVersion: number, reason: string) {
|
|
|
|
|
const response = await http.delete<ApiEnvelope<ContainerBookingResource>, ApiEnvelope<ContainerBookingResource>>(`${containerPath}/${encodeURIComponent(containerId)}/ocean-booking-link`, { data: { expectedVersion, reason } })
|
|
|
|
|
return container(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 15:36:53 +08:00
|
|
|
export async function updateContainer(containerId: string, version: number, data: ContainerSaveRequest) {
|
|
|
|
|
const response = await http.put<ApiEnvelope<ContainerBookingResource>, ApiEnvelope<ContainerBookingResource>>(`${containerPath}/${encodeURIComponent(containerId)}`, { version, data })
|
|
|
|
|
return container(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function updateContainerTransport(containerId: string, version: number, payload: { containerNo: string | null; voyageNo: string | null }) {
|
|
|
|
|
const response = await http.patch<ApiEnvelope<ContainerBookingResource>, ApiEnvelope<ContainerBookingResource>>(`${containerPath}/${encodeURIComponent(containerId)}/transport`, { ...payload, version })
|
|
|
|
|
return container(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 11:57:44 +08:00
|
|
|
export async function getContainerClearance(containerId: string) {
|
|
|
|
|
const response = await http.get<ApiEnvelope<BusinessDocument | null>, ApiEnvelope<BusinessDocument | null>>(`${containerPath}/${encodeURIComponent(containerId)}/clearance`)
|
|
|
|
|
return document(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function uploadContainerClearance(containerId: string, file: File, expectedVersion: number, operationReason?: string | null) {
|
|
|
|
|
const body = new FormData()
|
|
|
|
|
body.append('file', file)
|
|
|
|
|
body.append('expectedVersion', String(expectedVersion))
|
|
|
|
|
if (operationReason) body.append('operationReason', operationReason)
|
|
|
|
|
const response = await http.post<ApiEnvelope<BusinessDocument>, ApiEnvelope<BusinessDocument>>(`${containerPath}/${encodeURIComponent(containerId)}/clearance/document/upload`, body, { headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': operationId('container-clearance-upload') } })
|
|
|
|
|
return document(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function completeContainerClearance(containerId: string, expectedVersion: number, operationReason?: string | null) {
|
|
|
|
|
const response = await http.post<ApiEnvelope<BusinessDocument>, ApiEnvelope<BusinessDocument>>(`${containerPath}/${encodeURIComponent(containerId)}/clearance/complete`, { expectedVersion, operationReason }, { headers: { 'Idempotency-Key': operationId('container-clearance-complete') } })
|
|
|
|
|
return document(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 18:43:54 +08:00
|
|
|
export async function listSmartLoadingCandidates(bookingId: string, warehouseId: string) {
|
|
|
|
|
const response = await http.get<ApiEnvelope<SmartLoadingCandidate[]>, ApiEnvelope<SmartLoadingCandidate[]>>(`${smartLoadingPath}/candidates`, { params: { bookingId, warehouseId } })
|
2026-08-10 15:36:53 +08:00
|
|
|
return unwrapApiResponse(response).map((raw) => ({
|
|
|
|
|
...raw, accountId: id(raw.accountId), inboundId: id(raw.inboundId), inboundDetailId: id(raw.inboundDetailId),
|
|
|
|
|
orderId: id(raw.orderId), warehouseId: id(raw.warehouseId), destinationWarehouseId: optionalId(raw.destinationWarehouseId),
|
|
|
|
|
availablePackageCount: numberValue(raw.availablePackageCount),
|
|
|
|
|
unitWeightKg: raw.unitWeightKg == null ? null : numberValue(raw.unitWeightKg),
|
|
|
|
|
unitVolumeCbm: raw.unitVolumeCbm == null ? null : numberValue(raw.unitVolumeCbm),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getSmartLoadingPlanByBooking(bookingId: string) {
|
|
|
|
|
const response = await http.get<ApiEnvelope<SmartLoadingPlan>, ApiEnvelope<SmartLoadingPlan>>(`${smartLoadingPath}/plans/by-booking/${encodeURIComponent(bookingId)}`)
|
|
|
|
|
return plan(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getSmartLoadingPlan(planId: string) {
|
|
|
|
|
const response = await http.get<ApiEnvelope<SmartLoadingPlan>, ApiEnvelope<SmartLoadingPlan>>(`${smartLoadingPath}/plans/${encodeURIComponent(planId)}`)
|
|
|
|
|
return plan(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createSmartLoadingPlan(bookingId: string, remark: string | null) {
|
|
|
|
|
const response = await http.post<ApiEnvelope<SmartLoadingPlan>, ApiEnvelope<SmartLoadingPlan>>(`${smartLoadingPath}/plans`, { bookingId, remark })
|
|
|
|
|
return plan(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function assignSmartLoadingItem(planId: string, payload: { inboundId: string; inboundDetailId: string; packageCount: number; expectedVersion: number }) {
|
|
|
|
|
const response = await http.post<ApiEnvelope<SmartLoadingPlan>, ApiEnvelope<SmartLoadingPlan>>(`${smartLoadingPath}/plans/${encodeURIComponent(planId)}/assignments`, { ...payload, operationId: operationId('assign') })
|
|
|
|
|
return plan(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function recommendSmartLoading(planId: string, expectedVersion: number) {
|
|
|
|
|
const response = await http.post<ApiEnvelope<SmartLoadingPlan>, ApiEnvelope<SmartLoadingPlan>>(`${smartLoadingPath}/plans/${encodeURIComponent(planId)}/recommend`, { expectedVersion, operationId: operationId('recommend') })
|
|
|
|
|
return plan(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function adjustSmartLoadingItem(planId: string, itemId: string, packageCount: number, expectedPlanVersion: number, expectedItemVersion: number) {
|
|
|
|
|
const response = await http.patch<ApiEnvelope<SmartLoadingPlan>, ApiEnvelope<SmartLoadingPlan>>(`${smartLoadingPath}/plans/${encodeURIComponent(planId)}/items/${encodeURIComponent(itemId)}`, { packageCount, expectedPlanVersion, expectedItemVersion, operationId: operationId('adjust') })
|
|
|
|
|
return plan(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function removeSmartLoadingItem(planId: string, itemId: string, expectedPlanVersion: number, expectedItemVersion: number) {
|
|
|
|
|
const response = await http.delete<ApiEnvelope<SmartLoadingPlan>, ApiEnvelope<SmartLoadingPlan>>(`${smartLoadingPath}/plans/${encodeURIComponent(planId)}/items/${encodeURIComponent(itemId)}`, { data: { expectedPlanVersion, expectedItemVersion, operationId: operationId('remove') } })
|
|
|
|
|
return plan(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function pushSmartLoadingPreload(planId: string, expectedVersion: number) {
|
|
|
|
|
const response = await http.post<ApiEnvelope<SmartLoadingPlan>, ApiEnvelope<SmartLoadingPlan>>(`${smartLoadingPath}/plans/${encodeURIComponent(planId)}/push-preload`, { expectedVersion }, { headers: { 'Idempotency-Key': operationId('push-preload') } })
|
|
|
|
|
return plan(unwrapApiResponse(response))
|
|
|
|
|
}
|