import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' const inboundBasePath = '/platform/v1/inbounds' export interface OutboundGateItem { code: string; name: string; passed: boolean; actual?: string | null; requirement?: string | null } export interface OutboundDispatchAggregate { orderId: string | null orderNo: string | null inboundId: string | null inboundCode: string | null containerBookingId: string | null containerNo: string | null loadingPlanId: string | null loadingPlanStatus: string | null oceanBookingId: string | null oceanBookingStatus: string | null truckAppointmentId: string | null truckAppointmentStatus: string | null customsStatus?: string | null sailingStatus?: string | null gate: { passed: boolean; items: OutboundGateItem[] } | null } export interface ContainerBooking { id: string containerNo: string | null cabinetNo: string | null warehouseId: string | null warehouseName: string | null destinationWarehouseCode: string | null destinationWarehouseName: string | null capacityCbm: number | null maxWeightKg: number | null version: number } export interface SmartLoadingCandidate { accountId: string inboundId: string inboundDetailId: string orderId: string orderNo: string | null inboundCode: string | null warehouseId: string destinationWarehouseId: string | null destinationWarehouseCode: string | null destinationWarehouseName: string | null customerName: string | null goodsName: string | null availablePackageCount: number unitWeightKg: number unitVolumeCbm: number } export interface SmartLoadingPlanItem { id: string loadingPlanId: string accountId: string customerName: string | null inboundId: string inboundDetailId: string orderId: string orderNo: string | null inboundCode: string | null destinationWarehouseCode: string | null destinationWarehouseName: string | null skuCode: string | null productNameZh: string | null packageCount: number grossWeightKg: number volumeCbm: number loadSequence: number | null version: number } export interface SmartLoadingPlan { id: string planNo: string | null status: string bookingId: string containerNo: string | null voyageNo: string | null version: number items: SmartLoadingPlanItem[] } export interface CreateSmartLoadingPlanRequest { bookingId: string; remark?: string } export interface AssignSmartLoadingItemRequest { inboundId: string inboundDetailId: string packageCount: number expectedVersion: number operationId: string } export interface RecommendSmartLoadingRequest { expectedVersion: number; operationId: string } export interface AdjustSmartLoadingItemRequest { packageCount: number expectedPlanVersion: number expectedItemVersion: number operationId: string } export interface DeleteSmartLoadingItemRequest { expectedPlanVersion: number expectedItemVersion: number operationId: string } export type PreloadStatus = 'PENDING' | 'CONFIRMING' | 'CONFIRMED' | string /** 五类装柜取证。旧接口仍使用 PHOTO_STEP_* / SEAL 编码,页面统一映射为业务名称。 */ export type EvidenceType = 'PHOTO_STEP_1' | 'PHOTO_STEP_2' | 'PHOTO_STEP_3' | 'PHOTO_STEP_4' | 'SEAL' export interface PreloadItem { containerCode: string | null cargoReference: string | null packageCount: number | null volumeCbm: number | null remark: string | null } export interface PreloadPlan { id: string inboundId: string tenantId: string status: PreloadStatus version: number items: PreloadItem[] remark: string | null createdAt: string | null updatedAt: string | null } export interface SplitActionResponse { mainStatus: string preloadPlan: PreloadPlan | null } export interface PickingTask { id: number | string inboundId: number | string status: string version: number expectedCount: number | null pickedCount: number | null remark: string | null } export interface Evidence { id: number | string inboundId: number | string tenantId: number | string evidenceType: EvidenceType fileId: number | string | null fileName: string | null sealNo: string | null remark: string | null version: number status: 'ACTIVE' | 'VOIDED' | string createdAt: string | null updatedAt: string | null voidReason?: string | null voidedBy?: number | string | null voidedByName?: string | null voidedAt?: string | null } export interface EvidenceList { inboundId: number | string evidences: Evidence[] } export interface LoadingActionResponse { mainStatus: string evidences: EvidenceList | null } export interface PickingRequest { expectedVersion: number expectedCount: number pickedCount: number remark?: string } export interface SmartSplitRequest { expectedVersion: number items: PreloadItem[] remark?: string } export interface UpsertEvidenceRequest { expectedVersion: number evidenceType: EvidenceType fileId?: number | string fileName?: string sealNo?: string remark?: string } export interface UploadEvidenceResult { fileId: number | string fileName: string | null contentType?: string | null sizeBytes?: number | null } export interface VoidEvidenceRequest { expectedVersion: number reason: string } export interface CompleteLoadingRequest { expectedVersion: number remark?: string } function query(tenantId: string | number) { return { tenantId: String(tenantId) } } function idempotencyKey(operation: string, inboundId: string | number) { return `admin-${operation}-${inboundId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` } function operationId(operation: string, resourceId: string | number) { return `admin-${operation}-${resourceId}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}` } function nullableId(value: number | string | null | undefined) { return value === null || value === undefined || value === '' ? null : String(value) } function numberValue(value: number | string | null | undefined, fallback = 0) { const parsed = Number(value) return Number.isFinite(parsed) ? parsed : fallback } function normalizeContainer(raw: Record): ContainerBooking { return { id: String(raw.id), containerNo: raw.containerNo == null ? null : String(raw.containerNo), cabinetNo: raw.cabinetNo == null ? null : String(raw.cabinetNo), warehouseId: nullableId(raw.warehouseId as number | string | null), warehouseName: raw.warehouseName == null ? null : String(raw.warehouseName), destinationWarehouseCode: raw.destinationWarehouseCode == null ? null : String(raw.destinationWarehouseCode), destinationWarehouseName: raw.destinationWarehouseName == null ? null : String(raw.destinationWarehouseName), capacityCbm: raw.capacityCbm == null ? null : numberValue(raw.capacityCbm as number | string), maxWeightKg: raw.maxWeightKg == null ? null : numberValue(raw.maxWeightKg as number | string), version: numberValue(raw.version as number | string), } } function normalizeCandidate(raw: Record): SmartLoadingCandidate { return { accountId: String(raw.accountId), inboundId: String(raw.inboundId), inboundDetailId: String(raw.inboundDetailId), orderId: String(raw.orderId), orderNo: raw.orderNo == null ? null : String(raw.orderNo), inboundCode: raw.inboundCode == null ? null : String(raw.inboundCode), warehouseId: String(raw.warehouseId), destinationWarehouseId: nullableId(raw.destinationWarehouseId as number | string | null), destinationWarehouseCode: raw.destinationWarehouseCode == null ? null : String(raw.destinationWarehouseCode), destinationWarehouseName: raw.destinationWarehouseName == null ? null : String(raw.destinationWarehouseName), customerName: raw.customerName == null ? null : String(raw.customerName), goodsName: raw.goodsName == null ? null : String(raw.goodsName), availablePackageCount: numberValue(raw.availablePackageCount as number | string), unitWeightKg: numberValue(raw.unitWeightKg as number | string), unitVolumeCbm: numberValue(raw.unitVolumeCbm as number | string), } } function normalizeSmartLoadingItem(raw: Record): SmartLoadingPlanItem { return { id: String(raw.id), loadingPlanId: String(raw.loadingPlanId), accountId: String(raw.accountId), customerName: raw.customerName == null ? null : String(raw.customerName), inboundId: String(raw.inboundId), inboundDetailId: String(raw.inboundDetailId), orderId: String(raw.orderId), orderNo: raw.orderNo == null ? null : String(raw.orderNo), inboundCode: raw.inboundCode == null ? null : String(raw.inboundCode), destinationWarehouseCode: raw.destinationWarehouseCode == null ? null : String(raw.destinationWarehouseCode), destinationWarehouseName: raw.destinationWarehouseName == null ? null : String(raw.destinationWarehouseName), skuCode: raw.skuCode == null ? null : String(raw.skuCode), productNameZh: raw.productNameZh == null ? null : String(raw.productNameZh), packageCount: numberValue(raw.packageCount as number | string), grossWeightKg: numberValue(raw.grossWeightKg as number | string), volumeCbm: numberValue(raw.volumeCbm as number | string), loadSequence: raw.loadSequence == null ? null : numberValue(raw.loadSequence as number | string), version: numberValue(raw.version as number | string), } } function normalizeSmartLoadingPlan(raw: Record): SmartLoadingPlan { return { id: String(raw.id), planNo: raw.planNo == null ? null : String(raw.planNo), status: String(raw.status || ''), bookingId: String(raw.bookingId), containerNo: raw.containerNo == null ? null : String(raw.containerNo), voyageNo: raw.voyageNo == null ? null : String(raw.voyageNo), version: numberValue(raw.version as number | string), items: Array.isArray(raw.items) ? raw.items.map((item) => normalizeSmartLoadingItem(item as Record)) : [], } } function normalizeOutboundDispatch(raw: Record): OutboundDispatchAggregate { return { orderId: nullableId(raw.orderId as number | string | null), orderNo: raw.orderNo == null ? null : String(raw.orderNo), inboundId: nullableId(raw.inboundId as number | string | null), inboundCode: raw.inboundCode == null ? null : String(raw.inboundCode), containerBookingId: nullableId(raw.containerBookingId as number | string | null), containerNo: raw.containerNo == null ? null : String(raw.containerNo), loadingPlanId: nullableId(raw.loadingPlanId as number | string | null), loadingPlanStatus: raw.loadingPlanStatus == null ? null : String(raw.loadingPlanStatus), oceanBookingId: nullableId(raw.oceanBookingId as number | string | null), oceanBookingStatus: raw.oceanBookingStatus == null ? null : String(raw.oceanBookingStatus), truckAppointmentId: nullableId(raw.truckAppointmentId as number | string | null), truckAppointmentStatus: raw.truckAppointmentStatus == null ? null : String(raw.truckAppointmentStatus), customsStatus: raw.customsStatus == null ? null : String(raw.customsStatus), sailingStatus: raw.sailingStatus == null ? null : String(raw.sailingStatus), gate: raw.gate == null ? null : raw.gate as OutboundDispatchAggregate['gate'], } } function normalizePreloadPlan(raw: Record): PreloadPlan { return { id: String(raw.id), inboundId: String(raw.inboundId), tenantId: String(raw.tenantId), status: String(raw.status || ''), version: numberValue(raw.version as number | string), items: Array.isArray(raw.items) ? raw.items.map((item) => ({ ...(item as PreloadItem) })) : [], remark: raw.remark == null ? null : String(raw.remark), createdAt: raw.createdAt == null ? null : String(raw.createdAt), updatedAt: raw.updatedAt == null ? null : String(raw.updatedAt), } } function normalizeSplitAction(raw: Record): SplitActionResponse { return { mainStatus: String(raw.mainStatus || ''), preloadPlan: raw.preloadPlan == null ? null : normalizePreloadPlan(raw.preloadPlan as Record), } } const smartLoadingBasePath = '/platform/v1/first-leg/smart-loading' export async function getContainerBooking(bookingId: string | number) { const response = await http.get>, ApiEnvelope>>( `/platform/v1/containers/${bookingId}`, ) return normalizeContainer(unwrapApiResponse(response)) } export async function listSmartLoadingCandidates(bookingId: string | number, warehouseId: string | number) { const response = await http.get[]>, ApiEnvelope[]>>( `${smartLoadingBasePath}/candidates`, { params: { bookingId: String(bookingId), warehouseId: String(warehouseId) } }, ) return unwrapApiResponse(response).map(normalizeCandidate) } export async function getSmartLoadingPlan(planId: string | number) { const response = await http.get>, ApiEnvelope>>( `${smartLoadingBasePath}/plans/${planId}`, ) return normalizeSmartLoadingPlan(unwrapApiResponse(response)) } export async function getSmartLoadingPlanByBooking(bookingId: string | number) { const response = await http.get>, ApiEnvelope>>( `${smartLoadingBasePath}/plans/by-booking/${bookingId}`, ) return normalizeSmartLoadingPlan(unwrapApiResponse(response)) } export async function assignSmartLoadingItem(planId: string | number, request: Omit) { const response = await http.post>, ApiEnvelope>>( `${smartLoadingBasePath}/plans/${planId}/assignments`, { ...request, operationId: operationId('smart-loading-assign', planId) }, ) return normalizeSmartLoadingPlan(unwrapApiResponse(response)) } export async function recommendSmartLoading(planId: string | number, expectedVersion: number) { const response = await http.post>, ApiEnvelope>>( `${smartLoadingBasePath}/plans/${planId}/recommend`, { expectedVersion, operationId: operationId('smart-loading-recommend', planId) }, ) return normalizeSmartLoadingPlan(unwrapApiResponse(response)) } export async function adjustSmartLoadingItem( planId: string | number, itemId: string | number, request: Omit, ) { const response = await http.patch>, ApiEnvelope>>( `${smartLoadingBasePath}/plans/${planId}/items/${itemId}`, { ...request, operationId: operationId('smart-loading-adjust', itemId) }, ) return normalizeSmartLoadingPlan(unwrapApiResponse(response)) } export async function deleteSmartLoadingItem( planId: string | number, itemId: string | number, request: Omit, ) { const response = await http.delete>, ApiEnvelope>>( `${smartLoadingBasePath}/plans/${planId}/items/${itemId}`, { data: { ...request, operationId: operationId('smart-loading-delete', itemId) } }, ) return normalizeSmartLoadingPlan(unwrapApiResponse(response)) } function writeOptions(operation: string, inboundId: string | number, tenantId: string | number) { return { params: query(tenantId), headers: { 'Idempotency-Key': idempotencyKey(operation, inboundId) } } } export async function getPreload(tenantId: string | number, inboundId: string | number) { const response = await http.get>, ApiEnvelope>>( `${inboundBasePath}/${inboundId}/preload`, { params: query(tenantId) }, ) return normalizePreloadPlan(unwrapApiResponse(response)) } export async function getOutboundDispatch(tenantId: string | number, orderId?: string | number, inboundId?: string | number) { const response = await http.get[]>, ApiEnvelope[]>>( '/platform/v1/first-leg/outbound-dispatches', { params: { tenantId: String(tenantId), ...(orderId ? { orderId: String(orderId) } : {}), ...(inboundId ? { inboundId: String(inboundId) } : {}) } }, ) const result = unwrapApiResponse(response) return result?.[0] ? normalizeOutboundDispatch(result[0]) : null } export async function smartSplit( tenantId: string | number, inboundId: string | number, request: SmartSplitRequest, ) { const response = await http.post>, ApiEnvelope>>( `${inboundBasePath}/${inboundId}/split/smart`, request, writeOptions('smart-split', inboundId, tenantId), ) return normalizeSplitAction(unwrapApiResponse(response)) } export async function getPicking(tenantId: string | number, inboundId: string | number) { const response = await http.get, ApiEnvelope>( `${inboundBasePath}/${inboundId}/loading/picking`, { params: query(tenantId) }, ) return unwrapApiResponse(response) } export async function startPicking( tenantId: string | number, inboundId: string | number, request: PickingRequest, ) { const response = await http.post, ApiEnvelope>( `${inboundBasePath}/${inboundId}/loading/picking/start`, request, writeOptions('picking-start', inboundId, tenantId), ) return unwrapApiResponse(response) } export async function completePicking( tenantId: string | number, inboundId: string | number, request: PickingRequest, ) { const response = await http.post, ApiEnvelope>( `${inboundBasePath}/${inboundId}/loading/picking/complete`, request, writeOptions('picking-complete', inboundId, tenantId), ) return unwrapApiResponse(response) } export async function getEvidence(tenantId: string | number, inboundId: string | number) { const response = await http.get, ApiEnvelope>( `${inboundBasePath}/${inboundId}/loading/evidence`, { params: query(tenantId) }, ) return unwrapApiResponse(response) } export async function upsertEvidence( tenantId: string | number, inboundId: string | number, request: UpsertEvidenceRequest, ) { const response = await http.post, ApiEnvelope>( `${inboundBasePath}/${inboundId}/loading/evidence`, request, writeOptions(`evidence-${request.evidenceType}`, inboundId, tenantId), ) return unwrapApiResponse(response) } /** 真实 File 服务上传;返回的 fileId 才能提交证据,不接受本地文件名冒充。 */ export async function uploadEvidenceFile( tenantId: string | number, inboundId: string | number, evidenceType: EvidenceType, file: File, ) { const body = new FormData() body.append('evidenceType', evidenceType) body.append('file', file) const response = await http.post, ApiEnvelope>( `/platform/v1/files/loading-evidence/${tenantId}/${inboundId}`, body, { headers: { 'Content-Type': 'multipart/form-data' } }, ) return unwrapApiResponse(response) } export function downloadEvidenceFile( tenantId: string | number, inboundId: string | number, fileId: string | number, ) { return http.get( `/platform/v1/files/loading-evidence/${encodeURIComponent(String(tenantId))}/${encodeURIComponent(String(inboundId))}/${encodeURIComponent(String(fileId))}`, { responseType: 'blob' }, ) } export async function voidEvidence( tenantId: string | number, inboundId: string | number, evidenceId: string | number, request: VoidEvidenceRequest, ) { const response = await http.post, ApiEnvelope>( `${inboundBasePath}/${inboundId}/loading/evidence/${evidenceId}/void`, request, writeOptions('evidence-void', inboundId, tenantId), ) return unwrapApiResponse(response) } export async function completeLoading( tenantId: string | number, inboundId: string | number, request: CompleteLoadingRequest, ) { const response = await http.post, ApiEnvelope>( `${inboundBasePath}/${inboundId}/loading/complete`, request, writeOptions('loading-complete', inboundId, tenantId), ) return unwrapApiResponse(response) }