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 } 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[] ): (args: { row: PlPlanListDisplayRow column: { type?: string; property?: string } rowIndex: number columnIndex: number }) => { rowspan: number; colspan: number } | undefined { const planMergeCount = PLAN_FIXED_MERGE_COL_COUNT + 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 = [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) }