diff --git a/src/modules/overseas-master-data/components/CapacityDialogClosing.test.ts b/src/modules/overseas-master-data/components/CapacityDialogClosing.test.ts index 07ad3b5b..be75ddee 100644 --- a/src/modules/overseas-master-data/components/CapacityDialogClosing.test.ts +++ b/src/modules/overseas-master-data/components/CapacityDialogClosing.test.ts @@ -11,19 +11,21 @@ import type { CapacityOffer } from '../capacityOfferTypes' import type { FleetTrip } from '../tripTypes' import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork' import { getCapacityOffer, listCapacityOffers } from '../api/capacityOffers' +import { getTrip, getTripCapacityPlan } from '../api/trips' import { isWorkspaceRoute, workspaceRouteId } from '@/core/workspace-tabs/routeWorkspace' vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) +vi.mock('../api/trips', () => ({ getTrip: vi.fn(), getTripCapacityPlan: vi.fn(), getTripLoading: vi.fn(), confirmTripCapacityPlan: vi.fn(), confirmTripLoading: vi.fn(), updateTripPlan: vi.fn() })) vi.mock('../api/capacityOffers', () => ({ listCapacityOffers: vi.fn(), getCapacityOffer: vi.fn(), getCapacityInquiry: vi.fn(), listCapacityInquiries: vi.fn(), listOfferCustomers: vi.fn(), listOfferQuotes: vi.fn(), saveCapacityInquiry: vi.fn(), saveCapacityOffer: vi.fn(), commandCapacityOffer: vi.fn(), commandCapacityInquiry: vi.fn() })) const offer: CapacityOffer = { offerNo: 'CO-CLOSE', tripNo: 'TR-CLOSE', status: 'OPEN', recordVersion: 1, tripRecordVersion: 2, warehouseId: '9', warehouseName: '测试仓', plannedDepartureAt: '2026-10-20T12:00:00Z', cutoffAt: '2026-10-20T10:00:00Z', contactName: null, contactPhone: null, remark: null, destinations: [{ destinationKey: 'FBA:ONT8', name: 'ONT8', deliveryIndex: 1, remaining: null }], segments: [], capacityKnown: false, canCreateInquiry: true, canConfirm: false, blockReason: '待核对', actions: { canEdit: true, canPublish: false, canPause: true, canResume: false, canClose: true } } const wrappers: VueWrapper[] = [] -beforeEach(() => { vi.clearAllMocks(); vi.mocked(listCapacityOffers).mockResolvedValue({ records: [offer], total: 1, page: 1, size: 1, pages: 1 }); vi.mocked(getCapacityOffer).mockResolvedValue({ offer, inquiries: { records: [], total: 0, page: 1, size: 20, pages: 0 } }) }) +beforeEach(() => { vi.clearAllMocks(); vi.mocked(getTrip).mockResolvedValue({ trip: { tripNo: offer.tripNo, recordVersion: 2, plannedDepartureAt: offer.plannedDepartureAt, stops: [{ name: '起运仓' }, { name: 'ONT8', destinationKey: 'FBA:ONT8' }] } } as Awaited>); vi.mocked(getTripCapacityPlan).mockResolvedValue({ tripNo: offer.tripNo, recordVersion: 2, cargo: [], manifestHash: 'hash', capacityPlan: null, planValid: false, blockReason: null, availableSegments: [] }); vi.mocked(listCapacityOffers).mockResolvedValue({ records: [offer], total: 1, page: 1, size: 1, pages: 1 }); vi.mocked(getCapacityOffer).mockResolvedValue({ offer, inquiries: { records: [], total: 0, page: 1, size: 20, pages: 0 } }) }) afterEach(async () => { ElMessageBox.close(); wrappers.splice(0).forEach(wrapper => wrapper.unmount()); await flushPromises(); document.body.innerHTML = ''; vi.restoreAllMocks() }) function mountDialog(kind: 'inquiry' | 'settings') { const pinia = createPinia() const wrapper = kind === 'inquiry' ? mount(CapacityInquiryDialog, { attachTo: document.body, props: { modelValue: true, offer, inquiry: null, readonly: false }, global: { plugins: [pinia, ElementPlus] } }) - : mount(CapacityOfferSettingsDialog, { attachTo: document.body, props: { modelValue: false, trip: { tripNo: offer.tripNo, recordVersion: 2 } as FleetTrip }, global: { plugins: [pinia, ElementPlus] } }) + : mount(CapacityOfferSettingsDialog, { attachTo: document.body, props: { modelValue: false, trip: { tripNo: offer.tripNo, recordVersion: 2 } as FleetTrip }, global: { plugins: [pinia, ElementPlus, createRouter({ history: createMemoryHistory(), routes: [{ path: '/', component: { template: '
' } }] })] } }) wrappers.push(wrapper) return { wrapper, pinia } } @@ -38,6 +40,7 @@ it.each(['inquiry', 'settings'] as const)('keeps the real %s dialog and edited i const { wrapper, pinia } = mountDialog(kind) if (kind === 'settings') await wrapper.setProps({ modelValue: true }) await flushPromises() + if (kind === 'settings') await wrapper.get('details summary').trigger('click') const input = wrapper.find(kind === 'inquiry' ? 'input[placeholder="选填"]' : 'input[placeholder="填写公开给销售的联系人(选填)"]') expect(input.exists()).toBe(true) await input.setValue('UI-未保存检查'); await flushPromises() diff --git a/src/modules/overseas-master-data/components/CapacityOfferSettingsDialog.test.ts b/src/modules/overseas-master-data/components/CapacityOfferSettingsDialog.test.ts new file mode 100644 index 00000000..a9518832 --- /dev/null +++ b/src/modules/overseas-master-data/components/CapacityOfferSettingsDialog.test.ts @@ -0,0 +1,73 @@ +import { shallowMount, flushPromises } from '@vue/test-utils' +import { defineComponent } from 'vue' +import { createPinia } from 'pinia' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import Dialog from './CapacityOfferSettingsDialog.vue' +import * as offers from '../api/capacityOffers' +import * as trips from '../api/trips' +import type { FleetTrip } from '../tripTypes' +const state = vi.hoisted(() => ({ plan: vi.fn(), push: vi.fn() })) +vi.mock('vue-router', () => ({ useRouter: () => ({ push: state.push }) })) +vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) +vi.mock('element-plus', () => ({ ElMessage: { success: vi.fn(), warning: vi.fn() }, ElMessageBox: { confirm: vi.fn() } })) +vi.mock('../api/capacityOffers', () => ({ listCapacityOffers: vi.fn(), saveCapacityOffer: vi.fn(), commandCapacityOffer: vi.fn() })) +vi.mock('../api/trips', () => ({ getTrip: vi.fn(), getTripLoading: vi.fn(), updateTripPlan: vi.fn(), getTripCapacityPlan: vi.fn(), confirmTripCapacityPlan: vi.fn(), confirmTripLoading: vi.fn() })) +const trip = { tripNo: 'TR-1', recordVersion: 5, plannedDepartureAt: '2099-01-01T12:00:00Z', stops: [{ name: '起运仓' }, { name: 'ACY2', destinationKey: 'FBA:ACY2' }] } as FleetTrip +const draft = { offerNo: 'FO-1', tripNo: 'TR-1', status: 'DRAFT', recordVersion: 1, tripRecordVersion: 6, actions: { canEdit: true } } +const wrappers: ReturnType[] = [] +function mount(realPlanner = false) { + const wrapper = shallowMount(Dialog, { props: { modelValue: true, trip }, global: { plugins: [createPinia()], stubs: { + AppDialog: { name: 'AppDialog', props: ['confirmText'], emits: ['confirm'], template: '
' }, + TripLoadingPanel: realPlanner ? false : defineComponent({ name: 'TripLoadingPanel', setup(_, { expose }) { expose({ save: state.plan }); return {} }, template: '
' }), + DataTable: { name: 'DataTable', props: ['data'], template: '
' }, + ElInputNumber: { name: 'ElInputNumber', props: ['modelValue'], emits: ['update:modelValue'], template: '' }, + ElForm: { template: '
' }, ElFormItem: { template: '
' }, + ElButton: { template: '' }, ElDatePicker: true, ElInput: true, LoadErrorState: true, ApiErrorMeta: true, + } } }); wrappers.push(wrapper); return wrapper +} +beforeEach(() => { + vi.clearAllMocks() + vi.mocked(trips.getTrip).mockResolvedValue({ trip } as Awaited>) + vi.mocked(offers.listCapacityOffers).mockResolvedValue({ records: [], total: 0, page: 1, size: 1, pages: 0 }) + state.plan.mockResolvedValue({ tripNo: 'TR-1', recordVersion: 6 }) + vi.mocked(offers.saveCapacityOffer).mockResolvedValue({ offer: draft, replayed: false } as Awaited>) + vi.mocked(offers.commandCapacityOffer).mockResolvedValue({ offer: { ...draft, status: 'OPEN', recordVersion: 2 }, replayed: false } as Awaited>) +}) +afterEach(() => wrappers.splice(0).forEach(wrapper => wrapper.unmount())) +it('confirms capacity, saves metadata and publishes from one click using returned versions', async () => { + const wrapper = mount(); await flushPromises() + expect(wrapper.findComponent({ name: 'ElDatePicker' }).attributes('modelvalue')).toBe(trip.plannedDepartureAt) + wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises() + expect(state.plan).toHaveBeenCalledOnce() + expect(offers.saveCapacityOffer).toHaveBeenCalledWith(null, expect.objectContaining({ tripRecordVersion: 6, cutoffAt: trip.plannedDepartureAt })) + expect(offers.commandCapacityOffer).toHaveBeenCalledWith('FO-1', 'publish', expect.objectContaining({ tripRecordVersion: 6, offerRecordVersion: 1 })) + expect(wrapper.text()).toContain('查看需求池发布') +}) +it('does not publish if capacity validation is incomplete', async () => { + state.plan.mockResolvedValue(undefined) + const wrapper = mount(); await flushPromises() + wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises() + expect(offers.saveCapacityOffer).not.toHaveBeenCalled(); expect(offers.commandCapacityOffer).not.toHaveBeenCalled() +}) +it('retries only the uncertain publish command with the original key', async () => { + vi.mocked(offers.commandCapacityOffer).mockRejectedValueOnce(new Error('network')) + const wrapper = mount(); await flushPromises() + wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises() + const first = vi.mocked(offers.commandCapacityOffer).mock.calls[0]! + expect(wrapper.findComponent({ name: 'AppDialog' }).props('confirmText')).toBe('重试原请求') + wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises() + expect(offers.commandCapacityOffer).toHaveBeenNthCalledWith(2, ...first) + expect(state.plan).toHaveBeenCalledOnce(); expect(offers.saveCapacityOffer).toHaveBeenCalledOnce() +}) + +it('submits measurements from the embedded panel before saving and publishing', async () => { + const basis = { tripNo: 'TR-1', recordVersion: 5, cargo: [{ dispatchNo: 'DP-1', customerId: '1', dispatchItemId: '2', warehousePalletId: '3', palletNo: 'PLT-1', packages: 1, destinationKey: 'FBA:ACY2', destinationLabel: 'ACY2' }], manifestHash: 'hash', capacityPlan: null, planValid: false, blockReason: null, availableSegments: [] } + vi.mocked(trips.getTripCapacityPlan).mockResolvedValue(basis) + vi.mocked(trips.confirmTripCapacityPlan).mockResolvedValue({ ...basis, recordVersion: 6, planValid: true }) + const wrapper = mount(true); await flushPromises() + const fields = wrapper.findAllComponents({ name: 'ElInputNumber' }) + fields[0]!.vm.$emit('update:modelValue', 100); fields[1]!.vm.$emit('update:modelValue', 2); await flushPromises() + wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises() + expect(trips.confirmTripCapacityPlan).toHaveBeenCalledWith('TR-1', expect.objectContaining({ recordVersion: 5, pallets: [{ warehousePalletId: '3', palletSpaces: 1, weightKg: 100, volumeCbm: 2, deliveryIndex: 1 }] })) + expect(offers.commandCapacityOffer).toHaveBeenCalledOnce() +}) diff --git a/src/modules/overseas-master-data/components/CapacityOfferSettingsDialog.vue b/src/modules/overseas-master-data/components/CapacityOfferSettingsDialog.vue index 7f748e32..855a8877 100644 --- a/src/modules/overseas-master-data/components/CapacityOfferSettingsDialog.vue +++ b/src/modules/overseas-master-data/components/CapacityOfferSettingsDialog.vue @@ -1,6 +1,9 @@ -