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, SmartLoadingCandidatePage, SmartLoadingCandidateQuery, SmartLoadingWorkbenchContainerPage, 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, id: optionalId(entry.id), eventId: optionalId(entry.eventId), idempotencyKey: optionalId(entry.idempotencyKey), 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), terminalTruckingCompanyId: optionalId(raw.terminalTruckingCompanyId), 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), totalPackageCount: numberValue(raw.totalPackageCount, raw.packageCount), remainingPackageCount: numberValue(raw.remainingPackageCount), 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), coLoadGroups: (raw.coLoadGroups || []).map((group) => ({ ...group, groupId: id(group.groupId), lockedPlanId: optionalId(group.lockedPlanId), memberOrderCount: numberValue(group.memberOrderCount), inboundFragmentCount: numberValue(group.inboundFragmentCount), fullPackageCount: numberValue(group.fullPackageCount), memberInboundIds: (group.memberInboundIds || []).map(id), })), 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' }) } function normalizeContainerPhotoDownloadPath(downloadPath: string) { if (typeof downloadPath !== 'string' || !downloadPath.startsWith('/')) throw new Error('invalid container photo path') const normalized = downloadPath.startsWith('/api/') ? downloadPath.slice(4) : downloadPath if (!/^\/platform\/v1\/containers\/[1-9][0-9]*\/pda\/photos\/[1-9][0-9]*\/download$/.test(normalized)) throw new Error('invalid container photo path') return normalized } export function downloadContainerPdaPhoto(downloadPath: string) { return http.get(normalizeContainerPhotoDownloadPath(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 getContainerClearance(containerId: string) { const response = await http.get, ApiEnvelope>(`${containerPath}/${encodeURIComponent(containerId)}/clearance`) return document(unwrapApiResponse(response)) } export function downloadContainerClearance(containerId: string, fileId: string) { return http.get(`${containerPath}/${encodeURIComponent(containerId)}/clearance/document/${encodeURIComponent(fileId)}/download`, { responseType: 'blob' }) } 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)) } function smartLoadingQueryParams(query: SmartLoadingCandidateQuery, includeOrderStatuses = false) { const params = new URLSearchParams() for (const channelGroupCode of query.channelGroupCodes) params.append('channelGroupCodes', channelGroupCode) if (includeOrderStatuses) { for (const orderStatus of query.orderStatuses || []) params.append('orderStatuses', orderStatus) } const optionalParams: Array<[string, string | number | undefined]> = [ ['warehouseId', query.warehouseId], ['customerId', query.customerId], ['inboundCode', query.inboundCode], ['createdStart', query.createdStart], ['createdEnd', query.createdEnd], ['page', query.page], ['size', query.size], ] for (const [key, value] of optionalParams) if (value !== undefined && value !== '') params.append(key, String(value)) return params } export async function listSmartLoadingCandidates(query: SmartLoadingCandidateQuery) { const params = smartLoadingQueryParams(query, true) const response = await http.get, ApiEnvelope>(`${smartLoadingPath}/inbound-candidates`, { params }) const result = unwrapApiResponse(response) return { ...result, page: numberValue(result.page, query.page), size: numberValue(result.size, query.size), total: numberValue(result.total), pages: numberValue(result.pages), records: result.records.map((raw) => { const accountId = id(raw.accountId) return { ...raw, accountId, customerId: accountId, inboundId: optionalId(raw.inboundId), orderId: id(raw.orderId), warehouseId: id(raw.warehouseId), destinationWarehouseId: optionalId(raw.destinationWarehouseId), coLoadGroupId: optionalId(raw.coLoadGroupId), memberCount: raw.memberCount == null ? null : numberValue(raw.memberCount), groupTotalBoxes: raw.groupTotalBoxes == null ? null : numberValue(raw.groupTotalBoxes), locked: raw.locked == null ? null : Boolean(raw.locked), availablePackageCount: numberValue(raw.availablePackageCount), unitGrossWeightKg: raw.unitGrossWeightKg == null ? null : numberValue(raw.unitGrossWeightKg), unitNetWeightKg: raw.unitNetWeightKg == null ? null : numberValue(raw.unitNetWeightKg), averageUnitVolumeCbm: raw.averageUnitVolumeCbm == null ? null : numberValue(raw.averageUnitVolumeCbm), availableVolumeCbm: raw.availableVolumeCbm == null ? null : numberValue(raw.availableVolumeCbm), groupMembers: raw.groupMembers.map((member) => ({ ...member, accountId: id(member.accountId), inboundId: id(member.inboundId), orderId: id(member.orderId), availablePackageCount: numberValue(member.availablePackageCount), })), } }), } } export async function listSmartLoadingWorkbenchContainers(query: SmartLoadingCandidateQuery) { const response = await http.get, ApiEnvelope>(`${smartLoadingPath}/containers`, { params: smartLoadingQueryParams(query) }) const result = unwrapApiResponse(response) return { ...result, page: numberValue(result.page, query.page), size: numberValue(result.size, query.size), total: numberValue(result.total), pages: numberValue(result.pages), records: result.records.map((raw) => ({ ...raw, bookingId: id(raw.bookingId), warehouseId: id(raw.warehouseId), planId: optionalId(raw.planId), capacityCbm: raw.capacityCbm == null ? null : numberValue(raw.capacityCbm), maxWeightKg: raw.maxWeightKg == null ? null : numberValue(raw.maxWeightKg), version: numberValue(raw.version), })), } } 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 assignSmartLoadingItem(planId: string, payload: { inboundId: string; packageCount: number; expectedVersion: number }) { const response = await http.post, ApiEnvelope>(`${smartLoadingPath}/plans/${encodeURIComponent(planId)}/inbound-assignments`, { ...payload, operationId: operationId('assign-inbound') }) return plan(unwrapApiResponse(response)) } export async function assignPendingReviewSmartLoadingOrder(planId: string, payload: { customerId: string; orderId: string; inboundCode: string; packageCount: number; expectedVersion: number }) { const response = await http.post, ApiEnvelope>( `${smartLoadingPath}/plans/${encodeURIComponent(planId)}/pending-review-assignments`, { ...payload, operationId: operationId('assign-pending-review') }, ) return plan(unwrapApiResponse(response)) } export async function assignSmartLoadingGroup(planId: string, inboundId: string, expectedVersion: number) { const response = await http.post, ApiEnvelope>( `${smartLoadingPath}/plans/${encodeURIComponent(planId)}/group-assignments`, { inboundId, expectedVersion, operationId: operationId('assign-coload-group') }, ) 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 removeSmartLoadingGroup(planId: string, groupId: string, expectedPlanVersion: number) { const response = await http.delete, ApiEnvelope>( `${smartLoadingPath}/plans/${encodeURIComponent(planId)}/group-assignments/${encodeURIComponent(groupId)}`, { data: { expectedPlanVersion, operationId: operationId('remove-coload-group') } }, ) return plan(unwrapApiResponse(response)) } export async function removeAllUnpickedSmartLoadingItems(planId: string, expectedPlanVersion: number) { const response = await http.delete, ApiEnvelope>( `${smartLoadingPath}/plans/${encodeURIComponent(planId)}/unpicked-items`, { data: { expectedPlanVersion, operationId: operationId('remove-unpicked') } }, ) return plan(unwrapApiResponse(response)) }