import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' export interface CustomerContainerTransitSummary { customerId: string containerCount: number totalTeu: number | string unknownContainerCount: number asOf: string definition: string } import type { ContainerBookingResource, ContainerPage, ContainerQuery, ContainerSaveRequest, ContainerSimulationCreateRequest, ContainerOceanBookingLinkRequest, SmartLoadingCandidate, SmartLoadingItem, SmartLoadingPlan, BusinessDocument, ContainerDocumentType, ContainerOverview, ContainerOverviewDocument, } 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)}` } 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) })), } } function container(raw: ContainerBookingResource): ContainerBookingResource { return { ...raw, id: id(raw.id), planId: optionalId(raw.planId), planNo: raw.planNo || null, mergedIntoContainerBookingId: optionalId(raw.mergedIntoContainerBookingId), warehouseId: id(raw.warehouseId), oceanBookingId: optionalId(raw.oceanBookingId), oceanBookingRequirementId: optionalId(raw.oceanBookingRequirementId), customerId: optionalId(raw.customerId), shippingCompanyId: optionalId(raw.shippingCompanyId), emptyReportSourceVersion: optionalId(raw.emptyReportSourceVersion), version: numberValue(raw.version), capacityCbm: raw.capacityCbm == null ? null : numberValue(raw.capacityCbm), maxWeightKg: raw.maxWeightKg == null ? null : numberValue(raw.maxWeightKg), exceptionActive: Boolean(raw.exceptionActive), clearanceDocument: document(raw.clearanceDocument), } } 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), pickedPackageCount: numberValue(raw.pickedPackageCount), minPackageCount: numberValue(raw.minPackageCount, 1), canAdjust: Boolean(raw.canAdjust), canRemove: Boolean(raw.canRemove), 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 { 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), } } export async function listContainers(query: ContainerQuery): Promise { const response = await http.get, ApiEnvelope>(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) } } export async function getCustomerContainerTransitSummary(customerId: string): Promise { const response = await http.get>, ApiEnvelope>>( `/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), } } export async function getContainer(containerId: string) { const response = await http.get, ApiEnvelope>(`${containerPath}/${encodeURIComponent(containerId)}`) return container(unwrapApiResponse(response)) } export async function getContainerOverview(containerId: string) { const response = await http.get, ApiEnvelope>(`${containerPath}/${encodeURIComponent(containerId)}/overview`) return overview(unwrapApiResponse(response)) } export async function markContainerException(containerId: string, expectedVersion: number, reason: string) { const response = await http.post, ApiEnvelope>(`${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>(`${containerPath}/${encodeURIComponent(containerId)}/exception`, { data: { expectedVersion, reason }, headers: { 'Idempotency-Key': operationId('container-exception-cancel') } }) unwrapApiResponse(response) } export async function uploadContainerDocument(containerId: string, documentType: Extract, 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>(`${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, fileId: string) { return http.get(`${containerPath}/${encodeURIComponent(containerId)}/documents/${documentType}/${encodeURIComponent(fileId)}/download`, { responseType: 'blob' }) } export function downloadContainerPdaPhoto(downloadPath: string) { 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') return http.get(downloadPath, { responseType: 'blob' }) } export async function createContainerSimulation(payload: ContainerSimulationCreateRequest) { const response = await http.post, ApiEnvelope>(containerPath, payload) return container(unwrapApiResponse(response)) } export async function linkContainerOceanBooking(containerId: string, payload: ContainerOceanBookingLinkRequest) { const response = await http.post, ApiEnvelope>(`${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>(`${containerPath}/${encodeURIComponent(containerId)}/ocean-booking-link`, { data: { expectedVersion, reason } }) return container(unwrapApiResponse(response)) } export async function updateContainer(containerId: string, version: number, data: ContainerSaveRequest) { const response = await http.put, ApiEnvelope>(`${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>(`${containerPath}/${encodeURIComponent(containerId)}/transport`, { ...payload, version }) return container(unwrapApiResponse(response)) } export async function getContainerClearance(containerId: string) { const response = await http.get, ApiEnvelope>(`${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>(`${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>(`${containerPath}/${encodeURIComponent(containerId)}/clearance/complete`, { expectedVersion, operationReason }, { headers: { 'Idempotency-Key': operationId('container-clearance-complete') } }) return document(unwrapApiResponse(response)) } export async function listSmartLoadingCandidates(bookingId: string, warehouseId: string) { const response = await http.get, ApiEnvelope>(`${smartLoadingPath}/candidates`, { params: { bookingId, warehouseId } }) 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>(`${smartLoadingPath}/plans/by-booking/${encodeURIComponent(bookingId)}`) return plan(unwrapApiResponse(response)) } export async function getSmartLoadingPlan(planId: string) { const response = await http.get, ApiEnvelope>(`${smartLoadingPath}/plans/${encodeURIComponent(planId)}`) return plan(unwrapApiResponse(response)) } export async function createSmartLoadingPlan(bookingId: string, remark: string | null) { const response = await http.post, ApiEnvelope>(`${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>(`${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>(`${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>(`${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>(`${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>(`${smartLoadingPath}/plans/${encodeURIComponent(planId)}/push-preload`, { expectedVersion }, { headers: { 'Idempotency-Key': operationId('push-preload') } }) return plan(unwrapApiResponse(response)) }