import type { Plan, PlanContractLink, PlanDelivery, PlanLoad, PlanStopProduct } from '@/api/pl' import type { PlanFormProductRow } from '@/composables/usePlContractToPlan' import type { BrandOption } from '@/utils/crm-product-brand' import { formatBrandConstraintLabel } from '@/utils/crm-product-brand' import { isPlDibangOptionalVehiclePlate, isPlInternalCustomerName } from '@/utils/pl-internal-customer' export interface PlanStopProductFormRow { id?: string planProductId?: string productId: string productNo: string productName: string /** 展示用:主数据标准名 */ standardProductName?: string /** 展示用:别名 */ aliasName?: string aliasOptions?: string[] brandMode?: string brandId?: string brandName?: string brandOptions?: BrandOption[] model?: string unit: string packageSpec?: string deliveryMethod?: string planQty?: number /** 装货段归档:入库数量即实际数量 */ inboundQty?: number inboundPrice?: number inboundAmount?: number /** 送货段归档:出库数量即实际数量 */ outboundQty?: number outboundPrice?: number outboundAmount?: number /** 本段归档品牌数量 */ brandQtys?: Array<{ brandId: string; brandName?: string; qty: number; unitPrice?: number }> remark?: string isExplosive?: number isToxic?: number } /** 段次查看/归档:装货段取 inbound,送货段取 outbound */ export function resolveStopActualQty( row: PlanStopProductFormRow, stopType: 'load' | 'delivery' ): number | undefined { const raw = stopType === 'load' ? row.inboundQty : row.outboundQty return raw != null ? Number(raw) : undefined } export function resolveStopUnitPrice( row: PlanStopProductFormRow, stopType: 'load' | 'delivery' ): number | undefined { const raw = stopType === 'load' ? row.inboundPrice : row.outboundPrice return raw != null ? Number(raw) : undefined } export function resolveStopArchiveAmount( row: PlanStopProductFormRow, stopType: 'load' | 'delivery' ): number | undefined { const stored = stopType === 'load' ? row.inboundAmount : row.outboundAmount if (stored != null) { const n = Number(stored) return Number.isFinite(n) ? n : undefined } const qty = resolveStopActualQty(row, stopType) const price = resolveStopUnitPrice(row, stopType) if (qty == null || price == null) return undefined const amount = qty * price return Number.isFinite(amount) ? amount : undefined } export function formatStopArchiveAmount( row: PlanStopProductFormRow, stopType: 'load' | 'delivery' ): string { const amount = resolveStopArchiveAmount(row, stopType) return amount != null ? amount.toFixed(2) : '—' } export interface PlanLoadForm { customerId: string customerName: string address: string lng: number | null lat: number | null loadTime: string contact: string notes: string generateDibang: number dibangDirection: string contracts: PlanContractLink[] products: PlanStopProductFormRow[] } export interface PlanDeliveryForm { seq: number customerId: string customerName: string address: string lng: number | null lat: number | null deliveryTime: string contact: string notes: string generateDibang: number dibangDirection: string contracts: PlanContractLink[] products: PlanStopProductFormRow[] } /** * 地磅勾选规则: * - 内部客户(山峡/云栋):默认勾选且不可改 * - 地磅可选车(如皖H21522)+ 内部客户:默认不勾,可手动勾选 * - 其他客户:默认不勾且不可勾 */ export function resolveStopDibangDefaults( customerName?: string | null, stopType: 'load' | 'delivery' = 'load', vehiclePlate?: string | null ): { generateDibang: number; dibangDirection: string } { if (!isPlInternalCustomerName(customerName)) { return { generateDibang: 0, dibangDirection: 'none' } } if (isPlDibangOptionalVehiclePlate(vehiclePlate)) { return { generateDibang: 0, dibangDirection: 'none' } } if (stopType === 'load') { return { generateDibang: 1, dibangDirection: 'outbound' } } return { generateDibang: 1, dibangDirection: 'inbound' } } /** 仅「内部客户 + 地磅可选车」允许用户手动勾选地磅单 */ export function canManualToggleStopDibang( customerName?: string | null, vehiclePlate?: string | null ): boolean { return isPlInternalCustomerName(customerName) && isPlDibangOptionalVehiclePlate(vehiclePlate) } function normalizeStopDibangFieldsFromApi( customerName: string | null | undefined, stopType: 'load' | 'delivery', generateDibang?: number | null, dibangDirection?: string | null, vehiclePlate?: string | null ): { generateDibang: number; dibangDirection: string } { const defaults = resolveStopDibangDefaults(customerName, stopType, vehiclePlate) // 非山峡/云栋:禁止生成地磅单(编辑回填也强制清掉历史勾选) if (!isPlInternalCustomerName(customerName)) { return { generateDibang: 0, dibangDirection: 'none' } } const gen = Number(generateDibang ?? defaults.generateDibang) === 1 ? 1 : 0 let direction = (dibangDirection || '').trim() if (gen) { if (direction !== 'outbound' && direction !== 'inbound') { // 可选车默认 direction 为 none,勾选回填时按段类型补方向 direction = stopType === 'load' ? 'outbound' : 'inbound' } } else { direction = 'none' } return { generateDibang: gen, dibangDirection: direction } } export function createStopProductsFromPlanProducts( products: PlanFormProductRow[], qtyField: 'quantity' | 'actualQty' = 'quantity' ): PlanStopProductFormRow[] { return products.map((item) => ({ planProductId: item.id, productId: item.productId, productNo: item.productNo, productName: item.productName, standardProductName: item.standardProductName || item.productName, aliasName: item.aliasName, aliasOptions: item.aliasOptions ? [...item.aliasOptions] : undefined, brandMode: item.brandMode, brandId: item.brandId, brandName: item.brandName, brandOptions: item.brandOptions ? [...item.brandOptions] : undefined, model: item.model, unit: item.unit || '吨', packageSpec: item.packageSpec, deliveryMethod: item.deliveryMethod, planQty: Number(item[qtyField] ?? item.quantity ?? 0), remark: item.remark, isExplosive: item.isExplosive, isToxic: item.isToxic })) } function matchPrevStopProduct( stopProducts: PlanStopProductFormRow[], planRow: PlanFormProductRow, used: Set ): PlanStopProductFormRow | undefined { if (planRow.id) { const byPlanProductId = stopProducts.findIndex( (row, index) => !used.has(index) && row.planProductId && String(row.planProductId) === String(planRow.id) ) if (byPlanProductId >= 0) { used.add(byPlanProductId) return stopProducts[byPlanProductId] } } const byProductId = stopProducts.findIndex( (row, index) => !used.has(index) && String(row.productId) === String(planRow.productId) ) if (byProductId >= 0) { used.add(byProductId) return stopProducts[byProductId] } return undefined } /** * 按计划产品重建段产品列表:增删对齐计划层;共享字段取自计划; * 可选保留段内已改的数量 / 备注。 */ export function reconcileStopProductsWithPlan( stopProducts: PlanStopProductFormRow[], planProducts: PlanFormProductRow[], options?: { preserveQty?: boolean preserveRemark?: boolean } ): PlanStopProductFormRow[] { const preserveQty = options?.preserveQty !== false const preserveRemark = options?.preserveRemark !== false const used = new Set() const nextRows = createStopProductsFromPlanProducts(planProducts) return nextRows.map((row, index) => { const planRow = planProducts[index] const prev = planRow ? matchPrevStopProduct(stopProducts, planRow, used) : undefined if (!prev) return row return { ...row, planQty: preserveQty ? (prev.planQty ?? row.planQty) : row.planQty, remark: preserveRemark ? (prev.remark ?? row.remark) : row.remark } }) } /** 用计划产品行补齐装货/送货产品表展示字段(别名、品牌等) */ export function sumStopProductsPlanQty(products: PlanStopProductFormRow[]): number { return products.reduce((sum, row) => sum + Number(row.planQty ?? 0), 0) } /** 段内紧凑表「产品」列副行:别名 · 品牌 · 规格 · 单位 */ export function formatStopProductMetaSubline( row: PlanStopProductFormRow, unitLabel?: string ): string { const parts: string[] = [] if (row.aliasName) parts.push(`别名:${row.aliasName}`) const brand = formatBrandConstraintLabel(row.brandMode, row.brandName) if (brand && brand !== '—') parts.push(`品牌:${brand}`) if (row.model) parts.push(row.model) const unit = unitLabel || row.unit if (unit) parts.push(unit) return parts.join(' · ') } export function enrichStopProductsDisplayFromPlan( stopProducts: PlanStopProductFormRow[], planProducts: Array<{ id?: string productId: string productNo?: string productName?: string standardProductName?: string aliasName?: string aliasOptions?: string[] brandMode?: string brandId?: string brandName?: string brandOptions?: BrandOption[] model?: string unit?: string packageSpec?: string deliveryMethod?: string quantity?: number isExplosive?: number isToxic?: number }> ): PlanStopProductFormRow[] { return stopProducts.map((row) => { const match = planProducts.find( (p) => row.planProductId && p.id && String(p.id) === String(row.planProductId) ) || planProducts.find((p) => String(p.productId) === String(row.productId)) if (!match) return row const planQty = row.planQty != null && Number.isFinite(Number(row.planQty)) ? Number(row.planQty) : match.quantity != null ? Number(match.quantity) : row.planQty return { ...row, productNo: row.productNo || match.productNo || row.productNo, standardProductName: match.standardProductName || match.productName || row.productName, aliasName: match.aliasName ?? row.aliasName, aliasOptions: match.aliasOptions?.length ? [...match.aliasOptions] : row.aliasOptions, brandMode: match.brandMode ?? row.brandMode, brandId: match.brandId ?? row.brandId, brandName: match.brandName ?? row.brandName, brandOptions: match.brandOptions?.length ? [...match.brandOptions] : row.brandOptions, model: row.model || match.model, unit: row.unit || match.unit || '吨', packageSpec: row.packageSpec || match.packageSpec, deliveryMethod: row.deliveryMethod || match.deliveryMethod, planQty, isExplosive: match.isExplosive ?? row.isExplosive, isToxic: match.isToxic ?? row.isToxic } }) } /** 查看态:段产品为空时回退计划产品,再补齐展示字段(与编辑弹窗 bootstrap 一致) */ export function resolveStopProductsForView( stopProducts: PlanStopProductFormRow[] | undefined | null, planProducts: PlanFormProductRow[] ): PlanStopProductFormRow[] { const rows = stopProducts?.length ? stopProducts : planProducts.length ? createStopProductsFromPlanProducts(planProducts) : [] return enrichStopProductsDisplayFromPlan(rows, planProducts) } export function createEmptyLoadForm(): PlanLoadForm { return { customerId: '', customerName: '', address: '', lng: null, lat: null, loadTime: '', contact: '', notes: '', generateDibang: 0, dibangDirection: 'none', contracts: [], products: [] } } export function createEmptyDeliveryForm(seq = 1): PlanDeliveryForm { return { seq, customerId: '', customerName: '', address: '', lng: null, lat: null, deliveryTime: '', contact: '', notes: '', generateDibang: 0, dibangDirection: 'none', contracts: [], products: [] } } function mapStopProductFromApi(row: PlanStopProduct): PlanStopProductFormRow { return { id: row.id, planProductId: row.planProductId, productId: row.productId, productNo: row.productNo, productName: row.productName, model: row.model, unit: row.unit || '吨', packageSpec: row.packageSpec, deliveryMethod: row.deliveryMethod, planQty: row.planQty != null ? Number(row.planQty) : undefined, inboundQty: row.inboundQty != null ? Number(row.inboundQty) : undefined, inboundPrice: row.inboundPrice != null ? Number(row.inboundPrice) : undefined, inboundAmount: row.inboundAmount != null ? Number(row.inboundAmount) : undefined, outboundQty: row.outboundQty != null ? Number(row.outboundQty) : undefined, outboundPrice: row.outboundPrice != null ? Number(row.outboundPrice) : undefined, outboundAmount: row.outboundAmount != null ? Number(row.outboundAmount) : undefined, brandQtys: (row.brandQtys || []) .filter((item) => Number(item.qty) > 0) .map((item) => ({ brandId: String(item.brandId || ''), brandName: item.brandName, qty: Number(item.qty), unitPrice: item.unitPrice != null && Number.isFinite(Number(item.unitPrice)) ? Number(item.unitPrice) : undefined })), remark: row.remark } } /** 查看态装货地址:优先段表 address,回退计划 pickupAddress */ export function resolveLoadViewAddress( plan?: { pickupAddress?: string | null } | null, load?: { address?: string | null } | null ): string { const fromStop = (load?.address || '').trim() if (fromStop) return fromStop return (plan?.pickupAddress || '').trim() } /** 查看态送货地址:优先段表 address,首段可回退计划 destinationAddress */ export function resolveDeliveryViewAddress( plan?: { destinationAddress?: string | null } | null, delivery?: { address?: string | null } | null, index = 0 ): string { const fromStop = (delivery?.address || '').trim() if (fromStop) return fromStop if (index === 0) { return (plan?.destinationAddress || '').trim() } return '' } export function mapLoadFromApi( load?: PlanLoad | null, vehiclePlate?: string | null ): PlanLoadForm { if (!load) return createEmptyLoadForm() const dibang = normalizeStopDibangFieldsFromApi( load.customerName, 'load', load.generateDibang, load.dibangDirection, vehiclePlate ) return { customerId: load.customerId || '', customerName: load.customerName || '', address: load.address || '', lng: load.lng != null ? Number(load.lng) : null, lat: load.lat != null ? Number(load.lat) : null, loadTime: (load.loadTime || '').slice(0, 10), contact: load.contact || '', notes: load.notes || '', generateDibang: dibang.generateDibang, dibangDirection: dibang.dibangDirection, contracts: (load.contracts || []).map((item) => ({ contractId: String(item.contractId || ''), contractNo: item.contractNo })), products: (load.products || []).map(mapStopProductFromApi) } } export function mapDeliveriesFromApi( deliveries?: PlanDelivery[], vehiclePlate?: string | null ): PlanDeliveryForm[] { if (!deliveries?.length) return [createEmptyDeliveryForm(1)] return deliveries.map((row, index) => { const dibang = normalizeStopDibangFieldsFromApi( row.customerName, 'delivery', row.generateDibang, row.dibangDirection, vehiclePlate ) return { seq: row.seq ?? index + 1, customerId: row.customerId || '', customerName: row.customerName || '', address: row.address || '', lng: row.lng != null ? Number(row.lng) : null, lat: row.lat != null ? Number(row.lat) : null, deliveryTime: (row.deliveryTime || '').slice(0, 10), contact: row.contact || '', notes: row.notes || '', generateDibang: dibang.generateDibang, dibangDirection: dibang.dibangDirection, contracts: (row.contracts || []).map((item) => ({ contractId: String(item.contractId || ''), contractNo: item.contractNo })), products: (row.products || []).map(mapStopProductFromApi) } }) } /** 旧数据无 load/deliveries 时从供方/需方字段回填 */ export function mapLegacyPlanToStops(plan: Plan): { load: PlanLoadForm deliveries: PlanDeliveryForm[] } { const plate = plan.licensePlate const loadDefaults = resolveStopDibangDefaults(plan.supplierName, 'load', plate) const deliveryDefaults = resolveStopDibangDefaults(plan.customerName, 'delivery', plate) const stopProducts = createStopProductsFromPlanProducts( (plan.products || []).map((item, index) => ({ id: item.id, productId: String(item.productId || index), productNo: item.productNo || '', productName: item.productName || '', model: item.model, unit: item.unit || '吨', quantity: Number(item.quantity ?? item.actualQty ?? 0), packageSpec: item.packageSpec, deliveryMethod: item.deliveryMethod, remark: item.remark })) ) return { load: { customerId: plan.supplierId || '', customerName: plan.supplierName || '', address: plan.pickupAddress || '', lng: plan.pickupLng != null ? Number(plan.pickupLng) : null, lat: plan.pickupLat != null ? Number(plan.pickupLat) : null, loadTime: (plan.pickupTime || '').slice(0, 10), contact: plan.pickupContact || '', notes: plan.pickupNotes || '', generateDibang: loadDefaults.generateDibang, dibangDirection: loadDefaults.dibangDirection, contracts: (plan.supplierContracts || []).map((item) => ({ contractId: String(item.contractId || ''), contractNo: item.contractNo })), products: stopProducts.length ? stopProducts.map((row) => ({ ...row })) : [] }, deliveries: [ { seq: 1, customerId: plan.customerId || '', customerName: plan.customerName || '', address: plan.destinationAddress || '', lng: plan.destinationLng != null ? Number(plan.destinationLng) : null, lat: plan.destinationLat != null ? Number(plan.destinationLat) : null, deliveryTime: (plan.deliveryTime || '').slice(0, 10), contact: plan.deliveryContact || '', notes: plan.destinationNotes || '', generateDibang: deliveryDefaults.generateDibang, dibangDirection: deliveryDefaults.dibangDirection, contracts: (plan.demanderContracts || []).map((item) => ({ contractId: String(item.contractId || ''), contractNo: item.contractNo })), products: stopProducts.length ? stopProducts.map((row) => ({ ...row })) : [] } ] } } export function syncLoadProductsFromPlan( load: PlanLoadForm, planProducts: PlanFormProductRow[], preserveQty = true ) { load.products = reconcileStopProductsWithPlan(load.products, planProducts, { preserveQty, preserveRemark: preserveQty }) } export function syncDeliveryProductsFromPlan( delivery: PlanDeliveryForm, planProducts: PlanFormProductRow[], preserveQty = false ) { delivery.products = reconcileStopProductsWithPlan(delivery.products, planProducts, { preserveQty, preserveRemark: preserveQty }) } function matchPrevDeliveryProduct( stopProducts: PlanStopProductFormRow[], loadRow: PlanStopProductFormRow, used: Set ): PlanStopProductFormRow | undefined { if (loadRow.planProductId) { const byPlanProductId = stopProducts.findIndex( (row, index) => !used.has(index) && row.planProductId && String(row.planProductId) === String(loadRow.planProductId) ) if (byPlanProductId >= 0) { used.add(byPlanProductId) return stopProducts[byPlanProductId] } } const byProductId = stopProducts.findIndex( (row, index) => !used.has(index) && String(row.productId) === String(loadRow.productId) ) if (byProductId >= 0) { used.add(byProductId) return stopProducts[byProductId] } return undefined } function cloneStopProductSharedFields( loadRow: PlanStopProductFormRow, planQty: number, remark?: string ): PlanStopProductFormRow { return { ...loadRow, aliasOptions: loadRow.aliasOptions ? [...loadRow.aliasOptions] : undefined, brandOptions: loadRow.brandOptions ? [...loadRow.brandOptions] : undefined, planQty, remark: remark ?? loadRow.remark } } /** * 从装货产品同步到各送货段(产品清单 ⊆ 装货)。 * C1:仅 1 个送货段时数量跟装货;≥2 段时保留已有段数量,新增行/新增段默认 0。 */ export function syncDeliveriesProductsFromLoad( deliveries: PlanDeliveryForm[], loadProducts: PlanStopProductFormRow[], options?: { forceQtyFromLoad?: boolean } ): { deliveries: PlanDeliveryForm[]; needSplitHint: boolean } { const multi = deliveries.length >= 2 const forceQty = options?.forceQtyFromLoad === true const next = deliveries.map((delivery) => { const used = new Set() const products = loadProducts.map((loadRow) => { const prev = matchPrevDeliveryProduct(delivery.products || [], loadRow, used) let planQty: number if (forceQty || !multi) { planQty = Number(loadRow.planQty ?? 0) } else if (prev) { planQty = Number(prev.planQty ?? 0) } else { planQty = 0 } return cloneStopProductSharedFields(loadRow, planQty, prev?.remark) }) return { ...delivery, products } }) return { deliveries: next, needSplitHint: multi } } /** 装货产品 → 计划层产品行(保存 pl_plan_product / 运费计算) */ export function mapLoadProductsToPlanFormRows( loadProducts: PlanStopProductFormRow[] ): PlanFormProductRow[] { return loadProducts.map((row) => ({ id: row.planProductId, productId: row.productId, productNo: row.productNo, productName: row.productName, standardProductName: row.standardProductName || row.productName, aliasName: row.aliasName, aliasOptions: row.aliasOptions ? [...row.aliasOptions] : undefined, brandMode: row.brandMode, brandId: row.brandId, brandName: row.brandName, brandOptions: row.brandOptions ? [...row.brandOptions] : undefined, model: row.model, unit: row.unit || '吨', quantity: Number(row.planQty ?? 0), actualQty: 0, packageSpec: row.packageSpec, deliveryMethod: row.deliveryMethod, remark: row.remark, isExplosive: row.isExplosive, isToxic: row.isToxic })) } /** 合并装货映射行与既有计划产品元数据(危化证、税额等) */ export function mergePlanProductsFromLoadMirror( fromLoad: PlanFormProductRow[], previous: PlanFormProductRow[] ): PlanFormProductRow[] { return fromLoad.map((row) => { const prev = (row.id && previous.find((item) => item.id && String(item.id) === String(row.id))) || previous.find((item) => String(item.productId) === String(row.productId)) if (!prev) return row return { ...row, id: prev.id || row.id, categoryName: row.categoryName || prev.categoryName, standardProductName: row.standardProductName || prev.standardProductName || row.productName, aliasOptions: row.aliasOptions?.length ? row.aliasOptions : prev.aliasOptions, brandOptions: row.brandOptions?.length ? row.brandOptions : prev.brandOptions, taxRate: prev.taxRate, taxPrice: prev.taxPrice ?? row.taxPrice, taxAmount: prev.taxAmount, taxSubtotal: prev.taxSubtotal, purchaseCertUrl: prev.purchaseCertUrl, transportCertUrl: prev.transportCertUrl, legalUseCertUrl: prev.legalUseCertUrl } }) } export function buildStopProductPayload(row: PlanStopProductFormRow) { return { planProductId: row.planProductId || undefined, productId: row.productId, productNo: row.productNo, productName: row.productName, model: row.model, unit: row.unit, packageSpec: row.packageSpec, deliveryMethod: row.deliveryMethod, planQty: row.planQty ?? 0, remark: row.remark } } export function buildLoadPayload(load: PlanLoadForm) { return { customerId: load.customerId, customerName: load.customerName, address: load.address || undefined, lng: load.lng, lat: load.lat, loadTime: load.loadTime || undefined, contact: load.contact || undefined, notes: load.notes || undefined, generateDibang: load.generateDibang, dibangDirection: load.dibangDirection, contracts: load.contracts, products: load.products.map(buildStopProductPayload) } } export function buildDeliveriesPayload(deliveries: PlanDeliveryForm[]) { return deliveries.map((row, index) => ({ seq: row.seq || index + 1, customerId: row.customerId, customerName: row.customerName, address: row.address || undefined, lng: row.lng, lat: row.lat, deliveryTime: row.deliveryTime || undefined, contact: row.contact || undefined, notes: row.notes || undefined, generateDibang: row.generateDibang, dibangDirection: row.dibangDirection, contracts: row.contracts, products: row.products.map(buildStopProductPayload) })) }