diff --git a/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts b/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts new file mode 100644 index 00000000..753fb4cd --- /dev/null +++ b/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts @@ -0,0 +1,135 @@ +import ElementPlus from 'element-plus' +import { DOMWrapper, enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import AppDialog from '@/shared/components/AppDialog.vue' +import LineClampText from '@/shared/components/LineClampText.vue' +import AppointmentCalendarView from './AppointmentCalendarView.vue' +import { getCalendarSources, getWarehouseArrivals, saveWarehouseArrival, type WarehouseArrival } from './scheduleApi' +import { listTripCalendar } from './tripCalendar' +import { createSourceAppointment, listPlanningCargo, previewPlanningBatch, resolvePlanningSource, type PlanningCargo, type PlanningBatchPreview } from './planningApi' +import { batchContext } from './batch.test-support' +import type { ScheduleEvent } from './schedule' +import { getWorkDetail, type WorkAction } from './workApi' +import { workDetail, workItem } from './work.test-support' + +const routing = vi.hoisted(() => ({ query: { date: '2026-10-19' } as Record, push: vi.fn(), replace: vi.fn(), denied: new Set() })) +vi.mock('vue-router', () => ({ useRoute: () => routing, useRouter: () => routing, onBeforeRouteLeave: vi.fn(), onBeforeRouteUpdate: vi.fn() })) +vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (permission: string) => !routing.denied.has(permission) }) })) +vi.mock('./scheduleApi', () => ({ getCalendarSources: vi.fn(), getWarehouseArrivals: vi.fn(), saveWarehouseArrival: vi.fn() })) +vi.mock('./tripCalendar', async original => ({ ...await original(), listTripCalendar: vi.fn() })) +vi.mock('./planningApi', async original => ({ ...await original(), listPlanningCargo: vi.fn(), previewPlanningBatch: vi.fn(), resolvePlanningSource: vi.fn(), createSourceAppointment: vi.fn() })) +vi.mock('./workApi', async original => ({ ...await original(), getWorkDetail: vi.fn() })) +enableAutoUnmount(afterEach) +afterEach(() => { vi.unstubAllGlobals() }) +const warehouse: WarehouseArrival = { sourceContainerBookingId: '9007199254740999', containerNo: 'TEST-CONT', warehouseName: '美国仓', warehouseCode: 'US-W', plannedAt: null, actualAt: null, version: 3, planCount: 0, canEdit: true } +const event: ScheduleEvent = { key: 'warehouse-1', type: 'warehouse', date: '2026-10-21', time: '', title: 'TEST-CONT 计划到仓', fields: [], warehouse, canEdit: true } +const cargo: PlanningCargo = { sourceKey: 'ORDER:9007199254740998', orderId: '9007199254740998', customerId: '9007199254740997', customerName: '预约长客户名称'.repeat(12), inboundCode: 'IN-TEST', containerBookingId: 'container-1', containerNo: 'TEST-CONT', originWarehouseId: 'origin-1', originWarehouseCode: 'US-W', originWarehouseName: '美国长仓库名称'.repeat(12), destinationWarehouseCode: 'ONT8', packages: 20, receivedPackages: 0, shippedPackages: 0, sourcePalletCount: null, volumeCbm: null, estimatedArrivalAt: null, estimatedWarehouseAt: null, orderVersion: 1, standardNode: 'DEPARTED', shippingVersion: null, shippingObservedAt: null, shippingProvider: null, hasInboundPlan: false, hasActiveAppointment: false, detained: false, mixedDestinations: false } +const source = { sourceKey: cargo.sourceKey, customerId: cargo.customerId, orderId: cargo.orderId, inboundCode: cargo.inboundCode, bookingNo: null, containerNo: cargo.containerNo, originWarehouseId: cargo.originWarehouseId, originWarehouseName: cargo.originWarehouseName, destinationKey: 'FBA:LAX9', destinationWarehouseCode: 'LAX9', packages: 20, sourceVersion: 'v1', canFollowup: true, blockReasons: [] } +const preview: PlanningBatchPreview = { group: { originWarehouseId: cargo.originWarehouseId, destinationKey: source.destinationKey, packages: 20, sourceCount: 1, customerCount: 1, readinessIncomplete: true, readyFrom: null, readyTo: null, unknownQuantitySources: 0 }, originWarehouseName: cargo.originWarehouseName, destinationWarehouseCode: 'LAX9', destinationTimezone: 'America/Los_Angeles', sources: [source], missingFields: [] } +beforeEach(() => { + vi.clearAllMocks(); routing.denied.clear(); routing.query = { date: '2026-10-19' } + vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }) + vi.mocked(getCalendarSources).mockResolvedValue({ containers: [], emptyReturns: [], emptyReturnsReadable: true }) + vi.mocked(getWarehouseArrivals).mockResolvedValue([]) + vi.mocked(listTripCalendar).mockResolvedValue({ records: [], unresolvedRecords: [] }) + vi.mocked(saveWarehouseArrival).mockResolvedValue({}) + vi.mocked(listPlanningCargo).mockResolvedValue({ records: [cargo], total: 1, page: 1, size: 20, pages: 1 }) + vi.mocked(resolvePlanningSource).mockResolvedValue({ source, coverage: { status: 'NONE', missingFields: [], changeReviewRequired: false, appointments: [] } }) + vi.mocked(previewPlanningBatch).mockResolvedValue(preview) + vi.mocked(createSourceAppointment).mockResolvedValue(batchContext().appointment) + vi.mocked(getWorkDetail).mockResolvedValue(workDetail()) +}) +async function open() { + const wrapper = mount(AppointmentCalendarView, { global: { plugins: [ElementPlus], stubs: { + AppointmentCalendarPanel: { name: 'AppointmentCalendarPanel', template: '
' }, + } } }) + await flushPromises() + return wrapper +} +async function create(wrapper: Awaited>) { + wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('create'); await vi.dynamicImportSettled(); await flushPromises() + const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '新增FBA送仓预约')! + const content = creationBody() + await content.get('input[type="checkbox"]').setValue(true) + await content.findAll('button').find(button => button.text().startsWith('下一步'))!.trigger('click'); await flushPromises() + return dialog +} +function creationBody() { return new DOMWrapper(Array.from(document.querySelectorAll('.el-dialog')).find(item => item.textContent?.includes('新增FBA送仓预约'))!) } +it('keeps the arrival context and typed local time across native fullscreen and submits the original version', async () => { + const wrapper = await open() + wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('edit', event); await flushPromises() + const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '调整计划到仓')! + expect(dialog.props('width')).toBe('520px') + expect(dialog.get('[aria-label="到仓业务信息"]').text()).toContain('TEST-CONT 计划到仓') + expect(dialog.text()).not.toContain(warehouse.sourceContainerBookingId) + await dialog.get('input[type="datetime-local"]').setValue('2026-10-22T09:30') + await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises() + expect(dialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true) + expect((dialog.get('input').element as HTMLInputElement).value).toBe('2026-10-22T09:30') + await dialog.get('.el-dialog__footer .el-button--primary').trigger('click'); await flushPromises() + expect(saveWarehouseArrival).toHaveBeenCalledExactlyOnceWith(warehouse.sourceContainerBookingId, { estimatedWarehouseArrivalAt: '2026-10-22T09:30:00', expectedVersion: 3 }, expect.any(String)) +}) +it('locks the real arrival input after an unknown outcome and reuses its immutable request', async () => { + vi.mocked(saveWarehouseArrival).mockRejectedValueOnce(new Error('connection lost')) + const wrapper = await open(); wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('edit', event); await flushPromises() + const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '调整计划到仓')! + await dialog.get('input').setValue('2026-10-22T09:30') + await dialog.get('.el-dialog__footer .el-button--primary').trigger('click'); await flushPromises() + expect(dialog.get('input').attributes('disabled')).toBeDefined() + expect(dialog.text()).toContain('上次提交结果尚未确认') + expect(dialog.get('.el-dialog__footer .el-button--primary').text()).toBe('重试确认保存结果') + await dialog.get('.el-dialog__footer .el-button--primary').trigger('click'); await flushPromises() + expect(vi.mocked(saveWarehouseArrival).mock.calls[1]).toEqual(vi.mocked(saveWarehouseArrival).mock.calls[0]) +}) +it('keeps complete long cargo names accessible in the selectable table', async () => { + const wrapper = await open() + wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('create'); await vi.dynamicImportSettled(); await flushPromises() + const names = wrapper.findAllComponents(LineClampText).map(item => item.props('content')) + expect(names).toContain(cargo.customerName); expect(names).toContain(cargo.originWarehouseName) + expect(wrapper.getComponent({ name: 'DataTable' }).props('maxHeight')).toBe('') + expect(wrapper.text()).not.toMatch(/900719925474099[7-9]/) + expect(resolvePlanningSource).not.toHaveBeenCalled() +}) +it('preserves real FBA fields and entered values when expanding the create dialog', async () => { + const wrapper = await open(); const dialog = await create(wrapper) + const fields = dialog.getComponent({ name: 'FbaAppointmentFields' }) + expect(fields.props('continuous')).toBe(true) + const date = fields.findAllComponents({ name: 'ElDatePicker' })[0]! + date.vm.$emit('update:modelValue', '2026-10-25'); await flushPromises() + await creationBody().get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises() + expect(dialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true) + expect(date.props('modelValue')).toBe('2026-10-25') + await creationBody().findAll('button').find(button => button.text() === '保存预约安排')!.trigger('click'); await flushPromises() + expect(createSourceAppointment).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: 'FBA:LAX9' }], details: expect.objectContaining({ plannedDate: '2026-10-25', plannedAt: null, appointmentStatus: 'PENDING' }), idempotencyKey: expect.any(String) })) + expect(creationBody().get('[aria-label="预约保存回执"]').findAll('input, textarea, select')).toHaveLength(0) + expect(creationBody().text()).toContain('AP-001') +}) +it('shows every saved server field as readonly text including confirmed offsets and long remarks', async () => { + const saved = { ...batchContext().appointment, plannedDate: '2026-10-26', plannedAt: '2026-10-26T08:00:00', plannedEndAt: '2026-10-26T18:00:00', timezone: 'America/Los_Angeles', appointmentStatus: 'SUCCESS', confirmationNo: 'CONFIRM-001', appointmentAt: '2026-11-01T01:30:00', appointmentEndAt: '2026-11-01T01:45:00', appointmentStartUtc: '2026-11-01T08:30:00Z', appointmentEndUtc: '2026-11-01T09:45:00Z', appointmentRemark: '完整预约备注'.repeat(30) } + vi.mocked(createSourceAppointment).mockResolvedValue(saved) + const wrapper = await open(); await create(wrapper) + await creationBody().findAll('button').find(button => button.text() === '保存预约安排')!.trigger('click'); await flushPromises() + const receipt = creationBody().get('[aria-label="预约保存回执"]') + for (const text of ['AP-001', saved.plannedDate, saved.plannedAt, saved.plannedEndAt, saved.timezone, '已取得对方正式确认', saved.confirmationNo, saved.appointmentAt, saved.appointmentEndAt, '-07:00', '-08:00', saved.appointmentRemark]) expect(receipt.text()).toContain(text) + expect(receipt.findAll('input, textarea, select')).toHaveLength(0) + expect(receipt.text()).not.toMatch(/item-1|customer-a|FBA_APPOINTMENT:1/) +}) +it('does not open or request creation data without its original write permission', async () => { + routing.denied.add('platform:overseas-warehouse:inbound-order:fba-appointment') + const wrapper = await open(); wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('create'); await flushPromises() + expect(listPlanningCargo).not.toHaveBeenCalled(); expect(createSourceAppointment).not.toHaveBeenCalled() + expect(wrapper.findAllComponents(AppDialog).some(dialog => dialog.props('title') === '新增FBA送仓预约')).toBe(false) +}) +it('uses the same real continuous form through the original work detail deep link', async () => { + routing.query.workItem = 'work-delivery-1' + const action: WorkAction = { code: 'SAVE_PLAN', label: '安排预约', targetWorkItemKey: null, target: { kind: 'FBA_APPOINTMENT', appointmentId: null, batchKey: null, sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: source.destinationKey }] } } + vi.mocked(getWorkDetail).mockResolvedValue(workDetail({ item: workItem({ allowedActions: [action], nextAction: action }) })) + const wrapper = await open(); await vi.dynamicImportSettled(); await flushPromises() + const content = new DOMWrapper(Array.from(document.querySelectorAll('.el-dialog')).find(item => item.textContent?.includes('原业务预约办理'))!) + await content.get('.primary-next-action').trigger('click'); await flushPromises() + expect(wrapper.getComponent({ name: 'FbaAppointmentFields' }).props('continuous')).toBe(true) + expect(previewPlanningBatch).toHaveBeenCalledExactlyOnceWith(action.target.kind === 'FBA_APPOINTMENT' ? action.target.sources : []) + await content.findAll('button').find(button => button.text() === '保存预约安排')!.trigger('click'); await flushPromises() + expect(createSourceAppointment).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: source.destinationKey }], details: expect.objectContaining({ plannedAt: null, appointmentStatus: 'PENDING' }) })) + expect(content.text()).toContain('本次保存已完成') +}) diff --git a/src/modules/appointment-calendar/AppointmentCalendarView.test.ts b/src/modules/appointment-calendar/AppointmentCalendarView.test.ts index ac11323b..98e7c540 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarView.test.ts +++ b/src/modules/appointment-calendar/AppointmentCalendarView.test.ts @@ -21,6 +21,9 @@ export function renderSchedule() { AppointmentCalendarPanel: { name: 'AppointmentCalendarPanel', props: ['events', 'loading', 'incomplete', 'initialDate', 'initialMode', 'canCreate'], template: '
' }, AppDialog: { name: 'AppDialog', props: ['modelValue', 'beforeClose', 'confirmDisabled', 'confirmLoading'], emits: ['confirm'], template: '
' }, WorkDetailPanel: true, SourceAppointmentPanel: true, ApiErrorMeta: true, + ElForm: { template: '
' }, + ElFormItem: { props: ['label'], template: '' }, + ElInput: { props: ['modelValue', 'disabled'], emits: ['update:modelValue'], template: '' }, AppointmentCreatePanel: { name: 'AppointmentCreatePanel', props: ['canWrite', 'canLeave'], emits: ['created'], template: '
' }, LoadErrorState: { props: ['title', 'message'], emits: ['retry'], template: '
{{ title }} {{ message }}
' }, } } }) diff --git a/src/modules/appointment-calendar/AppointmentCalendarView.vue b/src/modules/appointment-calendar/AppointmentCalendarView.vue index 5c58b0c9..044a7ede 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarView.vue +++ b/src/modules/appointment-calendar/AppointmentCalendarView.vue @@ -175,15 +175,21 @@ onBeforeUnmount(() => { ++generation; controller?.abort() }) - - + 返回原事项 @@ -194,7 +200,5 @@ onBeforeUnmount(() => { ++generation; controller?.abort() }) diff --git a/src/modules/appointment-calendar/AppointmentCreatePanel.test.ts b/src/modules/appointment-calendar/AppointmentCreatePanel.test.ts index 134fac61..302e91a2 100644 --- a/src/modules/appointment-calendar/AppointmentCreatePanel.test.ts +++ b/src/modules/appointment-calendar/AppointmentCreatePanel.test.ts @@ -89,7 +89,9 @@ it('submits the real form, shows the saved appointment, and prevents another sub expect(wrapper.emitted('created')).toEqual([[batchContext().appointment]]) expect(wrapper.get('[role="status"]').text()).toContain(batchContext().appointment.appointmentNo) expect(wrapper.findAll('button').some(button => button.text() === '保存预约安排')).toBe(false) - expect(wrapper.get('[data-date]').attributes('disabled')).toBeDefined() + expect(wrapper.find('[data-date]').exists()).toBe(false) + expect(wrapper.get('[aria-label="预约保存回执"]').text()).toContain('等待对方确认') + expect(wrapper.get('[aria-label="预约保存回执"]').findAll('input, textarea, select')).toHaveLength(0) wrapper.unmount() }) it('preserves the original request and blocks returning or closing after an unknown creation result', async () => { diff --git a/src/modules/appointment-calendar/AppointmentCreatePanel.vue b/src/modules/appointment-calendar/AppointmentCreatePanel.vue index dce81716..236414f9 100644 --- a/src/modules/appointment-calendar/AppointmentCreatePanel.vue +++ b/src/modules/appointment-calendar/AppointmentCreatePanel.vue @@ -1,11 +1,13 @@ diff --git a/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue b/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue index 9ad0cf9c..249802e7 100644 --- a/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue +++ b/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue @@ -44,18 +44,23 @@ async function save() { onMounted(load) diff --git a/src/modules/appointment-calendar/WorkDispatchForm.test.ts b/src/modules/appointment-calendar/WorkDispatchForm.test.ts index a36bb194..847c5f69 100644 --- a/src/modules/appointment-calendar/WorkDispatchForm.test.ts +++ b/src/modules/appointment-calendar/WorkDispatchForm.test.ts @@ -18,10 +18,10 @@ const clone = (value: T): T => JSON.parse(JSON.stringify(value)) as T const batch = 'FBA_APPOINTMENT:1' function receiving(key = batch, vehicles: ReceivingContext['transport']['vehicles'] = []): ReceivingContext { return { batch: batchContext('AP-1', key).appointment, plans: [], agreements: [], pendingChanges: [], transport: { version: 'transport', vehicles, blockReasons: [] }, executionScope: { version: 'execution', members: [] }, allowedActions: [], blockReasons: [] } } function row(id = '9007199254740993', customerId = 'customer-a', key = batch): BatchDispatchCandidate { return { planPalletId: id, palletNo: `PALLET-${id.slice(-2)}`, planId: `plan-${id}`, planNo: 'IP-001', customerId, sourceStage: 'RECEIVED', packageCount: 12, allocations: [{ planPalletId: id, batchKey: key, customerId, sourceKey: customerId === 'customer-a' ? 'ORDER:1' : 'BOOKING:2', destinationKey: 'FBA:ONT8', packageCount: 12, recordVersion: 4 }], blockReasons: [] } } -function candidate(value = row()): Candidate { return { selection: { sourceType: 'INBOUND_PLAN', planPalletId: value.planPalletId, returnReceiptId: null, relabelNo: null, warehousePalletId: `pallet-${value.planPalletId}`, sourceVersion: `source-${value.planPalletId}` }, sourceKey: `cargo-${value.planPalletId}`, sourceVersion: `source-${value.planPalletId}`, customerId: value.customerId, customerName: value.customerId === 'customer-a' ? '客户甲' : '客户乙', warehouseId: 'origin-1', warehouseCode: 'LAX', warehouseName: '真实起运仓', orderNo: 'LM-1', inboundCode: 'IN-A', planNo: value.planNo, palletNo: value.palletNo, containerNo: null, destination: { key: 'CODE:ONT8', type: 'FBA', warehouseId: null, warehouseCode: 'ONT8', warehouseName: 'ONT8', address: null, postalCode: null, contactName: null, contactPhone: null }, availablePackages: 12, suggestedWeightKg: null, suggestedVolumeCbm: null, measurementSource: null, appointmentBatches: [], blockReasons: [] } } +function candidate(value = row()): Candidate { const base = { selection: { sourceType: 'INBOUND_PLAN', planPalletId: value.planPalletId, returnReceiptId: null, relabelNo: null, warehousePalletId: `pallet-${value.planPalletId}`, sourceVersion: `source-${value.planPalletId}` }, sourceKey: `cargo-${value.planPalletId}`, sourceVersion: `source-${value.planPalletId}`, customerId: value.customerId, customerName: value.customerId === 'customer-a' ? '客户甲' : '客户乙', warehouseId: 'origin-1', warehouseCode: 'LAX', warehouseName: '真实起运仓', orderNo: 'LM-1', inboundCode: 'IN-A', planNo: value.planNo, palletNo: value.palletNo, containerNo: null, destination: { key: 'CODE:ONT8', type: 'FBA', warehouseId: null, warehouseCode: 'ONT8', warehouseName: 'ONT8', address: null, postalCode: null, contactName: null, contactPhone: null }, availablePackages: 12, suggestedWeightKg: null, suggestedVolumeCbm: null, measurementSource: null, appointmentBatches: [], blockReasons: [] }; return { ...base, selection: { ...base.selection, sourceType: 'INBOUND_PLAN' }, selectionGroup: { groupKey: base.sourceKey, groupVersion: 'group-1', sources: [{ selection: { ...base.selection, sourceType: 'INBOUND_PLAN' }, sourceKey: base.sourceKey, customerId: base.customerId, customerName: base.customerName, sourceBusinessType: null, sourceBusinessNo: null, packages: base.availablePackages }], pallets: [{ warehousePalletId: base.selection.warehousePalletId, palletNo: base.palletNo, sourceKeys: [base.sourceKey], packageCount: base.availablePackages }], complete: true, blockReasons: [] } } } function detail(no = 'DS/001', version = 1): DispatchDetail { const review = { status: 'NOT_SUBMITTED' as const, reason: null, reviewerId: null, reviewedAt: null, reviewRound: 0, basisHash: null, invalidatedReason: null } - return { dispatchNo: no, status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: version, reviewState: 'DRAFT', reviews: { dispatchReview: review, loadingReview: review }, counts: { itemCount: 1, palletCount: 1, packageCount: 12, pickedPackageCount: 0, shippedPackageCount: 0 }, pickingReconciled: false, actions: { EDIT: { allowed: true, blockCode: null, blockReason: null }, ADD_CARGO: { allowed: true, blockCode: null, blockReason: null } }, vehicleInfo: { vehicle: null, driver: null }, dispatchInfo: { route: null, plannedDepartureAt: null, departedAt: null, estimatedArrivalAt: null, fbaAppointments: [], vehicleAppointment: null, appointmentBatches: [{ batchKey: batch, batchNo: 'AP-1', recordVersion: 4, customerId: 'customer-a', sources: [{ sourceKey: 'ORDER:1', destinationKey: 'FBA:ONT8', packageCount: 12 }] }] }, documents: { dispatchNo: no, outbounds: [], deliveries: [], createdAt: '', updatedAt: '', remark: null }, orderDetails: [] } + return { dispatchNo: no, status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: version, reviewState: 'DRAFT', reviews: { dispatchReview: review, loadingReview: review }, counts: { itemCount: 1, palletCount: 1, packageCount: 12, pickedPackageCount: 0, shippedPackageCount: 0 }, pickingReconciled: false, actions: { EDIT: { allowed: true, blockCode: null, blockReason: null }, ADD_CARGO: { allowed: true, blockCode: null, blockReason: null } }, vehicleInfo: { vehicle: null, driver: null }, dispatchInfo: { route: null, plannedDepartureAt: null, departedAt: null, estimatedArrivalAt: null, fbaAppointments: [], appointmentBatches: [{ batchKey: batch, batchNo: 'AP-1', recordVersion: 4, customerId: 'customer-a', sources: [{ sourceKey: 'ORDER:1', destinationKey: 'FBA:ONT8', packageCount: 12 }] }] }, documents: { dispatchNo: no, outbounds: [], deliveries: [], createdAt: '', updatedAt: '', remark: null }, orderDetails: [] } } function proof(key: string): CommandResult { return { dispatchNo: 'DS/001', action: 'CREATE', idempotencyKey: key, recordVersion: 1, status: 'ACTIVE', stage: 'BEFORE_LOADING', occurredAt: '2026-10-19T00:00:00Z', effect: 'APPLIED', counts: detail().counts, result: { dispatchNo: 'DS/001', changedItemIds: [] }, replayed: false, alreadyArranged: false } } let guard: LeaveGuard, natives: Candidate[], draft: DispatchDraft diff --git a/src/modules/appointment-calendar/WorkDispatchForm.vue b/src/modules/appointment-calendar/WorkDispatchForm.vue index 8786a0a1..2f23dfb9 100644 --- a/src/modules/appointment-calendar/WorkDispatchForm.vue +++ b/src/modules/appointment-calendar/WorkDispatchForm.vue @@ -1,9 +1,10 @@