fix(admin): load outbound facts when access becomes ready
This commit is contained in:
@@ -5,6 +5,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ContainerDetailView from './ContainerDetailView.vue'
|
||||
import { getContainer, getContainerOverview } from '../api/platformContainerLoading'
|
||||
import { recordTerminalDrayageEmptyReturn } from '../api/terminalDrayages'
|
||||
import { getContainerOutboundDispatch } from '@/modules/outbound-dispatch'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const canAccess = vi.hoisted(() => vi.fn<(code: string) => boolean>(() => true))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canAccess }) }))
|
||||
@@ -29,11 +31,34 @@ async function mountDetail(workbench: 'DOMESTIC' | 'OVERSEAS' = 'OVERSEAS') {
|
||||
describe('ContainerDetailView empty return', () => {
|
||||
beforeEach(() => {
|
||||
canAccess.mockReset().mockReturnValue(true)
|
||||
vi.mocked(getContainerOutboundDispatch).mockReset().mockResolvedValue({ aggregate: { dispatchStatus: 'WAITING_FOR_LOADING', gate: { inbounds: [] } }, timeline: [] } as never)
|
||||
vi.mocked(getContainer).mockReset().mockResolvedValue(base as never)
|
||||
vi.mocked(getContainerOverview).mockReset().mockResolvedValue(overview as never)
|
||||
vi.mocked(recordTerminalDrayageEmptyReturn).mockReset().mockResolvedValue({ ...base.terminalDrayage, status: 'EMPTY_RETURNED', version: 4, actualEmptyReturnAt: '2026-09-09T12:00:00', allowedActions: [] } as never)
|
||||
})
|
||||
|
||||
it('loads departed gates exactly once when read permission arrives after the container detail', async () => {
|
||||
const granted = ref(false)
|
||||
canAccess.mockImplementation((code) => code === 'platform:firstleg:outbound-dispatch:view' ? granted.value : true)
|
||||
const wrapper = await mountDetail('DOMESTIC')
|
||||
expect(getContainerOutboundDispatch).not.toHaveBeenCalled()
|
||||
granted.value = true
|
||||
await flushPromises()
|
||||
expect(getContainerOutboundDispatch).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.text()).toContain('已确认离仓')
|
||||
expect(wrapper.text()).toContain('整柜已确认离仓;')
|
||||
expect(wrapper.text()).not.toContain('全部门禁已通过,可确认整柜离仓')
|
||||
})
|
||||
|
||||
it('reloads authoritative departure state once on each newly opened domestic detail', async () => {
|
||||
const first = await mountDetail('DOMESTIC')
|
||||
expect(first.text()).toContain('已确认离仓')
|
||||
first.unmount()
|
||||
const reopened = await mountDetail('DOMESTIC')
|
||||
expect(reopened.text()).toContain('已确认离仓')
|
||||
expect(getContainerOutboundDispatch).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it.each(['DOMESTIC', 'OVERSEAS'] as const)('gates fee tabs by granted permissions in %s', async (workbench) => {
|
||||
for (const [firstLeg, lastLeg] of [[true, false], [false, true], [true, true], [false, false]]) {
|
||||
canAccess.mockImplementation((code) => code === 'firstleg:inbound:fee-detail:view' ? firstLeg! : code === 'platform:finance:customer-arap:list' ? lastLeg! : false)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft, ClipboardList, FileUp, Link2, Package, Receipt, Wallet } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
|
||||
@@ -191,8 +191,9 @@ const canConfirmDeparture = computed(() => canConfirmOutboundDispatch.value
|
||||
&& hasAction('CONFIRM_OUTBOUND')
|
||||
&& outboundDetail.value?.aggregate.dispatchStatus === 'READY_FOR_DISPATCH'
|
||||
&& outboundGateSummary.value.blocked === 0)
|
||||
const customsDepartureState = computed<'DEPARTED' | 'WAITING_OTHER_GATES'>(() =>
|
||||
outboundDetail.value?.aggregate.dispatchStatus === 'WAITING_FOR_LOADING' ? 'DEPARTED' : 'WAITING_OTHER_GATES')
|
||||
const customsDepartureState = computed<'DEPARTED' | 'WAITING_OTHER_GATES' | 'UNKNOWN'>(() =>
|
||||
!outboundDetail.value ? 'UNKNOWN'
|
||||
: outboundDetail.value.aggregate.dispatchStatus === 'WAITING_FOR_LOADING' ? 'DEPARTED' : 'WAITING_OTHER_GATES')
|
||||
function openOceanBookingLink() {
|
||||
if (!detail.value || !canLinkOceanBooking.value) return
|
||||
void router.push({ name: 'domestic-container-workbench', query: { linkContainerId: detail.value.id } })
|
||||
@@ -274,11 +275,19 @@ async function load() {
|
||||
workbenchDetail.value = resource; detail.value = resource; overview.value = aggregate
|
||||
if (activeTab.value === 'loading-list' && !canViewCurrentLoadingList.value) activeTab.value = 'overview'
|
||||
if (isDomestic.value && activeTab.value === 'loading-list') await loadLoadingDetail(resource, aggregate)
|
||||
if (isDomestic.value && resource.planStatus === 'COMPLETED' && canViewOutboundDispatch.value) await loadOutboundDispatch()
|
||||
} catch (cause) { error.value = getApiErrorDetails(cause, '货柜详情加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
watch(
|
||||
() => !loading.value && isDomestic.value && detail.value?.planStatus === 'COMPLETED'
|
||||
&& canViewOutboundDispatch.value ? containerId.value : null,
|
||||
(eligibleContainerId) => {
|
||||
if (eligibleContainerId) void loadOutboundDispatch()
|
||||
else { outboundDetail.value = null; outboundError.value = null }
|
||||
},
|
||||
)
|
||||
|
||||
async function loadOutboundDispatch() {
|
||||
if (!containerId.value || !canViewOutboundDispatch.value) return
|
||||
outboundLoading.value = true; outboundError.value = null; showPassedGates.value = false
|
||||
@@ -524,7 +533,7 @@ onMounted(async () => { await load(); if (isOverseas.value && route.query.action
|
||||
<template v-else-if="outboundDetail">
|
||||
<div class="gate-summary"><span>总门禁 <strong>{{ outboundGateSummary.total }}</strong></span><span class="gate-summary__passed">已通过 <strong>{{ outboundGateSummary.passed }}</strong></span><span :class="{ 'gate-summary__blocked': outboundGateSummary.blocked }">阻断 <strong>{{ outboundGateSummary.blocked }}</strong></span><span class="gate-summary__reference">拖车状态(参考):{{ truckReferenceText(outboundDetail.aggregate.truckAppointmentStatus) }}</span><el-button v-if="outboundGateSummary.passed" link type="primary" @click="showPassedGates = !showPassedGates">{{ showPassedGates ? '收起已通过项' : `查看全部门禁(${outboundGateSummary.total})` }}</el-button></div>
|
||||
<p v-if="outboundGateSummary.blocked" class="outbound-panel__hint">默认仅展示阻断项;处理完成后重新加载门禁即可确认离仓。</p>
|
||||
<p v-else-if="!showPassedGates" class="outbound-panel__hint outbound-panel__hint--success">全部门禁已通过,可确认整柜离仓;展开可查看去重后的完整门禁。</p>
|
||||
<p v-else-if="!showPassedGates" class="outbound-panel__hint outbound-panel__hint--success">{{ customsDepartureState === 'DEPARTED' ? '整柜已确认离仓;展开可查看去重后的完整门禁。' : '全部门禁已通过,可确认整柜离仓;展开可查看去重后的完整门禁。' }}</p>
|
||||
<div class="gate-list">
|
||||
<article v-for="row in visibleOutboundGateRows" :key="row.key" class="gate-row">
|
||||
<el-tag size="small" :type="row.passed ? 'success' : 'danger'">{{ row.passed ? '通过' : '阻断' }}</el-tag><strong>{{ row.name }}</strong><span class="gate-row__actual">{{ row.actual || '暂无事实' }}</span><small>要求:{{ row.requirement || '—' }}</small>
|
||||
|
||||
Reference in New Issue
Block a user