fix: align appointment procurement and carpool workflows with agreed views

This commit is contained in:
2026-10-10 15:33:10 +08:00
parent 172cb05c78
commit 0cb8a5c2b2
18 changed files with 407 additions and 58 deletions
@@ -21,7 +21,7 @@ const pageOf = <T,>(records: T[], total = records.length) => ({ records, total,
const wrappers: VueWrapper[] = []
const layout = { template: '<div><slot name="header"/><slot/></div>' }
const field = { props: ['modelValue'], emits: ['update:modelValue', 'focus'], template: '<input />' }
function mount(component: object, props: Record<string, unknown> = {}) {
function mount(component: object, props: Record<string, unknown> = {}): { wrapper: VueWrapper; pinia: ReturnType<typeof createPinia> } {
const pinia = createPinia()
const wrapper = shallowMount(component, { props, global: { plugins: [pinia], directives: { loading: () => undefined }, stubs: {
AdminPage: layout, ListPageLayout: layout, DetailPageLayout: layout,
@@ -69,7 +69,7 @@ it('reads detail once, permits drafting while capacity is unknown, and never pre
it('preserves nullable estimates and retries the original immutable create request after an uncertain result', async () => {
vi.mocked(api.saveCapacityInquiry).mockRejectedValueOnce(new Error('network')).mockResolvedValueOnce({ offer, inquiry, replayed: true })
const { wrapper, pinia } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry: null, readonly: false }); await flushPromises()
const selects = wrapper.findAllComponents({ name: 'ElSelect' }); selects[0]!.vm.$emit('update:modelValue', inquiry.customerId); selects[2]!.vm.$emit('update:modelValue', inquiry.cargo.destinationKey); await flushPromises()
const selects = wrapper.findAllComponents({ name: 'ElSelect' }); selects[0]!.vm.$emit('update:modelValue', inquiry.customerId); selects[1]!.vm.$emit('update:modelValue', inquiry.cargo.destinationKey); await flushPromises()
const dialog = wrapper.findComponent({ name: 'AppDialog' }); dialog.vm.$emit('confirm'); await flushPromises()
const first = vi.mocked(api.saveCapacityInquiry).mock.calls[0]![2]
expect(first).toMatchObject({ expectedPalletCount: null, cargo: { packageCount: null, palletSpaces: null }, idempotencyKey: expect.any(String) })
@@ -136,10 +136,14 @@ it('keeps the create button disabled with a reason after cutoff, while hiding it
const readonly = mount(CapacityOfferDetailView); await flushPromises()
expect(readonly.wrapper.findAll('button').some(node => node.text() === '登记客户货物')).toBe(false)
})
it('defaults administrators to available offers and requires an explicit history filter', async () => {
it('defaults minimal managers to their TODO inquiries without loading offers, and explicitly switches to available offers', async () => {
state.manage = true
const { wrapper } = mount(CapacityOffersView); await flushPromises()
expect(api.listCapacityOffers).toHaveBeenLastCalledWith(expect.objectContaining({ scope: 'AVAILABLE' }))
expect(api.listCapacityInquiries).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ scope: 'TODO' }))
expect(api.listCapacityOffers).not.toHaveBeenCalled()
expect(state.links.size).toBe(0)
await button(wrapper, '可拼车次').trigger('click'); await flushPromises()
expect(api.listCapacityOffers).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ scope: 'AVAILABLE' }))
const query = wrapper.findComponent({ name: 'QueryToolbar' })
query.vm.$emit('update:modelValue', { scope: 'ALL' }); await flushPromises(); query.vm.$emit('search'); await flushPromises()
expect(api.listCapacityOffers).toHaveBeenLastCalledWith(expect.objectContaining({ scope: 'ALL' }))
@@ -191,3 +195,31 @@ it('shows follow-up destinations only with the matching read permission', async
expect(second.wrapper.emitted('action')).toEqual([['trip', row]])
expect(api.commandCapacityInquiry).not.toHaveBeenCalled()
})
it('keeps initial inputs compact, reveals confirmation fields, and keeps unknown amounts null', async () => {
const { wrapper } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry: null, readonly: false })
await flushPromises()
const visibleInputs = wrapper.findAll('input').filter(input => !input.element.closest('details:not([open])'))
expect(visibleInputs).toHaveLength(2)
expect(wrapper.text()).toContain('业务类型:本土散板')
const cargo = wrapper.findAll('details').find(section => section.find('summary').text().startsWith('已知货量'))!
expect(cargo.attributes('open')).toBeUndefined()
await wrapper.setProps({ confirmMode: true }); await flushPromises()
expect(cargo.attributes('open')).toBeDefined()
expect(wrapper.findAll('details').find(section => section.find('summary').text().startsWith('确认承运前待补资料'))!.attributes('open')).toBeDefined()
})
it('highlights only the allowed current step and keeps other actions secondary', async () => {
state.manage = true
const { wrapper } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry, readonly: true })
await flushPromises()
const section = wrapper.find('[aria-label="继续处理本票货物"]')
expect(section.findAll('button').filter(item => !item.element.closest('details')).map(item => item.text())).toEqual(['补充资料'])
expect(section.find('details').text()).toContain('核对并确认')
await button(wrapper, '补充资料').trigger('click')
expect(wrapper.emitted('action')?.[0]).toEqual(['edit', inquiry])
await wrapper.setProps({ inquiry: { ...inquiry, progress: { ...inquiry.progress, primaryAction: 'INCOMING' }, actions: { ...inquiry.actions, canEdit: false, canConfirm: false } } })
expect(section.findAll('button').filter(item => !item.element.closest('details'))).toHaveLength(0)
state.links.add('platform:fleet:trip:booking-edit')
await wrapper.setProps({ inquiry: { ...inquiry, progress: { ...inquiry.progress, primaryAction: 'INCOMING' } } })
expect(section.findAll('button').filter(item => !item.element.closest('details')).map(item => item.text())).toEqual(['安排来货'])
})
@@ -17,7 +17,9 @@ import { listCapacityOffers, listCapacityInquiries, getCapacityOffer, getCapacit
import { offerStatusLabels, inquiryStatusLabels, offerTime, loadText, type CapacityOffer, type CapacityInquiry, type InquiryResult, type OfferQuery, type InquiryQuery } from '../capacityOfferTypes'
import { useCapacityInquiryCommands } from '../useCapacityInquiryCommands'
const router = useRouter(), access = useAccessPreviewStore()
const tab = ref<'offers' | 'inquiries'>('offers'), filters = ref<QueryModel>({}), applied = ref<QueryModel>({})
const manage = computed(() => access.can('platform:fleet:capacity-offer:manage'))
const tab = ref<'offers' | 'inquiries'>(manage.value ? 'inquiries' : 'offers')
const filters = ref<QueryModel>(manage.value ? { scope: 'TODO' } : {}), applied = ref<QueryModel>({ ...filters.value })
const page = ref(1), size = ref(20), total = ref(0), loading = ref(false), error = ref<ApiErrorDetails | null>(null)
const offers = ref<CapacityOffer[]>([]), inquiries = ref<CapacityInquiry[]>([])
const editorOpen = ref(false), editing = ref<CapacityInquiry | null>(null), selectedOffer = ref<CapacityOffer | null>(null), readonly = ref(true)
@@ -33,7 +35,6 @@ function openFulfillment(key: string, row: CapacityInquiry) {
return true
}
let sequence = 0, editorSequence = 0
const manage = computed(() => access.can('platform:fleet:capacity-offer:manage'))
const fields = computed<QueryField[]>(() => tab.value === 'offers' ? [
{ key: 'origin', label: '起运仓', type: 'input', placeholder: '起运仓名称', width: 170 }, { key: 'destination', label: '目的站', type: 'input', placeholder: '目的站名称', width: 170 },
{ key: 'departureFrom', label: '发车起日', type: 'date', placeholder: '发车起日', width: 170 }, { key: 'departureTo', label: '发车止日', type: 'date', placeholder: '发车止日', width: 170 },