提交
This commit is contained in:
1 parent
e56d227fe8
commit
4611dbc2b2
237 files changed
+1384060
-24942
No files matched your search
@@ -35,6 +35,19 @@ export function buildAmapSearchUrl(keyword?: string | null): string | null {
|
||||
return `https://www.amap.com/search?query=${encodeURIComponent(text)}`
|
||||
}
|
||||
|
||||
/** 优先坐标导航,无完整坐标时按地址关键词搜索兜底 */
|
||||
export function buildAmapNavigationUrlWithFallback(options: {
|
||||
lng?: number | null
|
||||
lat?: number | null
|
||||
name?: string
|
||||
address?: string
|
||||
}): string | null {
|
||||
const label = (options.name || options.address || '').trim() || '目的地'
|
||||
const nav = buildAmapNavigationUrl(options.lng, options.lat, label)
|
||||
if (nav) return nav
|
||||
return buildAmapSearchUrl(options.address || options.name || label)
|
||||
}
|
||||
|
||||
/** 优先坐标标记,否则按地址搜索 */
|
||||
export function buildAmapLocationUrl(options: {
|
||||
lng?: number | null
|
||||
|
||||
@@ -1,27 +1,138 @@
|
||||
import type { InventoryRecord } from '@/api/db/inventory'
|
||||
import type { TransportInventoryLink } from '@/api/pl'
|
||||
import type { PlanDelivery, PlanLoad, PlanStopProduct } from '@/api/pl'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
|
||||
const SITE_INSTRUCTION = '说明:请打开链接上传现场附件并填写备注。'
|
||||
|
||||
type SiteSummaryRecord = {
|
||||
export type SiteSummaryStopBlock = {
|
||||
label: string
|
||||
customerName?: string
|
||||
businessType?: string
|
||||
address?: string
|
||||
productLines: string[]
|
||||
}
|
||||
|
||||
export type SiteSummaryRecord = {
|
||||
productName?: string
|
||||
productText?: string
|
||||
batchNo?: string
|
||||
planNo?: string
|
||||
siteUrl?: string
|
||||
businessType?: string
|
||||
siteFeedbackRemark?: string
|
||||
siteFeedbackTime?: string
|
||||
}
|
||||
|
||||
function formatSiteSummaryProductLine(record: SiteSummaryRecord) {
|
||||
return record.productText || record.productName || '—'
|
||||
}
|
||||
|
||||
function isSegmentSiteRecord(record: SiteSummaryRecord) {
|
||||
return (record.businessType || '').includes('按段')
|
||||
}
|
||||
|
||||
function formatStopProductLine(product: PlanStopProduct) {
|
||||
const name = (product.productName || '').trim() || '—'
|
||||
const qty = product.planQty
|
||||
const unit = (product.unit || '吨').trim()
|
||||
if (qty == null) return name
|
||||
return `${name} ${qty}${unit}`
|
||||
}
|
||||
|
||||
function isSiteVisibleStop(customerName?: string | null, generateDibang?: number | null) {
|
||||
return isPlInternalCustomerName(customerName) && Number(generateDibang || 0) === 1
|
||||
}
|
||||
|
||||
export function buildSiteStopBlocksFromPlan(
|
||||
plan?: {
|
||||
load?: PlanLoad | null
|
||||
deliveries?: PlanDelivery[] | null
|
||||
} | null
|
||||
): SiteSummaryStopBlock[] {
|
||||
if (!plan) return []
|
||||
const blocks: SiteSummaryStopBlock[] = []
|
||||
const load = plan.load
|
||||
if (load && isSiteVisibleStop(load.customerName, load.generateDibang)) {
|
||||
blocks.push({
|
||||
label: '装货',
|
||||
customerName: load.customerName || undefined,
|
||||
businessType: load.dibangDirection || undefined,
|
||||
address: load.address || undefined,
|
||||
productLines: (load.products || []).map(formatStopProductLine).filter(Boolean)
|
||||
})
|
||||
}
|
||||
for (const delivery of plan.deliveries || []) {
|
||||
if (!isSiteVisibleStop(delivery.customerName, delivery.generateDibang)) continue
|
||||
const seq = delivery.seq != null ? Number(delivery.seq) : 0
|
||||
blocks.push({
|
||||
label: seq > 0 ? `送货 #${seq}` : '送货',
|
||||
customerName: delivery.customerName || undefined,
|
||||
businessType: delivery.dibangDirection || undefined,
|
||||
address: delivery.address || undefined,
|
||||
productLines: (delivery.products || []).map(formatStopProductLine).filter(Boolean)
|
||||
})
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
function appendSiteRecordCardLines(
|
||||
lines: string[],
|
||||
record: SiteSummaryRecord,
|
||||
options: { showHeadline?: boolean }
|
||||
) {
|
||||
const headline = (record.productName || '').trim()
|
||||
const productText = formatSiteSummaryProductLine(record)
|
||||
if (options.showHeadline && headline) {
|
||||
lines.push(headline)
|
||||
}
|
||||
if (record.businessType && record.businessType !== '—') {
|
||||
lines.push(`业务类型:${record.businessType}`)
|
||||
}
|
||||
if (record.siteFeedbackTime && record.siteFeedbackTime !== '—') {
|
||||
lines.push(`保存时间:${record.siteFeedbackTime}`)
|
||||
}
|
||||
if (record.batchNo) {
|
||||
lines.push(`出入库单号:${record.batchNo}`)
|
||||
}
|
||||
if (productText && productText !== '—' && productText !== headline) {
|
||||
lines.push(`产品:${productText}`)
|
||||
}
|
||||
if (record.siteFeedbackRemark && record.siteFeedbackRemark !== '—') {
|
||||
lines.push(`现场备注:${record.siteFeedbackRemark}`)
|
||||
}
|
||||
lines.push(`现场链接:${record.siteUrl}`)
|
||||
}
|
||||
|
||||
function formatSiteDibangDirectionLabel(direction?: string | null) {
|
||||
const value = (direction || '').trim()
|
||||
if (!value || value === 'none') return ''
|
||||
if (value === 'outbound') return '出库'
|
||||
if (value === 'inbound') return '入库'
|
||||
return value
|
||||
}
|
||||
|
||||
function appendStopBlockLines(lines: string[], stop: SiteSummaryStopBlock) {
|
||||
const bizLabel = formatSiteDibangDirectionLabel(stop.businessType)
|
||||
const biz = bizLabel ? `(${bizLabel})` : ''
|
||||
lines.push(`【${stop.label} · ${stop.customerName || '—'}】${biz}`)
|
||||
if (stop.address) {
|
||||
lines.push(`地址:${stop.address}`)
|
||||
}
|
||||
if (stop.productLines.length) {
|
||||
stop.productLines.forEach((line) => lines.push(`产品:${line}`))
|
||||
} else {
|
||||
lines.push('产品:—')
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSiteSummaryFromInventoryLinks(options: {
|
||||
planNo?: string
|
||||
licensePlate?: string
|
||||
pickupTime?: string
|
||||
planDate?: string
|
||||
records: SiteSummaryRecord[]
|
||||
stopBlocks?: SiteSummaryStopBlock[]
|
||||
}) {
|
||||
const lines = ['【运输现场】']
|
||||
const planNo = options.planNo || options.records[0]?.planNo
|
||||
@@ -30,23 +141,33 @@ export function buildSiteSummaryFromInventoryLinks(options: {
|
||||
const time = formatDate(options.pickupTime) || options.planDate
|
||||
if (time) lines.push(`时间:${time}`)
|
||||
|
||||
const validRecords = options.records.filter(item => item.siteUrl)
|
||||
const validRecords = options.records.filter((item) => item.siteUrl)
|
||||
if (!validRecords.length) {
|
||||
lines.push('现场链接:—')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
if (validRecords.length === 1) {
|
||||
const stopBlocks = options.stopBlocks || []
|
||||
const useSegmentLayout =
|
||||
stopBlocks.length > 0 && validRecords.some((record) => isSegmentSiteRecord(record))
|
||||
|
||||
if (useSegmentLayout) {
|
||||
lines.push('')
|
||||
stopBlocks.forEach((stop, index) => {
|
||||
if (index > 0) lines.push('')
|
||||
appendStopBlockLines(lines, stop)
|
||||
})
|
||||
lines.push('')
|
||||
const record = validRecords[0]
|
||||
lines.push(`产品:${formatSiteSummaryProductLine(record)}`)
|
||||
if (record.batchNo) lines.push(`出入库单号:${record.batchNo}`)
|
||||
lines.push(`现场链接:${record.siteUrl}`)
|
||||
} else if (validRecords.length === 1) {
|
||||
lines.push('')
|
||||
appendSiteRecordCardLines(lines, validRecords[0], { showHeadline: true })
|
||||
} else {
|
||||
lines.push('')
|
||||
validRecords.forEach((record, index) => {
|
||||
lines.push(`【${index + 1}】${formatSiteSummaryProductLine(record)}`)
|
||||
if (record.batchNo) lines.push(`出入库单号:${record.batchNo}`)
|
||||
lines.push(`现场链接:${record.siteUrl}`)
|
||||
appendSiteRecordCardLines(lines, record, { showHeadline: false })
|
||||
if (index < validRecords.length - 1) lines.push('')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/** 送(提)货单:按送货客户聚合(一车多卸) */
|
||||
import type { Plan, PlanDelivery, PlanProduct, PlanStopProduct } from '@/api/pl'
|
||||
|
||||
export interface DeliveryPrintCustomerOption {
|
||||
customerId: string
|
||||
customerName: string
|
||||
deliveryCount: number
|
||||
}
|
||||
|
||||
function customerKey(customerId?: string | null, customerName?: string | null) {
|
||||
const id = String(customerId || '').trim()
|
||||
if (id) return `id:${id}`
|
||||
const name = String(customerName || '').trim()
|
||||
if (name) return `name:${name}`
|
||||
return ''
|
||||
}
|
||||
|
||||
function resolveCustomerId(customerId?: string | null, customerName?: string | null) {
|
||||
const id = String(customerId || '').trim()
|
||||
if (id) return id
|
||||
return String(customerName || '').trim()
|
||||
}
|
||||
|
||||
function deliveryMatchesCustomer(delivery: PlanDelivery, customerId: string) {
|
||||
const target = String(customerId || '').trim()
|
||||
if (!target) return false
|
||||
const id = String(delivery.customerId || '').trim()
|
||||
if (id && id === target) return true
|
||||
const name = String(delivery.customerName || '').trim()
|
||||
return !id && name === target
|
||||
}
|
||||
|
||||
/** 从计划送货段统计去重客户(仅 1 个时可跳过选客户弹窗) */
|
||||
export function listDeliveryPrintCustomers(plan?: Pick<Plan, 'deliveries'> | null): DeliveryPrintCustomerOption[] {
|
||||
const deliveries = plan?.deliveries || []
|
||||
const grouped = new Map<string, DeliveryPrintCustomerOption & { minSeq: number }>()
|
||||
for (const delivery of deliveries) {
|
||||
const key = customerKey(delivery.customerId, delivery.customerName)
|
||||
if (!key) continue
|
||||
const existing = grouped.get(key)
|
||||
if (!existing) {
|
||||
grouped.set(key, {
|
||||
customerId: resolveCustomerId(delivery.customerId, delivery.customerName),
|
||||
customerName: String(delivery.customerName || '').trim() || '—',
|
||||
deliveryCount: 1,
|
||||
minSeq: delivery.seq ?? 9999
|
||||
})
|
||||
continue
|
||||
}
|
||||
existing.deliveryCount += 1
|
||||
existing.minSeq = Math.min(existing.minSeq, delivery.seq ?? 9999)
|
||||
}
|
||||
return [...grouped.values()]
|
||||
.sort((a, b) => a.minSeq - b.minSeq || a.customerName.localeCompare(b.customerName, 'zh-CN'))
|
||||
.map(({ customerId, customerName, deliveryCount }) => ({
|
||||
customerId,
|
||||
customerName,
|
||||
deliveryCount
|
||||
}))
|
||||
}
|
||||
|
||||
function stopProductToPlanProduct(product: PlanStopProduct, planId: string): PlanProduct {
|
||||
return {
|
||||
id: product.id || product.planProductId || product.productId,
|
||||
planId,
|
||||
productId: product.productId,
|
||||
productNo: product.productNo,
|
||||
productName: product.productName,
|
||||
model: product.model,
|
||||
unit: product.unit,
|
||||
packageSpec: product.packageSpec,
|
||||
deliveryMethod: product.deliveryMethod,
|
||||
quantity: product.planQty,
|
||||
outboundEstimateQty: product.planQty,
|
||||
outboundQty: product.outboundQty,
|
||||
outboundPrice: product.outboundPrice,
|
||||
outboundAmount: product.outboundAmount,
|
||||
inboundQty: product.inboundQty,
|
||||
inboundPrice: product.inboundPrice,
|
||||
inboundAmount: product.inboundAmount,
|
||||
isExplosive: 0,
|
||||
isToxic: 0,
|
||||
remark: product.remark
|
||||
}
|
||||
}
|
||||
|
||||
function mergePlanProducts(products: PlanProduct[]): PlanProduct[] {
|
||||
const merged = new Map<string, PlanProduct>()
|
||||
const order: string[] = []
|
||||
for (const product of products) {
|
||||
const key = String(product.productId || '').trim() || product.id
|
||||
const existing = merged.get(key)
|
||||
if (!existing) {
|
||||
merged.set(key, { ...product })
|
||||
order.push(key)
|
||||
continue
|
||||
}
|
||||
const sum = (a?: number | null, b?: number | null) => {
|
||||
if (a == null && b == null) return undefined
|
||||
return Number(a || 0) + Number(b || 0)
|
||||
}
|
||||
existing.quantity = sum(existing.quantity, product.quantity)
|
||||
existing.outboundEstimateQty = sum(existing.outboundEstimateQty, product.outboundEstimateQty)
|
||||
existing.outboundQty = sum(existing.outboundQty, product.outboundQty)
|
||||
existing.inboundQty = sum(existing.inboundQty, product.inboundQty)
|
||||
if (!String(existing.remark || '').trim() && String(product.remark || '').trim()) {
|
||||
existing.remark = product.remark
|
||||
}
|
||||
}
|
||||
return order.map((key) => merged.get(key)!)
|
||||
}
|
||||
|
||||
/**
|
||||
* 按送货客户生成送(提)货单打印视图:
|
||||
* - 收货单位 = 客户名
|
||||
* - 联系方式/地址取 seq 最小送货段
|
||||
* - 同客户多段产品行合并
|
||||
*/
|
||||
export function applyDeliveryCustomerToPlan(plan: Plan, customerId: string): Plan {
|
||||
const deliveries = [...(plan.deliveries || [])]
|
||||
.filter((item) => deliveryMatchesCustomer(item, customerId))
|
||||
.sort((a, b) => (a.seq ?? 9999) - (b.seq ?? 9999))
|
||||
if (!deliveries.length) {
|
||||
throw new Error('未找到匹配的送货客户')
|
||||
}
|
||||
const primary = deliveries[0]
|
||||
const products = mergePlanProducts(
|
||||
deliveries.flatMap((delivery) =>
|
||||
(delivery.products || []).map((product) => stopProductToPlanProduct(product, plan.id))
|
||||
)
|
||||
)
|
||||
return {
|
||||
...plan,
|
||||
customerId: resolveCustomerId(primary.customerId, primary.customerName),
|
||||
customerName: String(primary.customerName || '').trim(),
|
||||
destinationName: String(primary.customerName || '').trim(),
|
||||
destinationAddress: String(primary.address || '').trim() || plan.destinationAddress,
|
||||
deliveryContact: String(primary.contact || '').trim() || plan.deliveryContact,
|
||||
destinationNotes: String(primary.notes || '').trim() || plan.destinationNotes,
|
||||
products: products.length ? products : plan.products
|
||||
}
|
||||
}
|
||||
|
||||
/** 入库单:送货单位 = 装货客户 */
|
||||
export function resolveInboundDeliveryUnit(plan: Plan): string {
|
||||
const loadName = String(plan.load?.customerName || '').trim()
|
||||
if (loadName) return loadName
|
||||
return String(plan.supplierName || '').trim() || String(plan.customerName || '').trim()
|
||||
}
|
||||
@@ -160,6 +160,10 @@ function buildReceivingUnit(plan: Plan): string {
|
||||
return plan.customerName?.trim() || plan.destinationName?.trim() || ''
|
||||
}
|
||||
|
||||
function buildReceiveContact(plan: Plan): string {
|
||||
return plan.deliveryContact?.trim() || plan.contactPhone?.trim() || plan.contactName?.trim() || ''
|
||||
}
|
||||
|
||||
function formatDeliverySlipProductGridCellContent(content: string): string {
|
||||
const plain = content
|
||||
.trim()
|
||||
@@ -1856,7 +1860,7 @@ export function replaceDeliverySlipPlaceholders(
|
||||
'{{companyName}}': DELIVERY_SLIP_DEFAULTS.companyName,
|
||||
'{{receivingUnit}}': isBlank ? '' : buildReceivingUnit(plan),
|
||||
'{{planNo}}': isBlank ? '' : plan.planNo || '',
|
||||
'{{receiveContact}}': '',
|
||||
'{{receiveContact}}': isBlank ? '' : buildReceiveContact(plan),
|
||||
'{{driverInfo}}': '',
|
||||
'{{planDateChinese}}': isBlank ? '' : formatPlanDateChinese(plan.planDate),
|
||||
'{{productTable}}': buildProductRows(plan.products, plan.planNo || '', ctx, options),
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import type { PlanDelivery, PlanLoad, PlanProduct, PlanStopProduct } from '@/api/pl'
|
||||
import {
|
||||
appendAmapNavLine,
|
||||
buildAmapNavigationUrlWithFallback
|
||||
} from '@/utils/amap/navigation'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { formatPlanProductListText } from '@/utils/pl-display'
|
||||
|
||||
export type DriverSummaryStopBlock = {
|
||||
label: string
|
||||
customerName?: string
|
||||
stopTime?: string
|
||||
address?: string
|
||||
lng?: number | null
|
||||
lat?: number | null
|
||||
contact?: string
|
||||
notes?: string
|
||||
productLines: string[]
|
||||
}
|
||||
|
||||
type DriverSummaryPlan = {
|
||||
planNo?: string
|
||||
licensePlate?: string
|
||||
pickupTime?: string
|
||||
planDate?: string
|
||||
personnelSummary?: string
|
||||
pickupAddress?: string
|
||||
pickupLng?: number | null
|
||||
pickupLat?: number | null
|
||||
pickupContact?: string
|
||||
pickupNotes?: string
|
||||
destinationAddress?: string
|
||||
destinationName?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
deliveryContact?: string
|
||||
destinationNotes?: string
|
||||
startPoint?: string
|
||||
endPoint?: string
|
||||
load?: PlanLoad | null
|
||||
deliveries?: PlanDelivery[] | null
|
||||
}
|
||||
|
||||
function formatDriverStopProductLine(
|
||||
product: PlanStopProduct,
|
||||
getUnitLabel?: (unit: string) => string
|
||||
) {
|
||||
return formatPlanProductListText(
|
||||
{
|
||||
productName: product.productName,
|
||||
model: product.model,
|
||||
packageSpec: product.packageSpec,
|
||||
unit: product.unit,
|
||||
quantity: product.planQty
|
||||
},
|
||||
getUnitLabel,
|
||||
{ usePlannedQuantity: true }
|
||||
)
|
||||
}
|
||||
|
||||
function formatPlanLevelProductLines(
|
||||
products: PlanProduct[],
|
||||
getUnitLabel?: (unit: string) => string
|
||||
) {
|
||||
return products
|
||||
.map((product) => formatPlanProductListText(product, getUnitLabel, { usePlannedQuantity: true }))
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function appendDriverStopBlockLines(lines: string[], stop: DriverSummaryStopBlock) {
|
||||
lines.push(`【${stop.label} · ${stop.customerName || '—'}】`)
|
||||
if (stop.stopTime) {
|
||||
lines.push(`时间:${stop.stopTime}`)
|
||||
}
|
||||
if (stop.address) {
|
||||
lines.push(`地址:${stop.address}`)
|
||||
}
|
||||
appendAmapNavLine(
|
||||
lines,
|
||||
'导航',
|
||||
buildAmapNavigationUrlWithFallback({
|
||||
lng: stop.lng,
|
||||
lat: stop.lat,
|
||||
address: stop.address,
|
||||
name: stop.label
|
||||
})
|
||||
)
|
||||
if (stop.contact) {
|
||||
lines.push(`联系方式:${stop.contact}`)
|
||||
}
|
||||
if (stop.notes) {
|
||||
lines.push(`注意事项:${stop.notes}`)
|
||||
}
|
||||
if (stop.productLines.length) {
|
||||
stop.productLines.forEach((line) => lines.push(`产品:${line}`))
|
||||
} else {
|
||||
lines.push('产品:—')
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDriverStopBlocksFromPlan(
|
||||
plan?: DriverSummaryPlan | null,
|
||||
getUnitLabel?: (unit: string) => string
|
||||
): DriverSummaryStopBlock[] {
|
||||
if (!plan) return []
|
||||
const blocks: DriverSummaryStopBlock[] = []
|
||||
const load = plan.load
|
||||
if (load) {
|
||||
blocks.push({
|
||||
label: '装货',
|
||||
customerName: load.customerName || undefined,
|
||||
stopTime: formatDate(load.loadTime) || undefined,
|
||||
address: load.address || undefined,
|
||||
lng: load.lng,
|
||||
lat: load.lat,
|
||||
contact: load.contact || undefined,
|
||||
notes: load.notes || undefined,
|
||||
productLines: (load.products || []).map((product) =>
|
||||
formatDriverStopProductLine(product, getUnitLabel)
|
||||
)
|
||||
})
|
||||
}
|
||||
for (const delivery of plan.deliveries || []) {
|
||||
const seq = delivery.seq != null ? Number(delivery.seq) : 0
|
||||
blocks.push({
|
||||
label: seq > 0 ? `送货 #${seq}` : '送货',
|
||||
customerName: delivery.customerName || undefined,
|
||||
stopTime: formatDate(delivery.deliveryTime) || undefined,
|
||||
address: delivery.address || undefined,
|
||||
lng: delivery.lng,
|
||||
lat: delivery.lat,
|
||||
contact: delivery.contact || undefined,
|
||||
notes: delivery.notes || undefined,
|
||||
productLines: (delivery.products || []).map((product) =>
|
||||
formatDriverStopProductLine(product, getUnitLabel)
|
||||
)
|
||||
})
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
function buildLegacyDriverStopBlocks(
|
||||
plan: DriverSummaryPlan,
|
||||
overrides: {
|
||||
pickupAddress?: string
|
||||
pickupLng?: number | null
|
||||
pickupLat?: number | null
|
||||
pickupContact?: string
|
||||
pickupNotes?: string
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
deliveryContact?: string
|
||||
destinationNotes?: string
|
||||
startPoint?: string
|
||||
endPoint?: string
|
||||
},
|
||||
productLines: string[]
|
||||
): DriverSummaryStopBlock[] {
|
||||
const pickupAddress =
|
||||
overrides.pickupAddress || plan.pickupAddress || overrides.startPoint || plan.startPoint
|
||||
const destinationAddress =
|
||||
overrides.destinationAddress ||
|
||||
plan.destinationAddress ||
|
||||
plan.destinationName ||
|
||||
overrides.endPoint ||
|
||||
plan.endPoint
|
||||
const blocks: DriverSummaryStopBlock[] = []
|
||||
if (pickupAddress) {
|
||||
blocks.push({
|
||||
label: '取货',
|
||||
customerName: plan.load?.customerName || undefined,
|
||||
stopTime: formatDate(plan.pickupTime) || undefined,
|
||||
address: pickupAddress,
|
||||
lng: overrides.pickupLng ?? plan.pickupLng,
|
||||
lat: overrides.pickupLat ?? plan.pickupLat,
|
||||
contact: overrides.pickupContact || plan.pickupContact || undefined,
|
||||
notes: overrides.pickupNotes || plan.pickupNotes || undefined,
|
||||
productLines: productLines.length ? productLines : []
|
||||
})
|
||||
}
|
||||
if (destinationAddress) {
|
||||
blocks.push({
|
||||
label: '送货',
|
||||
customerName: undefined,
|
||||
stopTime: undefined,
|
||||
address: destinationAddress,
|
||||
lng: overrides.destinationLng ?? plan.destinationLng,
|
||||
lat: overrides.destinationLat ?? plan.destinationLat,
|
||||
contact: overrides.deliveryContact || plan.deliveryContact || undefined,
|
||||
notes: overrides.destinationNotes || plan.destinationNotes || undefined,
|
||||
productLines: []
|
||||
})
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
export function buildDriverSummaryFromPlan(options: {
|
||||
plan?: DriverSummaryPlan | null
|
||||
planNo?: string
|
||||
licensePlate?: string
|
||||
pickupTime?: string
|
||||
planDate?: string
|
||||
personnelSummary?: string
|
||||
driverUrl?: string
|
||||
products?: PlanProduct[]
|
||||
stopBlocks?: DriverSummaryStopBlock[]
|
||||
getUnitLabel?: (unit: string) => string
|
||||
pickupAddress?: string
|
||||
pickupLng?: number | null
|
||||
pickupLat?: number | null
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
pickupContact?: string
|
||||
pickupNotes?: string
|
||||
deliveryContact?: string
|
||||
destinationNotes?: string
|
||||
startPoint?: string
|
||||
endPoint?: string
|
||||
}) {
|
||||
const plan = options.plan || {}
|
||||
const plate = options.licensePlate || plan.licensePlate || '—'
|
||||
const lines = [`【${plate} - 运输计划】`]
|
||||
lines.push(`计划号:${options.planNo || plan.planNo || '—'}`)
|
||||
lines.push(`车牌:${plate}`)
|
||||
const time = formatDate(options.pickupTime || plan.pickupTime) || options.planDate || plan.planDate
|
||||
if (time) lines.push(`时间:${time}`)
|
||||
|
||||
const productLines = formatPlanLevelProductLines(options.products || [], options.getUnitLabel)
|
||||
let stopBlocks =
|
||||
options.stopBlocks ||
|
||||
buildDriverStopBlocksFromPlan(plan, options.getUnitLabel)
|
||||
if (!stopBlocks.length) {
|
||||
stopBlocks = buildLegacyDriverStopBlocks(plan, options, productLines)
|
||||
}
|
||||
|
||||
if (stopBlocks.length) {
|
||||
lines.push('')
|
||||
stopBlocks.forEach((stop, index) => {
|
||||
if (index > 0) lines.push('')
|
||||
appendDriverStopBlockLines(lines, stop)
|
||||
})
|
||||
} else if (productLines.length) {
|
||||
lines.push('')
|
||||
lines.push(`产品:${productLines.join(';')}`)
|
||||
}
|
||||
|
||||
lines.push('')
|
||||
lines.push(`司机链接:${options.driverUrl || '—'}`)
|
||||
return lines.join('\n')
|
||||
}
|
||||
@@ -151,7 +151,9 @@ function formatQty(value?: number | string | null): string {
|
||||
}
|
||||
|
||||
function buildDeliveryUnit(plan: Plan): string {
|
||||
return plan.customerName?.trim() || plan.destinationName?.trim() || ''
|
||||
const loadName = String(plan.load?.customerName || '').trim()
|
||||
if (loadName) return loadName
|
||||
return plan.supplierName?.trim() || plan.customerName?.trim() || plan.destinationName?.trim() || ''
|
||||
}
|
||||
|
||||
function formatInboundSlipProductGridCellContent(content: string): string {
|
||||
|
||||
@@ -8,3 +8,29 @@ export function isPlInternalCustomerName(name?: string | null): boolean {
|
||||
const n = (name || '').trim()
|
||||
return (PL_INTERNAL_CUSTOMER_NAMES as readonly string[]).includes(n)
|
||||
}
|
||||
|
||||
/** 装货或送货段任一段客户为山峡/云栋时,需要现场链接。 */
|
||||
export function planRequiresSiteLink(
|
||||
plan?: {
|
||||
load?: { customerName?: string | null } | null
|
||||
deliveries?: { customerName?: string | null }[] | null
|
||||
supplierName?: string | null
|
||||
customerName?: string | null
|
||||
destinationName?: string | null
|
||||
} | null
|
||||
): boolean {
|
||||
if (!plan) return false
|
||||
if (plan.load) {
|
||||
if (isPlInternalCustomerName(plan.load.customerName)) return true
|
||||
} else if (isPlInternalCustomerName(plan.supplierName)) {
|
||||
return true
|
||||
}
|
||||
const deliveries = plan.deliveries || []
|
||||
if (deliveries.length) {
|
||||
return deliveries.some(item => isPlInternalCustomerName(item.customerName))
|
||||
}
|
||||
return (
|
||||
isPlInternalCustomerName(plan.customerName) ||
|
||||
isPlInternalCustomerName(plan.destinationName)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import type { Plan, PlanDelivery, PlanLoad, PlanProduct, PlanStopProduct } from '@/api/pl'
|
||||
import { formatDate } from '@/utils/format'
|
||||
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[]
|
||||
} {
|
||||
if (plan.load && (plan.deliveries?.length ?? 0) > 0) {
|
||||
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 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)
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
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 { 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
|
||||
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[]
|
||||
}
|
||||
|
||||
/** 地磅默认勾选以装货客户是否为内部客户(山峡/云栋)为准,与送货段客户无关。 */
|
||||
export function resolveStopDibangDefaults(
|
||||
loadCustomerName?: string | null,
|
||||
stopType: 'load' | 'delivery' = 'load'
|
||||
): { generateDibang: number; dibangDirection: string } {
|
||||
if (!isPlInternalCustomerName(loadCustomerName)) {
|
||||
return { generateDibang: 0, dibangDirection: 'none' }
|
||||
}
|
||||
if (stopType === 'load') {
|
||||
return { generateDibang: 1, dibangDirection: 'outbound' }
|
||||
}
|
||||
return { generateDibang: 1, dibangDirection: 'inbound' }
|
||||
}
|
||||
|
||||
export function syncDeliveryDibangFromLoad(
|
||||
deliveries: PlanDeliveryForm[],
|
||||
loadCustomerName?: string | null
|
||||
): PlanDeliveryForm[] {
|
||||
const defaults = resolveStopDibangDefaults(loadCustomerName, 'delivery')
|
||||
return deliveries.map((row) => ({
|
||||
...row,
|
||||
generateDibang: defaults.generateDibang,
|
||||
dibangDirection: defaults.dibangDirection
|
||||
}))
|
||||
}
|
||||
|
||||
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<number>
|
||||
): 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<number>()
|
||||
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,
|
||||
loadCustomerName?: string | null
|
||||
): PlanDeliveryForm {
|
||||
const defaults = resolveStopDibangDefaults(loadCustomerName, 'delivery')
|
||||
return {
|
||||
seq,
|
||||
customerId: '',
|
||||
customerName: '',
|
||||
address: '',
|
||||
lng: null,
|
||||
lat: null,
|
||||
deliveryTime: '',
|
||||
contact: '',
|
||||
notes: '',
|
||||
generateDibang: defaults.generateDibang,
|
||||
dibangDirection: defaults.dibangDirection,
|
||||
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,
|
||||
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): PlanLoadForm {
|
||||
if (!load) return createEmptyLoadForm()
|
||||
const defaults = resolveStopDibangDefaults(load.customerName, 'load')
|
||||
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: load.generateDibang ?? defaults.generateDibang,
|
||||
dibangDirection: load.dibangDirection || defaults.dibangDirection,
|
||||
contracts: (load.contracts || []).map((item) => ({
|
||||
contractId: String(item.contractId || ''),
|
||||
contractNo: item.contractNo
|
||||
})),
|
||||
products: (load.products || []).map(mapStopProductFromApi)
|
||||
}
|
||||
}
|
||||
|
||||
export function mapDeliveriesFromApi(
|
||||
deliveries?: PlanDelivery[],
|
||||
loadCustomerName?: string | null
|
||||
): PlanDeliveryForm[] {
|
||||
if (!deliveries?.length) return [createEmptyDeliveryForm(1)]
|
||||
return deliveries.map((row, index) => {
|
||||
const defaults = resolveStopDibangDefaults(loadCustomerName, 'delivery')
|
||||
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: row.generateDibang ?? defaults.generateDibang,
|
||||
dibangDirection: row.dibangDirection || defaults.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 loadDefaults = resolveStopDibangDefaults(plan.supplierName, 'load')
|
||||
const deliveryDefaults = resolveStopDibangDefaults(plan.supplierName, 'delivery')
|
||||
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<number>
|
||||
): 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<number>()
|
||||
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)
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Plan, TransportInventoryLink } from '@/api/pl'
|
||||
|
||||
export function formatTransportInventoryLinkLabel(
|
||||
record: TransportInventoryLink,
|
||||
plan?: Plan | null
|
||||
): string {
|
||||
const product = (record.productName || '').trim() || '—'
|
||||
let stopLabel = '地磅单'
|
||||
if (record.stopType === 'load') {
|
||||
stopLabel = '装货'
|
||||
} else if (record.stopType === 'delivery') {
|
||||
const deliveries = plan?.deliveries || []
|
||||
const match = record.stopId
|
||||
? deliveries.find((item) => item.id && String(item.id) === String(record.stopId))
|
||||
: undefined
|
||||
stopLabel = match?.seq ? `送货 #${match.seq}` : '送货'
|
||||
}
|
||||
const batch = (record.batchNo || '').trim()
|
||||
return batch ? `${stopLabel} · ${product} · ${batch}` : `${stopLabel} · ${product}`
|
||||
}
|
||||
Reference in new issue
Block a user