328 lines
11 KiB
TypeScript
328 lines
11 KiB
TypeScript
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)
|
||
}
|