Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a041650ba1 | ||
|
|
22e545fef9 | ||
|
|
3ae9004b8b |
@@ -8,11 +8,11 @@ const event = (type: ScheduleEvent['type'], index = 0): ScheduleEvent => ({ key:
|
||||
const render = (events: ScheduleEvent[] = [], initialDate = '2026-10-19') => mount(AppointmentCalendarPanel, { props: { initialDate, events },
|
||||
global: { stubs: { ElButton: { template: '<button><slot /></button>' } } } })
|
||||
function reportedDispatch() {
|
||||
const row: TripCalendarEntry = { sourceType: 'DISPATCH', dispatchNo: 'PC-CALENDAR-001', containerNumbers: ['TSTU1234567', 'TSTU7654321'], status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'LAX', originWarehouseName: '洛杉矶仓', originCountryCode: 'US', originTimezone: 'America/Los_Angeles', originWarehouseStatus: 'ACTIVE', plannedDepartureAt: '2026-10-22T16:00:00Z', expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [{ name: '洛杉矶仓' }, { name: 'ONT8' }], incompleteReasons: [] }
|
||||
const row: TripCalendarEntry = { sourceType: 'DISPATCH', dispatchNo: 'PC-CALENDAR-001', containerNumbers: ['TSTU1234567', 'TSTU7654321'], cabinetNumbers: ['D1821', 'D1822'], status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'LAX', originWarehouseName: '洛杉矶仓', originCountryCode: 'US', originTimezone: 'America/Los_Angeles', originWarehouseStatus: 'ACTIVE', plannedDepartureAt: '2026-10-22T16:00:00Z', expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [{ name: '洛杉矶仓' }, { name: 'ONT8' }], incompleteReasons: [] }
|
||||
return projectTripCalendar({ records: [row, row], unresolvedRecords: [] }, { from: '2026-10-01', to: '2026-11-01' }).events
|
||||
}
|
||||
describe('four-type appointment calendar', () => {
|
||||
it.each(['TSTU1234567', 'tstu1234567', 'TSTU7654321', ' tstu7654321 '])('finds the one shared dispatch by associated container %s on its origin-local date, not the Beijing date', async keyword => {
|
||||
it.each(['TSTU1234567', 'tstu1234567', 'TSTU7654321', ' tstu7654321 ', 'D1821', ' d1821 ', 'D1822', '1821'])('finds the one shared dispatch by associated container %s on its origin-local date, not the Beijing date', async keyword => {
|
||||
const wrapper = render(reportedDispatch(), '2026-10-23')
|
||||
const search = wrapper.get('[aria-label="搜索当前日历"]')
|
||||
expect(search.attributes('placeholder')).toBe('柜号、排车单号、仓库')
|
||||
@@ -26,6 +26,7 @@ describe('four-type appointment calendar', () => {
|
||||
expect(wrapper.get('.detail-event').text()).toContain('PC-CALENDAR-001')
|
||||
expect(wrapper.get('.detail-event').text()).toContain('09:00')
|
||||
expect(wrapper.get('.detail-event').text()).toContain('TSTU1234567、TSTU7654321')
|
||||
expect(wrapper.get('.detail-event').text()).toContain('D1821、D1822')
|
||||
expect(wrapper.get('.detail-event').text()).toContain('2026-10-22 09:00')
|
||||
expect(wrapper.emitted('range')).toBeUndefined()
|
||||
wrapper.unmount()
|
||||
|
||||
@@ -17,7 +17,7 @@ vi.mock('element-plus', () => ({ ElMessage: { success: vi.fn() }, ElMessageBox:
|
||||
const warehouse: WarehouseArrival = { sourceContainerBookingId: '1', containerNo: 'TEST-CONT', warehouseName: '美国仓', warehouseCode: 'US-W', plannedAt: null, actualAt: null, version: 0, planCount: 0, canEdit: true }
|
||||
const row: ScheduleEvent = { key: 'warehouse-1', type: 'warehouse', date: '2026-10-21', time: '', title: 'TEST-CONT 计划到仓', fields: [], warehouse, canEdit: true }
|
||||
function dispatchEntry(overrides: Partial<TripCalendarEntry> = {}): TripCalendarEntry {
|
||||
return { sourceType: 'DISPATCH', dispatchNo: 'PC-CALENDAR-001', containerNumbers: ['TSTU1234567', 'TSTU7654321'], status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'LAX', originWarehouseName: '洛杉矶仓', originCountryCode: 'US', originTimezone: 'America/Los_Angeles', originWarehouseStatus: 'ACTIVE', plannedDepartureAt: '2026-10-22T16:00:00Z', expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [{ name: '洛杉矶仓' }, { name: 'ONT8' }], incompleteReasons: [], ...overrides }
|
||||
return { sourceType: 'DISPATCH', dispatchNo: 'PC-CALENDAR-001', containerNumbers: ['TSTU1234567', 'TSTU7654321'], cabinetNumbers: [], status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'LAX', originWarehouseName: '洛杉矶仓', originCountryCode: 'US', originTimezone: 'America/Los_Angeles', originWarehouseStatus: 'ACTIVE', plannedDepartureAt: '2026-10-22T16:00:00Z', expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [{ name: '洛杉矶仓' }, { name: 'ONT8' }], incompleteReasons: [], ...overrides }
|
||||
}
|
||||
export function renderSchedule() {
|
||||
return mount(AppointmentCalendarView, { global: { stubs: {
|
||||
|
||||
@@ -4,15 +4,16 @@ import { http } from '@/shared/api/http'
|
||||
vi.mock('@/shared/api/http', () => ({ http: { get: vi.fn() }, unwrapApiResponse: (value: {data: unknown}) => value.data }))
|
||||
const range = { from: '2026-10-01', to: '2026-11-01' }
|
||||
function trip(dispatchNo: string, plannedDepartureAt: string, originTimezone = 'America/Los_Angeles'): TripCalendarEntry {
|
||||
return { sourceType: 'DISPATCH', dispatchNo, containerNumbers: [], status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'US-W', originWarehouseName: 'US warehouse', originCountryCode: 'US', originTimezone, originWarehouseStatus: 'ACTIVE', plannedDepartureAt, expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [{name:'US warehouse'}, {name:'FBA'}], incompleteReasons: [] }
|
||||
return { sourceType: 'DISPATCH', dispatchNo, containerNumbers: [], cabinetNumbers: [], status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'US-W', originWarehouseName: 'US warehouse', originCountryCode: 'US', originTimezone, originWarehouseStatus: 'ACTIVE', plannedDepartureAt, expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [{name:'US warehouse'}, {name:'FBA'}], incompleteReasons: [] }
|
||||
}
|
||||
describe('location-based trip calendar', () => {
|
||||
it('places the reported UTC plan on October 22 at 09:00 in Los Angeles and exposes every container in one event', () => {
|
||||
const row = { ...trip('PC-CALENDAR-001', '2026-10-22T16:00:00Z'), containerNumbers: ['TSTU1234567', 'TSTU7654321'] }
|
||||
const row = { ...trip('PC-CALENDAR-001', '2026-10-22T16:00:00Z'), containerNumbers: ['TSTU1234567', 'TSTU7654321'], cabinetNumbers: ['D1821', 'D1822'] }
|
||||
const projected = projectTripCalendar({ records: [row, row], unresolvedRecords: [] }, range)
|
||||
expect(projected.events).toHaveLength(1)
|
||||
expect(projected.events[0]).toMatchObject({ key: 'dispatch-PC-CALENDAR-001', date: '2026-10-22', time: '09:00', sourcePath: '/overseas-warehouse/dispatches/PC-CALENDAR-001' })
|
||||
expect(projected.events[0]!.fields).toContainEqual({ label: '关联柜号', value: 'TSTU1234567、TSTU7654321' })
|
||||
expect(projected.events[0]!.fields).toContainEqual({ label: '关联柜次号', value: 'D1821、D1822' })
|
||||
expect(projected.events[0]!.fields).toContainEqual({ label: '计划出发(起运仓当地)', value: '2026-10-22 09:00' })
|
||||
expect(projected.events[0]!.fields).toContainEqual({ label: '计划出发(UTC)', value: '2026-10-22T16:00:00Z' })
|
||||
const beijing = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface TripCalendarEntry {
|
||||
sourceType: 'DISPATCH'
|
||||
dispatchNo: string
|
||||
containerNumbers: string[]
|
||||
cabinetNumbers: string[]
|
||||
status: string
|
||||
originWarehouseId: string | null
|
||||
originWarehouseCode: string | null
|
||||
@@ -81,6 +82,7 @@ export function projectTripCalendar(result: TripCalendarResult, range: { from: s
|
||||
fields: [
|
||||
{ label: '运输状态', value: labels[row.status] || '待核实' },
|
||||
{ label: '关联柜号', value: row.containerNumbers.join('、') },
|
||||
{ label: '关联柜次号', value: row.cabinetNumbers.join('、') },
|
||||
{ label: '起运仓', value: row.originWarehouseName || row.originWarehouseCode },
|
||||
{ label: '地点时区', value: row.originTimezone },
|
||||
{ label: '计划出发(起运仓当地)', value: planned ? `${planned.date} ${planned.time}` : null },
|
||||
|
||||
@@ -6,13 +6,17 @@
|
||||
|
||||
| 页面/路由 | 操作 | 参考承载与布局 | 当前实现 | 页面核对 |
|
||||
|---|---|---|---|---|
|
||||
| `/ocean-bookings/new`、`/ocean-bookings/:bookingId` | 草稿新增、维护 | 独立页面;左主资料、右三方并排,下方放舱资料 | 按用户红框移除客户选择、港名、预计到港、箱种、容量、限重、备注及包装单位选择;仓库和渠道保留在默认收起的“作业配置”,放舱缺配置时自动展开;委实成对输入,新增保存留在详情 | 前批已核对新增保存与控件状态;本次精简及折叠区待 TEST 页面核对,完整长字段展示待人工确认 |
|
||||
| `/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` 位于同一证据目录。
|
||||
|
||||
同日流程断开修复经 TEST #699 发布:刷新已放舱测试单后,第 1~3 步保持通过、连接线连续,第 4 步仍为当前待预览;托运单提示“可重新预览 / 打印”。两份定向测试 24 项通过;截图为同一证据目录的 `ui-workflow-continuous.png`。本次未修改后端或测试业务数据,未重新验收内嵌 PDF 显示。
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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() })
|
||||
<el-form-item label="起运港"><el-input v-model="form.data.departurePortCode" placeholder="港代码" aria-label="起运港代码" /></el-form-item>
|
||||
<el-form-item label="目的港"><el-input v-model="form.data.destinationPortCode" placeholder="港代码" aria-label="目的港代码" /></el-form-item>
|
||||
<el-form-item label="预计开船" prop="estimatedSailingAt"><el-date-picker v-model="form.data.estimatedSailingAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计开船时间" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="预计到港" prop="estimatedArrivalAt"><el-date-picker v-model="form.data.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计到港时间" style="width:100%" /></el-form-item>
|
||||
<el-form-item class="booking-field--wide" label="交货地点"><el-input v-model="form.data.placeOfDelivery" placeholder="请输入最终交货地点" /></el-form-item>
|
||||
<el-form-item label="运费条款"><el-input v-model="form.data.freightTerm" maxlength="64" /></el-form-item>
|
||||
<el-form-item label="运输条款"><el-input v-model="form.data.serviceTerm" maxlength="64" /></el-form-item>
|
||||
@@ -622,6 +623,7 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<el-descriptions-item label="船名">{{ form.data.vesselName || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="航次">{{ form.data.voyageNo || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计开船">{{ form.data.estimatedSailingAt || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计到港">{{ form.data.estimatedArrivalAt || '—' }}</el-descriptions-item>
|
||||
</el-descriptions></section>
|
||||
</div>
|
||||
</AppDialog>
|
||||
|
||||
Reference in New Issue
Block a user