fix(appointments): unify calendar layout and cabinet numbers

This commit is contained in:
2026-10-12 11:45:00 +08:00
parent 20b4d1425b
commit 92bdd32f86
25 changed files with 178 additions and 65 deletions
@@ -40,7 +40,7 @@ function render(openCalendar = true) {
const unregister = register(() => !routing.containerPending)
onBeforeUnmount(unregister)
}, template: '<div>整柜跟进工作区</div>' },
AppointmentCalendarPanel: { name: 'AppointmentCalendarPanel', mounted() { this.$emit('layers', { arrivals: true, trips: true }) }, props: ['events', 'incomplete', 'notices'], template: '<div><button data-create @click="$emit(\'create\', \'2026-10-09\')">新增预约</button><slot name="notice-actions" /></div>' },
AppointmentCalendarPanel: { name: 'AppointmentCalendarPanel', mounted() { this.$emit('layers', { arrivals: true, trips: true }) }, props: ['events', 'incomplete', 'notices'], template: '<div><button data-create @click="$emit(\'create\', \'2026-10-09\')">新增预约</button><slot name="notice-actions" /><slot name="toolbar-actions" /></div>' },
AppDialog: { name: 'AppDialog', props: ['modelValue', 'showConfirm', 'beforeClose'], template: '<div 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></div>' },
ElForm: { template: '<form><slot /></form>' },
ElFormItem: { template: '<label><slot /></label>' },
@@ -70,6 +70,19 @@ describe('appointment calendar data integration', () => {
vi.mocked(createFbaAppointment).mockResolvedValue({ version: 0, id: '1', appointmentNo: 'APT-AB', plannedDate: '2026-10-09', appointmentAt: null, originWarehouseId: '100', appointmentStatus: 'PENDING', selectedCount: 2, fbaWarehouseCode: 'ONT8', items: [] })
})
it('displays the existing D cabinet number in both arrival and pickup events', async () => {
vi.mocked(listArrivalCalendar).mockResolvedValueOnce([{ containerBookingId: '1', cabinetNo: 'D0007', arrivalAt: '2026-10-09T08:00:00', arrivalType: 'ESTIMATED' }])
vi.mocked(listPickupCalendar).mockResolvedValueOnce({ records: [{ containerBookingId: '1', cabinetNo: 'D0007', containerNo: null, destinationPortName: null, destinationWarehouseCode: null, fleetSupplierName: null, pickupAppointmentAt: '2026-10-09T09:00:00', actualPickupAt: null }], truncated: false, unclassifiedCount: 0, scopeBasis: 'US_DESTINATION_WAREHOUSE', timeBasis: 'SOURCE_LOCAL_UNSPECIFIED' })
const wrapper = render(); await flushPromises()
const events = wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).props('events')
expect(events).toHaveLength(2)
for (const event of events) {
expect(event.fields).toContainEqual({ label: '柜次号', value: 'D0007' })
expect(event.title).toContain('D0007')
}
wrapper.unmount()
})
it('opens a dated calendar appointment read-only for list-only users', async () => {
routing.canWrite=false
const record={version:0,id:'1',appointmentNo:'AP-DATED',plannedDate:'2026-10-15',originWarehouseId:'100',appointmentStatus:'PENDING',selectedCount:1,fbaWarehouseCode:'ABE3',items:[]}
@@ -90,6 +103,7 @@ describe('appointment calendar data integration', () => {
vi.mocked(listUnscheduledAppointments).mockResolvedValue({records: [record], total: 1})
vi.mocked(getFbaAppointment).mockResolvedValue(record)
const wrapper=render(); await flushPromises()
await wrapper.get('[data-unscheduled]').trigger('click')
await wrapper.findAll('button').find(button=>button.text()==='查看 / 继续')!.trigger('click'); await flushPromises()
expect(wrapper.find('option[value="CANCELLED"]').exists()).toBe(false)
await wrapper.get('[data-cancel-editing]').trigger('click'); await flushPromises()
@@ -97,13 +111,13 @@ describe('appointment calendar data integration', () => {
expect(wrapper.find('[data-save]').exists()).toBe(false)
expect(updateFbaAppointment).not.toHaveBeenCalled(); wrapper.unmount()
})
it('loads cancellation records only after expansion and opens cancelled appointments read-only', async () => {
it('loads cancellation records only after opening the dialog and opens cancelled appointments read-only', async () => {
const record = { version: 1, id: '1', appointmentNo: 'AP-CANCEL', originWarehouseId: '100', appointmentStatus: 'CANCELLED', selectedCount: 1, fbaWarehouseCode: 'ABE3', items: [] }
vi.mocked(getFbaAppointment).mockResolvedValue(record)
const wrapper=render();await flushPromises()
expect(wrapper.findComponent({name:'AppointmentCancellationRecords'}).exists()).toBe(false)
const records=wrapper.findAll('details').find(d=>d.find('summary').text()==='取消记录')!
;(records.element as HTMLDetailsElement).open=true;await records.trigger('toggle')
const records=wrapper.get('[data-cancellations]')
await records.trigger('click')
await wrapper.get('[data-history-view]').trigger('click');await flushPromises()
expect(getFbaAppointment).toHaveBeenCalledWith('AP-CANCEL')
expect(wrapper.find('[data-save]').exists()).toBe(false)
@@ -119,8 +133,8 @@ describe('appointment calendar data integration', () => {
it('refreshes an expanded cancellation list after a successful cancellation', async () => {
vi.mocked(listFbaAppointmentCalendar).mockResolvedValue([{version:0,id:'1',appointmentNo:'AP-CANCEL',originWarehouseId:'100',appointmentStatus:'PENDING',selectedCount:1,fbaWarehouseCode:'ABE3',items:[],plannedDate:'2026-10-15'}])
const wrapper=render();await flushPromises()
const records=wrapper.findAll('details').find(d=>d.find('summary').text()==='取消记录')!
;(records.element as HTMLDetailsElement).open=true;await records.trigger('toggle')
const records=wrapper.get('[data-cancellations]')
await records.trigger('click')
const previous=wrapper.getComponent({name:'AppointmentCancellationRecords'}).vm
wrapper.getComponent({name:'AppointmentCalendarPanel'}).vm.$emit('delete',{key:'AP-CANCEL'});await flushPromises()
wrapper.getComponent({name:'AppointmentCancellationDialog'}).vm.$emit('cancelled');await flushPromises()
@@ -150,6 +164,7 @@ describe('appointment calendar data integration', () => {
vi.mocked(listUnscheduledAppointments).mockResolvedValue({ records: [record], total: 1 })
vi.mocked(getFbaAppointment).mockResolvedValue(record)
const wrapper = render(); await flushPromises()
await wrapper.get('[data-unscheduled]').trigger('click')
await wrapper.findAll('button').find(b => b.text() === '查看 / 继续')!.trigger('click'); await flushPromises()
expect(wrapper.find('[data-save]').exists()).toBe(false)
const dialog = wrapper.findAllComponents({ name: 'AppDialog' }).at(-1)!
@@ -179,7 +194,7 @@ describe('appointment calendar data integration', () => {
expect(wrapper.text()).not.toContain('缩小日期范围')
})
it('shows the original pickup time once without claiming a port time zone and retains partial-source warnings', async () => {
vi.mocked(listPickupCalendar).mockResolvedValueOnce({records: [{containerBookingId: '123',bookingNo: 'CB-TEST',containerNo: 'TEST-CONT',destinationPortName: 'Port',destinationWarehouseCode: 'US-W',fleetSupplierName: 'Fleet',pickupAppointmentAt: '2026-10-09T11:00:00',actualPickupAt: null}],truncated: false,unclassifiedCount: 1,scopeBasis: 'US_DESTINATION_WAREHOUSE',timeBasis: 'SOURCE_LOCAL_UNSPECIFIED'})
vi.mocked(listPickupCalendar).mockResolvedValueOnce({records: [{containerBookingId: '123',cabinetNo: 'D0001',containerNo: 'TEST-CONT',destinationPortName: 'Port',destinationWarehouseCode: 'US-W',fleetSupplierName: 'Fleet',pickupAppointmentAt: '2026-10-09T11:00:00',actualPickupAt: null}],truncated: false,unclassifiedCount: 1,scopeBasis: 'US_DESTINATION_WAREHOUSE',timeBasis: 'SOURCE_LOCAL_UNSPECIFIED'})
const wrapper=render(); await flushPromises()
const panel=wrapper.findComponent({name:'AppointmentCalendarPanel'})
expect(panel.props('events')).toEqual([expect.objectContaining({type:'pickup',date:'2026-10-09',time:'11:00',sourcePath:'/container-loading/overseas/123'})])
@@ -196,7 +211,7 @@ describe('appointment calendar data integration', () => {
})
it('keeps successful arrival data visible when the pickup source fails', async () => {
vi.mocked(listPickupCalendar).mockRejectedValueOnce(new Error('pickup unavailable'))
vi.mocked(listArrivalCalendar).mockResolvedValueOnce([{containerBookingId:'1',bookingNo:'CB-1',arrivalAt:'2026-10-09T09:00:00',arrivalType:'ESTIMATED'}])
vi.mocked(listArrivalCalendar).mockResolvedValueOnce([{containerBookingId:'1',cabinetNo:'D0001',arrivalAt:'2026-10-09T09:00:00',arrivalType:'ESTIMATED'}])
const wrapper=render(); await flushPromises()
const panel=wrapper.findComponent({name:'AppointmentCalendarPanel'})
expect(panel.props('events')).toEqual([expect.objectContaining({type:'arrival',key:'arrival-1'})])
@@ -248,7 +263,7 @@ describe('appointment calendar data integration', () => {
expect(listPlatformWarehouses).toHaveBeenCalledWith(expect.objectContaining({ warehouseType: 'FBA', status: 'ACTIVE', size: 50 }))
expect(wrapper.find('input[placeholder="例如 ONT8"]').exists()).toBe(false)
expect(wrapper.text()).not.toContain('查询可预约货物')
expect(wrapper.find('details').attributes('open')).toBeUndefined()
expect(wrapper.findComponent({name:'AppointmentCancellationRecords'}).exists()).toBe(false)
await wrapper.get('select[aria-label="预约仓库"]').setValue('ONT8')
await flushPromises()
expect(listInboundOrders).toHaveBeenLastCalledWith({ page: 1, size: 10, fbaWarehouseCode: 'ONT8' })
@@ -283,6 +298,8 @@ describe('appointment calendar data integration', () => {
const wrapper=render(); await flushPromises()
expect(wrapper.text()).toContain('未定时安排(1)')
expect(wrapper.findComponent({name:'AppointmentCalendarPanel'}).props('events')).toEqual([])
expect(wrapper.findComponent({name:'DataTable'}).exists()).toBe(false)
await wrapper.get('[data-unscheduled]').trigger('click')
expect(wrapper.findComponent({name:'DataTable'}).props('data')[0]).not.toHaveProperty('appointmentAt')
})
@@ -335,7 +352,7 @@ describe('appointment calendar data integration', () => {
it('keeps available arrivals and marks the result incomplete when appointments fail', async () => {
vi.mocked(listFbaAppointmentCalendar).mockRejectedValueOnce(new Error('预约暂不可用'))
vi.mocked(listArrivalCalendar).mockResolvedValueOnce([{ containerBookingId: '1', bookingNo: 'C-1', arrivalAt: '2026-10-09T09:00:00', arrivalType: 'ESTIMATED' }])
vi.mocked(listArrivalCalendar).mockResolvedValueOnce([{ containerBookingId: '1', cabinetNo: 'D0001', arrivalAt: '2026-10-09T09:00:00', arrivalType: 'ESTIMATED' }])
const wrapper = render()
await flushPromises()
const calendar = wrapper.findComponent({ name: 'AppointmentCalendarPanel' })