fix(admin): load outbound facts when access becomes ready
This commit is contained in:
@@ -23,7 +23,7 @@ const props = defineProps<{
|
|||||||
loadingPlanId: string
|
loadingPlanId: string
|
||||||
canList: boolean
|
canList: boolean
|
||||||
canManage: boolean
|
canManage: boolean
|
||||||
departureState: 'DEPARTED' | 'WAITING_OTHER_GATES'
|
departureState: 'DEPARTED' | 'WAITING_OTHER_GATES' | 'UNKNOWN'
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{ refreshed: [] }>()
|
const emit = defineEmits<{ refreshed: [] }>()
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ onMounted(load)
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody></table>
|
</tbody></table>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="customs.status === 'COMPLETED'" class="departure-state"><strong>国内离仓状态:</strong><el-tag :type="departureState === 'DEPARTED' ? 'success' : 'warning'">{{ departureState === 'DEPARTED' ? '已自动离仓' : '报关已完成,待其他离仓条件' }}</el-tag></div>
|
<div v-if="customs.status === 'COMPLETED'" class="departure-state"><strong>国内离仓状态:</strong><el-tag :type="departureState === 'DEPARTED' ? 'success' : 'warning'">{{ departureState === 'DEPARTED' ? '已自动离仓' : departureState === 'UNKNOWN' ? '离仓状态尚未读取' : '报关已完成,待其他离仓条件' }}</el-tag></div>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
import ContainerDetailView from './ContainerDetailView.vue'
|
import ContainerDetailView from './ContainerDetailView.vue'
|
||||||
import { getContainer, getContainerOverview } from '../api/platformContainerLoading'
|
import { getContainer, getContainerOverview } from '../api/platformContainerLoading'
|
||||||
import { recordTerminalDrayageEmptyReturn } from '../api/terminalDrayages'
|
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))
|
const canAccess = vi.hoisted(() => vi.fn<(code: string) => boolean>(() => true))
|
||||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canAccess }) }))
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canAccess }) }))
|
||||||
@@ -29,11 +31,34 @@ async function mountDetail(workbench: 'DOMESTIC' | 'OVERSEAS' = 'OVERSEAS') {
|
|||||||
describe('ContainerDetailView empty return', () => {
|
describe('ContainerDetailView empty return', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
canAccess.mockReset().mockReturnValue(true)
|
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(getContainer).mockReset().mockResolvedValue(base as never)
|
||||||
vi.mocked(getContainerOverview).mockReset().mockResolvedValue(overview 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)
|
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) => {
|
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]]) {
|
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)
|
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">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ArrowLeft, ClipboardList, FileUp, Link2, Package, Receipt, Wallet } from '@lucide/vue'
|
import { ArrowLeft, ClipboardList, FileUp, Link2, Package, Receipt, Wallet } from '@lucide/vue'
|
||||||
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
|
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
|
||||||
@@ -191,8 +191,9 @@ const canConfirmDeparture = computed(() => canConfirmOutboundDispatch.value
|
|||||||
&& hasAction('CONFIRM_OUTBOUND')
|
&& hasAction('CONFIRM_OUTBOUND')
|
||||||
&& outboundDetail.value?.aggregate.dispatchStatus === 'READY_FOR_DISPATCH'
|
&& outboundDetail.value?.aggregate.dispatchStatus === 'READY_FOR_DISPATCH'
|
||||||
&& outboundGateSummary.value.blocked === 0)
|
&& outboundGateSummary.value.blocked === 0)
|
||||||
const customsDepartureState = computed<'DEPARTED' | 'WAITING_OTHER_GATES'>(() =>
|
const customsDepartureState = computed<'DEPARTED' | 'WAITING_OTHER_GATES' | 'UNKNOWN'>(() =>
|
||||||
outboundDetail.value?.aggregate.dispatchStatus === 'WAITING_FOR_LOADING' ? 'DEPARTED' : 'WAITING_OTHER_GATES')
|
!outboundDetail.value ? 'UNKNOWN'
|
||||||
|
: outboundDetail.value.aggregate.dispatchStatus === 'WAITING_FOR_LOADING' ? 'DEPARTED' : 'WAITING_OTHER_GATES')
|
||||||
function openOceanBookingLink() {
|
function openOceanBookingLink() {
|
||||||
if (!detail.value || !canLinkOceanBooking.value) return
|
if (!detail.value || !canLinkOceanBooking.value) return
|
||||||
void router.push({ name: 'domestic-container-workbench', query: { linkContainerId: detail.value.id } })
|
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
|
workbenchDetail.value = resource; detail.value = resource; overview.value = aggregate
|
||||||
if (activeTab.value === 'loading-list' && !canViewCurrentLoadingList.value) activeTab.value = 'overview'
|
if (activeTab.value === 'loading-list' && !canViewCurrentLoadingList.value) activeTab.value = 'overview'
|
||||||
if (isDomestic.value && activeTab.value === 'loading-list') await loadLoadingDetail(resource, aggregate)
|
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, '货柜详情加载失败') }
|
} catch (cause) { error.value = getApiErrorDetails(cause, '货柜详情加载失败') }
|
||||||
finally { loading.value = false }
|
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() {
|
async function loadOutboundDispatch() {
|
||||||
if (!containerId.value || !canViewOutboundDispatch.value) return
|
if (!containerId.value || !canViewOutboundDispatch.value) return
|
||||||
outboundLoading.value = true; outboundError.value = null; showPassedGates.value = false
|
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">
|
<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>
|
<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-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">
|
<div class="gate-list">
|
||||||
<article v-for="row in visibleOutboundGateRows" :key="row.key" class="gate-row">
|
<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>
|
<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