fix(fleet): refresh negotiated price controls when reopening booking

This commit is contained in:
2026-10-03 20:37:03 +08:00
parent 60dd86ab88
commit 6df6f6dabc
2 changed files with 22 additions and 4 deletions
@@ -4,6 +4,7 @@ import TripBookingPanel from './TripBookingPanel.vue'
import * as api from '../api/tripBookings'
import type { FleetTrip } from '../tripTypes'
import type { TripBooking } from '../bookingTypes'
import type { Component } from 'vue'
const access = vi.hoisted(() => ({ writable: true, quote: true }))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (permission: string) => permission === 'platform:fleet:quote:list' ? access.quote : access.writable }) }))
vi.mock('element-plus', () => ({ ElMessage: { success: vi.fn(), warning: vi.fn() }, ElMessageBox: { confirm: vi.fn().mockResolvedValue(undefined) } }))
@@ -12,14 +13,14 @@ const row: TripBooking = { bookingNo: 'FB-1', tripNo: 'TR-1', customerId: '9', c
cargo: { destinationKey: 'FBA:ONT8', cargoName: null, packageCount: 10, palletSpaces: 2, weightKg: 100, volumeCbm: 1, priceBasis: 'PALLET', unitPrice: 50, currency: 'USD', contactName: null, contactPhone: null, remark: null } }
const trip = { tripNo: 'TR-1', status: 'PLANNED', recordVersion: 4, stops: [{ name: '起点' }, { name: 'ONT8', destinationKey: 'FBA:ONT8' }] } as FleetTrip
const wrappers: VueWrapper[] = []
function mount() {
function mount(realNumber?: Component, realInput?: Component) {
const field = { props: ['modelValue'], emits: ['update:modelValue'], template: '<input />' }
const wrapper = shallowMount(TripBookingPanel, { props: { trip }, global: { stubs: {
DataTable: { props: ['data'], template: '<div><slot name="toolbar-actions"/><div v-for="row in data" :key="row.bookingNo"><slot name="actions" :row="row"/></div></div>' },
AppDialog: { name: 'AppDialog', props: ['modelValue', 'showConfirm'], emits: ['confirm', 'update:modelValue'], template: '<div v-if="modelValue"><slot/></div>' },
AppDialog: { name: 'AppDialog', props: { modelValue: Boolean, showConfirm: Boolean, destroyOnClose: Boolean }, emits: ['confirm', 'update:modelValue'], template: realNumber ? '<div v-if="modelValue || !destroyOnClose" v-show="modelValue"><slot/></div>' : '<div v-if="modelValue"><slot/></div>' },
ElButton: { props: ['disabled'], template: '<button :disabled="disabled"><slot/></button>' },
ElForm: { template: '<div><slot/></div>' }, ElFormItem: { template: '<div><slot/></div>' },
ElSelect: { ...field, name: 'ElSelect' }, ElInputNumber: { ...field, name: 'ElInputNumber' }, ElInput: field, ElOption: true,
ElSelect: { ...field, name: 'ElSelect' }, ElInputNumber: realNumber || { ...field, name: 'ElInputNumber' }, ElInput: realInput || field, ElOption: true,
LoadErrorState: true, ApiErrorMeta: true,
} } }); wrappers.push(wrapper); return wrapper
}
@@ -33,6 +34,23 @@ it('confirms using both current versions and updates the parent version without
expect(wrapper.emitted('changed')?.[0]).toEqual([5]); expect(api.listTripBookings).toHaveBeenCalledOnce()
expect(wrapper.findAll('button').some(node => node.text() === '确认承运')).toBe(false)
})
it('reopens saved negotiated price in a fresh numeric control while retaining its separate quote source', async () => {
const { ElInputNumber, ElInput } = await vi.importActual<typeof import('element-plus')>('element-plus')
const initial = { ...row, cargo: { ...row.cargo, unitPrice: 16 }, quoteSource: { ...source, quotedUnitPrice: 16 } }
const saved = { ...initial, cargo: { ...initial.cargo, unitPrice: 9 }, agreedAmount: 18, recordVersion: 1 }
vi.mocked(api.listTripBookings).mockResolvedValue({ tripRecordVersion: 4, records: [initial] })
vi.mocked(api.updateTripBooking).mockResolvedValue({ tripRecordVersion: 5, booking: saved, replayed: false })
const wrapper = mount(ElInputNumber, ElInput); await flushPromises()
await button(wrapper, '编辑').trigger('click'); await flushPromises()
const price = wrapper.findAllComponents({ name: 'ElInputNumber' })[4]!
await price.find('input').setValue('9'); await flushPromises()
wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
expect(wrapper.findAllComponents({ name: 'ElInputNumber' })).toHaveLength(0)
await button(wrapper, '详情').trigger('click'); await flushPromises()
expect(wrapper.findAllComponents({ name: 'ElInputNumber' })[4]!.find<HTMLInputElement>('input').element.value).toBe('9.00')
expect(wrapper.text()).toContain('来源单价:16.00 USD')
expect(wrapper.text()).toContain('约定运费:18.00 USD')
})
it('keeps inquiry estimates and contacts optional and retries the same uncertain create command', async () => {
vi.mocked(api.createTripBooking).mockRejectedValueOnce(new Error('network')).mockResolvedValueOnce({ tripRecordVersion: 5, booking: row, replayed: true })
const wrapper = mount(); await flushPromises(); await button(wrapper, '登记外接询价').trigger('click'); await flushPromises()
@@ -151,7 +151,7 @@ onBeforeUnmount(() => { generation++; searchSequence++; quoteSequence++; clearTi
<template #documents="{ row }"><div>{{ row.incomingNo || '尚未登记来货' }}</div><div>{{ row.dispatchNo || '尚未关联排车' }}</div></template>
<template #amount="{ row }">{{ row.agreedAmount == null ? '待报价' : `${row.agreedAmount.toFixed(2)} ${row.cargo.currency || ''}` }}</template>
</DataTable>
<AppDialog v-model="open" class="admin-density-dialog" :title="readOnly ? '外接预订详情' : editing ? '编辑外接询价' : '登记外接询价'" :show-confirm="!readOnly" width="1080px" :confirm-text="pendingCreate ? '重试原登记' : '保存询价'" :confirm-loading="busy" @confirm="save">
<AppDialog v-model="open" class="admin-density-dialog" destroy-on-close :title="readOnly ? '外接预订详情' : editing ? '编辑外接询价' : '登记外接询价'" :show-confirm="!readOnly" width="1080px" :confirm-text="pendingCreate ? '重试原登记' : '保存询价'" :confirm-loading="busy" @confirm="save">
<el-form label-position="top" class="density-form booking-form" :disabled="readOnly || busy || !!pendingCreate">
<el-form-item label="委托客户" required><el-select v-model="customerId" filterable remote :remote-method="searchCustomers" :loading="customersLoading" :disabled="!!editing" placeholder="按客户名称、编号搜索" @focus="searchCustomers()"><el-option v-if="editing" :value="editing.customerId" :label="editing.customerName" /><el-option v-for="customer in customers" :key="customer.id" :value="customer.id" :label="`${customer.code} · ${customer.name}`" /></el-select></el-form-item>
<el-form-item label="本车卸货站" required><el-select v-model="form.destinationKey" placeholder="选择顺路卸货站"><el-option v-for="stop in trip.stops.slice(1).filter(row => row.destinationKey)" :key="stop.destinationKey!" :value="stop.destinationKey!" :label="stop.name" /></el-select></el-form-item>