From a041650ba18f273650783e51b212b277ee1053e2 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 22 Oct 2026 18:47:54 +0800 Subject: [PATCH] fix(booking): restore arrival time and release validation --- .../ocean-bookings/PROTOTYPE_MATRIX.md | 6 ++- .../OceanBookingDetailView.release.test.ts | 39 ++++++++++++++++--- .../pages/OceanBookingDetailView.vue | 4 +- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/modules/ocean-bookings/PROTOTYPE_MATRIX.md b/src/modules/ocean-bookings/PROTOTYPE_MATRIX.md index ae02802e..fa2532b2 100644 --- a/src/modules/ocean-bookings/PROTOTYPE_MATRIX.md +++ b/src/modules/ocean-bookings/PROTOTYPE_MATRIX.md @@ -6,13 +6,15 @@ | 页面/路由 | 操作 | 参考承载与布局 | 当前实现 | 页面核对 | |---|---|---|---|---| -| `/ocean-bookings/new`、`/ocean-bookings/:bookingId` | 草稿新增、维护 | 独立页面;左主资料、右三方并排,下方放舱资料 | 按用户红框移除客户选择、港名、预计到港、箱种、容量、限重、备注及包装单位选择;仓库和渠道保留在默认收起的“作业配置”,放舱缺配置时自动展开;委实成对输入,新增保存留在详情 | TEST #700 已核对移除字段、默认值、折叠及展开、草稿保存;完整长字段展示待人工确认 | +| `/ocean-bookings/new`、`/ocean-bookings/:bookingId` | 草稿新增、维护 | 独立页面;左主资料、右三方并排,下方放舱资料 | 移除客户选择、港名、箱种、容量、限重、备注及包装单位选择;预计到港恢复在预计开船旁,并在放舱确认摘要展示;仓库和渠道保留在默认收起的“作业配置”,放舱缺配置时自动展开;委实成对输入,新增保存留在详情 | TEST #700 已核对此前精简;本次预计到港恢复待 TEST 页面核对,完整长字段展示待人工确认 | | `/ocean-bookings/:bookingId` | 已放舱补实 | 客服补录实际货量 | 运输及预报只读,实值可按编辑权限补录,原因必填并版本审计 | 已核对放舱、原因校验、补实保存和历史;最终 TEST #698 回验实值保存后立即回显,无需刷新 | | `/ocean-bookings/:bookingId` | 托运单、提单确认件 | 两个作业入口,A4单据预览 | 两张业务卡片底部右侧的蓝色主按钮,分别直达现有PDF预览;读已保存委/实数据,缺失显示“—” | 弹框开关/重开、PDF 生成与数据映射已核对;内嵌 PDF 空白,显示未通过 | 人工按 1920×1080、100% 缩放核对三方长名称/地址、两类货量、保存失败与版本冲突、冻结/无编辑权限,以及两类 PDF 预览的打开、关闭和重开。自动检查结果在交付中单独报告,不能代替人工截图。 -新增订舱默认运费条款 `FREIGHT PREPAID`、运输条款 `CY-CY`、委值 `500 / 15000 / 68`,实值留空;包装按 `CARTONS` 保存,新柜需求为整箱 FCL,容量和限重沿平台柜型规格计算。默认值只用于新增,不覆盖已保存资料。预计到港取消放舱必填,已有计划时间仍按原值保留。 +新增订舱默认运费条款 `FREIGHT PREPAID`、运输条款 `CY-CY`、委值 `500 / 15000 / 68`,实值留空;包装按 `CARTONS` 保存,新柜需求为整箱 FCL,容量和限重沿平台柜型规格计算。默认值只用于新增,不覆盖已保存资料。预计到港草稿可留空,放舱前必填且不得早于预计开船;放舱后仍遵守运输资料冻结规则。 + +用户后续确认预计到港供日历和入库操作使用,恢复该字段及前后端放舱门禁。代码核对:关联柜次时 `ContainerRepository.linkReleasedBooking` 传入订舱预计到港;日历在无实际到港时读柜级预计到港;海外 `ContainerWarehouseArrivalService` 在无人工或计划冻结日期时按到港当地日期加 2 天解析默认到仓,入库计划使用该解析结果,不能把订舱预计到港直接当成计划到仓日期。 该轮经 Admin #700、Firstleg #512 发布,运行容器 healthy;前端 28 项、后端订舱 26 项定向测试及前端类型检查通过。真实 TEST 新增页以默认值保存为 `OB202610220013`,服务端读回条款和委值一致,实值及预计到港未填写;本轮没有再次推进放舱,取消预计到港必填由前后端定向测试覆盖。页面证据 `ui-booking-simplified-new.png`、`ui-booking-simplified-saved.png` 和 `simplified-form-verification.json` 位于同一证据目录。 diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.release.test.ts b/src/modules/ocean-bookings/pages/OceanBookingDetailView.release.test.ts index 4b73ab74..1576bf70 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.release.test.ts +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.release.test.ts @@ -1,6 +1,6 @@ import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' -import ElementPlus from 'element-plus' +import ElementPlus, { ElDatePicker } from 'element-plus' import { ApiResponseError } from '@/shared/api/http' import { listPlatformPartners } from '@/modules/master-data' import { listPlatformWarehouses } from '@/modules/warehouses' @@ -133,7 +133,8 @@ describe('OceanBookingDetailView one-click release', () => { expect(wrapper.get('[aria-label="预报毛重"]').element).toHaveProperty('value', '15000.000') expect(wrapper.get('[aria-label="预报体积"]').element).toHaveProperty('value', '68.000') const labels = wrapper.findAll('.el-form-item__label').map(item => item.text()) - for (const removed of ['客户(选填)', '预计到港', '包装单位', '备注']) expect(labels).not.toContain(removed) + for (const removed of ['客户(选填)', '包装单位', '备注']) expect(labels).not.toContain(removed) + expect(labels).toContain('预计到港') expect(wrapper.find('[aria-label="起运港名称"]').exists()).toBe(false) expect(wrapper.find('[aria-label="目的港名称"]').exists()).toBe(false) expect(wrapper.findAll('.container-table th').map(item => item.text())).toEqual(['操作', '柜型', '数量']) @@ -228,15 +229,43 @@ describe('OceanBookingDetailView one-click release', () => { wrapper.unmount() }) - it('allows release without an estimated arrival while preserving other requirements', async () => { + it('blocks release without estimated arrival while still allowing a partial draft save', async () => { const saved = booking() saved.estimatedArrivalAt = null saved.oceanBooking.estimatedArrivalAt = null vi.mocked(getOceanBooking).mockResolvedValue(saved) - vi.mocked(releaseOceanBooking).mockResolvedValue({ ...saved, status: 'RELEASED', version: 4 }) + vi.mocked(updateOceanBookingDraft).mockResolvedValue({ ...saved, version: 4 }) const { vm, wrapper } = await mountView() await vm.submitRelease() - expect(releaseOceanBooking).toHaveBeenCalledWith('123', { expectedVersion: 3 }) + expect(vm.actionError?.message).toBe('放舱前请填写预计到港时间') + expect(releaseOceanBooking).not.toHaveBeenCalled() + await vm.save() + expect(updateOceanBookingDraft).toHaveBeenCalledWith('123', expect.objectContaining({ data: expect.objectContaining({ estimatedArrivalAt: null }) })) + wrapper.unmount() + }) + + it('loads and saves estimated arrival from the date input and includes it in release confirmation', async () => { + const arrival = '2026-09-25T10:30:00' + vi.mocked(updateOceanBookingDraft).mockResolvedValue({ ...booking('DRAFT', 4), estimatedArrivalAt: arrival }) + const { vm, wrapper } = await mountView() + const picker = wrapper.findAllComponents(ElDatePicker).find(item => item.props('placeholder') === '请选择预计到港时间')! + expect(picker.props('modelValue')).toBe('2026-09-20T08:00:00') + picker.vm.$emit('update:modelValue', arrival) + await flushPromises() + await vm.save() + expect(updateOceanBookingDraft).toHaveBeenCalledWith('123', expect.objectContaining({ data: expect.objectContaining({ estimatedArrivalAt: arrival }) })) + await wrapper.findAll('button').find(button => button.text() === '放舱')!.trigger('click') + await flushPromises() + expect(wrapper.get('.release-summary').text()).toContain(arrival) + wrapper.unmount() + }) + + it('blocks an arrival before the sailing date', async () => { + const { vm, wrapper } = await mountView() + vm.form.data.estimatedArrivalAt = '2026-08-31T08:00:00' + await vm.submitRelease() + expect(vm.actionError?.message).toBe('预计到港时间不能早于预计开船时间') + expect(releaseOceanBooking).not.toHaveBeenCalled() wrapper.unmount() }) diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue index 19216880..840f76e8 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue @@ -373,7 +373,7 @@ function releaseValidationError() { const requiredFields: Array<[unknown, string]> = [ [form.data.warehouseId, '装柜仓库'], [form.data.channelCode, '物流渠道'], [form.data.billOfLadingNo, '提单号'], [form.data.vesselName, '船名'], [form.data.voyageNo, '航次'], - [form.data.estimatedSailingAt, '预计开船时间'], + [form.data.estimatedSailingAt, '预计开船时间'], [form.data.estimatedArrivalAt, '预计到港时间'], [form.data.placeOfDelivery, '交货地点'], [form.data.shippingCompanyId, '船公司'], [form.data.bookingAgentId, '订舱代理'], ] @@ -488,6 +488,7 @@ onMounted(() => { void load(); void loadOptions() }) + @@ -622,6 +623,7 @@ onMounted(() => { void load(); void loadOptions() }) {{ form.data.vesselName || '—' }} {{ form.data.voyageNo || '—' }} {{ form.data.estimatedSailingAt || '—' }} + {{ form.data.estimatedArrivalAt || '—' }}