import type { Plan, PlanDelivery, PlanLoad, PlanProduct, PlanStopProduct } from '@/api/pl' import { formatDate } from '@/utils/format' import { isPlInternalCustomerName } from '@/utils/pl-internal-customer' import { planContractSideLines, type PlanContractDisplayLine } from '@/utils/pl-plan-contract-display' import { mapLegacyPlanToStops } from '@/utils/pl-plan-stop-form' export type PlPlanListDisplayRowKind = 'load' | 'delivery' export type PlPlanListColumnScope = 'plan' | 'stop' export interface PlPlanListDisplayRow { rowKey: string plan: Plan kind: PlPlanListDisplayRowKind stop: PlanLoad | PlanDelivery deliverySeq?: number /** 当前页内计划序号(0-based),用于整单斑马纹 */ planListIndex: number isPlanFirstRow: boolean planRowSpan: number isPlanLastRow: boolean } const PLAN_FIXED_MERGE_COL_COUNT = 3 export function resolvePlanStopsForList(plan: Plan): { load: PlanLoad deliveries: PlanDelivery[] } { // 有段次结构时优先用 load/deliveries(含段上 contracts);勿因 deliveries 为空回退 legacy, // 否则会丢掉 stop.contracts,误用空的 supplierContracts/demanderContracts。 if (plan.load) { return { load: plan.load, deliveries: plan.deliveries || [] } } const legacy = mapLegacyPlanToStops(plan) return { load: legacy.load as PlanLoad, deliveries: legacy.deliveries as PlanDelivery[] } } export function flattenPlanListRows(plans: Plan[]): PlPlanListDisplayRow[] { const rows: PlPlanListDisplayRow[] = [] plans.forEach((plan, planListIndex) => { const { load, deliveries } = resolvePlanStopsForList(plan) const planRowSpan = 1 + deliveries.length rows.push({ rowKey: `${plan.id}-load`, plan, kind: 'load', stop: load, planListIndex, isPlanFirstRow: true, planRowSpan, isPlanLastRow: deliveries.length === 0 }) deliveries.forEach((delivery, index) => { rows.push({ rowKey: `${plan.id}-delivery-${delivery.seq ?? index + 1}`, plan, kind: 'delivery', stop: delivery, deliverySeq: delivery.seq ?? index + 1, planListIndex, isPlanFirstRow: false, planRowSpan, isPlanLastRow: index === deliveries.length - 1 }) }) }) return rows } function stopHasProductId( stop: PlanLoad | PlanDelivery | null | undefined, productId: string ): boolean { const pid = String(productId || '').trim() if (!pid || !stop) return false return (stop.products || []).some((p) => String(p.productId || '') === pid) } /** 仅保留指定产品(计划产品行 + 各段产品行);不含该产品的送货段丢弃。 */ export function filterPlanToProductId(plan: Plan, productId: string): Plan { const pid = String(productId || '').trim() if (!pid) return plan const match = (p: { productId?: string }) => String(p.productId || '') === pid const products = (plan.products || []).filter(match) const load = plan.load ? { ...plan.load, products: (plan.load.products || []).filter(match) } : plan.load const deliveries = (plan.deliveries || []) .map((d) => ({ ...d, products: (d.products || []).filter(match) })) .filter((d) => (d.products || []).length > 0) return { ...plan, products, load, deliveries } } /** * 展平运输计划列表行,但只保留含指定产品的段次(不展示同计划其他商品)。 * 用于合同用量等「单产品」弹窗列表。 */ export function flattenPlanListRowsForProduct( plans: Plan[], productId: string ): PlPlanListDisplayRow[] { const pid = String(productId || '').trim() if (!pid) return flattenPlanListRows(plans) const rows: PlPlanListDisplayRow[] = [] let planListIndex = 0 for (const raw of plans || []) { if (!raw?.id) continue const plan = filterPlanToProductId(raw, pid) const { load, deliveries } = resolvePlanStopsForList(plan) type StopSlot = { kind: PlPlanListDisplayRowKind stop: PlanLoad | PlanDelivery deliverySeq?: number } const stops: StopSlot[] = [] if (stopHasProductId(load, pid)) { stops.push({ kind: 'load', stop: load }) } deliveries.forEach((delivery, index) => { if (!stopHasProductId(delivery, pid)) return stops.push({ kind: 'delivery', stop: delivery, deliverySeq: delivery.seq ?? index + 1 }) }) if (!stops.length) continue const planRowSpan = stops.length stops.forEach((slot, index) => { const isLoad = slot.kind === 'load' rows.push({ rowKey: isLoad ? `${plan.id}-load` : `${plan.id}-delivery-${slot.deliverySeq ?? index + 1}`, plan, kind: slot.kind, stop: slot.stop, deliverySeq: slot.deliverySeq, planListIndex, isPlanFirstRow: index === 0, planRowSpan, isPlanLastRow: index === stops.length - 1 }) }) planListIndex += 1 } return rows } export function getPlanListDisplayIndex( displayRows: PlPlanListDisplayRow[], displayRowIndex: number, page: number, pageSize: number ): number { const row = displayRows[displayRowIndex] if (!row?.isPlanFirstRow) return 0 let planIndex = 0 for (let i = 0; i < displayRowIndex; i++) { if (displayRows[i]?.isPlanFirstRow) planIndex++ } return (page - 1) * pageSize + planIndex + 1 } export function isPlPlanListPlanMergeColumn( columnIndex: number, visiblePlanProps: string[] ): boolean { if (columnIndex < PLAN_FIXED_MERGE_COL_COUNT) return true const planColIndex = columnIndex - PLAN_FIXED_MERGE_COL_COUNT return planColIndex >= 0 && planColIndex < visiblePlanProps.length } export function buildPlPlanListSpanMethod( visiblePlanProps: string[], fixedMergeColCount: number = PLAN_FIXED_MERGE_COL_COUNT ): (args: { row: PlPlanListDisplayRow column: { type?: string; property?: string } rowIndex: number columnIndex: number }) => { rowspan: number; colspan: number } | undefined { const fixed = Math.max(0, fixedMergeColCount) const planMergeCount = fixed + visiblePlanProps.length return ({ row, columnIndex }) => { if (columnIndex >= planMergeCount) return undefined if (row.isPlanFirstRow) { return { rowspan: row.planRowSpan, colspan: 1 } } return { rowspan: 0, colspan: 0 } } } export function getPlPlanListRowClassName({ row }: { row: PlPlanListDisplayRow }): string { const classes = [ 'pl-plan-list-row', row.kind === 'load' ? 'pl-plan-list-row--load' : 'pl-plan-list-row--delivery' ] if (row.planListIndex % 2 === 1) classes.push('pl-plan-list-row--plan-striped') if (row.isPlanLastRow) classes.push('pl-plan-list-row--plan-end') return classes.join(' ') } export function getStopKindLabel(row: PlPlanListDisplayRow): string { if (row.kind === 'load') return '装货' return `送货 #${row.deliverySeq ?? 1}` } export function formatStopDibangLabel(stop: PlanLoad | PlanDelivery): string { if (!stop.generateDibang) return '' const direction = (stop.dibangDirection || '').trim() if (direction === 'outbound') return '出库' if (direction === 'inbound') return '入库' if (direction === 'none') return '无' return '是' } export function getStopDibangTagType( stop: PlanLoad | PlanDelivery ): 'success' | 'info' | 'warning' | undefined { if (!stop.generateDibang) return undefined const direction = (stop.dibangDirection || '').trim() if (direction === 'outbound') return 'success' if (direction === 'inbound') return 'success' return 'info' } export function getStopAddress(row: PlPlanListDisplayRow): string { return (row.stop.address || '').trim() || '—' } export function getStopTime(row: PlPlanListDisplayRow): string { const raw = row.kind === 'load' ? (row.stop as PlanLoad).loadTime : (row.stop as PlanDelivery).deliveryTime const text = (raw || '').trim() if (!text) return '—' return formatDate(text) || text.slice(0, 10) || '—' } export function getStopCustomerName(row: PlPlanListDisplayRow): string { return (row.stop.customerName || '').trim() || '—' } /** * 归档列表:入库/出库列展示方向(按本段客户)。 * - 装货:山峡/云栋 → 出库;其他 → 入库 * - 送货:山峡/云栋 → 入库;其他 → 出库 */ export function resolveArchiveListIoColumn( kind: PlPlanListDisplayRowKind, customerName?: string | null ): 'inbound' | 'outbound' { const internal = isPlInternalCustomerName(customerName) if (kind === 'load') return internal ? 'outbound' : 'inbound' return internal ? 'inbound' : 'outbound' } /** 段次归档实际数量(装货读 inboundQty,送货读 outboundQty) */ export function getStopArchiveActualQty(product: PlanStopProduct, kind: PlPlanListDisplayRowKind): number | null { const raw = kind === 'load' ? product.inboundQty : product.outboundQty if (raw == null || raw === '') return null const num = Number(raw) return Number.isFinite(num) ? num : null } export function getStopArchiveUnitPrice( product: PlanStopProduct, kind: PlPlanListDisplayRowKind ): number | null { const raw = kind === 'load' ? product.inboundPrice : product.outboundPrice if (raw == null || raw === '') return null const num = Number(raw) return Number.isFinite(num) ? num : null } export function getStopArchiveAmount( product: PlanStopProduct, kind: PlPlanListDisplayRowKind ): number | null { const stored = kind === 'load' ? product.inboundAmount : product.outboundAmount if (stored != null && stored !== '') { const num = Number(stored) if (Number.isFinite(num)) return num } const qty = getStopArchiveActualQty(product, kind) const price = getStopArchiveUnitPrice(product, kind) if (qty == null || price == null) return null const amount = qty * price return Number.isFinite(amount) ? amount : null } export function sumStopArchiveActualQty( stopProducts: PlanStopProduct[], kind: PlPlanListDisplayRowKind ): number { return stopProducts.reduce((sum, item) => sum + (getStopArchiveActualQty(item, kind) || 0), 0) } export function sumStopArchiveIoQty( stopProducts: PlanStopProduct[], kind: PlPlanListDisplayRowKind, customerName: string | null | undefined, column: 'inbound' | 'outbound' ): number { if (resolveArchiveListIoColumn(kind, customerName) !== column) return 0 return sumStopArchiveActualQty(stopProducts, kind) } export function sumStopArchiveUnitPrice( stopProducts: PlanStopProduct[], kind: PlPlanListDisplayRowKind ): number { return stopProducts.reduce((sum, item) => sum + (getStopArchiveUnitPrice(item, kind) || 0), 0) } export function sumStopArchiveAmount( stopProducts: PlanStopProduct[], kind: PlPlanListDisplayRowKind ): number { return stopProducts.reduce((sum, item) => sum + (getStopArchiveAmount(item, kind) || 0), 0) } /** 装货行展示供方;送货行留空 */ export function getStopSupplierNameDisplay(row: PlPlanListDisplayRow): string { if (row.kind !== 'load') return '—' return (row.plan.supplierName || row.stop.customerName || '').trim() || '—' } /** 送货行展示需方;装货行留空 */ export function getStopDemanderNameDisplay(row: PlPlanListDisplayRow): string { if (row.kind !== 'delivery') return '—' return (row.stop.customerName || row.plan.customerName || '').trim() || '—' } export function getStopContractLines(row: PlPlanListDisplayRow): PlanContractDisplayLine[] { if (row.kind === 'load') { return planContractSideLines('供', row.stop.contracts, row.stop.customerName) } return planContractSideLines('需', row.stop.contracts, row.stop.customerName) } export function getStopProducts(row: PlPlanListDisplayRow): PlanStopProduct[] { return row.stop.products || [] } /** 段内危化证展示:按 planProductId 关联计划产品行 */ export function resolveStopCertProducts( plan: Plan, stopProducts: PlanStopProduct[] ): PlanProduct[] { const planProducts = plan.products || [] const resolved: PlanProduct[] = [] for (const stopProduct of stopProducts) { const match = planProducts.find( (item) => stopProduct.planProductId && item.id && String(item.id) === String(stopProduct.planProductId) ) || planProducts.find((item) => String(item.productId) === String(stopProduct.productId)) if (match) { resolved.push(match) continue } resolved.push({ id: stopProduct.planProductId || stopProduct.id || stopProduct.productId, planId: plan.id, productId: stopProduct.productId, productNo: stopProduct.productNo, productName: stopProduct.productName, model: stopProduct.model, unit: stopProduct.unit || '吨', packageSpec: stopProduct.packageSpec, deliveryMethod: stopProduct.deliveryMethod, quantity: stopProduct.planQty, isExplosive: 0, isToxic: 0 }) } return resolved } export function sumStopProductsField( stopProducts: PlanStopProduct[], field: 'planQty' | 'inboundQty' | 'outboundQty' ): number { return stopProducts.reduce((sum, item) => sum + (Number(item[field]) || 0), 0) } export function sumStopActualQty(plan: Plan, stopProducts: PlanStopProduct[]): number { const planProducts = plan.products || [] return stopProducts.reduce((sum, stopProduct) => { const match = planProducts.find( (item) => stopProduct.planProductId && item.id && String(item.id) === String(stopProduct.planProductId) ) || planProducts.find((item) => String(item.productId) === String(stopProduct.productId)) return sum + (Number(match?.actualQty) || 0) }, 0) }