pl-plan-stop-form.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. import type { Plan, PlanContractLink, PlanDelivery, PlanLoad, PlanStopProduct } from '@/api/pl'
  2. import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
  3. import type { BrandOption } from '@/utils/crm-product-brand'
  4. import { formatBrandConstraintLabel } from '@/utils/crm-product-brand'
  5. import {
  6. isPlDibangOptionalVehiclePlate,
  7. isPlInternalCustomerName
  8. } from '@/utils/pl-internal-customer'
  9. export interface PlanStopProductFormRow {
  10. id?: string
  11. planProductId?: string
  12. productId: string
  13. productNo: string
  14. productName: string
  15. /** 展示用:主数据标准名 */
  16. standardProductName?: string
  17. /** 展示用:别名 */
  18. aliasName?: string
  19. aliasOptions?: string[]
  20. brandMode?: string
  21. brandId?: string
  22. brandName?: string
  23. brandOptions?: BrandOption[]
  24. model?: string
  25. unit: string
  26. packageSpec?: string
  27. deliveryMethod?: string
  28. planQty?: number
  29. /** 装货段归档:入库数量即实际数量 */
  30. inboundQty?: number
  31. inboundPrice?: number
  32. inboundAmount?: number
  33. /** 送货段归档:出库数量即实际数量 */
  34. outboundQty?: number
  35. outboundPrice?: number
  36. outboundAmount?: number
  37. /** 本段归档品牌数量 */
  38. brandQtys?: Array<{ brandId: string; brandName?: string; qty: number; unitPrice?: number }>
  39. remark?: string
  40. isExplosive?: number
  41. isToxic?: number
  42. }
  43. /** 段次查看/归档:装货段取 inbound,送货段取 outbound */
  44. export function resolveStopActualQty(
  45. row: PlanStopProductFormRow,
  46. stopType: 'load' | 'delivery'
  47. ): number | undefined {
  48. const raw = stopType === 'load' ? row.inboundQty : row.outboundQty
  49. return raw != null ? Number(raw) : undefined
  50. }
  51. export function resolveStopUnitPrice(
  52. row: PlanStopProductFormRow,
  53. stopType: 'load' | 'delivery'
  54. ): number | undefined {
  55. const raw = stopType === 'load' ? row.inboundPrice : row.outboundPrice
  56. return raw != null ? Number(raw) : undefined
  57. }
  58. export function resolveStopArchiveAmount(
  59. row: PlanStopProductFormRow,
  60. stopType: 'load' | 'delivery'
  61. ): number | undefined {
  62. const stored = stopType === 'load' ? row.inboundAmount : row.outboundAmount
  63. if (stored != null) {
  64. const n = Number(stored)
  65. return Number.isFinite(n) ? n : undefined
  66. }
  67. const qty = resolveStopActualQty(row, stopType)
  68. const price = resolveStopUnitPrice(row, stopType)
  69. if (qty == null || price == null) return undefined
  70. const amount = qty * price
  71. return Number.isFinite(amount) ? amount : undefined
  72. }
  73. export function formatStopArchiveAmount(
  74. row: PlanStopProductFormRow,
  75. stopType: 'load' | 'delivery'
  76. ): string {
  77. const amount = resolveStopArchiveAmount(row, stopType)
  78. return amount != null ? amount.toFixed(2) : '—'
  79. }
  80. export interface PlanLoadForm {
  81. customerId: string
  82. customerName: string
  83. address: string
  84. lng: number | null
  85. lat: number | null
  86. loadTime: string
  87. contact: string
  88. notes: string
  89. generateDibang: number
  90. dibangDirection: string
  91. contracts: PlanContractLink[]
  92. products: PlanStopProductFormRow[]
  93. }
  94. export interface PlanDeliveryForm {
  95. seq: number
  96. customerId: string
  97. customerName: string
  98. address: string
  99. lng: number | null
  100. lat: number | null
  101. deliveryTime: string
  102. contact: string
  103. notes: string
  104. generateDibang: number
  105. dibangDirection: string
  106. contracts: PlanContractLink[]
  107. products: PlanStopProductFormRow[]
  108. }
  109. /**
  110. * 地磅勾选规则:
  111. * - 内部客户(山峡/云栋):默认勾选且不可改
  112. * - 地磅可选车(如皖H21522)+ 内部客户:默认不勾,可手动勾选
  113. * - 其他客户:默认不勾且不可勾
  114. */
  115. export function resolveStopDibangDefaults(
  116. customerName?: string | null,
  117. stopType: 'load' | 'delivery' = 'load',
  118. vehiclePlate?: string | null
  119. ): { generateDibang: number; dibangDirection: string } {
  120. if (!isPlInternalCustomerName(customerName)) {
  121. return { generateDibang: 0, dibangDirection: 'none' }
  122. }
  123. if (isPlDibangOptionalVehiclePlate(vehiclePlate)) {
  124. return { generateDibang: 0, dibangDirection: 'none' }
  125. }
  126. if (stopType === 'load') {
  127. return { generateDibang: 1, dibangDirection: 'outbound' }
  128. }
  129. return { generateDibang: 1, dibangDirection: 'inbound' }
  130. }
  131. /** 仅「内部客户 + 地磅可选车」允许用户手动勾选地磅单 */
  132. export function canManualToggleStopDibang(
  133. customerName?: string | null,
  134. vehiclePlate?: string | null
  135. ): boolean {
  136. return isPlInternalCustomerName(customerName) && isPlDibangOptionalVehiclePlate(vehiclePlate)
  137. }
  138. function normalizeStopDibangFieldsFromApi(
  139. customerName: string | null | undefined,
  140. stopType: 'load' | 'delivery',
  141. generateDibang?: number | null,
  142. dibangDirection?: string | null,
  143. vehiclePlate?: string | null
  144. ): { generateDibang: number; dibangDirection: string } {
  145. const defaults = resolveStopDibangDefaults(customerName, stopType, vehiclePlate)
  146. // 非山峡/云栋:禁止生成地磅单(编辑回填也强制清掉历史勾选)
  147. if (!isPlInternalCustomerName(customerName)) {
  148. return { generateDibang: 0, dibangDirection: 'none' }
  149. }
  150. const gen = Number(generateDibang ?? defaults.generateDibang) === 1 ? 1 : 0
  151. let direction = (dibangDirection || '').trim()
  152. if (gen) {
  153. if (direction !== 'outbound' && direction !== 'inbound') {
  154. // 可选车默认 direction 为 none,勾选回填时按段类型补方向
  155. direction = stopType === 'load' ? 'outbound' : 'inbound'
  156. }
  157. } else {
  158. direction = 'none'
  159. }
  160. return { generateDibang: gen, dibangDirection: direction }
  161. }
  162. export function createStopProductsFromPlanProducts(
  163. products: PlanFormProductRow[],
  164. qtyField: 'quantity' | 'actualQty' = 'quantity'
  165. ): PlanStopProductFormRow[] {
  166. return products.map((item) => ({
  167. planProductId: item.id,
  168. productId: item.productId,
  169. productNo: item.productNo,
  170. productName: item.productName,
  171. standardProductName: item.standardProductName || item.productName,
  172. aliasName: item.aliasName,
  173. aliasOptions: item.aliasOptions ? [...item.aliasOptions] : undefined,
  174. brandMode: item.brandMode,
  175. brandId: item.brandId,
  176. brandName: item.brandName,
  177. brandOptions: item.brandOptions ? [...item.brandOptions] : undefined,
  178. model: item.model,
  179. unit: item.unit || '吨',
  180. packageSpec: item.packageSpec,
  181. deliveryMethod: item.deliveryMethod,
  182. planQty: Number(item[qtyField] ?? item.quantity ?? 0),
  183. remark: item.remark,
  184. isExplosive: item.isExplosive,
  185. isToxic: item.isToxic
  186. }))
  187. }
  188. function matchPrevStopProduct(
  189. stopProducts: PlanStopProductFormRow[],
  190. planRow: PlanFormProductRow,
  191. used: Set<number>
  192. ): PlanStopProductFormRow | undefined {
  193. if (planRow.id) {
  194. const byPlanProductId = stopProducts.findIndex(
  195. (row, index) =>
  196. !used.has(index) && row.planProductId && String(row.planProductId) === String(planRow.id)
  197. )
  198. if (byPlanProductId >= 0) {
  199. used.add(byPlanProductId)
  200. return stopProducts[byPlanProductId]
  201. }
  202. }
  203. const byProductId = stopProducts.findIndex(
  204. (row, index) => !used.has(index) && String(row.productId) === String(planRow.productId)
  205. )
  206. if (byProductId >= 0) {
  207. used.add(byProductId)
  208. return stopProducts[byProductId]
  209. }
  210. return undefined
  211. }
  212. /**
  213. * 按计划产品重建段产品列表:增删对齐计划层;共享字段取自计划;
  214. * 可选保留段内已改的数量 / 备注。
  215. */
  216. export function reconcileStopProductsWithPlan(
  217. stopProducts: PlanStopProductFormRow[],
  218. planProducts: PlanFormProductRow[],
  219. options?: {
  220. preserveQty?: boolean
  221. preserveRemark?: boolean
  222. }
  223. ): PlanStopProductFormRow[] {
  224. const preserveQty = options?.preserveQty !== false
  225. const preserveRemark = options?.preserveRemark !== false
  226. const used = new Set<number>()
  227. const nextRows = createStopProductsFromPlanProducts(planProducts)
  228. return nextRows.map((row, index) => {
  229. const planRow = planProducts[index]
  230. const prev = planRow ? matchPrevStopProduct(stopProducts, planRow, used) : undefined
  231. if (!prev) return row
  232. return {
  233. ...row,
  234. planQty: preserveQty ? (prev.planQty ?? row.planQty) : row.planQty,
  235. remark: preserveRemark ? (prev.remark ?? row.remark) : row.remark
  236. }
  237. })
  238. }
  239. /** 用计划产品行补齐装货/送货产品表展示字段(别名、品牌等) */
  240. export function sumStopProductsPlanQty(products: PlanStopProductFormRow[]): number {
  241. return products.reduce((sum, row) => sum + Number(row.planQty ?? 0), 0)
  242. }
  243. /** 段内紧凑表「产品」列副行:别名 · 品牌 · 规格 · 单位 */
  244. export function formatStopProductMetaSubline(
  245. row: PlanStopProductFormRow,
  246. unitLabel?: string
  247. ): string {
  248. const parts: string[] = []
  249. if (row.aliasName) parts.push(`别名:${row.aliasName}`)
  250. const brand = formatBrandConstraintLabel(row.brandMode, row.brandName)
  251. if (brand && brand !== '—') parts.push(`品牌:${brand}`)
  252. if (row.model) parts.push(row.model)
  253. const unit = unitLabel || row.unit
  254. if (unit) parts.push(unit)
  255. return parts.join(' · ')
  256. }
  257. export function enrichStopProductsDisplayFromPlan(
  258. stopProducts: PlanStopProductFormRow[],
  259. planProducts: Array<{
  260. id?: string
  261. productId: string
  262. productNo?: string
  263. productName?: string
  264. standardProductName?: string
  265. aliasName?: string
  266. aliasOptions?: string[]
  267. brandMode?: string
  268. brandId?: string
  269. brandName?: string
  270. brandOptions?: BrandOption[]
  271. model?: string
  272. unit?: string
  273. packageSpec?: string
  274. deliveryMethod?: string
  275. quantity?: number
  276. isExplosive?: number
  277. isToxic?: number
  278. }>
  279. ): PlanStopProductFormRow[] {
  280. return stopProducts.map((row) => {
  281. const match =
  282. planProducts.find(
  283. (p) => row.planProductId && p.id && String(p.id) === String(row.planProductId)
  284. ) || planProducts.find((p) => String(p.productId) === String(row.productId))
  285. if (!match) return row
  286. const planQty =
  287. row.planQty != null && Number.isFinite(Number(row.planQty))
  288. ? Number(row.planQty)
  289. : match.quantity != null
  290. ? Number(match.quantity)
  291. : row.planQty
  292. return {
  293. ...row,
  294. productNo: row.productNo || match.productNo || row.productNo,
  295. standardProductName: match.standardProductName || match.productName || row.productName,
  296. aliasName: match.aliasName ?? row.aliasName,
  297. aliasOptions: match.aliasOptions?.length ? [...match.aliasOptions] : row.aliasOptions,
  298. brandMode: match.brandMode ?? row.brandMode,
  299. brandId: match.brandId ?? row.brandId,
  300. brandName: match.brandName ?? row.brandName,
  301. brandOptions: match.brandOptions?.length ? [...match.brandOptions] : row.brandOptions,
  302. model: row.model || match.model,
  303. unit: row.unit || match.unit || '吨',
  304. packageSpec: row.packageSpec || match.packageSpec,
  305. deliveryMethod: row.deliveryMethod || match.deliveryMethod,
  306. planQty,
  307. isExplosive: match.isExplosive ?? row.isExplosive,
  308. isToxic: match.isToxic ?? row.isToxic
  309. }
  310. })
  311. }
  312. /** 查看态:仅展示段上真实产品(空段不回退计划产品);有段产品时补齐展示字段 */
  313. export function resolveStopProductsForView(
  314. stopProducts: PlanStopProductFormRow[] | undefined | null,
  315. planProducts: PlanFormProductRow[]
  316. ): PlanStopProductFormRow[] {
  317. const rows = stopProducts?.length ? stopProducts : []
  318. return enrichStopProductsDisplayFromPlan(rows, planProducts)
  319. }
  320. export function createEmptyLoadForm(): PlanLoadForm {
  321. return {
  322. customerId: '',
  323. customerName: '',
  324. address: '',
  325. lng: null,
  326. lat: null,
  327. loadTime: '',
  328. contact: '',
  329. notes: '',
  330. generateDibang: 0,
  331. dibangDirection: 'none',
  332. contracts: [],
  333. products: []
  334. }
  335. }
  336. export function createEmptyDeliveryForm(seq = 1): PlanDeliveryForm {
  337. return {
  338. seq,
  339. customerId: '',
  340. customerName: '',
  341. address: '',
  342. lng: null,
  343. lat: null,
  344. deliveryTime: '',
  345. contact: '',
  346. notes: '',
  347. generateDibang: 0,
  348. dibangDirection: 'none',
  349. contracts: [],
  350. products: []
  351. }
  352. }
  353. function mapStopProductFromApi(row: PlanStopProduct): PlanStopProductFormRow {
  354. return {
  355. id: row.id,
  356. planProductId: row.planProductId,
  357. productId: row.productId,
  358. productNo: row.productNo,
  359. productName: row.productName,
  360. model: row.model,
  361. unit: row.unit || '吨',
  362. packageSpec: row.packageSpec,
  363. deliveryMethod: row.deliveryMethod,
  364. planQty: row.planQty != null ? Number(row.planQty) : undefined,
  365. inboundQty: row.inboundQty != null ? Number(row.inboundQty) : undefined,
  366. inboundPrice: row.inboundPrice != null ? Number(row.inboundPrice) : undefined,
  367. inboundAmount: row.inboundAmount != null ? Number(row.inboundAmount) : undefined,
  368. outboundQty: row.outboundQty != null ? Number(row.outboundQty) : undefined,
  369. outboundPrice: row.outboundPrice != null ? Number(row.outboundPrice) : undefined,
  370. outboundAmount: row.outboundAmount != null ? Number(row.outboundAmount) : undefined,
  371. brandQtys: (row.brandQtys || [])
  372. .filter((item) => Number(item.qty) > 0)
  373. .map((item) => ({
  374. brandId: String(item.brandId || ''),
  375. brandName: item.brandName,
  376. qty: Number(item.qty),
  377. unitPrice:
  378. item.unitPrice != null && Number.isFinite(Number(item.unitPrice))
  379. ? Number(item.unitPrice)
  380. : undefined
  381. })),
  382. remark: row.remark
  383. }
  384. }
  385. /** 查看态装货地址:优先段表 address,回退计划 pickupAddress */
  386. export function resolveLoadViewAddress(
  387. plan?: { pickupAddress?: string | null } | null,
  388. load?: { address?: string | null } | null
  389. ): string {
  390. const fromStop = (load?.address || '').trim()
  391. if (fromStop) return fromStop
  392. return (plan?.pickupAddress || '').trim()
  393. }
  394. /** 查看态送货地址:优先段表 address,首段可回退计划 destinationAddress */
  395. export function resolveDeliveryViewAddress(
  396. plan?: { destinationAddress?: string | null } | null,
  397. delivery?: { address?: string | null } | null,
  398. index = 0
  399. ): string {
  400. const fromStop = (delivery?.address || '').trim()
  401. if (fromStop) return fromStop
  402. if (index === 0) {
  403. return (plan?.destinationAddress || '').trim()
  404. }
  405. return ''
  406. }
  407. export function mapLoadFromApi(
  408. load?: PlanLoad | null,
  409. vehiclePlate?: string | null
  410. ): PlanLoadForm {
  411. if (!load) return createEmptyLoadForm()
  412. const dibang = normalizeStopDibangFieldsFromApi(
  413. load.customerName,
  414. 'load',
  415. load.generateDibang,
  416. load.dibangDirection,
  417. vehiclePlate
  418. )
  419. return {
  420. customerId: load.customerId || '',
  421. customerName: load.customerName || '',
  422. address: load.address || '',
  423. lng: load.lng != null ? Number(load.lng) : null,
  424. lat: load.lat != null ? Number(load.lat) : null,
  425. loadTime: (load.loadTime || '').slice(0, 10),
  426. contact: load.contact || '',
  427. notes: load.notes || '',
  428. generateDibang: dibang.generateDibang,
  429. dibangDirection: dibang.dibangDirection,
  430. contracts: (load.contracts || []).map((item) => ({
  431. contractId: String(item.contractId || ''),
  432. contractNo: item.contractNo
  433. })),
  434. products: (load.products || []).map(mapStopProductFromApi)
  435. }
  436. }
  437. export function mapDeliveriesFromApi(
  438. deliveries?: PlanDelivery[],
  439. vehiclePlate?: string | null
  440. ): PlanDeliveryForm[] {
  441. if (!deliveries?.length) return [createEmptyDeliveryForm(1)]
  442. return deliveries.map((row, index) => {
  443. const dibang = normalizeStopDibangFieldsFromApi(
  444. row.customerName,
  445. 'delivery',
  446. row.generateDibang,
  447. row.dibangDirection,
  448. vehiclePlate
  449. )
  450. return {
  451. seq: row.seq ?? index + 1,
  452. customerId: row.customerId || '',
  453. customerName: row.customerName || '',
  454. address: row.address || '',
  455. lng: row.lng != null ? Number(row.lng) : null,
  456. lat: row.lat != null ? Number(row.lat) : null,
  457. deliveryTime: (row.deliveryTime || '').slice(0, 10),
  458. contact: row.contact || '',
  459. notes: row.notes || '',
  460. generateDibang: dibang.generateDibang,
  461. dibangDirection: dibang.dibangDirection,
  462. contracts: (row.contracts || []).map((item) => ({
  463. contractId: String(item.contractId || ''),
  464. contractNo: item.contractNo
  465. })),
  466. products: (row.products || []).map(mapStopProductFromApi)
  467. }
  468. })
  469. }
  470. /** 旧数据无 load/deliveries 时从供方/需方字段回填 */
  471. export function mapLegacyPlanToStops(plan: Plan): {
  472. load: PlanLoadForm
  473. deliveries: PlanDeliveryForm[]
  474. } {
  475. const plate = plan.licensePlate
  476. const loadDefaults = resolveStopDibangDefaults(plan.supplierName, 'load', plate)
  477. const deliveryDefaults = resolveStopDibangDefaults(plan.customerName, 'delivery', plate)
  478. const stopProducts = createStopProductsFromPlanProducts(
  479. (plan.products || []).map((item, index) => ({
  480. id: item.id,
  481. productId: String(item.productId || index),
  482. productNo: item.productNo || '',
  483. productName: item.productName || '',
  484. model: item.model,
  485. unit: item.unit || '吨',
  486. quantity: Number(item.quantity ?? item.actualQty ?? 0),
  487. packageSpec: item.packageSpec,
  488. deliveryMethod: item.deliveryMethod,
  489. remark: item.remark
  490. }))
  491. )
  492. return {
  493. load: {
  494. customerId: plan.supplierId || '',
  495. customerName: plan.supplierName || '',
  496. address: plan.pickupAddress || '',
  497. lng: plan.pickupLng != null ? Number(plan.pickupLng) : null,
  498. lat: plan.pickupLat != null ? Number(plan.pickupLat) : null,
  499. loadTime: (plan.pickupTime || '').slice(0, 10),
  500. contact: plan.pickupContact || '',
  501. notes: plan.pickupNotes || '',
  502. generateDibang: loadDefaults.generateDibang,
  503. dibangDirection: loadDefaults.dibangDirection,
  504. contracts: (plan.supplierContracts || []).map((item) => ({
  505. contractId: String(item.contractId || ''),
  506. contractNo: item.contractNo
  507. })),
  508. products: stopProducts.length ? stopProducts.map((row) => ({ ...row })) : []
  509. },
  510. deliveries: [
  511. {
  512. seq: 1,
  513. customerId: plan.customerId || '',
  514. customerName: plan.customerName || '',
  515. address: plan.destinationAddress || '',
  516. lng: plan.destinationLng != null ? Number(plan.destinationLng) : null,
  517. lat: plan.destinationLat != null ? Number(plan.destinationLat) : null,
  518. deliveryTime: (plan.deliveryTime || '').slice(0, 10),
  519. contact: plan.deliveryContact || '',
  520. notes: plan.destinationNotes || '',
  521. generateDibang: deliveryDefaults.generateDibang,
  522. dibangDirection: deliveryDefaults.dibangDirection,
  523. contracts: (plan.demanderContracts || []).map((item) => ({
  524. contractId: String(item.contractId || ''),
  525. contractNo: item.contractNo
  526. })),
  527. products: stopProducts.length ? stopProducts.map((row) => ({ ...row })) : []
  528. }
  529. ]
  530. }
  531. }
  532. export function syncLoadProductsFromPlan(
  533. load: PlanLoadForm,
  534. planProducts: PlanFormProductRow[],
  535. preserveQty = true
  536. ) {
  537. load.products = reconcileStopProductsWithPlan(load.products, planProducts, {
  538. preserveQty,
  539. preserveRemark: preserveQty
  540. })
  541. }
  542. export function syncDeliveryProductsFromPlan(
  543. delivery: PlanDeliveryForm,
  544. planProducts: PlanFormProductRow[],
  545. preserveQty = false
  546. ) {
  547. delivery.products = reconcileStopProductsWithPlan(delivery.products, planProducts, {
  548. preserveQty,
  549. preserveRemark: preserveQty
  550. })
  551. }
  552. function matchPrevDeliveryProduct(
  553. stopProducts: PlanStopProductFormRow[],
  554. loadRow: PlanStopProductFormRow,
  555. used: Set<number>
  556. ): PlanStopProductFormRow | undefined {
  557. if (loadRow.planProductId) {
  558. const byPlanProductId = stopProducts.findIndex(
  559. (row, index) =>
  560. !used.has(index) &&
  561. row.planProductId &&
  562. String(row.planProductId) === String(loadRow.planProductId)
  563. )
  564. if (byPlanProductId >= 0) {
  565. used.add(byPlanProductId)
  566. return stopProducts[byPlanProductId]
  567. }
  568. }
  569. const byProductId = stopProducts.findIndex(
  570. (row, index) => !used.has(index) && String(row.productId) === String(loadRow.productId)
  571. )
  572. if (byProductId >= 0) {
  573. used.add(byProductId)
  574. return stopProducts[byProductId]
  575. }
  576. return undefined
  577. }
  578. function cloneStopProductSharedFields(
  579. loadRow: PlanStopProductFormRow,
  580. planQty: number,
  581. remark?: string
  582. ): PlanStopProductFormRow {
  583. return {
  584. ...loadRow,
  585. aliasOptions: loadRow.aliasOptions ? [...loadRow.aliasOptions] : undefined,
  586. brandOptions: loadRow.brandOptions ? [...loadRow.brandOptions] : undefined,
  587. planQty,
  588. remark: remark ?? loadRow.remark
  589. }
  590. }
  591. /**
  592. * 从装货产品同步到各送货段(产品清单 ⊆ 装货)。
  593. * C1:仅 1 个送货段时数量跟装货;≥2 段时保留已有段数量,新增行/新增段默认 0。
  594. */
  595. export function syncDeliveriesProductsFromLoad(
  596. deliveries: PlanDeliveryForm[],
  597. loadProducts: PlanStopProductFormRow[],
  598. options?: { forceQtyFromLoad?: boolean }
  599. ): { deliveries: PlanDeliveryForm[]; needSplitHint: boolean } {
  600. const multi = deliveries.length >= 2
  601. const forceQty = options?.forceQtyFromLoad === true
  602. const next = deliveries.map((delivery) => {
  603. const used = new Set<number>()
  604. const products = loadProducts.map((loadRow) => {
  605. const prev = matchPrevDeliveryProduct(delivery.products || [], loadRow, used)
  606. let planQty: number
  607. if (forceQty || !multi) {
  608. planQty = Number(loadRow.planQty ?? 0)
  609. } else if (prev) {
  610. planQty = Number(prev.planQty ?? 0)
  611. } else {
  612. planQty = 0
  613. }
  614. return cloneStopProductSharedFields(loadRow, planQty, prev?.remark)
  615. })
  616. return { ...delivery, products }
  617. })
  618. return { deliveries: next, needSplitHint: multi }
  619. }
  620. /** 装货产品 → 计划层产品行(保存 pl_plan_product / 运费计算) */
  621. export function mapLoadProductsToPlanFormRows(
  622. loadProducts: PlanStopProductFormRow[]
  623. ): PlanFormProductRow[] {
  624. return loadProducts.map((row) => ({
  625. id: row.planProductId,
  626. productId: row.productId,
  627. productNo: row.productNo,
  628. productName: row.productName,
  629. standardProductName: row.standardProductName || row.productName,
  630. aliasName: row.aliasName,
  631. aliasOptions: row.aliasOptions ? [...row.aliasOptions] : undefined,
  632. brandMode: row.brandMode,
  633. brandId: row.brandId,
  634. brandName: row.brandName,
  635. brandOptions: row.brandOptions ? [...row.brandOptions] : undefined,
  636. model: row.model,
  637. unit: row.unit || '吨',
  638. quantity: Number(row.planQty ?? 0),
  639. actualQty: 0,
  640. packageSpec: row.packageSpec,
  641. deliveryMethod: row.deliveryMethod,
  642. remark: row.remark,
  643. isExplosive: row.isExplosive,
  644. isToxic: row.isToxic
  645. }))
  646. }
  647. /** 合并装货映射行与既有计划产品元数据(危化证、税额等) */
  648. export function mergePlanProductsFromLoadMirror(
  649. fromLoad: PlanFormProductRow[],
  650. previous: PlanFormProductRow[]
  651. ): PlanFormProductRow[] {
  652. return fromLoad.map((row) => {
  653. const prev =
  654. (row.id && previous.find((item) => item.id && String(item.id) === String(row.id))) ||
  655. previous.find((item) => String(item.productId) === String(row.productId))
  656. if (!prev) return row
  657. return {
  658. ...row,
  659. id: prev.id || row.id,
  660. categoryName: row.categoryName || prev.categoryName,
  661. standardProductName: row.standardProductName || prev.standardProductName || row.productName,
  662. aliasOptions: row.aliasOptions?.length ? row.aliasOptions : prev.aliasOptions,
  663. brandOptions: row.brandOptions?.length ? row.brandOptions : prev.brandOptions,
  664. taxRate: prev.taxRate,
  665. taxPrice: prev.taxPrice ?? row.taxPrice,
  666. taxAmount: prev.taxAmount,
  667. taxSubtotal: prev.taxSubtotal,
  668. purchaseCertUrl: prev.purchaseCertUrl,
  669. transportCertUrl: prev.transportCertUrl,
  670. legalUseCertUrl: prev.legalUseCertUrl
  671. }
  672. })
  673. }
  674. export function buildStopProductPayload(row: PlanStopProductFormRow) {
  675. return {
  676. planProductId: row.planProductId || undefined,
  677. productId: row.productId,
  678. productNo: row.productNo,
  679. productName: row.productName,
  680. model: row.model,
  681. unit: row.unit,
  682. packageSpec: row.packageSpec,
  683. deliveryMethod: row.deliveryMethod,
  684. planQty: row.planQty ?? 0,
  685. remark: row.remark
  686. }
  687. }
  688. export function buildLoadPayload(load: PlanLoadForm) {
  689. return {
  690. customerId: load.customerId,
  691. customerName: load.customerName,
  692. address: load.address || undefined,
  693. lng: load.lng,
  694. lat: load.lat,
  695. loadTime: load.loadTime || undefined,
  696. contact: load.contact || undefined,
  697. notes: load.notes || undefined,
  698. generateDibang: load.generateDibang,
  699. dibangDirection: load.dibangDirection,
  700. contracts: load.contracts,
  701. products: load.products.map(buildStopProductPayload)
  702. }
  703. }
  704. export function buildDeliveriesPayload(deliveries: PlanDeliveryForm[]) {
  705. return deliveries.map((row, index) => ({
  706. seq: row.seq || index + 1,
  707. customerId: row.customerId,
  708. customerName: row.customerName,
  709. address: row.address || undefined,
  710. lng: row.lng,
  711. lat: row.lat,
  712. deliveryTime: row.deliveryTime || undefined,
  713. contact: row.contact || undefined,
  714. notes: row.notes || undefined,
  715. generateDibang: row.generateDibang,
  716. dibangDirection: row.dibangDirection,
  717. contracts: row.contracts,
  718. products: row.products.map(buildStopProductPayload)
  719. }))
  720. }