fix: align appointment viewing and fleet follow-up navigation
This commit is contained in:
@@ -5,13 +5,15 @@ import { getDispatch } from '../api/dispatches'
|
||||
import DispatchDetailView from './DispatchDetailView.vue'
|
||||
import { DataTableStub } from './phase5TestStubs'
|
||||
|
||||
const sourceQuery = vi.hoisted(() => ({} as Record<string, string>))
|
||||
const routerPush = vi.hoisted(() => vi.fn())
|
||||
vi.mock('vue-router', () => ({ useRoute: () => ({ params: { dispatchNo: 'PC/1' } }), useRouter: () => ({ push: routerPush }) }))
|
||||
vi.mock('vue-router', () => ({ useRoute: () => ({ params: { dispatchNo: 'PC/1' }, query: sourceQuery }), useRouter: () => ({ push: routerPush }) }))
|
||||
vi.mock('../api/dispatches', () => ({ getDispatch: vi.fn() }))
|
||||
|
||||
describe('DispatchDetailView', () => {
|
||||
beforeEach(() => {
|
||||
routerPush.mockReset()
|
||||
Object.keys(sourceQuery).forEach(key => delete sourceQuery[key])
|
||||
vi.mocked(getDispatch).mockReset().mockResolvedValue({
|
||||
id: '1', dispatchNo: 'PC/1', vehiclePlate: 'CA-100', driverName: '张师傅', status: 'PENDING_DEPARTURE',
|
||||
route: 'LAX → ONT8', items: [{ id: '11', planPalletId: '20', orderNo: 'LM-1', warehousePalletId: '21', fbaWarehouseCode: 'ONT8', grossWeightKg: 88.5, volumeCbm: 1.25 }],
|
||||
@@ -40,4 +42,13 @@ describe('DispatchDetailView', () => {
|
||||
expect(wrapper.findComponent(DataTableStub).props('data')).toEqual([expect.objectContaining({ weight: null })])
|
||||
wrapper.unmount()
|
||||
})
|
||||
it.each([false, true])('returns to the original inquiry only for a valid source: %s', async fromInquiry => {
|
||||
if (fromInquiry) Object.assign(sourceQuery, { fleetSource: 'inquiry', fleetOfferNo: 'FO-1', fleetBookingNo: 'FB-2' })
|
||||
const wrapper = mount(DispatchDetailView, { global: { plugins: [ElementPlus], stubs: { DataTable: DataTableStub } } })
|
||||
await flushPromises()
|
||||
wrapper.findComponent({ name: 'OverseasDetailShell' }).vm.$emit('back')
|
||||
expect(routerPush).toHaveBeenCalledWith(fromInquiry ? { name: 'overseas-master-data-capacity-offer-detail', params: { offerNo: 'FO-1' }, query: { inquiry: 'FB-2' } } : { name: 'overseas-dispatches' })
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { inquiryReturnTarget } from '@/modules/overseas-master-data'
|
||||
import { physicalWeightKg } from '../dispatchPhysicalWeight'
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
@@ -128,13 +129,13 @@ async function load() {
|
||||
catch (error) { loadError.value = getApiErrorDetails(error, '排车详情加载失败,请重试') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function back() { void router.push({ name: 'overseas-dispatches' }) }
|
||||
function back() { void router.push(inquiryReturnTarget(route.query) || { name: 'overseas-dispatches' }) }
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OverseasDetailShell class="admin-density" dense title="排车详情" reference-label="排车单号" :reference-value="detail?.dispatchNo || dispatchNo" back-label="返回排车列表" domain="排车详情" :pending="false" :status-text="detail ? dispatchStatus(detail.status) : '加载中'" @back="back">
|
||||
<OverseasDetailShell class="admin-density" dense title="排车详情" reference-label="排车单号" :reference-value="detail?.dispatchNo || dispatchNo" :back-label="inquiryReturnTarget(route.query) ? '返回原拼车登记' : '返回排车列表'" domain="排车详情" :pending="false" :status-text="detail ? dispatchStatus(detail.status) : '加载中'" @back="back">
|
||||
<LoadErrorState v-if="loadError" title="排车详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
<div v-else v-loading="loading" class="dispatch-detail" :aria-busy="loading">
|
||||
<div class="summary-grid">
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
|
||||
import InboundOrdersView from './InboundOrdersView.vue'
|
||||
import { getFbaAppointment, updateFbaAppointment } from '../api/inboundOrders'
|
||||
vi.mock('vue-router', () => ({ useRouter: () => ({push: vi.fn()}), onBeforeRouteLeave: vi.fn(), onBeforeRouteUpdate: vi.fn() }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({can: () => true}) }))
|
||||
const access=vi.hoisted(()=>({canWrite:true}))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({can: (permission:string) => !permission.endsWith(':fba-appointment') || access.canWrite}) }))
|
||||
vi.mock('../api/inboundOrders', () => ({
|
||||
listInboundOrders: vi.fn().mockResolvedValue({records:[{lineId:'1',planNo:'IP-1',customerId:'A',appointmentNo:'AP-1',fbaWarehouseCode:'ONT8'}],total:1}),
|
||||
getFbaAppointment: vi.fn(), updateFbaAppointment: vi.fn(), createFbaAppointment: vi.fn(), updateInboundPlan: vi.fn(),
|
||||
}))
|
||||
vi.mock('element-plus', () => ({ ElMessage: {warning:vi.fn(),success:vi.fn()} }))
|
||||
it('keeps the inbound appointment retry across close, reopen and route attempts', async () => {
|
||||
vi.mocked(getFbaAppointment).mockResolvedValue({id:'1',appointmentNo:'AP-1',version:2,originWarehouseId:'100',fbaWarehouseCode:'ONT8',appointmentStatus:'PENDING',selectedCount:1,items:[]})
|
||||
vi.mocked(updateFbaAppointment).mockRejectedValueOnce(new Error('connection lost')).mockResolvedValueOnce({} as never)
|
||||
const wrapper=mount(InboundOrdersView,{global:{directives:{loading:{}},stubs:{
|
||||
const render=()=>mount(InboundOrdersView,{global:{directives:{loading:{}},stubs:{
|
||||
AdminPage:{template:'<main><slot/></main>'},ListPageLayout:{template:'<div><slot/></div>'},
|
||||
DataTable:{props:['data'],template:'<div><slot v-for="row in data" name="actions" :row="row"/></div>'},
|
||||
AppDialog:{name:'AppDialog',props:['modelValue','beforeClose'],template:'<section v-if="modelValue"><slot/><button data-close @click="$emit(\'update:model-value\',false)">关闭</button><button data-save @click="$emit(\'confirm\')">保存</button></section>'},
|
||||
AppDialog:{name:'AppDialog',props:['modelValue','beforeClose','showConfirm'],template:'<section v-if="modelValue"><slot/><button data-close @click="$emit(\'update:model-value\',false)">关闭</button><button v-if="showConfirm !== false" data-save @click="$emit(\'confirm\')">保存</button></section>'},
|
||||
ElButton:{template:'<button><slot/></button>'}, FbaAppointmentFields:true,SmartDispatchDialog:true,QueryToolbar:true,OperationErrorToast:true,
|
||||
}}})
|
||||
beforeEach(()=>{vi.clearAllMocks();access.canWrite=true;vi.mocked(getFbaAppointment).mockResolvedValue({id:'1',appointmentNo:'AP-1',version:2,originWarehouseId:'100',fbaWarehouseCode:'ONT8',appointmentStatus:'PENDING',selectedCount:1,items:[]})})
|
||||
it('opens linked appointment details for list-only users and blocks saving',async()=>{
|
||||
access.canWrite=false
|
||||
const wrapper=render();await flushPromises()
|
||||
expect(wrapper.findAll('button').some(b=>b.text()==='预约')).toBe(false)
|
||||
await wrapper.findAll('button').find(b=>b.text()==='查看预约')!.trigger('click');await flushPromises()
|
||||
expect(getFbaAppointment).toHaveBeenCalledWith('AP-1')
|
||||
expect(wrapper.findComponent({name:'FbaAppointmentFields'}).props('disabled')).toBe(true)
|
||||
expect(wrapper.find('[data-save]').exists()).toBe(false)
|
||||
wrapper.findComponent({name:'AppDialog'}).vm.$emit('confirm');await flushPromises()
|
||||
expect(updateFbaAppointment).not.toHaveBeenCalled();wrapper.unmount()
|
||||
})
|
||||
it('keeps the inbound appointment retry across close, reopen and route attempts', async () => {
|
||||
vi.mocked(updateFbaAppointment).mockRejectedValueOnce(new Error('connection lost')).mockResolvedValueOnce({} as never)
|
||||
const wrapper=render()
|
||||
await flushPromises()
|
||||
const open=()=>wrapper.findAll('button').find(b=>b.text()==='预约')!.trigger('click')
|
||||
await open();await flushPromises();await wrapper.get('[data-save]').trigger('click');await flushPromises()
|
||||
|
||||
@@ -58,7 +58,9 @@ const appointmentVersion = ref(0)
|
||||
const appointmentForm = reactive(createAppointmentForm())
|
||||
const appointmentTimezone = ref<string | null>(null)
|
||||
const pendingAppointment = ref<{appointmentNo: string | null; version: number; originWarehouseId: string; fbaWarehouseCode: string; lineIds: string[]; details: ReturnType<typeof appointmentPayload>; idempotencyKey: string} | null>(null)
|
||||
const appointmentTitle = computed(() => appointmentMode.value === 'edit' ? '编辑预约' : '预约')
|
||||
const appointmentViewOnly = ref(false)
|
||||
const appointmentReadOnly = computed(() => appointmentViewOnly.value || !access.can(permission.inboundOrderAppointment))
|
||||
const appointmentTitle = computed(() => appointmentReadOnly.value ? '查看预约' : appointmentMode.value === 'edit' ? '编辑预约' : '预约')
|
||||
const planEditOpen = ref(false); const planEditLoading = ref(false); const planEditSaving = ref(false); const planEditTarget = ref<InboundOrderListItem | null>(null); const planEditForm = reactive({ warehouseId: '', warehouseCode: '', warehouseName: '', version: 0 }); const warehouses = ref<ReceivingOverseasWarehouseDto[]>([])
|
||||
|
||||
function selectableRow(source: object) {
|
||||
@@ -127,7 +129,8 @@ function selectionError() {
|
||||
}
|
||||
|
||||
function openAppointment() {
|
||||
if (!canLeaveAppointment()) return
|
||||
if (!access.can(permission.inboundOrderAppointment) || !canLeaveAppointment()) return
|
||||
appointmentViewOnly.value = false
|
||||
const error = selectionError()
|
||||
if (error) { ElMessage.warning(error); return }
|
||||
appointmentMode.value = 'create'; editingAppointmentNo.value = ''
|
||||
@@ -142,10 +145,11 @@ function openSmartDispatch() {
|
||||
smartDispatchOpen.value = true
|
||||
}
|
||||
|
||||
async function openAppointmentEdit(source: object) {
|
||||
async function openAppointmentEdit(source: object, readOnly = false) {
|
||||
if (!canLeaveAppointment()) return
|
||||
const row = source as InboundOrderListItem
|
||||
if (!row.appointmentNo || row.appointmentNo === '—') return
|
||||
appointmentViewOnly.value = readOnly
|
||||
appointmentMode.value = 'edit'; editingAppointmentNo.value = row.appointmentNo
|
||||
appointmentLoading.value = true; mutationError.value = null; appointmentOpen.value = true
|
||||
try {
|
||||
@@ -169,7 +173,7 @@ function beforeCloseAppointment(done: () => void) { if (canLeaveAppointment()) d
|
||||
onBeforeRouteLeave(canLeaveAppointment)
|
||||
onBeforeRouteUpdate(canLeaveAppointment)
|
||||
async function submitAppointment() {
|
||||
if (saving.value || appointmentLoading.value) return
|
||||
if (appointmentReadOnly.value || saving.value || appointmentLoading.value) return
|
||||
if (appointmentMode.value === 'create') { const error = selectionError(); if (error) { ElMessage.warning(error); return } }
|
||||
const validation = appointmentFormError(appointmentForm)
|
||||
if (!pendingAppointment.value && validation) { ElMessage.warning(validation); return }
|
||||
@@ -220,14 +224,14 @@ onMounted(() => { void load() })
|
||||
|
||||
|
||||
<template #toolbar-actions><el-button v-if="access.can(permission.inboundOrderAppointment)" plain :disabled="!selectedRows.length" @click="openAppointment"><CalendarCheck2 :size="16" />预约</el-button><el-button v-if="access.can(permission.inboundOrderSmartDispatch)" type="primary" @click="openSmartDispatch"><Truck :size="16" />智能排车</el-button></template>
|
||||
<template #actions="{ row }"><div class="row-actions density-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" size="small" @click="openDetail(row)">查看</el-button><el-button class="density-action" data-tone="neutral" v-if="access.can(permission.inboundOrderEdit) && ['待入库'].includes((row as InboundOrderListItem).status)" size="small" @click="openPlanEdit(row)">编辑</el-button><el-button class="density-action" data-tone="neutral" v-if="(row as InboundOrderListItem).appointmentNo !== '—' && access.can(permission.inboundOrderAppointment)" size="small" @click="openAppointmentEdit(row)">预约</el-button></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions density-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" size="small" @click="openDetail(row)">查看</el-button><el-button class="density-action" data-tone="neutral" v-if="access.can(permission.inboundOrderEdit) && ['待入库'].includes((row as InboundOrderListItem).status)" size="small" @click="openPlanEdit(row)">编辑</el-button><el-button class="density-action" data-tone="neutral" v-if="(row as InboundOrderListItem).appointmentNo !== '—' && access.can(permission.inboundOrderList)" size="small" @click="openAppointmentEdit(row, true)">查看预约</el-button><el-button class="density-action" data-tone="neutral" v-if="(row as InboundOrderListItem).appointmentNo !== '—' && access.can(permission.inboundOrderAppointment)" size="small" @click="openAppointmentEdit(row)">预约</el-button></div></template>
|
||||
<template #pagination-left>共 {{ total }} 条入库单 · 已选 {{ selectedRows.length }} 条</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
<AppDialog class="admin-density-dialog" :model-value="appointmentOpen" @update:model-value="closeAppointment" :before-close="beforeCloseAppointment" :title="appointmentTitle" width="min(1080px, calc(100vw - 48px))" :confirm-text="pendingAppointment ? '重试本次保存' : '保存预约安排'" :confirm-loading="saving" @confirm="submitAppointment">
|
||||
<AppDialog class="admin-density-dialog" :model-value="appointmentOpen" @update:model-value="closeAppointment" :before-close="beforeCloseAppointment" :title="appointmentTitle" :show-confirm="!appointmentReadOnly" width="min(1080px, calc(100vw - 48px))" :confirm-text="pendingAppointment ? '重试本次保存' : '保存预约安排'" :confirm-loading="saving" @confirm="submitAppointment">
|
||||
<div v-loading="appointmentLoading">
|
||||
<div class="appointment-summary">已选择 {{ appointmentSelectedCount }} 条,FBA仓库:{{ appointmentFbaWarehouse }}</div>
|
||||
<FbaAppointmentFields :model="appointmentForm" :appointment-no="editingAppointmentNo || undefined" :timezone="appointmentTimezone" :disabled="saving || !!pendingAppointment" />
|
||||
<FbaAppointmentFields :model="appointmentForm" :appointment-no="editingAppointmentNo || undefined" :timezone="appointmentTimezone" :disabled="appointmentReadOnly || appointmentLoading || saving || !!pendingAppointment" />
|
||||
|
||||
</div>
|
||||
</AppDialog>
|
||||
|
||||
@@ -369,7 +369,8 @@ describe('overseas warehouse first-batch prototype contract', () => {
|
||||
expect(detail).toContain('单据信息')
|
||||
expect(detail).toContain('关联订单明细')
|
||||
expect(detail).toContain(':data="relatedOrders"')
|
||||
expect(detail).toContain("router.push({ name: 'overseas-dispatches' })")
|
||||
expect(detail).toContain("router.push(inquiryReturnTarget(route.query) || { name: 'overseas-dispatches' })")
|
||||
expect(detail).toContain("inquiryReturnTarget(route.query) ? '返回原拼车登记' : '返回排车列表'")
|
||||
expect(detail).not.toContain('ElMessage.success')
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user