2026-07-30 17:59:42 +08:00
|
|
|
import {
|
|
|
|
|
FIRST_LEG_ORDER_STATUSES,
|
|
|
|
|
FIRST_LEG_TRANSPORT_MODES,
|
|
|
|
|
type FirstLegOrderStatus,
|
|
|
|
|
type FirstLegTransportMode,
|
|
|
|
|
} from '@chongchong/shared'
|
|
|
|
|
import type { FirstLegCargoItem, FirstLegOrderDraft } from '../types/firstLegOrder.ts'
|
|
|
|
|
|
|
|
|
|
const statusLabels = new Map<FirstLegOrderStatus, string>(
|
|
|
|
|
FIRST_LEG_ORDER_STATUSES.map((item) => [item.code, item.label]),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const transportModeLabels = new Map<FirstLegTransportMode, string>(
|
|
|
|
|
FIRST_LEG_TRANSPORT_MODES.map((item) => [item.code, item.label]),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
export function getFirstLegStatusLabel(status: FirstLegOrderStatus) {
|
|
|
|
|
return statusLabels.get(status) ?? status
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getFirstLegTransportModeLabel(mode: FirstLegTransportMode) {
|
|
|
|
|
return transportModeLabels.get(mode) ?? mode
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getFirstLegStatusTone(status: FirstLegOrderStatus): 'success' | 'warning' | 'info' | 'danger' {
|
|
|
|
|
if (status === 'COMPLETED') return 'success'
|
|
|
|
|
if (status === 'DRAFT' || status === 'PENDING_REVIEW') return 'warning'
|
|
|
|
|
if (status === 'CANCELLED') return 'danger'
|
|
|
|
|
return 'info'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function canClientEditFirstLegOrder(status: FirstLegOrderStatus) {
|
|
|
|
|
return status === 'DRAFT'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function canClientCancelFirstLegOrder(status: FirstLegOrderStatus) {
|
|
|
|
|
return status === 'DRAFT' || status === 'PENDING_REVIEW'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createEmptyCargoItem(id = `cargo-${Date.now()}`): FirstLegCargoItem {
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
sku: '',
|
2026-08-01 12:17:33 +08:00
|
|
|
upc: '',
|
2026-07-30 17:59:42 +08:00
|
|
|
cargoNameCn: '',
|
|
|
|
|
cargoNameEn: '',
|
|
|
|
|
pieces: 1,
|
|
|
|
|
packageCount: 1,
|
|
|
|
|
grossWeightKg: 0,
|
|
|
|
|
volumeCbm: 0,
|
|
|
|
|
containerType: '',
|
|
|
|
|
containerCount: 0,
|
|
|
|
|
remark: '',
|
|
|
|
|
packageType: 'CARTON',
|
|
|
|
|
hsCode: '',
|
|
|
|
|
declaredValue: 0,
|
|
|
|
|
currency: 'USD',
|
|
|
|
|
dangerousGoods: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) {
|
|
|
|
|
const errors: string[] = []
|
|
|
|
|
|
|
|
|
|
if (!draft.destination.trim()) errors.push('请填写目的地')
|
2026-08-01 12:17:33 +08:00
|
|
|
if (!/^[A-Za-z]{2}$/.test(draft.destinationCountryCode.trim())) errors.push('请输入两位英文字母的目的国代码')
|
2026-07-30 17:59:42 +08:00
|
|
|
if (!draft.destinationWarehouseName.trim()) errors.push('请填写目的仓名称')
|
2026-08-04 11:14:29 +08:00
|
|
|
if (draft.destinationType !== 'PRIVATE' && !draft.destinationWarehouseId) errors.push('请选择平台维护的目的仓')
|
|
|
|
|
if (draft.destinationType === 'PRIVATE' && draft.destinationWarehouseId) errors.push('私人地址不能引用平台公共仓库')
|
2026-07-30 17:59:42 +08:00
|
|
|
if (!draft.channelCode.trim() || !draft.channelName.trim()) errors.push('请填写渠道编码和渠道名称')
|
|
|
|
|
if (!draft.receivingWarehouseId) errors.push('请选择平台收货仓')
|
2026-08-04 12:01:05 +08:00
|
|
|
if (draft.pickupMethod === 'PICKUP') {
|
|
|
|
|
if (!draft.shipperPartySnapshot?.contactName.trim()) errors.push('请填写提货联系人')
|
|
|
|
|
if (!draft.shipperPartySnapshot?.phone.trim()) errors.push('请填写提货联系电话')
|
|
|
|
|
if (!draft.shipperPartySnapshot?.city.trim()) errors.push('请填写提货城市')
|
|
|
|
|
if (!draft.shipperPartySnapshot?.address.trim()) errors.push('请填写提货地址')
|
|
|
|
|
}
|
|
|
|
|
if (draft.destinationType === 'PRIVATE') {
|
|
|
|
|
if (!draft.consigneePartySnapshot?.name.trim()) errors.push('请填写私人地址名称')
|
|
|
|
|
if (!draft.consigneePartySnapshot?.contactName.trim()) errors.push('请填写收件人姓名')
|
|
|
|
|
if (!draft.consigneePartySnapshot?.phone.trim()) errors.push('请填写收件人电话')
|
|
|
|
|
if (!draft.consigneePartySnapshot?.city.trim()) errors.push('请填写收件城市')
|
|
|
|
|
if (!draft.consigneePartySnapshot?.address.trim()) errors.push('请填写收件详细地址')
|
|
|
|
|
}
|
2026-07-30 17:59:42 +08:00
|
|
|
if (draft.items.length === 0) errors.push('请至少添加一条货物明细')
|
|
|
|
|
|
2026-08-01 12:17:33 +08:00
|
|
|
if (draft.destinationType === 'FBA') {
|
|
|
|
|
if (!draft.referenceNo.trim()) errors.push('FBA 订单请填写客户参考号')
|
|
|
|
|
if (!draft.amazonWarehouseCode.trim()) errors.push('FBA 订单请填写 Amazon 仓库编码')
|
|
|
|
|
if (!draft.shipmentId.trim()) errors.push('FBA 订单请填写 Shipment ID')
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 17:59:42 +08:00
|
|
|
draft.items.forEach((item, index) => {
|
|
|
|
|
const line = `第 ${index + 1} 条货物`
|
|
|
|
|
if (!item.cargoNameCn.trim()) errors.push(`${line}缺少中文品名`)
|
|
|
|
|
if (!item.packageType.trim()) errors.push(`${line}缺少包装类型`)
|
2026-08-01 12:17:33 +08:00
|
|
|
if (draft.destinationType === 'FBA' && !item.sku.trim()) errors.push(`${line}缺少客户 SKU`)
|
|
|
|
|
if (draft.destinationType === 'FBA' && !item.upc.trim()) errors.push(`${line}缺少 UPC`)
|
2026-07-30 17:59:42 +08:00
|
|
|
if (!Number.isInteger(item.pieces) || item.pieces <= 0) errors.push(`${line}件数必须为正整数`)
|
|
|
|
|
if (item.grossWeightKg <= 0) errors.push(`${line}毛重必须大于 0`)
|
|
|
|
|
if (item.volumeCbm <= 0) errors.push(`${line}体积必须大于 0`)
|
|
|
|
|
if (draft.transportMode === 'FCL' && (!item.containerType || item.containerCount <= 0)) {
|
|
|
|
|
errors.push(`${line}需要填写箱型和柜量`)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return errors
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function summarizeFirstLegCargo(items: FirstLegCargoItem[]) {
|
|
|
|
|
return items.reduce(
|
|
|
|
|
(summary, item) => ({
|
|
|
|
|
pieces: summary.pieces + item.pieces,
|
|
|
|
|
packageCount: summary.packageCount + item.packageCount,
|
|
|
|
|
grossWeightKg: summary.grossWeightKg + item.grossWeightKg,
|
|
|
|
|
volumeCbm: summary.volumeCbm + item.volumeCbm,
|
|
|
|
|
}),
|
|
|
|
|
{ pieces: 0, packageCount: 0, grossWeightKg: 0, volumeCbm: 0 },
|
|
|
|
|
)
|
|
|
|
|
}
|