From 84488adf12aa4742f49e8acd88701a88f56d6717 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 10 Oct 2026 08:42:03 +0800 Subject: [PATCH] feat(appointment): add shared container followup workspace --- .../AppointmentCalendarView.vue | 5 +- .../AppointmentContainerPanel.vue | 75 +++++++++++++++++++ .../AppointmentFollowupDialog.test.ts | 24 ++++-- .../AppointmentFollowupDialog.vue | 38 ++++++---- .../AppointmentTableRendering.test.ts | 22 +++++- .../appointment-calendar/PROTOTYPE_MATRIX.md | 1 + .../appointment-calendar/planningApi.ts | 30 ++++++++ .../overseas-master-data/PROTOTYPE_MATRIX.md | 1 + .../components/CapacityInquiryTable.vue | 4 +- 9 files changed, 173 insertions(+), 27 deletions(-) create mode 100644 src/modules/appointment-calendar/AppointmentContainerPanel.vue diff --git a/src/modules/appointment-calendar/AppointmentCalendarView.vue b/src/modules/appointment-calendar/AppointmentCalendarView.vue index 3928ab75..aedd3f55 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarView.vue +++ b/src/modules/appointment-calendar/AppointmentCalendarView.vue @@ -21,6 +21,7 @@ import { listPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@ import AppointmentCalendarPanel, { type CalendarEvent } from './AppointmentCalendarPanel.vue' import AppointmentInboundCodes from './AppointmentInboundCodes.vue' import AppointmentCargoPanel from './AppointmentCargoPanel.vue' +import AppointmentContainerPanel from './AppointmentContainerPanel.vue' import { calendarRange, dateKey } from './calendar' import { deleteAppointment, listArrivalCalendar, type ArrivalCalendarEntry } from './api' @@ -30,7 +31,7 @@ const linkedSource = ref(null) const linkError = ref(null) const linkLoading = ref(false) let linkRequest = 0 -const workView = ref<'calendar' | 'cargo'>('calendar') +const workView = ref<'calendar' | 'cargo' | 'containers'>('calendar') const appointments = ref([]) const arrivals = ref([]) const range = ref(calendarRange(dateKey(new Date()), 'month')) @@ -301,9 +302,11 @@ onBeforeUnmount(() => { ++calendarRequest; ++candidateRequest; ++detailRequest;
+
+
diff --git a/src/modules/appointment-calendar/AppointmentContainerPanel.vue b/src/modules/appointment-calendar/AppointmentContainerPanel.vue new file mode 100644 index 00000000..f2d37cea --- /dev/null +++ b/src/modules/appointment-calendar/AppointmentContainerPanel.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/modules/appointment-calendar/AppointmentFollowupDialog.test.ts b/src/modules/appointment-calendar/AppointmentFollowupDialog.test.ts index dda264b8..d2016ce1 100644 --- a/src/modules/appointment-calendar/AppointmentFollowupDialog.test.ts +++ b/src/modules/appointment-calendar/AppointmentFollowupDialog.test.ts @@ -1,14 +1,14 @@ import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import AppointmentFollowupDialog from './AppointmentFollowupDialog.vue' -import { getFollowups, saveFollowup, type PlanningSource } from './planningApi' +import { getFollowups, saveFollowup, getContainerFollowups, saveContainerFollowup, type PlanningSource, type ContainerFollowupContext } from './planningApi' -vi.mock('./planningApi', () => ({ getFollowups: vi.fn(), saveFollowup: vi.fn() })) +vi.mock('./planningApi', () => ({ getFollowups: vi.fn(), saveFollowup: vi.fn(), getContainerFollowups: vi.fn(), saveContainerFollowup: vi.fn() })) vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn(), success: vi.fn() } })) const history = { history: { records: [], page: 1, size: 10, total: 0, pages: 0 }, versions: { DRAYAGE_CONTACT: 0, TERMINAL_APPOINTMENT: 0, WAREHOUSE_RECEIVING: 0, FBA_RECEIVING: 0 } } const source: PlanningSource = { sourceKey: 'ORDER:1', customerId: 'A', canFollowup: true, blockReasons: [], orderId: '1', inboundCode: 'IN-1', bookingNo: null, containerNo: null, originWarehouseId: 'US-1', originWarehouseName: '美国仓', destinationKey: 'FBA:ONT8', destinationWarehouseCode: 'ONT8', packages: null, sourceVersion: null } -function render(canWrite = true) { - return mount(AppointmentFollowupDialog, { props: { source, coverage: { status: 'UNASSIGNED', appointments: [], missingFields: [], changeReviewRequired: false }, canWrite }, global: { stubs: { +function render(canWrite = true, container?: ContainerFollowupContext) { + return mount(AppointmentFollowupDialog, { props: container ? { container, canWrite } : { source, coverage: { status: 'UNASSIGNED', appointments: [], missingFields: [], changeReviewRequired: false }, canWrite }, global: { stubs: { AppDialog: { props: ['confirmDisabled'], template: '
' }, ElForm: { template: '
' }, ElFormItem: { template: '' }, ElInput: { props: ['modelValue', 'placeholder'], template: '' }, @@ -19,13 +19,25 @@ function render(canWrite = true) { describe('appointment followup recovery', () => { beforeEach(() => { vi.resetAllMocks(); vi.mocked(getFollowups).mockResolvedValue(history) - vi.mocked(saveFollowup).mockResolvedValue({ stage: 'DRAYAGE_CONTACT', version: 1, contactedParty: null, result: '已联系', nextAction: null, followupDate: null, operatorName: '运营', createdAt: '2026-10-09T12:00:00Z' }) + vi.mocked(getContainerFollowups).mockResolvedValue(history) + vi.mocked(saveFollowup).mockResolvedValue({ stage: 'FBA_RECEIVING', version: 1, contactedParty: null, result: '已联系', nextAction: null, followupDate: null, operatorName: '运营', createdAt: '2026-10-09T12:00:00Z' }) + }) + it.each([null, '100'])('saves the correct container stage for warehouse %s without customer-scoped calls', async warehouseId => { + const container = { containerBookingId: 'C1', containerNo: 'CONTAINER-A', warehouseId, warehouseName: warehouseId ? '洛杉矶仓' : null, canFollowup: true, blockReasons: [] } + const wrapper = render(true, container); await flushPromises() + await wrapper.get('input[placeholder="记录对方反馈,不会自动确认预约"]').setValue('同柜统一联系') + await wrapper.get('[data-save]').trigger('click'); await flushPromises() + expect(saveContainerFollowup).toHaveBeenCalledWith(container, expect.objectContaining({ stage: warehouseId ? 'WAREHOUSE_RECEIVING' : 'DRAYAGE_CONTACT', expectedVersion: 0, result: '同柜统一联系' })) + expect(getContainerFollowups).toHaveBeenCalledTimes(2) + expect(getFollowups).not.toHaveBeenCalled() + expect(saveFollowup).not.toHaveBeenCalled() + wrapper.unmount() }) it('saves contact facts without requiring appointment, inbound plan or driver fields', async () => { const wrapper = render(); await flushPromises() await wrapper.get('input[placeholder="记录对方反馈,不会自动确认预约"]').setValue('已联系,等待回复') await wrapper.get('[data-save]').trigger('click'); await flushPromises() - expect(saveFollowup).toHaveBeenCalledWith('ORDER:1', 'A', expect.objectContaining({ stage: 'DRAYAGE_CONTACT', expectedVersion: 0, result: '已联系,等待回复', followupDate: null })) + expect(saveFollowup).toHaveBeenCalledWith('ORDER:1', 'A', expect.objectContaining({ stage: 'FBA_RECEIVING', expectedVersion: 0, result: '已联系,等待回复', followupDate: null })) const body = vi.mocked(saveFollowup).mock.calls[0]![2] expect(body).not.toHaveProperty('appointmentNo'); expect(body).not.toHaveProperty('driverId'); expect(body).not.toHaveProperty('operatorId') expect(getFollowups).toHaveBeenCalledTimes(2) diff --git a/src/modules/appointment-calendar/AppointmentFollowupDialog.vue b/src/modules/appointment-calendar/AppointmentFollowupDialog.vue index e516c73c..b95d201f 100644 --- a/src/modules/appointment-calendar/AppointmentFollowupDialog.vue +++ b/src/modules/appointment-calendar/AppointmentFollowupDialog.vue @@ -1,14 +1,14 @@