Files
admin-web/src/modules/unified-orders/api/unifiedOrders.ts
T
2026-10-20 11:33:39 +08:00

701 lines
23 KiB
TypeScript

import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export const unifiedOrdersApiPath = '/platform/v1/orders'
export type UnifiedOrderMainStatus =
| 'PENDING_REVIEW'
| 'WAIT_MEASURE'
| 'WAIT_PALLETIZE'
| 'WAIT_SPLIT'
| 'LOADING'
| 'LOADED'
| 'DELIVERED'
| 'COMPLETED'
| 'WAIT_WEIGHT_CONFIRM' | 'WEIGHT_CONFIRMED' | 'WAIT_CUSTOMS_DOCS' | 'EXPORT_CUSTOMS'
| 'SAILING_CLEARANCE' | 'WAIT_UNLOAD' | 'WAIT_DISPATCH' | 'OUTBOUND_DOCUMENT' | 'DELIVERING' | 'SIGNED'
export const unifiedOrderStatusOptions: Array<{ value: UnifiedOrderMainStatus; label: string }> = [
{ value: 'PENDING_REVIEW', label: '待审核' },
{ value: 'WAIT_MEASURE', label: '待测量数据' },
{ value: 'WAIT_WEIGHT_CONFIRM', label: '待客户确认' },
{ value: 'WEIGHT_CONFIRMED', label: '已确认数据' },
{ value: 'WAIT_CUSTOMS_DOCS', label: '待上传资料' },
{ value: 'WAIT_PALLETIZE', label: '一件代发打托' },
{ value: 'WAIT_SPLIT', label: '待分柜' },
{ value: 'LOADING', label: '装柜中' },
{ value: 'LOADED', label: '装柜完成' },
{ value: 'EXPORT_CUSTOMS', label: '出口报关' },
{ value: 'SAILING_CLEARANCE', label: '已开船/待清关' },
{ value: 'WAIT_UNLOAD', label: '待拆柜' },
{ value: 'WAIT_DISPATCH', label: '待排车' },
{ value: 'OUTBOUND_DOCUMENT', label: '出库单' },
{ value: 'DELIVERING', label: '派送中' },
{ value: 'SIGNED', label: '签收完成' },
]
export interface UnifiedOrderMeasurementSummary {
reviewStatus: string | null
actualGrossWeightKg: number | null
actualVolumeCbm: number | null
chargeableWeightKg: number | null
completedAt: string | null
}
export interface UnifiedOrderPreloadSummary {
status: string | null
version: number | null
itemCount: number | null
updatedAt: string | null
}
export interface UnifiedOrderLoadingSummary {
status: string | null
loadingVersion: number | null
pickingVersion: number | null
expectedCount: number | null
pickedCount: number | null
evidenceCount: number | null
sealNo: string | null
updatedAt: string | null
}
export interface UnifiedOrderDeliverySummary {
status: string | null
completedAt: string | null
trackingNo: string | null
}
export interface UnifiedOrderTransportSummary {
loadingStatus: string | null
containerNo: string | null
cabinetNo: string | null
vesselName: string | null
voyageNo: string | null
estimatedSailingAt: string | null
actualSailingAt: string | null
effectiveSailingAt: string | null
effectiveSailingAtEstimated: boolean | null
estimatedPortArrivalAt: string | null
actualPortArrivalAt: string | null
effectivePortArrivalAt: string | null
effectivePortArrivalAtEstimated: boolean | null
}
export interface UnifiedOrderRelationSummary {
role: 'PARENT' | 'CHILD'
rootOrderId: number | string | null
parentOrderId: number | string | null
splitSequence: number | null
childCount: number
}
export interface UnifiedOrderCoLoadGroupSummary {
status: string
memberCount: number
memberInboundCodes: string[]
}
export interface UnifiedOrderResource {
orderId: number | string
orderVersion?: number | null
tenantId: number | string
orderNo: string | null
mainStatus: UnifiedOrderMainStatus
nextAction: string | null
relation: UnifiedOrderRelationSummary | null
internalOrderStatus: string | null
customerId: number | string | null
customerName: string | null
channelCode: string | null
channelName: string | null
channelChanged?: boolean
receivingWarehouseId: number | string | null
receivingWarehouseName: string | null
destinationWarehouseId: number | string | null
destinationWarehouseName: string | null
inboundId: number | string | null
inboundVersion: number | null
inboundCode: string | null
expectedShipDate: string | null
totalPackageCount: number | null
estimatedVolumeCbm?: number | null
expectedVolumeCbm?: number | null
locationCodes: string | null
actualGrossWeightKg: number | null
actualVolumeCbm: number | null
measurement: UnifiedOrderMeasurementSummary | null
preload: UnifiedOrderPreloadSummary | null
loading: UnifiedOrderLoadingSummary | null
delivery: UnifiedOrderDeliverySummary | null
transport: UnifiedOrderTransportSummary | null
coLoadGroup: UnifiedOrderCoLoadGroupSummary | null
createdAt: string | null
updatedAt: string | null
expectedArrivalAt: string | null
feeReviewStatus: string | null
holdFlag: boolean | null
currentHoldReason: string | null
companyName?: string | null
businessType?: string | null
fulfillmentType?: string | null
pickupMethod?: string | null
requestedPickupAt: string | null
pickupAt?: string | null
pickupContactPhone?: string | null
pickupAddress?: string | null
pickupPostalCode?: string | null
pickupContactEmail?: string | null
taxMode?: string | null
fullContainer?: boolean | null
customsInspection?: boolean | null
totalPalletCount: number | null
chargeableVolumeCbm: number | null
volumetricWeightKg: number | null
totalNetWeightKg: number | null
location?: string | null
destinationType?: string | null
destinationAddress?: string | null
destinationPostalCode?: string | null
destinationContact?: string | null
destinationContactPhone?: string | null
measurementCompletedAt: string | null
inboundCompletedAt: string | null
associatedFiles?: string[] | null
markFiles?: string[] | null
pcOrderFiles?: string[] | null
expressCompany: string | null
expressTrackingNo: string | null
skuNos: string[] | null
totalQuantity: number | null
shipmentId: string | null
referenceId: string | null
remark?: string | null
customerRemark?: string | null
latestInternalNote?: string | null
internalRemark?: string | null
warehouseRemark?: string | null
customerServiceRemark?: string | null
createdBy?: string | null
}
export interface UnifiedOrderTimelineEvent {
source: string | null
eventType: string | null
mainStatus: UnifiedOrderMainStatus | null
action: string | null
remark: string | null
operatorId: number | string | null
operatorName: string | null
occurredAt: string | null
}
export interface UnifiedOrderDetailOrderSnapshot {
id: number | string | null
version: number | null
tenantId: number | string | null
orderNo: string | null
customerId: number | string | null
customerName: string | null
transportMode: string | null
tradeTerm?: string | null
destinationCountryCode: string | null
destinationWarehouseId: number | string | null
destinationWarehouseName: string | null
expectedShipDate: string | null
customerRemark?: string | null
pickupMethod?: string | null
receivingWarehouseId: number | string | null
receivingWarehouseName: string | null
receivingWarehouseAddress?: string | null
receivingTime?: string | null
channelCode?: string | null
channelName: string | null
carrierId: number | string | null
carrierName: string | null
taxMode?: string | null
containerMode?: string | null
customsInspection?: boolean | null
destinationType?: string | null
amazonWarehouseCode?: string | null
shipmentId?: string | null
referenceId?: string | null
expressCompany?: string | null
expressTrackingNo?: string | null
submittedAt?: string | null
completedAt?: string | null
createdAt: string | null
updatedAt: string | null
}
export interface UnifiedOrderDetailOrderResource {
order: UnifiedOrderDetailOrderSnapshot | null
cargoes: Array<Record<string, unknown>>
parties: Array<Record<string, unknown>>
internalNotes: UnifiedOrderInternalNoteResource[]
statusHistory: UnifiedOrderStatusHistoryResource[]
}
export interface UnifiedOrderInternalNoteResource {
id: number | string
content: string
createdBy: number | string | null
createdAt: string | null
}
export interface UnifiedOrderStatusHistoryResource {
id: number | string
fromStatus: string | null
toStatus: string | null
action: string | null
reason: string | null
operatorId: number | string | null
operatorName: string | null
occurredAt: string | null
}
export interface UnifiedOrderDetailResource {
summary: UnifiedOrderResource
order: UnifiedOrderDetailOrderResource
timeline: UnifiedOrderTimelineEvent[]
}
export interface UnifiedOrderFeeDetailResource {
id: number | string
sourceFeeId?: number | string | null
sourceFeeVersion?: number | null
inboundId: number | string
inboundCode: string | null
reviewStatus: string | null
direction: string | null
customerName: string | null
invoiceTitle: string | null
businessDate: string | null
channelName: string | null
destinationName: string | null
fbaNo?: string | null
packageCount: number | null
actualGrossWeightKg: number | null
actualVolumeCbm: number | null
feeTypeNameSnapshot: string | null
currency: string | null
amount: number | null
chargeableQuantity: number | null
chargeUnit: string | null
unitPrice: number | null
inboundedAt: string | null
outboundAt?: string | null
storageDays?: number | null
billNo?: string | null
settlementStartDate?: string | null
settlementEndDate?: string | null
}
export interface UnifiedOrderPageResource {
records: UnifiedOrderResource[]
page: number
size: number
total: number
pages: number
}
export interface UnifiedOrderQuery {
tenantId?: string
keyword?: string
orderNo?: string
mainStatus?: UnifiedOrderMainStatus
customerId?: string
receivingWarehouseId?: string
destinationWarehouseId?: string
inboundCode?: string
internalOrderStatus?: string
expectedArrivalDate?: string
feeReviewStatus?: string
channelName?: string
containerNo?: string
cabinetNo?: string
sailingDate?: string
loaded?: boolean
fullContainer?: boolean
measurementReviewStatus?: string
fbaCode?: string
createdFrom?: string
createdTo?: string
page: number
size: number
}
export type UnifiedOrderStatusCountQuery = Omit<UnifiedOrderQuery, 'page' | 'size' | 'mainStatus'>
export interface UnifiedOrderStatusCounts {
counts: Partial<Record<UnifiedOrderMainStatus | 'MEASUREMENT_REVIEW', number>>
}
export async function getPlatformUnifiedOrderStatusCounts(query: UnifiedOrderStatusCountQuery): Promise<UnifiedOrderStatusCounts> {
// Strip these fields at runtime too, so callers cannot accidentally scope the other tabs.
const params = Object.fromEntries(Object.entries(query).filter(([key, value]) =>
!['page', 'size', 'mainStatus'].includes(key) && value !== undefined && value !== ''))
const response = await http.get<ApiEnvelope<UnifiedOrderStatusCounts>, ApiEnvelope<UnifiedOrderStatusCounts>>(
`${unifiedOrdersApiPath}/status-counts`, { params },
)
const result = unwrapApiResponse(response)
return { counts: Object.fromEntries(Object.entries(result.counts || {}).filter(([, count]) =>
typeof count === 'number' && Number.isSafeInteger(count) && count >= 0)) }
}
type PagePayload = Omit<UnifiedOrderPageResource, 'page' | 'size' | 'total' | 'pages'> & {
page: number | string
size: number | string
total: number | string
pages: number | string
}
function integer(value: number | string | null | undefined, fallback: number) {
const parsed = Number(value)
return Number.isFinite(parsed) && parsed >= 0 ? Math.trunc(parsed) : fallback
}
function nullableInteger(value: number | string | null | undefined) {
if (value === null || value === undefined || value === '') return null
return integer(value, 0)
}
function numberValue(value: number | string | null | undefined) {
if (value === null || value === undefined || value === '') return null
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
function id(value: number | string | null | undefined) {
return value === null || value === undefined || value === '' ? null : String(value)
}
export interface UnifiedOrderDeclarationCargo {
itemNo: number
cargoName: string
cargoNameEn?: string | null
skuNo?: string | null
model?: string | null
packageCount: number
totalQuantity: number
lengthCm: number
widthCm: number
heightCm: number
grossWeightKg: number
volumeCbm: number
netWeightKg?: number | null
hsCode?: string | null
customsHsCode?: string | null
unitsPerPackage?: number | null
unitDeclaredValue?: number | null
declaredValue?: number | null
currency?: string | null
material?: string | null
usage?: string | null
brand?: string | null
cargoRemark?: string | null
}
export interface UpdateUnifiedOrderDeclarationRequest {
expectedVersion: number
customerRemark?: string | null
expectedArrivalAt?: string | null
estimatedVolumeCbm?: number | null
channelCode: string
channelName: string
cargoes: UnifiedOrderDeclarationCargo[]
}
export interface UnifiedOrderDeclarationChange {
itemNo: number | null
field: string
beforeValue: string | null
afterValue: string | null
}
export interface UpdateUnifiedOrderDeclarationResult {
orderNo: string | null
inboundCode: string | null
previousVersion: number
version: number
channelChangedBefore: boolean
channelChanged: boolean
changes: UnifiedOrderDeclarationChange[]
}
export interface UpdateUnifiedOrderPlanningRequest {
expectedVersion: number
expectedArrivalAt?: string | null
estimatedVolumeCbm?: number | null
}
function optionalText(value: string | null | undefined) {
const normalized = value?.trim()
return normalized || null
}
function queryParams(query: UnifiedOrderQuery) {
return Object.fromEntries(Object.entries(query).filter(([, value]) => value !== undefined && value !== ''))
}
function normalizeRow(raw: UnifiedOrderResource): UnifiedOrderResource {
return {
...raw,
orderId: String(raw.orderId),
orderVersion: nullableInteger(raw.orderVersion),
tenantId: String(raw.tenantId),
customerId: id(raw.customerId),
channelCode: optionalText(raw.channelCode),
receivingWarehouseId: id(raw.receivingWarehouseId),
destinationWarehouseId: id(raw.destinationWarehouseId),
inboundId: id(raw.inboundId),
holdFlag: raw.holdFlag === null || raw.holdFlag === undefined ? null : Boolean(raw.holdFlag),
currentHoldReason: optionalText(raw.currentHoldReason),
inboundVersion: nullableInteger(raw.inboundVersion),
relation: raw.relation ? {
...raw.relation,
rootOrderId: id(raw.relation.rootOrderId),
parentOrderId: id(raw.relation.parentOrderId),
splitSequence: nullableInteger(raw.relation.splitSequence),
childCount: integer(raw.relation.childCount, 0),
} : null,
totalPackageCount: nullableInteger(raw.totalPackageCount),
estimatedVolumeCbm: numberValue(raw.estimatedVolumeCbm),
expectedVolumeCbm: numberValue(raw.expectedVolumeCbm),
channelChanged: Boolean(raw.channelChanged),
actualGrossWeightKg: numberValue(raw.actualGrossWeightKg),
actualVolumeCbm: numberValue(raw.actualVolumeCbm),
totalPalletCount: nullableInteger(raw.totalPalletCount),
chargeableVolumeCbm: numberValue(raw.chargeableVolumeCbm),
volumetricWeightKg: numberValue(raw.volumetricWeightKg),
totalNetWeightKg: numberValue(raw.totalNetWeightKg),
totalQuantity: nullableInteger(raw.totalQuantity),
skuNos: raw.skuNos?.filter((value) => value.trim()) ?? null,
locationCodes: raw.locationCodes ?? raw.location ?? null,
measurement: raw.measurement ? {
...raw.measurement,
actualGrossWeightKg: numberValue(raw.measurement.actualGrossWeightKg),
actualVolumeCbm: numberValue(raw.measurement.actualVolumeCbm),
chargeableWeightKg: numberValue(raw.measurement.chargeableWeightKg),
} : null,
preload: raw.preload ? { ...raw.preload, version: nullableInteger(raw.preload.version), itemCount: nullableInteger(raw.preload.itemCount) } : null,
loading: raw.loading ? {
...raw.loading,
loadingVersion: nullableInteger(raw.loading.loadingVersion),
pickingVersion: nullableInteger(raw.loading.pickingVersion),
expectedCount: nullableInteger(raw.loading.expectedCount),
pickedCount: nullableInteger(raw.loading.pickedCount),
evidenceCount: nullableInteger(raw.loading.evidenceCount),
} : null,
transport: raw.transport ? { ...raw.transport } : null,
coLoadGroup: raw.coLoadGroup ? {
status: String(raw.coLoadGroup.status || ''),
memberCount: integer(raw.coLoadGroup.memberCount, 0),
memberInboundCodes: (raw.coLoadGroup.memberInboundCodes || [])
.map((value) => typeof value === 'string' ? value.trim() : '')
.filter(Boolean),
} : null,
}
}
export async function listPlatformUnifiedOrders(query: UnifiedOrderQuery) {
const response = await http.get<ApiEnvelope<PagePayload>, ApiEnvelope<PagePayload>>(unifiedOrdersApiPath, {
params: queryParams(query),
})
const result = unwrapApiResponse(response)
return {
...result,
records: (result.records || []).map(normalizeRow),
page: integer(result.page, query.page),
size: integer(result.size, query.size),
total: integer(result.total, 0),
pages: integer(result.pages, 0),
} satisfies UnifiedOrderPageResource
}
export async function getPlatformUnifiedOrder(tenantId: string, orderId: string) {
const response = await http.get<ApiEnvelope<UnifiedOrderDetailResource>, ApiEnvelope<UnifiedOrderDetailResource>>(
`${unifiedOrdersApiPath}/${orderId}`,
{ params: { tenantId } },
)
const result = unwrapApiResponse(response)
return {
...result,
summary: normalizeRow(result.summary),
order: {
...result.order,
order: result.order.order ? { ...result.order.order, version: nullableInteger(result.order.order.version) } : null,
},
}
}
export function updatePlatformUnifiedOrderDeclaration(
tenantId: string,
orderId: string,
request: UpdateUnifiedOrderDeclarationRequest,
) {
return http.put<ApiEnvelope<UpdateUnifiedOrderDeclarationResult>, ApiEnvelope<UpdateUnifiedOrderDeclarationResult>>(
`${unifiedOrdersApiPath}/${orderId}/declaration`,
request,
{ params: { tenantId } },
).then(unwrapApiResponse)
}
export function updatePlatformUnifiedOrderPlanning(
tenantId: string,
orderId: string,
request: UpdateUnifiedOrderPlanningRequest,
) {
return http.put<ApiEnvelope<UpdateUnifiedOrderDeclarationResult>, ApiEnvelope<UpdateUnifiedOrderDeclarationResult>>(
`${unifiedOrdersApiPath}/${orderId}/planning`,
request,
{ params: { tenantId } },
).then(unwrapApiResponse)
}
export function downloadPlatformProductImage(tenantId: string, fileId: string) {
return http.get<Blob, Blob>(
`/platform/v1/files/product-images/${encodeURIComponent(tenantId)}/${encodeURIComponent(fileId)}`,
{ responseType: 'blob' },
)
}
export function approvePlatformUnifiedOrder(tenantId: string, orderId: string) {
return http.post<ApiEnvelope<void>, ApiEnvelope<void>>(
`/platform/v1/orders/first-leg/${orderId}/approve`,
undefined,
{ params: { tenantId } },
).then(unwrapApiResponse)
}
export interface UnifiedOrderBatchApprovalResult {
total: number
successCount: number
failureCount: number
successOrderIds: Array<number | string>
failedOrders: Array<{ orderId: number | string; code: string; message: string }>
}
export interface UnifiedOrderBatchReviewItem {
orderId: string
orderVersion: number
}
export interface UnifiedOrderBatchApprovalPreviewItem extends UnifiedOrderBatchReviewItem {
inboundCode: string | null
customerId: string | null
customerName: string | null
chineseGoodsNames: string[]
channelCode: string | null
channelName: string | null
destinationWarehouseCode: string | null
destinationWarehouseName: string | null
canApprove: boolean
blockReason: string | null
}
export interface UnifiedOrderBatchApprovalPreview {
total: number
approvableCount: number
items: UnifiedOrderBatchApprovalPreviewItem[]
}
function normalizeBatchApprovalPreview(result: UnifiedOrderBatchApprovalPreview): UnifiedOrderBatchApprovalPreview {
return {
...result,
total: integer(result.total, 0),
approvableCount: integer(result.approvableCount, 0),
items: (result.items || []).map((item) => ({
...item,
orderId: String(item.orderId),
orderVersion: integer(item.orderVersion, 0),
customerId: id(item.customerId),
chineseGoodsNames: (item.chineseGoodsNames || []).map((name) => name.trim()).filter(Boolean),
canApprove: Boolean(item.canApprove),
})),
}
}
export async function getPlatformUnifiedOrderBatchApprovalPreview(tenantId: string, items: UnifiedOrderBatchReviewItem[]) {
const response = await http.post<ApiEnvelope<UnifiedOrderBatchApprovalPreview>, ApiEnvelope<UnifiedOrderBatchApprovalPreview>>(
'/platform/v1/orders/first-leg/batch-review-preview',
{ items },
{ params: { tenantId } },
)
return normalizeBatchApprovalPreview(unwrapApiResponse(response))
}
export function batchApprovePlatformUnifiedOrders(tenantId: string, items: UnifiedOrderBatchReviewItem[]) {
return http.post<ApiEnvelope<UnifiedOrderBatchApprovalResult>, ApiEnvelope<UnifiedOrderBatchApprovalResult>>(
'/platform/v1/orders/first-leg/batch-approve',
{ items },
{ params: { tenantId } },
).then(unwrapApiResponse)
}
export interface UnifiedOrderRemarksUpdateRequest {
expectedVersion: number
internalRemark: string | null
warehouseRemark: string | null
customerServiceRemark: string | null
}
export interface LegacyOrderImportItem {
excelRow: number
success: boolean
legacyInboundCode: string | null
orderId: number | string | null
orderNo: string | null
errorMessage: string | null
}
export interface LegacyOrderImportResult {
total: number
succeeded: number
failed: number
items: LegacyOrderImportItem[]
}
export function importPlatformLegacyOrders(file: File, idempotencyKey: string) {
const form = new FormData()
form.append('file', file)
return http.post<ApiEnvelope<LegacyOrderImportResult>, ApiEnvelope<LegacyOrderImportResult>>(
'/platform/v1/orders/first-leg/legacy-import',
form,
{ headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': idempotencyKey } },
).then(unwrapApiResponse)
}
export function createLegacyOrderImportIdempotencyKey() {
const uuid = globalThis.crypto?.randomUUID?.()
return uuid ? `legacy-order-import-${uuid}` : `legacy-order-import-${Date.now()}-${Math.random().toString(36).slice(2)}`
}
export interface UnifiedOrderRemarksUpdateResponse {
version: number
customerRemark: string | null
internalRemark: string | null
warehouseRemark: string | null
customerServiceRemark: string | null
}
export function updatePlatformUnifiedOrderRemarks(tenantId: string, orderId: string, request: UnifiedOrderRemarksUpdateRequest) {
return http.put<ApiEnvelope<UnifiedOrderRemarksUpdateResponse>, ApiEnvelope<UnifiedOrderRemarksUpdateResponse>>(
`${unifiedOrdersApiPath}/${orderId}/remarks`,
request,
{ params: { tenantId } },
).then(unwrapApiResponse)
}
export async function listPlatformUnifiedOrderFeeDetails(accountId: string, inboundId: string) {
const response = await http.get<ApiEnvelope<UnifiedOrderFeeDetailResource[]>, ApiEnvelope<UnifiedOrderFeeDetailResource[]>>(
`/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fee-details`,
{ params: { accountId } },
)
return unwrapApiResponse(response)
}
export function unifiedOrderStatusLabel(status: UnifiedOrderMainStatus) {
if (status === 'DELIVERED') return '签收完成'
if (status === 'COMPLETED') return '已完成'
return unifiedOrderStatusOptions.find((item) => item.value === status)?.label || '未知状态'
}