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

182 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 | null) {
if (!mode) return '运输方式待更新'
return transportModeLabels.get(mode) ?? mode
}
export function getFirstLegStatusTone(status: FirstLegOrderStatus): 'success' | 'warning' | 'info' | 'danger' {
if (status === 'DELIVERED') return 'success'
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'
}
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 },
)
}