From 2e26477db523cebf23db8c28f8a4dc39cb0df1bd Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 21 Oct 2026 23:20:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=A2=E5=8D=95=E8=B4=A7?= =?UTF-8?q?=E6=9F=9C=E9=A2=84=E7=BA=A6=E4=B8=8E=E6=8E=92=E8=BD=A6=E7=BB=B4?= =?UTF-8?q?=E6=8A=A4=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentConfirmationCargo.vue | 4 +- .../AppointmentCreatePanel.test.ts | 6 ++ .../WorkAppointmentForm.test.ts | 10 +-- .../WorkAppointmentForm.vue | 2 +- .../WorkCreateAppointmentForm.test.ts | 14 +++- .../WorkReceivingPanel.test.ts | 4 +- .../WorkReceivingPanel.vue | 2 +- .../appointment-calendar/schedule.test.ts | 24 +++++-- src/modules/appointment-calendar/schedule.ts | 18 ++--- .../appointment-calendar/scheduleApi.test.ts | 2 +- .../appointment-calendar/scheduleApi.ts | 3 + .../api/containerArrivalTimes.ts | 2 + .../components/ContainerCustomsPanel.test.ts | 3 +- .../components/ContainerCustomsPanel.vue | 12 ++-- .../components/ContainerLastLegFeesPanel.vue | 4 +- .../components/ContainerOverviewPanel.vue | 6 +- .../components/OceanBookingLinkDialog.vue | 4 +- .../pages/ContainerDetailView.vue | 28 ++++---- .../pages/ContainersView.test.ts | 2 +- .../pages/ContainersView.vue | 11 ++- .../pages/GlobalSmartLoadingView.test.ts | 2 +- .../pages/GlobalSmartLoadingView.vue | 7 +- .../pages/SmartLoadingWorkbenchView.test.ts | 2 +- .../pages/SmartLoadingWorkbenchView.vue | 17 +---- .../warehouseArrival.test.ts | 14 ++-- .../container-loading/warehouseArrival.ts | 19 ++---- src/modules/finance/status.test.ts | 2 + src/modules/finance/status.ts | 1 + .../InboundMeasurementPanel.test.ts | 52 +++++++------- .../measurement/InboundMeasurementPanel.vue | 32 ++------- .../measurement/measurementDensity.css | 2 +- .../review/MeasurementReviewPanel.test.ts | 3 +- .../review/MeasurementReviewPanel.vue | 3 +- .../WeightOcrPanel.behavior.test.ts | 9 +-- .../components/ContainerFeesPanel.vue | 6 +- .../LoadingListDetailDrawer.behavior.test.ts | 28 +++++++- .../components/LoadingListDetailDrawer.vue | 12 +++- .../PreloadPlanDetailView.dialogs.test.ts | 4 +- .../pages/PreloadPlanDetailView.test.ts | 3 +- .../pages/PreloadPlanDetailView.vue | 13 ++-- .../api/platformOceanBookings.test.ts | 12 ++++ .../OceanBookingDetailView.release.test.ts | 35 +++++++++- .../pages/OceanBookingDetailView.vue | 8 +-- src/modules/ocean-bookings/types.ts | 2 +- .../overseas-warehouse/api/dispatches.test.ts | 14 ++++ .../appointmentForm.test.ts | 7 +- .../overseas-warehouse/appointmentForm.ts | 2 +- .../components/DispatchEvidenceFiles.test.ts | 2 + .../components/DispatchEvidenceFiles.vue | 13 ++-- .../components/DispatchResourceFields.test.ts | 30 +++++++- .../components/DispatchResourceFields.vue | 7 +- .../DispatchVehicleMaintenanceFees.test.ts | 47 +++++++++++++ .../DispatchVehicleMaintenanceFees.vue | 53 +++++++++++++++ .../DispatchVehicleMaintenanceForm.vue | 68 +++++++++++++++++++ .../DispatchWorkflowDialog.layout.test.ts | 33 +++++++-- .../components/DispatchWorkflowDialog.vue | 31 +++++---- .../components/FbaAppointmentFields.vue | 16 +++-- .../overseas-warehouse/dispatchContracts.ts | 5 +- .../dispatchVehicleMaintenance.ts | 21 ++++++ .../pages/InboundDialogs.behavior.test.ts | 5 +- .../pages/InboundOrdersView.vue | 2 +- .../components/CarrierScheduleQueryPanel.vue | 7 +- .../components/ContainerPortSchedulePanel.vue | 4 +- .../InboundFeeDetailManager.test.ts | 2 +- .../components/InboundFeeDetailManager.vue | 4 +- .../pages/UnifiedOrdersView.test.ts | 7 +- .../pages/UnifiedOrdersView.vue | 4 +- src/styles/adminDensity.test.ts | 8 ++- 68 files changed, 602 insertions(+), 239 deletions(-) create mode 100644 src/modules/overseas-warehouse/components/DispatchVehicleMaintenanceFees.test.ts create mode 100644 src/modules/overseas-warehouse/components/DispatchVehicleMaintenanceFees.vue create mode 100644 src/modules/overseas-warehouse/components/DispatchVehicleMaintenanceForm.vue create mode 100644 src/modules/overseas-warehouse/dispatchVehicleMaintenance.ts diff --git a/src/modules/appointment-calendar/AppointmentConfirmationCargo.vue b/src/modules/appointment-calendar/AppointmentConfirmationCargo.vue index 1e45c0db..dd32444a 100644 --- a/src/modules/appointment-calendar/AppointmentConfirmationCargo.vue +++ b/src/modules/appointment-calendar/AppointmentConfirmationCargo.vue @@ -63,7 +63,7 @@ watch(() => [props.active, JSON.stringify(props.sources)] as const, ([active]) = onBeforeUnmount(() => { ++generation; unregister?.() }) diff --git a/src/modules/appointment-calendar/AppointmentCreatePanel.test.ts b/src/modules/appointment-calendar/AppointmentCreatePanel.test.ts index 302e91a2..2bd708a7 100644 --- a/src/modules/appointment-calendar/AppointmentCreatePanel.test.ts +++ b/src/modules/appointment-calendar/AppointmentCreatePanel.test.ts @@ -30,6 +30,12 @@ async function selectAndPrepare(wrapper: ReturnType) { await wrapper.get('input[type="checkbox"]').setValue(true) await wrapper.findAll('button').find(button => button.text().startsWith('下一步'))!.trigger('click') await flushPromises() + const fields = wrapper.findComponent({ name: 'FbaAppointmentFields' }) + if (fields.exists()) { + Object.assign(fields.props('model'), { confirmationNo: 'EXT-CONFIRMED', appointmentAt: '2026-10-25T09:30:00' }) + fields.vm.$emit('confirmationReady', true) + } + await flushPromises() } beforeEach(() => { vi.clearAllMocks(); guards.clear() diff --git a/src/modules/appointment-calendar/WorkAppointmentForm.test.ts b/src/modules/appointment-calendar/WorkAppointmentForm.test.ts index a49c6759..09f4dc06 100644 --- a/src/modules/appointment-calendar/WorkAppointmentForm.test.ts +++ b/src/modules/appointment-calendar/WorkAppointmentForm.test.ts @@ -49,14 +49,16 @@ it('does not edit or cancel replaced snapshots and offers the stable batch conte expect(wrapper.emitted('batch')).toEqual([['FBA_APPOINTMENT:1']]); wrapper.unmount() }) const cancelled: FbaAppointmentDto = { ...detail, version: 4, appointmentStatus: 'CANCELLED', confirmationNo: null, plannedDate: null, plannedAt: null, plannedEndAt: null, appointmentAt: null, appointmentEndAt: null, appointmentStartUtc: null, appointmentEndUtc: null } -it('rebooks the cancelled original batch as undated pending without creating or reallocating cargo', async () => { - vi.mocked(getFbaAppointment).mockResolvedValueOnce(cancelled).mockResolvedValue({ ...cancelled, version: 5, appointmentStatus: 'PENDING' }) +it('rebooks the original cargo using a new confirmed appointment without reallocating cargo', async () => { + vi.mocked(getFbaAppointment).mockResolvedValueOnce(cancelled).mockResolvedValue({ ...detail, version: 5 }) const wrapper = render(); await flushPromises() expect(wrapper.text()).toContain('货物分配仍保留') const model = wrapper.getComponent({ name: 'FbaAppointmentFields' }).props('model') - expect(model.appointmentStatus).toBe('PENDING'); expect(model.confirmationNo).toBe(''); expect(model.appointmentAt).toBe('') + expect(model.appointmentStatus).toBe('SUCCESS'); expect(model.confirmationNo).toBe(''); expect(model.appointmentAt).toBe('') + Object.assign(model, { confirmationNo: 'NEW-EXT', appointmentAt: '2026-10-20T10:00:00' }) + wrapper.getComponent({ name: 'FbaAppointmentFields' }).vm.$emit('confirmationReady', true) await wrapper.findAll('button').find(button => button.text() === '保存本批重新预约')!.trigger('click'); await flushPromises() - expect(updateFbaAppointment).toHaveBeenCalledWith('AP-001', expect.objectContaining({ version: 4, idempotencyKey: expect.any(String), details: expect.objectContaining({ appointmentStatus: 'PENDING', plannedDate: null, confirmationNo: null, appointmentAt: null, appointmentEndAt: null }) })) + expect(updateFbaAppointment).toHaveBeenCalledWith('AP-001', expect.objectContaining({ version: 4, idempotencyKey: expect.any(String), details: expect.objectContaining({ appointmentStatus: 'SUCCESS', plannedDate: null, confirmationNo: 'NEW-EXT', appointmentAt: '2026-10-20T10:00:00', appointmentEndAt: null }) })) const payload = vi.mocked(updateFbaAppointment).mock.calls[0]![1] expect(payload).not.toHaveProperty('items'); expect(payload).not.toHaveProperty('sources') expect(deleteAppointment).not.toHaveBeenCalled(); expect(wrapper.emitted('saved')).toHaveLength(1) diff --git a/src/modules/appointment-calendar/WorkAppointmentForm.vue b/src/modules/appointment-calendar/WorkAppointmentForm.vue index a45c6435..4fc52086 100644 --- a/src/modules/appointment-calendar/WorkAppointmentForm.vue +++ b/src/modules/appointment-calendar/WorkAppointmentForm.vue @@ -30,7 +30,7 @@ async function load(preserve = false) { try { detail.value = await getFbaAppointment(props.appointmentNo) if (!preserve || !initialized) { - Object.assign(form, createAppointmentForm(detail.value)); if (rebooking.value) form.appointmentStatus = 'PENDING' + Object.assign(form, createAppointmentForm(detail.value)); if (rebooking.value) form.appointmentStatus = 'SUCCESS' baseline.value = JSON.stringify(appointmentPayload(form)); initialized = true } } diff --git a/src/modules/appointment-calendar/WorkCreateAppointmentForm.test.ts b/src/modules/appointment-calendar/WorkCreateAppointmentForm.test.ts index 75e9b56c..17ede40a 100644 --- a/src/modules/appointment-calendar/WorkCreateAppointmentForm.test.ts +++ b/src/modules/appointment-calendar/WorkCreateAppointmentForm.test.ts @@ -24,16 +24,23 @@ it('shows readable preview requirements without exposing missing-field machine c expect(wrapper.text()).not.toContain('CARGO_QUANTITY_REVIEW') wrapper.unmount() }) -it('previews and creates the exact canonical external source and customer without inventing times', async () => { +async function confirmRegistration(wrapper: ReturnType) { + const fields = wrapper.getComponent({ name: 'FbaAppointmentFields' }) + Object.assign(fields.props('model'), { confirmationNo: 'EXT-CONFIRMED', appointmentAt: '2026-10-16T09:30:00' }) + fields.vm.$emit('confirmationReady', true) + await flushPromises() +} +it('previews and creates the exact canonical external source and customer with real confirmation', async () => { const wrapper = render(); await flushPromises() + await confirmRegistration(wrapper) expect(previewPlanningBatch).toHaveBeenCalledWith(sources) await wrapper.get('input').setValue('2026-10-16'); await wrapper.get('button').trigger('click'); await flushPromises() - expect(createSourceAppointment).toHaveBeenCalledWith(expect.objectContaining({ sources, originWarehouseId: 'origin-1', fbaWarehouseCode: 'ONT8', details: expect.objectContaining({ plannedDate: '2026-10-16', plannedAt: null, plannedEndAt: null, appointmentStatus: 'PENDING' }) })) + expect(createSourceAppointment).toHaveBeenCalledWith(expect.objectContaining({ sources, originWarehouseId: 'origin-1', fbaWarehouseCode: 'ONT8', details: expect.objectContaining({ plannedDate: '2026-10-16', plannedAt: null, plannedEndAt: null, appointmentStatus: 'SUCCESS', confirmationNo: 'EXT-CONFIRMED' }) })) expect(wrapper.emitted('saved')).toHaveLength(1); expect(wrapper.text()).not.toContain('external-1'); wrapper.unmount() }) it('retains the same immutable request after an unknown outcome and blocks leaving', async () => { vi.mocked(createSourceAppointment).mockRejectedValueOnce(new Error('connection lost')) - const wrapper = render(); await flushPromises(); await wrapper.get('button').trigger('click'); await flushPromises() + const wrapper = render(); await flushPromises(); await confirmRegistration(wrapper); await wrapper.get('button').trigger('click'); await flushPromises() const request = vi.mocked(createSourceAppointment).mock.calls[0]![0] expect(await guard()).toBe(false) await wrapper.get('button').trigger('click'); await flushPromises() @@ -42,6 +49,7 @@ it('retains the same immutable request after an unknown outcome and blocks leavi it('refreshes qualification after conflict while keeping user-entered dates', async () => { vi.mocked(createSourceAppointment).mockRejectedValueOnce(new ApiResponseError('来源变化', 'A0409', 409)) const wrapper = render(); await flushPromises(); await wrapper.get('input').setValue('2026-10-16') + await confirmRegistration(wrapper) await wrapper.get('button').trigger('click'); await flushPromises() expect(previewPlanningBatch).toHaveBeenCalledTimes(2) expect((wrapper.get('input').element as HTMLInputElement).value).toBe('2026-10-16') diff --git a/src/modules/appointment-calendar/WorkReceivingPanel.test.ts b/src/modules/appointment-calendar/WorkReceivingPanel.test.ts index 431ecf95..fa91ccef 100644 --- a/src/modules/appointment-calendar/WorkReceivingPanel.test.ts +++ b/src/modules/appointment-calendar/WorkReceivingPanel.test.ts @@ -236,7 +236,7 @@ function mixedPlans(): api.ReceivingContext { c.agreements = [{ ...agreement(), members: [members()[0]!] }] c.batch.items.push({ ...c.batch.items[1]!, id: 'item-d', customerId: 'customer-d', sourceKey: 'ORDER:D', inboundCode: 'IN-D' }) c.executionScope.members.push({ member: { batchKey: c.batch.batchKey!, customerId: 'customer-d' }, completed: false, canRebook: true, reason: null }) - c.plans = [{ batchKey: c.batch.batchKey!, customerId: 'customer-b', timezone: 'America/Los_Angeles', details: appointmentPayload({ ...createAppointmentForm(), plannedDate: '2026-10-24', appointmentRemark: '等待仓库' }) }] + c.plans = [{ batchKey: c.batch.batchKey!, customerId: 'customer-b', timezone: 'America/Los_Angeles', details: appointmentPayload({ ...createAppointmentForm(), appointmentStatus: 'PENDING', plannedDate: '2026-10-24', appointmentRemark: '等待仓库' }) }] return c } async function enterBPlan(wrapper: ReturnType) { @@ -373,7 +373,7 @@ it('inherits unconfirmed legacy header plans only without active independent cov it('never inherits cancelled targets and keeps an explicit empty customer plan ahead of a pending header', async () => { for (const ownPlan of [false, true]) { const c = context(); Object.assign(c.batch, { appointmentStatus: ownPlan ? 'PENDING' : 'CANCELLED', plannedDate: '2026-10-27', appointmentRemark: '旧批原因' }) - if (ownPlan) c.plans = [{ batchKey: c.batch.batchKey!, customerId: 'customer-a', timezone: c.batch.timezone || null, details: appointmentPayload(createAppointmentForm()) }] + if (ownPlan) c.plans = [{ batchKey: c.batch.batchKey!, customerId: 'customer-a', timezone: c.batch.timezone || null, details: appointmentPayload({ ...createAppointmentForm(), appointmentStatus: 'PENDING' }) }] vi.mocked(api.getReceivingContext).mockResolvedValue(c) const wrapper = render(); await flushPromises(); await button(wrapper, '保存目标 / 未约成').trigger('click'); await flushPromises() expect((wrapper.get('[aria-label="客户计划日期"]').element as HTMLInputElement).value).toBe('') diff --git a/src/modules/appointment-calendar/WorkReceivingPanel.vue b/src/modules/appointment-calendar/WorkReceivingPanel.vue index 43b62634..e8211a6f 100644 --- a/src/modules/appointment-calendar/WorkReceivingPanel.vue +++ b/src/modules/appointment-calendar/WorkReceivingPanel.vue @@ -143,7 +143,7 @@ async function openPlan(id: string) { resetForm(); groupKey.value = null; group.value = null; customer.value = id; mode.value = 'plan' const saved = customerPlan(id) const header = context.value!.batch - const inherited = !context.value!.agreements.some(agreement => agreement.status === 'ACTIVE') && ['PENDING', 'FAILED'].includes(header.appointmentStatus || '') ? createAppointmentForm(header) : createAppointmentForm() + const inherited = !context.value!.agreements.some(agreement => agreement.status === 'ACTIVE') && ['PENDING', 'FAILED'].includes(header.appointmentStatus || '') ? createAppointmentForm(header) : { ...createAppointmentForm(), appointmentStatus: 'PENDING' as const } Object.assign(planForm, createAppointmentForm(), saved ? Object.fromEntries(Object.entries(saved.details).map(([key, value]) => [key, value ?? ''])) : { ...inherited, confirmationNo: '', appointmentAt: '', appointmentEndAt: '', startOffset: '', endOffset: '' }) baseline.value = state() } diff --git a/src/modules/appointment-calendar/schedule.test.ts b/src/modules/appointment-calendar/schedule.test.ts index 1e696e18..c8693aff 100644 --- a/src/modules/appointment-calendar/schedule.test.ts +++ b/src/modules/appointment-calendar/schedule.test.ts @@ -3,27 +3,39 @@ import { projectContainerSchedule } from './schedule' import type { CalendarSources, WarehouseArrival } from './scheduleApi' const range = { from: '2026-11-01', to: '2026-12-01' } const sources: CalendarSources = { containers: [{ containerBookingId: '1', cabinetNo: 'D0001', containerNo: 'TEST-CONT', vesselName: null, voyageNo: null, destinationPortName: 'Port', destinationWarehouseCode: null, arrivalAt: '2026-10-30T23:00:00', arrivalType: 'ACTUAL' }], emptyReturns: [], emptyReturnsReadable: true } -const warehouse: WarehouseArrival = { sourceContainerBookingId: '1', containerNo: 'TEST-CONT', warehouseName: 'US-W', warehouseCode: 'US-W', plannedAt: null, actualAt: null, version: 0, planCount: 0, canEdit: true } +const warehouse: WarehouseArrival = { sourceContainerBookingId: '1', cabinetNo: 'D0001', containerNo: 'TEST-CONT', warehouseName: 'US-W', warehouseCode: 'US-W', plannedAt: null, actualAt: null, version: 0, planCount: 0, canEdit: true, plannedDate: '2026-11-01', isDefault: true } describe('four-type schedule projection', () => { it('keeps inconsistent authoritative plan times undated instead of hiding them with an automatic date', () => { - const events = projectContainerSchedule(sources, [{ ...warehouse, version: -1, canEdit: false }], range, true) + const events = projectContainerSchedule(sources, [{ ...warehouse, version: -1, planCount: 1, isDefault: false, canEdit: false }], range, true) expect(events[0]).toMatchObject({ type: 'warehouse', date: null, subtitle: '柜内计划时间不一致,待核实' }) }) it('adds two local calendar days across months without creating a time', () => { const events = projectContainerSchedule(sources, [warehouse], range, true) expect(events).toHaveLength(1) - expect(events[0]).toMatchObject({ type: 'warehouse', date: '2026-11-01', time: '' }) + expect(events[0]).toMatchObject({ type: 'warehouse', date: '2026-11-01', time: '', title: 'D0001 计划到仓' }) }) it('changes the automatic date when the authoritative arrival changes and retains manual plans', () => { const changed = { ...sources, containers: [{ ...sources.containers[0]!, arrivalAt: '2026-11-02T05:00:00' }] } - expect(projectContainerSchedule(changed, [warehouse], range, true).find(row => row.type === 'warehouse')?.date).toBe('2026-11-04') - expect(projectContainerSchedule(changed, [{ ...warehouse, plannedAt: '2026-11-08T09:30:00', version: 1 }], range, true).find(row => row.type === 'warehouse')).toMatchObject({ date: '2026-11-08', time: '09:30', subtitle: '人工维护' }) + expect(projectContainerSchedule(changed, [{ ...warehouse, plannedDate: '2026-11-04' }], range, true).find(row => row.type === 'warehouse')?.date).toBe('2026-11-04') + expect(projectContainerSchedule(changed, [{ ...warehouse, plannedAt: '2026-11-08T09:30:00', version: 1, isDefault: false }], range, true).find(row => row.type === 'warehouse')).toMatchObject({ date: '2026-11-08', time: '09:30', subtitle: '人工维护' }) }) it('keeps manually moved containers visible when their arrival is outside this month', () => { const events = projectContainerSchedule({ ...sources, containers: [] }, [{ ...warehouse, plannedAt: '2026-11-08T09:30:00' }], range, true) expect(events).toHaveLength(1) expect(events[0]?.date).toBe('2026-11-08') }) + it('shows a date-only frozen plan as frozen without manufacturing midnight', () => { + const events = projectContainerSchedule(sources, [{ ...warehouse, planCount: 1, isDefault: false, canEdit: false }], range, true) + expect(events).toHaveLength(1) + expect(events[0]).toMatchObject({ date: '2026-11-01', time: '', subtitle: '已有入库计划,日期已冻结', canEdit: false }) + expect(events[0]?.fields).toContainEqual({ label: '计划到仓', value: '2026-11-01' }) + }) + it('keeps cross-month frozen date-only cards and their details readable without a port-arrival row', () => { + const events = projectContainerSchedule({ ...sources, containers: [] }, [{ ...warehouse, plannedDate: '2026-11-08', planCount: 1, isDefault: false, canEdit: false }], range, true) + expect(events).toHaveLength(1) + expect(events[0]).toMatchObject({ date: '2026-11-08', time: '', title: 'D0001 计划到仓', subtitle: '已有入库计划,日期已冻结' }) + expect(events[0]?.fields).toContainEqual({ label: '计划到仓', value: '2026-11-08' }) + }) it('does not replace the planned date with PDA actual arrival', () => { const event = projectContainerSchedule(sources, [{ ...warehouse, plannedAt: '2026-11-05T09:00:00', actualAt: '2026-11-06T10:00:00', planCount: 1, canEdit: false }], range, true)[0]! expect(event.date).toBe('2026-11-05') @@ -31,7 +43,7 @@ describe('four-type schedule projection', () => { expect(event.fields).toContainEqual({ label: '实际到仓', value: '2026-11-06 10:00' }) }) it('shows missing dates explicitly and never uses internal IDs as display numbers', () => { - const events = projectContainerSchedule({ ...sources, containers: [{ ...sources.containers[0]!, containerNo: null, cabinetNo: null, arrivalAt: null }] }, [warehouse], range, false) + const events = projectContainerSchedule({ ...sources, containers: [{ ...sources.containers[0]!, cabinetNo: null, arrivalAt: null }] }, [{ ...warehouse, plannedDate: null }], range, false) expect(events.map(row => row.date)).toEqual([null, null]) expect(events.every(row => row.sourcePath === undefined)).toBe(true) expect(events.map(row => row.title)).toEqual(['— 到港', '— 计划到仓']) diff --git a/src/modules/appointment-calendar/schedule.ts b/src/modules/appointment-calendar/schedule.ts index b3e2bccb..61553d59 100644 --- a/src/modules/appointment-calendar/schedule.ts +++ b/src/modules/appointment-calendar/schedule.ts @@ -22,8 +22,9 @@ const timestamp = (at: string | null) => at?.replace('T', ' ').slice(0, 16) || ' export function projectContainerSchedule(sources: CalendarSources, warehouses: WarehouseArrival[] | null, range: ScheduleRange, canViewContainer: boolean) { const events: ScheduleEvent[] = [] const warehouseMap = new Map(warehouses?.map(row => [row.sourceContainerBookingId, row]) || []) + const containerMap = new Map(sources.containers.map(row => [row.containerBookingId, row])) for (const row of sources.containers) { - const label = row.containerNo || row.cabinetNo || '—' + const label = row.cabinetNo || '—' const fields = [{ label: '集装箱号', value: row.containerNo }, { label: '柜次号', value: row.cabinetNo }, { label: '船名航次', value: [row.vesselName, row.voyageNo].filter(Boolean).join(' / ') }, { label: '目的港', value: row.destinationPortName }] const sourcePath = canViewContainer ? `/container-loading/overseas/${encodeURIComponent(row.containerBookingId)}` : undefined @@ -36,31 +37,32 @@ export function projectContainerSchedule(sources: CalendarSources, warehouses: W if (warehouses === null) continue const warehouse = warehouseMap.get(row.containerBookingId) warehouseMap.delete(row.containerBookingId) - const projected = projectWarehouseArrival(row.arrivalAt, warehouse) + const projected = projectWarehouseArrival(warehouse) const planned = projected.date if (inRange(planned, range)) events.push({ key: `warehouse-${row.containerBookingId}`, type: 'warehouse', date: planned, time: projected.time, title: `${label} 计划到仓`, sourcePath, warehouse, canEdit: warehouse?.canEdit === true, - subtitle: warehouse?.plannedAt ? warehouse.planCount > 0 ? '已有入库计划,日期已冻结' : '人工维护' : projected.isDefault ? '到港日期+2个自然日(默认)' : '柜内计划时间不一致,待核实', + subtitle: !planned ? '柜内计划时间不一致,待核实' : warehouse && warehouse.planCount > 0 ? '已有入库计划,日期已冻结' : projected.isDefault ? '到港日期+2个自然日(默认)' : '人工维护', fields: [{ label: '集装箱号', value: row.containerNo }, { label: '接仓仓库', value: warehouse?.warehouseName || warehouse?.warehouseCode }, { label: '计划到仓', value: warehouse?.plannedAt ? timestamp(warehouse.plannedAt) : planned }, { label: '实际到仓', value: timestamp(warehouse?.actualAt || null) }] }) } // 人工改期可能跨月,与当前月份的到港记录无关。 for (const row of warehouseMap.values()) { - const date = row.plannedAt?.slice(0, 10) || null + const projected = projectWarehouseArrival(row) + const date = projected.date if (!date || !inRange(date, range)) continue - events.push({ key: `warehouse-${row.sourceContainerBookingId}`, type: 'warehouse', date, time: row.plannedAt?.slice(11, 16) || '', - title: `${row.containerNo || '—'} 计划到仓`, subtitle: row.planCount > 0 ? '已有入库计划,日期已冻结' : '人工维护', warehouse: row, canEdit: row.canEdit, + events.push({ key: `warehouse-${row.sourceContainerBookingId}`, type: 'warehouse', date, time: projected.time, + title: `${row.cabinetNo || '—'} 计划到仓`, subtitle: row.planCount > 0 ? '已有入库计划,日期已冻结' : '人工维护', warehouse: row, canEdit: row.canEdit, sourcePath: canViewContainer ? `/container-loading/overseas/${encodeURIComponent(row.sourceContainerBookingId)}` : undefined, fields: [{ label: '集装箱号', value: row.containerNo }, { label: '接仓仓库', value: row.warehouseName || row.warehouseCode }, - { label: '计划到仓', value: timestamp(row.plannedAt) }, { label: '实际到仓', value: timestamp(row.actualAt) }] }) + { label: '计划到仓', value: row.plannedAt ? timestamp(row.plannedAt) : date }, { label: '实际到仓', value: timestamp(row.actualAt) }] }) } for (const row of sources.emptyReturns) { const at = row.actualEmptyReturnAt || row.emptyReturnWarning.deadlineAt if (!at || !inRange(at.slice(0, 10), range)) continue events.push({ key: `return-${row.containerBookingId}`, type: 'return', date: at.slice(0, 10), time: at.slice(11, 16), - title: `${row.containerNo || '—'} ${row.actualEmptyReturnAt ? '已还柜' : '还柜'}`, + title: `${containerMap.get(row.containerBookingId)?.cabinetNo || '—'} ${row.actualEmptyReturnAt ? '已还柜' : '还柜'}`, subtitle: row.actualEmptyReturnAt ? '实际还柜' : row.emptyReturnWarning.status === 'OVERDUE' ? '已超过还柜截止时间' : '还柜截止提醒', sourcePath: canViewContainer ? `/container-loading/overseas/${encodeURIComponent(row.containerBookingId)}` : undefined, fields: [{ label: '集装箱号', value: row.containerNo }, { label: '车队', value: row.fleetSupplierName }, diff --git a/src/modules/appointment-calendar/scheduleApi.test.ts b/src/modules/appointment-calendar/scheduleApi.test.ts index 80453af6..6c7cb3f2 100644 --- a/src/modules/appointment-calendar/scheduleApi.test.ts +++ b/src/modules/appointment-calendar/scheduleApi.test.ts @@ -35,7 +35,7 @@ async function readSchedule(overrides: Record = {}) { describe('warehouse calendar response to schedule', () => { it.each(['0', 0])('uses zero plan count %j to show arrival plus two days without inventing a time', async planCount => { - const { warehouses, event } = await readSchedule({ planCount }) + const { warehouses, event } = await readSchedule({ planCount, plannedDate: '2026-10-22', isDefault: true }) expect(warehouses[0]).toMatchObject({ sourceContainerBookingId: bookingId, planCount: 0, plannedAt: null, actualAt: null }) expect(event).toMatchObject({ date: '2026-10-22', time: '', canEdit: true, subtitle: '到港日期+2个自然日(默认)' }) expect(event?.fields).toContainEqual({ label: '计划到仓', value: '2026-10-22' }) diff --git a/src/modules/appointment-calendar/scheduleApi.ts b/src/modules/appointment-calendar/scheduleApi.ts index e8f19cf2..fce84c0d 100644 --- a/src/modules/appointment-calendar/scheduleApi.ts +++ b/src/modules/appointment-calendar/scheduleApi.ts @@ -21,6 +21,7 @@ export interface CalendarReturn { export interface CalendarSources { containers: CalendarContainer[]; emptyReturns: CalendarReturn[]; emptyReturnsReadable: boolean } export interface WarehouseArrival { sourceContainerBookingId: string + cabinetNo?: string | null containerNo: string | null warehouseName: string | null warehouseCode: string | null @@ -29,6 +30,8 @@ export interface WarehouseArrival { version: number planCount: number canEdit: boolean + plannedDate?: string | null + isDefault?: boolean } export interface ScheduleRange { from: string; to: string } type WarehouseArrivalWire = Omit & { diff --git a/src/modules/container-loading/api/containerArrivalTimes.ts b/src/modules/container-loading/api/containerArrivalTimes.ts index c458b5bc..53b26fab 100644 --- a/src/modules/container-loading/api/containerArrivalTimes.ts +++ b/src/modules/container-loading/api/containerArrivalTimes.ts @@ -7,6 +7,8 @@ export interface ContainerArrivalTime { actualWarehouseArrivalAt: string | null version: number planCount: number + plannedDate?: string | null + isDefault?: boolean } function normalizeArrival(item: ContainerArrivalTime): ContainerArrivalTime { diff --git a/src/modules/container-loading/components/ContainerCustomsPanel.test.ts b/src/modules/container-loading/components/ContainerCustomsPanel.test.ts index c8906a5b..ce80b16a 100644 --- a/src/modules/container-loading/components/ContainerCustomsPanel.test.ts +++ b/src/modules/container-loading/components/ContainerCustomsPanel.test.ts @@ -164,8 +164,7 @@ describe('ContainerCustomsPanel', () => { it('keeps READY_TO_START read-only while exposing source file name and download', async () => { const wrapper = await render('READY_TO_START') expect(wrapper.text()).toContain('开始报关') - expect(wrapper.get('[aria-label="报关概况"] dt').text()).toBe('实际箱数') - expect(wrapper.get('[aria-label="报关概况"] dd').text()).toBe('12') + expect(wrapper.find('[aria-label="报关概况"]').exists()).toBe(false) expect(wrapper.text()).toContain('客户退税资料.pdf') expect(wrapper.text()).toContain('客户退税资料') expect(wrapper.text()).toContain('报关行回执') diff --git a/src/modules/container-loading/components/ContainerCustomsPanel.vue b/src/modules/container-loading/components/ContainerCustomsPanel.vue index 3476bbca..46549a02 100644 --- a/src/modules/container-loading/components/ContainerCustomsPanel.vue +++ b/src/modules/container-loading/components/ContainerCustomsPanel.vue @@ -67,7 +67,6 @@ function clearanceResultLabel(value: ClearanceResultType) { } const missingInbounds = computed(() => customs.value?.inbounds.filter((item) => item.taxMode === 'TAX_REFUND' && !item.covered) || []) -const missingCoverageCount = computed(() => Math.max(0, (customs.value?.taxRefundCount || 0) - (customs.value?.coveredTaxRefundCount || 0))) const allBuy = computed(() => customs.value?.hasActualCargo === true && customs.value.taxRefundCount === 0) const brokerSubmission = computed(() => customs.value?.brokerSubmission || null) const brokerSubmitted = computed(() => Boolean(customs.value?.brokerSubmittedAt)) @@ -315,12 +314,9 @@ onMounted(load) -
- - - -
-
+

操作说明

@@ -274,8 +279,6 @@ watch([open, () => props.dispatchNo, () => props.action, () => props.operation],