184 lines
8.1 KiB
TypeScript
184 lines
8.1 KiB
TypeScript
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) ?? '未知状态'
|
||
}
|
||
|
||
export function getFirstLegTransportModeLabel(mode: FirstLegTransportMode | null) {
|
||
if (!mode) return '运输方式待更新'
|
||
return transportModeLabels.get(mode) ?? mode
|
||
}
|
||
|
||
export function getFirstLegStatusTone(status: FirstLegOrderStatus): 'success' | 'warning' | 'info' | 'primary' {
|
||
if (status === 'DELIVERED' || status === 'LOADED' || status === 'COMPLETED') return 'success'
|
||
if (status === 'DRAFT') return 'info'
|
||
if (status === 'PENDING_REVIEW') return 'warning'
|
||
if (['WAIT_MEASURE', 'WAIT_PALLETIZE', 'WAIT_SPLIT', 'LOADING'].includes(status)) return 'primary'
|
||
return 'info'
|
||
}
|
||
|
||
export function canClientEditFirstLegOrder(status: FirstLegOrderStatus) {
|
||
return status === 'DRAFT'
|
||
}
|
||
|
||
export function canClientCancelFirstLegOrder(status: FirstLegOrderStatus) {
|
||
return status === 'DRAFT' || status === 'PENDING_REVIEW'
|
||
}
|
||
|
||
export const FIRST_LEG_PROGRESS_STAGES = FIRST_LEG_ORDER_STATUSES.filter(
|
||
(item): item is (typeof FIRST_LEG_ORDER_STATUSES)[number] & { code: Exclude<FirstLegOrderStatus, 'DRAFT'> } => item.code !== 'DRAFT',
|
||
)
|
||
|
||
export function getFirstLegProgressIndex(status: FirstLegOrderStatus) {
|
||
if (status === 'DRAFT') return -1
|
||
return FIRST_LEG_PROGRESS_STAGES.findIndex((item) => item.code === status)
|
||
}
|
||
|
||
export function createEmptyCargoItem(id = `cargo-${Date.now()}`): FirstLegCargoItem {
|
||
return {
|
||
id,
|
||
sku: '',
|
||
upc: '',
|
||
cargoNameCn: '',
|
||
cargoNameEn: '',
|
||
productImageFileId: '',
|
||
productImageName: '',
|
||
productImagePreviewUrl: '',
|
||
productImageUploading: false,
|
||
productImageError: '',
|
||
customsHsCode: '',
|
||
lengthCm: 0,
|
||
widthCm: 0,
|
||
heightCm: 0,
|
||
unitsPerPackage: 1,
|
||
pieces: 1,
|
||
packageCount: 1,
|
||
packingGroupNo: '',
|
||
grossWeightKg: 0,
|
||
volumeCbm: 0,
|
||
netWeightKg: 0,
|
||
unitDeclaredValue: 0,
|
||
material: '',
|
||
usage: '',
|
||
model: '',
|
||
brand: '',
|
||
productLengthCm: 0,
|
||
productWidthCm: 0,
|
||
productHeightCm: 0,
|
||
shipmentId: '',
|
||
referenceId: '',
|
||
remark: '',
|
||
packageType: 'CARTON',
|
||
hsCode: '',
|
||
declaredValue: 0,
|
||
currency: 'USD',
|
||
dangerousGoods: false,
|
||
woodenProduct: false,
|
||
}
|
||
}
|
||
|
||
export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) {
|
||
const errors: string[] = []
|
||
|
||
if (!draft.destination.trim()) errors.push('请填写目的地')
|
||
if (!/^[A-Za-z]{2}$/.test(draft.destinationCountryCode.trim())) errors.push('请输入两位英文字母的目的国代码')
|
||
if (!draft.destinationWarehouseName.trim()) errors.push('请填写目的仓名称')
|
||
if (draft.destinationType !== 'PRIVATE' && !draft.destinationWarehouseId) errors.push('请选择平台维护的目的仓')
|
||
if (draft.destinationType === 'PRIVATE' && draft.destinationWarehouseId) errors.push('私人地址不能引用平台公共仓库')
|
||
if (!draft.channelCode.trim() || !draft.channelName.trim()) errors.push('请填写渠道编码和渠道名称')
|
||
if (!['TAX_INCLUDED', 'TAX_EXCLUDED'].includes(draft.pricingTaxMode)) errors.push('请选择报价包税方式')
|
||
if (!draft.receivingWarehouseId) errors.push('请选择平台收货仓')
|
||
if (draft.pickupMethod === 'PICKUP') {
|
||
if (!draft.senderAddressId) errors.push('请选择已保存的提货地址')
|
||
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.privateDestinationAddressId) errors.push('请选择已保存的私人仓地址')
|
||
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('请填写收件详细地址')
|
||
}
|
||
if (draft.items.length === 0) errors.push('请至少添加一条货物明细')
|
||
|
||
if (draft.destinationType === 'FBA') {
|
||
if (!draft.amazonWarehouseCode.trim()) errors.push('FBA 订单请填写 Amazon 仓库编码')
|
||
}
|
||
|
||
draft.items.forEach((item, index) => {
|
||
const line = `第 ${index + 1} 条货物`
|
||
if (!item.cargoNameCn.trim()) errors.push(`${line}缺少中文品名`)
|
||
if (!item.packageType.trim()) errors.push(`${line}缺少包装类型`)
|
||
if ((draft.destinationType === 'FBA' || draft.fulfillmentType === 'ONE_CLICK_DROPSHIP') && !item.sku.trim()) errors.push(`${line}缺少客户 SKU`)
|
||
if (draft.destinationType === 'FBA' && !item.shipmentId.trim()) errors.push(`${line}缺少货件编号`)
|
||
if (draft.destinationType === 'FBA' && !item.referenceId.trim()) errors.push(`${line}缺少跟踪编号`)
|
||
if (!Number.isInteger(item.pieces) || item.pieces <= 0) errors.push(`${line}件数必须为正整数`)
|
||
if (!Number.isInteger(item.packageCount) || item.packageCount <= 0) errors.push(`${line}箱数必须为正整数`)
|
||
if (!Number.isInteger(item.unitsPerPackage) || item.unitsPerPackage <= 0) errors.push(`${line}每箱数量必须为正整数`)
|
||
if (Number.isInteger(item.unitsPerPackage) && item.unitsPerPackage > 0
|
||
&& Number.isInteger(item.packageCount) && item.packageCount > 0
|
||
&& Number.isInteger(item.pieces) && item.pieces > 0
|
||
&& item.pieces !== item.unitsPerPackage * item.packageCount) {
|
||
errors.push(`${line}总数量应等于每箱数量 × 箱数(${item.unitsPerPackage * item.packageCount})`)
|
||
}
|
||
if (item.grossWeightKg <= 0) errors.push(`${line}毛重必须大于 0`)
|
||
if (item.volumeCbm <= 0) errors.push(`${line}体积必须大于 0`)
|
||
if (item.woodenProduct && draft.customsInspection !== true) errors.push(`${line}为木制品,“是否商检”必须选择“是”`)
|
||
})
|
||
|
||
if (draft.fulfillmentType === 'ONE_CLICK_DROPSHIP') {
|
||
const groups = new Map<string, FirstLegCargoItem>()
|
||
draft.items.forEach((item) => {
|
||
const group = item.packingGroupNo.trim()
|
||
if (!group) return
|
||
const first = groups.get(group)
|
||
if (!first) groups.set(group, item)
|
||
else if (first.packageCount !== item.packageCount || first.packageType !== item.packageType
|
||
|| first.lengthCm !== item.lengthCm || first.widthCm !== item.widthCm || first.heightCm !== item.heightCm
|
||
|| first.grossWeightKg !== item.grossWeightKg || first.netWeightKg !== item.netWeightKg
|
||
|| first.volumeCbm !== item.volumeCbm) {
|
||
errors.push(`同箱 SKU 的箱数、包装类型、箱规、重量和体积必须一致`)
|
||
}
|
||
})
|
||
}
|
||
|
||
return errors
|
||
}
|
||
|
||
export function summarizeFirstLegCargo(items: FirstLegCargoItem[]) {
|
||
const countedGroups = new Set<string>()
|
||
return items.reduce(
|
||
(summary, item) => {
|
||
const group = item.packingGroupNo.trim()
|
||
const alreadyCounted = Boolean(group && countedGroups.has(group))
|
||
const packageCount = alreadyCounted ? 0 : item.packageCount
|
||
if (group) countedGroups.add(group)
|
||
return ({
|
||
pieces: summary.pieces + item.pieces,
|
||
packageCount: summary.packageCount + packageCount,
|
||
grossWeightKg: summary.grossWeightKg + (alreadyCounted ? 0 : item.grossWeightKg),
|
||
volumeCbm: summary.volumeCbm + (alreadyCounted ? 0 : item.volumeCbm),
|
||
})
|
||
},
|
||
{ pieces: 0, packageCount: 0, grossWeightKg: 0, volumeCbm: 0 },
|
||
)
|
||
}
|