pl-dispatch-calendar.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. import assert from 'node:assert/strict'
  2. import test from 'node:test'
  3. import type { Plan } from '../src/api/pl'
  4. import {
  5. calendarColumnCount,
  6. countPlanTrips,
  7. monthCells,
  8. shiftMonth,
  9. toCalendarTrips,
  10. filterTrips,
  11. filterVehicleScope,
  12. groupVehicles,
  13. summarizeTrips,
  14. loadMonthPlans
  15. } from '../src/views/pl/dispatch-calendar/model'
  16. const plan = (value: Partial<Plan>): Plan =>
  17. ({ id: 'p1', planDate: '2026-10-24', followStatus: 'pending', ...value }) as Plan
  18. const product = (id: string, name: string) =>
  19. ({ productId: id, productName: name }) as NonNullable<Plan['products']>[number]
  20. test('每日默认仅自有车辆,全部包含承运商和未知车辆,不按车牌猜测', () => {
  21. const rows = toCalendarTrips(
  22. [
  23. plan({ id: 'own', calendarVehicleType: 'self' }),
  24. plan({ id: 'carrier', calendarVehicleType: 'carrier', licensePlate: '皖HA3690' }),
  25. plan({ id: 'unknown', calendarVehicleType: 'unknown' }),
  26. plan({
  27. id: 'snapshot',
  28. resourceSnapshot: {
  29. vehicleId: 'snapshot-vehicle',
  30. vehicleType: 'self'
  31. } as Plan['resourceSnapshot']
  32. })
  33. ],
  34. '2026-10'
  35. )
  36. assert.deepEqual(
  37. filterVehicleScope(rows).map((row) => row.planId),
  38. ['own', 'snapshot']
  39. )
  40. assert.equal(filterVehicleScope(rows, 'all').length, 4)
  41. })
  42. test('完整周历、跨年和闰月', () => {
  43. assert.equal(monthCells('2026-10').length, 35)
  44. assert.equal(monthCells('2026-10')[0], '2026-09-28')
  45. assert.equal(monthCells('2026-10').at(-1), '2026-11-01')
  46. assert.equal(monthCells('2026-11').length, 42)
  47. assert.ok(monthCells('2024-02').includes('2024-02-29'))
  48. assert.equal(shiftMonth('2026-12', 1), '2027-01')
  49. assert.equal(shiftMonth('2026-01', -1), '2025-12')
  50. })
  51. test('按计划日期、真实车辆和多站路线转换,不混入草稿/作废/无日期', () => {
  52. const rows = toCalendarTrips(
  53. [
  54. plan({
  55. licensePlate: '皖HA3690',
  56. vehicleId: 'v1',
  57. driverId: 'd1',
  58. driverName: '测试司机',
  59. load: { customerName: '山峡库' },
  60. deliveries: [
  61. { seq: 2, customerName: '乙' },
  62. { seq: 1, customerName: '甲' }
  63. ],
  64. products: [product('a', '98酸')]
  65. }),
  66. plan({ id: 'draft', followStatus: 'draft' }),
  67. plan({ id: 'cancel', followStatus: 'cancelled' }),
  68. plan({ id: 'null', planDate: null, createDate: '2026-10-24' })
  69. ],
  70. '2026-10'
  71. )
  72. assert.equal(rows.length, 2)
  73. assert.equal(countPlanTrips(rows), 1)
  74. assert.equal(rows[0].route, '山峡库 → 甲')
  75. assert.equal(rows[1].route, '山峡库 → 乙')
  76. assert.equal(rows[0].plate, '皖HA3690')
  77. assert.equal(filterTrips(rows, 'v1').length, 2)
  78. assert.equal(filterTrips(rows, 'other').length, 0)
  79. })
  80. test('多品种同车不增加总车次,同货品重复行去重', () => {
  81. const rows = toCalendarTrips(
  82. [
  83. plan({ products: [product('a', '98酸'), product('a', '98酸'), product('b', 'cp酸')] }),
  84. plan({ id: 'p2', products: [] })
  85. ],
  86. '2026-10'
  87. )
  88. assert.equal(rows.length, 2)
  89. assert.deepEqual(
  90. summarizeTrips(rows).map((x) => [x.name, x.count]),
  91. [
  92. ['98酸', 1],
  93. ['cp酸', 1],
  94. ['货品未注明', 1]
  95. ]
  96. )
  97. assert.equal(filterTrips(rows, '__missing__').length, 2)
  98. })
  99. test('资源快照回退车辆,货品别名优先展示', () => {
  100. const rows = toCalendarTrips(
  101. [
  102. plan({
  103. products: [{ ...product('a', '硫酸'), aliasName: '98酸' }],
  104. resourceSnapshot: {
  105. vehicleId: 'v2',
  106. licensePlate: '皖H10291',
  107. selectedDriverId: 'd2',
  108. personnelList: [{ personnelId: 'd2', personnelName: '张师傅', role: 'driver' }]
  109. } as Plan['resourceSnapshot']
  110. })
  111. ],
  112. '2026-10'
  113. )
  114. assert.equal(rows[0].plate, '皖H10291')
  115. assert.equal(rows[0].products[0].name, '98酸')
  116. assert.equal(filterTrips(rows, 'v2').length, 1)
  117. })
  118. test('整月超过一页时读全,使用计划日期边界', async () => {
  119. const calls: any[] = []
  120. const rows = await loadMonthPlans(async (query) => {
  121. calls.push(query)
  122. return {
  123. total: 501,
  124. list: Array.from({ length: query.page === 1 ? 500 : 1 }, (_, i) =>
  125. plan({ id: String((query.page! - 1) * 500 + i) })
  126. )
  127. }
  128. }, '2024-02')
  129. assert.equal(rows?.length, 501)
  130. assert.equal(calls.length, 2)
  131. assert.equal(calls[0].planDateFrom, '2024-02-01')
  132. assert.equal(calls[0].planDateTo, '2024-02-29')
  133. assert.equal(calls[0].excludeFollowStatus, 'draft,cancelled')
  134. assert.equal(calls[0].includeCustomerShortNames, 1)
  135. })
  136. test('翻月后停止旧请求;后续分页失败不返回部分月份', async () => {
  137. assert.equal(
  138. await loadMonthPlans(
  139. async () => ({ total: 0, list: [] }),
  140. '2026-10',
  141. () => false
  142. ),
  143. null
  144. )
  145. await assert.rejects(
  146. loadMonthPlans(async (query) => {
  147. if (query.page === 2) throw new Error('分页失败')
  148. return { total: 501, list: Array.from({ length: 500 }, (_, i) => plan({ id: String(i) })) }
  149. }, '2026-10'),
  150. /分页失败/
  151. )
  152. })
  153. test('同货品标准名和别名混用仍只计一次,优先显示别名', () => {
  154. const rows = toCalendarTrips(
  155. [
  156. plan({
  157. products: [
  158. product('acid', '98%工业硫酸'),
  159. { ...product('acid', '98%工业硫酸'), aliasName: '98酸' }
  160. ]
  161. })
  162. ],
  163. '2026-10'
  164. )
  165. assert.equal(rows[0].products.length, 1)
  166. assert.equal(rows[0].products[0].name, '98酸')
  167. assert.equal(summarizeTrips(rows)[0].count, 1)
  168. })
  169. test('侧栏派车日历目录紧跟库存管理,页面具有独立权限码', async () => {
  170. const { plRoutes } = await import('../src/router/modules/pl')
  171. const { toNonLeafSidebarTree } = await import('../src/utils/router/leaf-group')
  172. const transport = plRoutes.find((route) => route.path === '/pl')!
  173. const sidebar = toNonLeafSidebarTree([transport])[0].children!
  174. const stock = sidebar.findIndex((route) => route.name === 'PlStockModule')
  175. assert.equal(sidebar[stock + 1]?.name, 'PlCalendarModule')
  176. const group = transport.children!.find((route) => route.name === 'PlCalendarModule')!
  177. assert.equal(group.children![0].meta.menuCode, 'pl:dispatch-calendar:list')
  178. })
  179. test('日历优先使用客户简称路线,没有时回退现有全称', () => {
  180. const rows = toCalendarTrips(
  181. [
  182. plan({
  183. id: 'short',
  184. calendarRoute: '山峡 → 永恒',
  185. supplierName: '山峡化工有限公司',
  186. customerName: '永恒有限公司'
  187. }),
  188. plan({ id: 'full', supplierName: '山峡化工有限公司', customerName: '永恒有限公司' })
  189. ],
  190. '2026-10'
  191. )
  192. assert.equal(rows.find((row) => row.id === 'short')?.route, '山峡 → 永恒')
  193. assert.equal(rows.find((row) => row.id === 'full')?.route, '山峡化工有限公司 → 永恒有限公司')
  194. })
  195. test('卡片目标列数不超过三列,由容器宽度决定实际列数', () => {
  196. assert.deepEqual([0, 1, 2, 5, 6, 11].map(calendarColumnCount), [1, 1, 2, 2, 3, 3])
  197. })
  198. test('车辆卡片按车次数从多到少排序,同车内任务顺序保持不变', () => {
  199. const rows = toCalendarTrips(
  200. [
  201. plan({ id: '1', planNo: '1', vehicleId: 'a', licensePlate: '皖A1' }),
  202. plan({ id: '2', planNo: '2', vehicleId: 'b', licensePlate: '皖A2' }),
  203. plan({ id: '3', planNo: '3', vehicleId: 'c', licensePlate: '皖A3' }),
  204. plan({ id: '4', planNo: '4', vehicleId: 'b', licensePlate: '皖A2' }),
  205. plan({ id: '5', planNo: '5', vehicleId: 'c', licensePlate: '皖A3' }),
  206. plan({ id: '6', planNo: '6', vehicleId: 'b', licensePlate: '皖A2' })
  207. ],
  208. '2026-10'
  209. )
  210. const groups = groupVehicles(rows)
  211. assert.deepEqual(
  212. groups.map((group) => group.key),
  213. ['b', 'c', 'a']
  214. )
  215. assert.deepEqual(
  216. groups[0].trips.map((trip) => trip.id),
  217. ['2', '4', '6']
  218. )
  219. })
  220. test('同车同日同货品同路线合并车次,汇总及原计划保留', () => {
  221. const rows = toCalendarTrips(
  222. [
  223. plan({
  224. id: '1',
  225. planNo: 'PL1',
  226. vehicleId: 'v1',
  227. calendarRoute: '安徽金顺 → 山峡',
  228. products: [product('acid', '98% 工业硫酸')]
  229. }),
  230. plan({
  231. id: '2',
  232. planNo: 'PL2',
  233. vehicleId: 'v1',
  234. calendarRoute: '安徽金顺 → 山峡',
  235. products: [product('acid', '98% 工业硫酸')]
  236. }),
  237. plan({
  238. id: '3',
  239. planNo: 'PL3',
  240. vehicleId: 'v1',
  241. calendarRoute: '山峡 → 安徽金顺',
  242. products: [product('acid', '98% 工业硫酸')]
  243. }),
  244. plan({
  245. id: '4',
  246. planNo: 'PL4',
  247. vehicleId: 'v1',
  248. calendarRoute: '安徽金顺 → 山峡',
  249. products: [product('acid', '50% 工业硫酸')]
  250. }),
  251. plan({
  252. id: '5',
  253. planNo: 'PL5',
  254. vehicleId: 'v2',
  255. calendarRoute: '安徽金顺 → 山峡',
  256. products: [product('acid', '98% 工业硫酸')]
  257. })
  258. ],
  259. '2026-10'
  260. )
  261. const groups = groupVehicles(rows)
  262. assert.equal(groups[0].trips.length, 4)
  263. assert.equal(groups[0].tasks.length, 3)
  264. assert.deepEqual(
  265. groups[0].tasks.map((task) => task.count),
  266. [2, 1, 1]
  267. )
  268. assert.deepEqual(groups[0].tasks[0].planNos, ['PL1', 'PL2'])
  269. assert.equal(groups[1].tasks[0].count, 1)
  270. assert.equal(rows.length, 5)
  271. assert.equal(
  272. summarizeTrips(rows).reduce((sum, item) => sum + item.count, 0),
  273. 5
  274. )
  275. })
  276. test('多货品顺序不同仍合并,货品组合或日期不同不合并', () => {
  277. const rows = toCalendarTrips(
  278. [
  279. plan({ id: '1', vehicleId: 'v1', products: [product('a', '98酸'), product('b', '液碱')] }),
  280. plan({ id: '2', vehicleId: 'v1', products: [product('b', '液碱'), product('a', '98酸')] }),
  281. plan({ id: '3', vehicleId: 'v1', products: [product('a', '98酸')] }),
  282. plan({
  283. id: '4',
  284. vehicleId: 'v1',
  285. planDate: '2026-10-25',
  286. products: [product('a', '98酸'), product('b', '液碱')]
  287. })
  288. ],
  289. '2026-10'
  290. )
  291. assert.deepEqual(
  292. groupVehicles(rows)[0].tasks.map((task) => task.count),
  293. [2, 1, 1]
  294. )
  295. })
  296. test('路线连续重复客户只显示一次,保留跨站返回与原始送货段', () => {
  297. const source = [
  298. plan({ id: 'api', calendarRoute: '安庆山峡 → 安庆奇创 → 安庆奇创' }),
  299. plan({
  300. id: 'stops',
  301. load: { customerName: '安庆山峡' },
  302. deliveries: [
  303. { seq: 1, customerName: '安庆奇创' },
  304. { seq: 2, customerName: '安庆奇创' }
  305. ]
  306. }),
  307. plan({ id: 'return', calendarRoute: '安庆山峡 → 安庆奇创 → 安庆山峡' }),
  308. plan({ id: 'alias', calendarRoute: '安庆山峡 → 山峡 → 安庆奇创' })
  309. ]
  310. const rows = toCalendarTrips(source, '2026-10')
  311. assert.equal(rows.find((row) => row.id === 'api')?.route, '山峡 → 安庆奇创')
  312. assert.equal(rows.find((row) => row.planId === 'stops')?.route, '山峡 → 安庆奇创')
  313. assert.equal(rows.find((row) => row.id === 'return')?.route, '山峡 → 安庆奇创 → 山峡')
  314. assert.equal(rows.find((row) => row.id === 'alias')?.route, '山峡 → 安庆奇创')
  315. assert.equal(source[1].deliveries?.length, 2)
  316. assert.equal(rows.length, 5)
  317. assert.equal(countPlanTrips(rows), 4)
  318. })
  319. test('一个计划三段送货先拆分,货品及简称按段对应,再合并相同段', () => {
  320. const a = { ...product('a', '工业硫酸'), id: 'pp-a', aliasName: '98酸' }
  321. const b = { ...product('b', '次氯酸钠'), id: 'pp-b' }
  322. const rows = toCalendarTrips(
  323. [
  324. plan({
  325. id: 'multi',
  326. vehicleId: 'v1',
  327. products: [a, b],
  328. load: { customerName: '安庆山峡化工有限公司', calendarCustomerName: '安庆山峡' },
  329. deliveries: [
  330. {
  331. id: 's3',
  332. seq: 3,
  333. customerName: '乙有限公司',
  334. calendarCustomerName: '乙',
  335. products: [{ ...b, planProductId: 'pp-b' }]
  336. },
  337. {
  338. id: 's1',
  339. seq: 1,
  340. customerName: '甲有限公司',
  341. calendarCustomerName: '甲',
  342. products: [{ ...a, planProductId: 'pp-a' }]
  343. },
  344. {
  345. id: 's2',
  346. seq: 2,
  347. customerName: '甲有限公司',
  348. calendarCustomerName: '甲',
  349. products: [{ ...a, planProductId: 'pp-a' }]
  350. }
  351. ]
  352. })
  353. ],
  354. '2026-10'
  355. )
  356. assert.equal(rows.length, 3)
  357. assert.equal(new Set(rows.map((row) => row.id)).size, 3)
  358. assert.deepEqual(
  359. rows.map((row) => row.route),
  360. ['山峡 → 甲', '山峡 → 甲', '山峡 → 乙']
  361. )
  362. assert.deepEqual(
  363. rows.map((row) => row.products.map((item) => item.name)),
  364. [['98酸'], ['98酸'], ['次氯酸钠']]
  365. )
  366. const group = groupVehicles(rows)[0]
  367. assert.equal(group.tasks.length, 2)
  368. assert.equal(group.tasks[0].count, 1)
  369. assert.equal(group.tasks[0].segmentCount, 2)
  370. assert.equal(countPlanTrips(rows), 1)
  371. assert.deepEqual(
  372. summarizeTrips(rows).map((item) => [item.name, item.count]),
  373. [
  374. ['98酸', 1],
  375. ['次氯酸钠', 1]
  376. ]
  377. )
  378. })
  379. test('多段缺失货品不套用整车货品,单段旧数据可回退;同名异址段不合并', () => {
  380. const rows = toCalendarTrips(
  381. [
  382. plan({
  383. id: 'multi',
  384. products: [product('a', '98酸')],
  385. deliveries: [
  386. { id: 's1', seq: 1, customerId: 'c1', customerName: '甲', address: '一厂', products: [] },
  387. { id: 's2', seq: 2, customerId: 'c1', customerName: '甲', address: '二厂', products: [] }
  388. ]
  389. })
  390. ],
  391. '2026-10'
  392. )
  393. assert.deepEqual(
  394. rows.map((row) => row.products[0].name),
  395. ['货品未注明', '货品未注明']
  396. )
  397. assert.equal(groupVehicles(rows)[0].tasks.length, 2)
  398. const single = toCalendarTrips(
  399. [plan({ products: [product('a', '98酸')], deliveries: [{ customerName: '甲' }] })],
  400. '2026-10'
  401. )
  402. assert.equal(single[0].products[0].name, '98酸')
  403. })
  404. test('装货段无客户ID时不同站点不能被共同供方覆盖合并', () => {
  405. const rows = toCalendarTrips(
  406. ['一号装货点', '二号装货点'].map((name, index) =>
  407. plan({
  408. id: String(index),
  409. supplierId: 'supplier',
  410. vehicleId: 'v1',
  411. load: { customerName: name },
  412. deliveries: [
  413. { customerId: 'dest', customerName: '收货点', products: [product('a', '98酸')] }
  414. ]
  415. })
  416. ),
  417. '2026-10'
  418. )
  419. assert.equal(groupVehicles(rows)[0].tasks.length, 2)
  420. })
  421. test('同货品标准名和明确别名在送货段之间统一,规格不同仍保留', () => {
  422. const rows = toCalendarTrips(
  423. [
  424. plan({
  425. id: 'p',
  426. deliveries: [
  427. { id: 's1', seq: 1, customerName: '甲', products: [product('a', '98%工业硫酸')] },
  428. {
  429. id: 's2',
  430. seq: 2,
  431. customerName: '甲',
  432. products: [
  433. {
  434. ...product('a', '98%工业硫酸'),
  435. mobileProductMeta: { aliasName: '98酸', brandMode: '', brandId: '', brandName: '' }
  436. }
  437. ]
  438. }
  439. ]
  440. })
  441. ],
  442. '2026-10'
  443. )
  444. assert.deepEqual(
  445. rows.map((row) => row.products[0].name),
  446. ['98酸', '98酸']
  447. )
  448. assert.equal(summarizeTrips(rows).length, 1)
  449. assert.equal(groupVehicles(rows)[0].tasks.length, 1)
  450. const distinct = toCalendarTrips(
  451. [
  452. plan({
  453. deliveries: [
  454. { id: 's1', customerName: '甲', products: [{ ...product('a', '硫酸'), model: '50%' }] },
  455. { id: 's2', customerName: '甲', products: [{ ...product('a', '硫酸'), model: '98%' }] }
  456. ]
  457. })
  458. ],
  459. '2026-10'
  460. )
  461. assert.equal(groupVehicles(distinct)[0].tasks.length, 2)
  462. })