import assert from 'node:assert/strict' import test from 'node:test' import type { Plan } from '../src/api/pl' import { calendarColumnCount, countPlanTrips, monthCells, shiftMonth, toCalendarTrips, filterTrips, filterVehicleScope, groupVehicles, summarizeTrips, loadMonthPlans } from '../src/views/pl/dispatch-calendar/model' const plan = (value: Partial): Plan => ({ id: 'p1', planDate: '2026-10-24', followStatus: 'pending', ...value }) as Plan const product = (id: string, name: string) => ({ productId: id, productName: name }) as NonNullable[number] test('每日默认仅自有车辆,全部包含承运商和未知车辆,不按车牌猜测', () => { const rows = toCalendarTrips( [ plan({ id: 'own', calendarVehicleType: 'self' }), plan({ id: 'carrier', calendarVehicleType: 'carrier', licensePlate: '皖HA3690' }), plan({ id: 'unknown', calendarVehicleType: 'unknown' }), plan({ id: 'snapshot', resourceSnapshot: { vehicleId: 'snapshot-vehicle', vehicleType: 'self' } as Plan['resourceSnapshot'] }) ], '2026-10' ) assert.deepEqual( filterVehicleScope(rows).map((row) => row.planId), ['own', 'snapshot'] ) assert.equal(filterVehicleScope(rows, 'all').length, 4) }) test('完整周历、跨年和闰月', () => { assert.equal(monthCells('2026-10').length, 35) assert.equal(monthCells('2026-10')[0], '2026-09-28') assert.equal(monthCells('2026-10').at(-1), '2026-11-01') assert.equal(monthCells('2026-11').length, 42) assert.ok(monthCells('2024-02').includes('2024-02-29')) assert.equal(shiftMonth('2026-12', 1), '2027-01') assert.equal(shiftMonth('2026-01', -1), '2025-12') }) test('按计划日期、真实车辆和多站路线转换,不混入草稿/作废/无日期', () => { const rows = toCalendarTrips( [ plan({ licensePlate: '皖HA3690', vehicleId: 'v1', driverId: 'd1', driverName: '测试司机', load: { customerName: '山峡库' }, deliveries: [ { seq: 2, customerName: '乙' }, { seq: 1, customerName: '甲' } ], products: [product('a', '98酸')] }), plan({ id: 'draft', followStatus: 'draft' }), plan({ id: 'cancel', followStatus: 'cancelled' }), plan({ id: 'null', planDate: null, createDate: '2026-10-24' }) ], '2026-10' ) assert.equal(rows.length, 2) assert.equal(countPlanTrips(rows), 1) assert.equal(rows[0].route, '山峡库 → 甲') assert.equal(rows[1].route, '山峡库 → 乙') assert.equal(rows[0].plate, '皖HA3690') assert.equal(filterTrips(rows, 'v1').length, 2) assert.equal(filterTrips(rows, 'other').length, 0) }) test('多品种同车不增加总车次,同货品重复行去重', () => { const rows = toCalendarTrips( [ plan({ products: [product('a', '98酸'), product('a', '98酸'), product('b', 'cp酸')] }), plan({ id: 'p2', products: [] }) ], '2026-10' ) assert.equal(rows.length, 2) assert.deepEqual( summarizeTrips(rows).map((x) => [x.name, x.count]), [ ['98酸', 1], ['cp酸', 1], ['货品未注明', 1] ] ) assert.equal(filterTrips(rows, '__missing__').length, 2) }) test('资源快照回退车辆,货品别名优先展示', () => { const rows = toCalendarTrips( [ plan({ products: [{ ...product('a', '硫酸'), aliasName: '98酸' }], resourceSnapshot: { vehicleId: 'v2', licensePlate: '皖H10291', selectedDriverId: 'd2', personnelList: [{ personnelId: 'd2', personnelName: '张师傅', role: 'driver' }] } as Plan['resourceSnapshot'] }) ], '2026-10' ) assert.equal(rows[0].plate, '皖H10291') assert.equal(rows[0].products[0].name, '98酸') assert.equal(filterTrips(rows, 'v2').length, 1) }) test('整月超过一页时读全,使用计划日期边界', async () => { const calls: any[] = [] const rows = await loadMonthPlans(async (query) => { calls.push(query) return { total: 501, list: Array.from({ length: query.page === 1 ? 500 : 1 }, (_, i) => plan({ id: String((query.page! - 1) * 500 + i) }) ) } }, '2024-02') assert.equal(rows?.length, 501) assert.equal(calls.length, 2) assert.equal(calls[0].planDateFrom, '2024-02-01') assert.equal(calls[0].planDateTo, '2024-02-29') assert.equal(calls[0].excludeFollowStatus, 'draft,cancelled') assert.equal(calls[0].includeCustomerShortNames, 1) }) test('翻月后停止旧请求;后续分页失败不返回部分月份', async () => { assert.equal( await loadMonthPlans( async () => ({ total: 0, list: [] }), '2026-10', () => false ), null ) await assert.rejects( loadMonthPlans(async (query) => { if (query.page === 2) throw new Error('分页失败') return { total: 501, list: Array.from({ length: 500 }, (_, i) => plan({ id: String(i) })) } }, '2026-10'), /分页失败/ ) }) test('同货品标准名和别名混用仍只计一次,优先显示别名', () => { const rows = toCalendarTrips( [ plan({ products: [ product('acid', '98%工业硫酸'), { ...product('acid', '98%工业硫酸'), aliasName: '98酸' } ] }) ], '2026-10' ) assert.equal(rows[0].products.length, 1) assert.equal(rows[0].products[0].name, '98酸') assert.equal(summarizeTrips(rows)[0].count, 1) }) test('侧栏派车日历目录紧跟库存管理,页面具有独立权限码', async () => { const { plRoutes } = await import('../src/router/modules/pl') const { toNonLeafSidebarTree } = await import('../src/utils/router/leaf-group') const transport = plRoutes.find((route) => route.path === '/pl')! const sidebar = toNonLeafSidebarTree([transport])[0].children! const stock = sidebar.findIndex((route) => route.name === 'PlStockModule') assert.equal(sidebar[stock + 1]?.name, 'PlCalendarModule') const group = transport.children!.find((route) => route.name === 'PlCalendarModule')! assert.equal(group.children![0].meta.menuCode, 'pl:dispatch-calendar:list') }) test('日历优先使用客户简称路线,没有时回退现有全称', () => { const rows = toCalendarTrips( [ plan({ id: 'short', calendarRoute: '山峡 → 永恒', supplierName: '山峡化工有限公司', customerName: '永恒有限公司' }), plan({ id: 'full', supplierName: '山峡化工有限公司', customerName: '永恒有限公司' }) ], '2026-10' ) assert.equal(rows.find((row) => row.id === 'short')?.route, '山峡 → 永恒') assert.equal(rows.find((row) => row.id === 'full')?.route, '山峡化工有限公司 → 永恒有限公司') }) test('卡片目标列数不超过三列,由容器宽度决定实际列数', () => { assert.deepEqual([0, 1, 2, 5, 6, 11].map(calendarColumnCount), [1, 1, 2, 2, 3, 3]) }) test('车辆卡片按车次数从多到少排序,同车内任务顺序保持不变', () => { const rows = toCalendarTrips( [ plan({ id: '1', planNo: '1', vehicleId: 'a', licensePlate: '皖A1' }), plan({ id: '2', planNo: '2', vehicleId: 'b', licensePlate: '皖A2' }), plan({ id: '3', planNo: '3', vehicleId: 'c', licensePlate: '皖A3' }), plan({ id: '4', planNo: '4', vehicleId: 'b', licensePlate: '皖A2' }), plan({ id: '5', planNo: '5', vehicleId: 'c', licensePlate: '皖A3' }), plan({ id: '6', planNo: '6', vehicleId: 'b', licensePlate: '皖A2' }) ], '2026-10' ) const groups = groupVehicles(rows) assert.deepEqual( groups.map((group) => group.key), ['b', 'c', 'a'] ) assert.deepEqual( groups[0].trips.map((trip) => trip.id), ['2', '4', '6'] ) }) test('同车同日同货品同路线合并车次,汇总及原计划保留', () => { const rows = toCalendarTrips( [ plan({ id: '1', planNo: 'PL1', vehicleId: 'v1', calendarRoute: '安徽金顺 → 山峡', products: [product('acid', '98% 工业硫酸')] }), plan({ id: '2', planNo: 'PL2', vehicleId: 'v1', calendarRoute: '安徽金顺 → 山峡', products: [product('acid', '98% 工业硫酸')] }), plan({ id: '3', planNo: 'PL3', vehicleId: 'v1', calendarRoute: '山峡 → 安徽金顺', products: [product('acid', '98% 工业硫酸')] }), plan({ id: '4', planNo: 'PL4', vehicleId: 'v1', calendarRoute: '安徽金顺 → 山峡', products: [product('acid', '50% 工业硫酸')] }), plan({ id: '5', planNo: 'PL5', vehicleId: 'v2', calendarRoute: '安徽金顺 → 山峡', products: [product('acid', '98% 工业硫酸')] }) ], '2026-10' ) const groups = groupVehicles(rows) assert.equal(groups[0].trips.length, 4) assert.equal(groups[0].tasks.length, 3) assert.deepEqual( groups[0].tasks.map((task) => task.count), [2, 1, 1] ) assert.deepEqual(groups[0].tasks[0].planNos, ['PL1', 'PL2']) assert.equal(groups[1].tasks[0].count, 1) assert.equal(rows.length, 5) assert.equal( summarizeTrips(rows).reduce((sum, item) => sum + item.count, 0), 5 ) }) test('多货品顺序不同仍合并,货品组合或日期不同不合并', () => { const rows = toCalendarTrips( [ plan({ id: '1', vehicleId: 'v1', products: [product('a', '98酸'), product('b', '液碱')] }), plan({ id: '2', vehicleId: 'v1', products: [product('b', '液碱'), product('a', '98酸')] }), plan({ id: '3', vehicleId: 'v1', products: [product('a', '98酸')] }), plan({ id: '4', vehicleId: 'v1', planDate: '2026-10-25', products: [product('a', '98酸'), product('b', '液碱')] }) ], '2026-10' ) assert.deepEqual( groupVehicles(rows)[0].tasks.map((task) => task.count), [2, 1, 1] ) }) test('路线连续重复客户只显示一次,保留跨站返回与原始送货段', () => { const source = [ plan({ id: 'api', calendarRoute: '安庆山峡 → 安庆奇创 → 安庆奇创' }), plan({ id: 'stops', load: { customerName: '安庆山峡' }, deliveries: [ { seq: 1, customerName: '安庆奇创' }, { seq: 2, customerName: '安庆奇创' } ] }), plan({ id: 'return', calendarRoute: '安庆山峡 → 安庆奇创 → 安庆山峡' }), plan({ id: 'alias', calendarRoute: '安庆山峡 → 山峡 → 安庆奇创' }) ] const rows = toCalendarTrips(source, '2026-10') assert.equal(rows.find((row) => row.id === 'api')?.route, '山峡 → 安庆奇创') assert.equal(rows.find((row) => row.planId === 'stops')?.route, '山峡 → 安庆奇创') assert.equal(rows.find((row) => row.id === 'return')?.route, '山峡 → 安庆奇创 → 山峡') assert.equal(rows.find((row) => row.id === 'alias')?.route, '山峡 → 安庆奇创') assert.equal(source[1].deliveries?.length, 2) assert.equal(rows.length, 5) assert.equal(countPlanTrips(rows), 4) }) test('一个计划三段送货先拆分,货品及简称按段对应,再合并相同段', () => { const a = { ...product('a', '工业硫酸'), id: 'pp-a', aliasName: '98酸' } const b = { ...product('b', '次氯酸钠'), id: 'pp-b' } const rows = toCalendarTrips( [ plan({ id: 'multi', vehicleId: 'v1', products: [a, b], load: { customerName: '安庆山峡化工有限公司', calendarCustomerName: '安庆山峡' }, deliveries: [ { id: 's3', seq: 3, customerName: '乙有限公司', calendarCustomerName: '乙', products: [{ ...b, planProductId: 'pp-b' }] }, { id: 's1', seq: 1, customerName: '甲有限公司', calendarCustomerName: '甲', products: [{ ...a, planProductId: 'pp-a' }] }, { id: 's2', seq: 2, customerName: '甲有限公司', calendarCustomerName: '甲', products: [{ ...a, planProductId: 'pp-a' }] } ] }) ], '2026-10' ) assert.equal(rows.length, 3) assert.equal(new Set(rows.map((row) => row.id)).size, 3) assert.deepEqual( rows.map((row) => row.route), ['山峡 → 甲', '山峡 → 甲', '山峡 → 乙'] ) assert.deepEqual( rows.map((row) => row.products.map((item) => item.name)), [['98酸'], ['98酸'], ['次氯酸钠']] ) const group = groupVehicles(rows)[0] assert.equal(group.tasks.length, 2) assert.equal(group.tasks[0].count, 1) assert.equal(group.tasks[0].segmentCount, 2) assert.equal(countPlanTrips(rows), 1) assert.deepEqual( summarizeTrips(rows).map((item) => [item.name, item.count]), [ ['98酸', 1], ['次氯酸钠', 1] ] ) }) test('多段缺失货品不套用整车货品,单段旧数据可回退;同名异址段不合并', () => { const rows = toCalendarTrips( [ plan({ id: 'multi', products: [product('a', '98酸')], deliveries: [ { id: 's1', seq: 1, customerId: 'c1', customerName: '甲', address: '一厂', products: [] }, { id: 's2', seq: 2, customerId: 'c1', customerName: '甲', address: '二厂', products: [] } ] }) ], '2026-10' ) assert.deepEqual( rows.map((row) => row.products[0].name), ['货品未注明', '货品未注明'] ) assert.equal(groupVehicles(rows)[0].tasks.length, 2) const single = toCalendarTrips( [plan({ products: [product('a', '98酸')], deliveries: [{ customerName: '甲' }] })], '2026-10' ) assert.equal(single[0].products[0].name, '98酸') }) test('装货段无客户ID时不同站点不能被共同供方覆盖合并', () => { const rows = toCalendarTrips( ['一号装货点', '二号装货点'].map((name, index) => plan({ id: String(index), supplierId: 'supplier', vehicleId: 'v1', load: { customerName: name }, deliveries: [ { customerId: 'dest', customerName: '收货点', products: [product('a', '98酸')] } ] }) ), '2026-10' ) assert.equal(groupVehicles(rows)[0].tasks.length, 2) }) test('同货品标准名和明确别名在送货段之间统一,规格不同仍保留', () => { const rows = toCalendarTrips( [ plan({ id: 'p', deliveries: [ { id: 's1', seq: 1, customerName: '甲', products: [product('a', '98%工业硫酸')] }, { id: 's2', seq: 2, customerName: '甲', products: [ { ...product('a', '98%工业硫酸'), mobileProductMeta: { aliasName: '98酸', brandMode: '', brandId: '', brandName: '' } } ] } ] }) ], '2026-10' ) assert.deepEqual( rows.map((row) => row.products[0].name), ['98酸', '98酸'] ) assert.equal(summarizeTrips(rows).length, 1) assert.equal(groupVehicles(rows)[0].tasks.length, 1) const distinct = toCalendarTrips( [ plan({ deliveries: [ { id: 's1', customerName: '甲', products: [{ ...product('a', '硫酸'), model: '50%' }] }, { id: 's2', customerName: '甲', products: [{ ...product('a', '硫酸'), model: '98%' }] } ] }) ], '2026-10' ) assert.equal(groupVehicles(distinct)[0].tasks.length, 2) })