fix(dispatch): preview selected driver master license before saving

This commit is contained in:
2026-10-23 00:55:21 +08:00
parent 4a3456756f
commit cd86255b13
4 changed files with 49 additions and 5 deletions
@@ -3,11 +3,21 @@ import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, expect, it, vi } from 'vitest'
import DispatchResourceFields from './DispatchResourceFields.vue'
import { resources } from '../dispatchTestFixtures'
import { listDrivers, listVehicles, type VehicleDetail } from '@/modules/overseas-master-data'
import { listDrivers, listVehicles, type DriverDetail, type VehicleDetail } from '@/modules/overseas-master-data'
const can = vi.hoisted(() => vi.fn(() => true))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
vi.mock('@/modules/overseas-master-data', () => ({ listDrivers: vi.fn(), listVehicles: vi.fn(), overseasMasterDataPermissions: { vehicleList: 'vehicle:list', driverList: 'driver:list' } }))
beforeEach(() => { vi.clearAllMocks(); can.mockReturnValue(true); vi.mocked(listVehicles).mockResolvedValue({ records: [], page: 1, size: 100, total: 0, pages: 0 }) })
it('carries the selected master license for preview without carrying another driver’s trip photos', async () => {
const candidate: DriverDetail = { id: '22', driverNo: 'DR22', details: { name: '司机乙', phone: '555-0022', ownership: 'OWNED', region: 'OVERSEAS', licenseNumber: 'LICENSE22', licenseType: null, licenseIssuedOn: null, licenseExpiresOn: '2028-01-01', licenseZone: 'UTC', remark: null, profile: null }, active: true, reviewStatus: 'APPROVED', licenseFileId: '33', recordVersion: 1, updatedBy: 'operator', updatedAt: '2026-10-23T00:00:00Z', qualification: { canDispatch: true, expiringSoon: false, reason: 'ELIGIBLE', licenseExpiresOn: '2028-01-01' }, licenseState: 'VALID', warehouseCode: null, warehouseName: null }
vi.mocked(listDrivers).mockResolvedValue({ records: [candidate], page: 1, size: 100, total: 1, pages: 1, summary: { total: 1, owned: 1, dispatched: 0, expiringSoon: 0, expired: 0 } })
const value = resources()
const wrapper = mount(DispatchResourceFields, { props: { ...value, disabled: false }, global: { plugins: [ElementPlus] } })
const state = wrapper.vm as unknown as { searchDrivers(): Promise<void>; selectDriver(id: string): void }
await state.searchDrivers(); state.selectDriver('22'); await flushPromises()
expect(wrapper.emitted('update:driver')!.at(-1)![0]).toMatchObject({ driverId: '22', evidence: { fileIds: [], masterLicenseFileId: '33' } })
wrapper.unmount()
})
it.each([['本趟承运公司', '555-0123'], [null, null]])('shows continuous master snapshot fields read-only and uses a dash for missing facts (%s)', async (carrierName, phone) => {
const value = resources(); value.vehicle.mode = 'MASTER'; value.vehicle.vehicleId = 'vehicle-1'; value.vehicle.carrierName = carrierName; value.driver.mode = 'MASTER'; value.driver.driverId = 'driver-1'; value.driver.phone = phone
const wrapper = mount(DispatchResourceFields, { props: { vehicle: value.vehicle, driver: value.driver, disabled: false, continuous: true }, global: { plugins: [ElementPlus] } }); await flushPromises()
@@ -49,7 +49,7 @@ function selectVehicle(id: string | null) {
}
function selectDriver(id: string) {
const selected = drivers.value.find(row => row.id === id), end = driver.value.evidence?.expectedTaskEndAt || null
driver.value = { mode: 'MASTER', driverId: id, name: selected?.details.name || null, phone: selected?.details.phone || null, evidence: { licenseNo: null, licenseExpiresOn: null, fileIds: [], expectedTaskEndAt: end } }
driver.value = { mode: 'MASTER', driverId: id, name: selected?.details.name || null, phone: selected?.details.phone || null, evidence: { licenseNo: null, licenseExpiresOn: null, fileIds: [], expectedTaskEndAt: end, masterLicenseFileId: selected?.licenseFileId || null } }
}
</script>
<template>
@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import DispatchWorkflowDialog from './DispatchWorkflowDialog.vue'
import DispatchEvidenceFiles from './DispatchEvidenceFiles.vue'
import DispatchResourceFields from './DispatchResourceFields.vue'
import { downloadDriverLicense } from '@/modules/overseas-master-data'
import { blankVehicleMaintenance } from '../dispatchVehicleMaintenance'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
@@ -16,7 +17,7 @@ const identity = vi.hoisted(() => ({ id: '' }))
const can = vi.hoisted(() => vi.fn((_code: string) => true))
vi.mock('@/core/auth/auth', () => ({ useAuthStore: () => ({ user: identity }) }))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
vi.mock('@/modules/overseas-master-data', () => ({ listVehicles: vi.fn(), listDrivers: vi.fn(), overseasMasterDataPermissions: { vehicleList: 'vehicle:list', driverList: 'driver:list' } }))
vi.mock('@/modules/overseas-master-data', () => ({ listVehicles: vi.fn(), listDrivers: vi.fn(), downloadDriverLicense: vi.fn(), overseasMasterDataPermissions: { vehicleList: 'vehicle:list', driverList: 'driver:list' } }))
vi.mock('../api/dispatches', () => ({ getDispatch: vi.fn(), getDispatchItems: vi.fn(), getDispatchRoute: vi.fn(), reorderDispatchRoute: vi.fn(), removeDispatchItem: vi.fn(), getDispatchLoading: vi.fn(), getDispatchCommand: vi.fn(), listDispatches: vi.fn(), listDispatchCandidates: vi.fn(), completeDispatchLoading: vi.fn(), reviewDispatch: vi.fn(), reviewDispatchLoading: vi.fn(), returnDispatchToReview: vi.fn(), cancelDispatch: vi.fn(), updateDispatch: vi.fn(), addDispatchCargo: vi.fn(), submitDispatchReview: vi.fn(), getDispatchEvidence: vi.fn(), getDispatchEvidenceContent: vi.fn(), uploadDispatchEvidence: vi.fn() }))
enableAutoUnmount(afterEach)
@@ -53,6 +54,31 @@ async function render(action: DispatchAction, operation?: 'ROUTE', files = false
function field(wrapper: Pick<VueWrapper, 'findAllComponents'>, label: string) { return wrapper.findAllComponents({ name: 'ElFormItem' }).find(item => item.props('label') === label)! }
describe('dispatch workflow native layout', () => {
it('previews the selected master license without treating it as a trip upload and clears it on driver change', async () => {
vi.mocked(downloadDriverLicense).mockResolvedValue(new Blob(['png'], { type: 'image/png' }))
const { dialog } = await render('EDIT')
const fields = dialog.findComponent(DispatchResourceFields)
const selected = { ...resources().driver, mode: 'MASTER' as const, driverId: '22', evidence: { ...resources().driver.evidence!, fileIds: [], photoFileIds: [], masterLicenseFileId: '33' } }
fields.vm.$emit('update:driver', selected); await flushPromises()
const license = () => dialog.findAllComponents(DispatchEvidenceFiles).find(item => item.props('kind') === 'DRIVER')!
expect(license().find('img').attributes('alt')).toBe('驾驶证照片:驾驶证照片')
expect(downloadDriverLicense).toHaveBeenCalledExactlyOnceWith('22', '33')
expect(license().props('modelValue')).toEqual([])
expect(license().text()).not.toContain('移除引用')
expect(api.getDispatchEvidence).not.toHaveBeenCalledWith('PC/1', 'DRIVER', '33')
fields.vm.$emit('update:driver', { ...selected, driverId: '23', evidence: { ...selected.evidence, masterLicenseFileId: null } }); await flushPromises()
expect(license().find('img').exists()).toBe(false)
expect(license().text()).toContain('暂无照片')
})
it('reads an already frozen master license through the dispatch permission without a new driver directory request', async () => {
const saved = resources()
saved.driver = { ...saved.driver, mode: 'MASTER', driverId: '22', evidence: { ...saved.driver.evidence!, fileIds: ['33'], masterLicenseFileId: '33' } }
const { dialog } = await render('EDIT', undefined, false, { vehicleInfo: saved })
const license = dialog.findAllComponents(DispatchEvidenceFiles).find(item => item.props('kind') === 'DRIVER')!
expect(license.findAll('img')).toHaveLength(1)
expect(api.getDispatchEvidence).toHaveBeenCalledWith('PC/1', 'DRIVER', '33')
expect(downloadDriverLicense).not.toHaveBeenCalled()
})
it('brings ten orders at one destination into one appointment input with confirmed time and all shipments', async () => {
const initial = detail(), shipmentNumbers = Array.from({ length: 10 }, (_, index) => `SHIP-${index}`)
const appointment = { batchKey: null, batchNo: null, customerId: null, appointmentNo: 'ISA-ONE', destinationKey: 'FBA:ONT8', destinationWarehouseCode: 'ONT8', status: 'SUCCESS', confirmedAt: '2026-10-25T17:00:00Z', blockReason: null, shipmentNumbers }
@@ -172,7 +198,7 @@ describe('dispatch workflow native layout', () => {
expect(dialog.get('[aria-label="本车审核依据"] h3').text()).toBe('车辆与货物')
expect(dialog.get('[aria-label="本车审核依据"]').text()).toContain('联系电话')
expect(dialog.get('[aria-label="排车与预约"]').findAll('input')).toHaveLength(0)
const photos = dialog.get('[aria-label="审核照片"]').findAllComponents(DispatchEvidenceFiles)
const photos = dialog.findAllComponents(DispatchEvidenceFiles)
expect(photos.map(photo => photo.props('label'))).toEqual(['车辆图片', '司机照片', '驾驶证照片'])
expect(photos.every(photo => photo.props('readonly'))).toBe(true)
expect(dialog.get('[aria-label="操作原因与意见"] h3').text()).toBe('审核意见')
@@ -40,6 +40,14 @@ const blankVehicle = (): Vehicle => ({ mode: 'MASTER', vehicleId: null, plate: n
const blankDriver = (): Driver => ({ mode: 'MASTER', driverId: null, name: null, phone: null, evidence: { licenseNo: null, licenseExpiresOn: null, fileIds: [], expectedTaskEndAt: null } })
const vehicle = ref<Vehicle>(blankVehicle()), driver = ref<Driver>(blankDriver())
const driverPhotos = computed({ get: () => driver.value.evidence?.photoFileIds || [], set: (ids: string[]) => { if (driver.value.evidence) driver.value.evidence.photoFileIds = ids } })
// 新选司机的主档证照先按司机读取,已保存的冻结证照仍按排车读取。
const masterLicenseReference = computed(() => driver.value.mode === 'MASTER' ? driver.value.evidence?.masterLicenseFileId : null)
const unsavedMasterLicense = computed(() => {
const fileId = masterLicenseReference.value, driverId = driver.value.driverId
const saved = detail.value?.vehicleInfo.driver
if (!fileId || !driverId || saved?.driverId === driverId && saved.evidence?.masterLicenseFileId === fileId) return null
return { driverId, fileId }
})
const reviewEvidence = computed(() => dispatchReviewEvidence(detail.value))
const isReview = computed(() => ['DISPATCH_REVIEW', 'LOADING_REVIEW'].includes(currentAction.value))
const approvalBlock = computed(() => isReview.value && detail.value?.actions[currentAction.value]?.blockCode === 'APPROVAL_BLOCKED' ? detail.value.actions[currentAction.value]?.blockReason : null)
@@ -244,7 +252,7 @@ watch([open, () => props.dispatchNo, () => props.action, () => props.operation],
<div class="workflow-photo-grid density-dialog-sections" aria-label="车辆司机照片">
<DispatchEvidenceFiles photo-card v-model="vehicle.evidenceFileIds" :dispatch-no="currentNo" kind="VEHICLE" label="车辆图片" :disabled="frozen || loading || uploadingEvidence || !allowed" @busy="evidenceBusy.VEHICLE = $event" />
<DispatchEvidenceFiles photo-card v-model="driverPhotos" :dispatch-no="currentNo" kind="DRIVER_PHOTO" label="司机照片" :disabled="frozen || loading || uploadingEvidence || !allowed" @busy="evidenceBusy.DRIVER_PHOTO = $event" />
<DispatchEvidenceFiles photo-card v-if="driver.evidence" v-model="driver.evidence.fileIds" :dispatch-no="currentNo" kind="DRIVER" label="驾驶证照片" :disabled="frozen || loading || uploadingEvidence || !allowed" @busy="evidenceBusy.DRIVER = $event" />
<DispatchEvidenceFiles photo-card v-if="driver.evidence" v-model="driver.evidence.fileIds" :dispatch-no="currentNo" kind="DRIVER" label="驾驶证照片" :reference-ids="masterLicenseReference ? [masterLicenseReference] : []" :master-license="unsavedMasterLicense" :disabled="frozen || loading || uploadingEvidence || !allowed" @busy="evidenceBusy.DRIVER = $event" />
</div>
</template>
</DispatchVehicleMaintenanceForm>