Files
client-web/src/modules/first-leg/utils/firstLegOrder.ts
T

175 lines
7.2 KiB
TypeScript
Raw Normal View History

2026-07-30 17:59:42 +08:00
import {
FIRST_LEG_ORDER_STATUSES,
FIRST_LEG_TRANSPORT_MODES,
type FirstLegOrderStatus,
type FirstLegTransportMode,
} from '@chongchong/shared'
2026-08-07 18:15:59 +08:00
import type { CargoBoxSizeType, FirstLegCargoItem, FirstLegOrderDraft } from '../types/firstLegOrder.ts'
export const CARGO_BOX_SIZES = [
{ code: 'SMALL', label: '小箱', inner: '30 × 20 × 20', lengthCm: 34, widthCm: 24, heightCm: 24 },
{ code: 'MEDIUM', label: '中箱', inner: '40 × 30 × 30', lengthCm: 44, widthCm: 34, heightCm: 34 },
{ code: 'LARGE', label: '大箱', inner: '50 × 40 × 40', lengthCm: 54, widthCm: 44, heightCm: 44 },
{ code: 'EXTRA_LARGE', label: '特大箱', inner: '60 × 50 × 50', lengthCm: 64, widthCm: 54, heightCm: 54 },
] as const satisfies ReadonlyArray<{
code: Exclude<CargoBoxSizeType, ''>
label: string
inner: string
lengthCm: number
widthCm: number
heightCm: number
}>
export function inferCargoBoxSizeType(lengthCm?: number, widthCm?: number, heightCm?: number): CargoBoxSizeType {
return CARGO_BOX_SIZES.find((box) => box.lengthCm === lengthCm && box.widthCm === widthCm && box.heightCm === heightCm)?.code ?? ''
}
export function applyCargoBoxSize(item: FirstLegCargoItem, type: CargoBoxSizeType) {
item.boxSizeType = type
const box = CARGO_BOX_SIZES.find((option) => option.code === type)
if (!box) {
item.lengthCm = 0
item.widthCm = 0
item.heightCm = 0
item.volumeCbm = 0
return
}
item.lengthCm = box.lengthCm
item.widthCm = box.widthCm
item.heightCm = box.heightCm
updateCargoBoxVolume(item)
}
export function updateCargoBoxVolume(item: FirstLegCargoItem) {
if (!item.boxSizeType || item.packageCount <= 0) return
item.volumeCbm = Number(((item.lengthCm * item.widthCm * item.heightCm * item.packageCount) / 1_000_000).toFixed(3))
}
2026-07-30 17:59:42 +08:00
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
}
2026-08-06 13:56:13 +08:00
export function getFirstLegTransportModeLabel(mode: FirstLegTransportMode | null) {
if (!mode) return '运输方式待更新'
2026-07-30 17:59:42 +08:00
return transportModeLabels.get(mode) ?? mode
}
export function getFirstLegStatusTone(status: FirstLegOrderStatus): 'success' | 'warning' | 'info' | 'danger' {
2026-08-06 13:56:13 +08:00
if (status === 'DELIVERED') return 'success'
2026-07-30 17:59:42 +08:00
if (status === 'DRAFT' || status === 'PENDING_REVIEW') return 'warning'
return 'info'
}
export function canClientEditFirstLegOrder(status: FirstLegOrderStatus) {
return status === 'DRAFT'
}
export function canClientCancelFirstLegOrder(status: FirstLegOrderStatus) {
return status === 'DRAFT' || status === 'PENDING_REVIEW'
}
2026-08-06 13:56:13 +08:00
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)
}
2026-07-30 17:59:42 +08:00
export function createEmptyCargoItem(id = `cargo-${Date.now()}`): FirstLegCargoItem {
return {
id,
sku: '',
upc: '',
2026-07-30 17:59:42 +08:00
cargoNameCn: '',
cargoNameEn: '',
pieces: 1,
packageCount: 1,
grossWeightKg: 0,
volumeCbm: 0,
2026-08-07 18:15:59 +08:00
boxSizeType: '',
lengthCm: 0,
widthCm: 0,
heightCm: 0,
2026-07-30 17:59:42 +08:00
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('请填写目的地')
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('请填写渠道编码和渠道名称')
2026-08-07 14:47:00 +08:00
if (!['TAX_INCLUDED', 'TAX_EXCLUDED'].includes(draft.pricingTaxMode)) errors.push('请选择报价包税方式')
2026-07-30 17:59:42 +08:00
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('请至少添加一条货物明细')
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-07 18:15:59 +08:00
if (!item.boxSizeType) errors.push(`${line}请选择箱规`)
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 },
)
}