feat(admin): complete customs from container detail
This commit is contained in:
@@ -11,7 +11,7 @@ import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { CarrierScheduleQueryPanel } from '@/modules/port-schedules'
|
||||
import { uploadLoadingDeclaration } from '@/modules/loading-lists'
|
||||
import { completeLoadingDeclaration, uploadLoadingDeclaration } from '@/modules/loading-lists'
|
||||
import { confirmContainerOutboundDispatch, getContainerOutboundDispatch } from '@/modules/outbound-dispatch/api/platformOutboundDispatch'
|
||||
import type { ContainerOutboundDispatchDetail, ContainerOutboundGateItem } from '@/modules/outbound-dispatch/types'
|
||||
import ContainerOverviewPanel from '../components/ContainerOverviewPanel.vue'
|
||||
@@ -48,6 +48,8 @@ const documentFile = ref<File | null>(null)
|
||||
const documentSaving = ref(false)
|
||||
const documentDownloading = ref(false)
|
||||
const documentError = ref<ApiErrorDetails | null>(null)
|
||||
const customsCompleting = ref(false)
|
||||
const customsCompletionError = ref<ApiErrorDetails | null>(null)
|
||||
const clearanceOpen = ref(false)
|
||||
const clearanceDocument = ref<BusinessDocument | null>(null)
|
||||
const clearanceFile = ref<File | null>(null)
|
||||
@@ -70,11 +72,12 @@ const documentTitle = computed(() => ({ DELIVERY_ORDER: 'DO 资料', PICKUP_ORDE
|
||||
const title = computed(() => detail.value?.containerNo || detail.value?.cabinetNo || '货柜详情')
|
||||
const exceptionTitle = computed(() => overview.value?.exception.active ? '取消货柜异常' : '标记货柜异常')
|
||||
type AggregatedGateRow = ContainerOutboundGateItem & { key: string; inboundCodes: string[] }
|
||||
const referenceOnlyGateCodes = new Set(['TRUCK_DEPARTED', 'SAILING_SCHEDULE'])
|
||||
const outboundGateRows = computed<AggregatedGateRow[]>(() => {
|
||||
const grouped = new Map<string, AggregatedGateRow>()
|
||||
for (const inbound of outboundDetail.value?.aggregate.gate.inbounds || []) {
|
||||
for (const item of inbound.gate.items || []) {
|
||||
if (item.code === 'TRUCK_DEPARTED') continue
|
||||
if (referenceOnlyGateCodes.has(item.code)) continue
|
||||
const key = JSON.stringify([item.code, item.passed, item.actual ?? null, item.requirement ?? null, item.name])
|
||||
const existing = grouped.get(key)
|
||||
if (existing) {
|
||||
@@ -175,6 +178,32 @@ async function saveDocument() {
|
||||
} catch (cause) { documentError.value = getApiErrorDetails(cause, `${documentTitle.value}上传失败`) }
|
||||
finally { documentSaving.value = false }
|
||||
}
|
||||
async function refreshCustomsFacts() {
|
||||
overview.value = await getContainerOverview(containerId.value)
|
||||
await loadOutboundDispatch()
|
||||
}
|
||||
async function completeCustomsDeclaration(document: ContainerOverviewDocument) {
|
||||
if (!canManageCustoms.value || customsCompleting.value || document.completed) return
|
||||
if (!overview.value?.loadingPlanId || !detail.value) {
|
||||
customsCompletionError.value = { code: 'LOADING_PLAN_REQUIRED', message: '当前货柜没有装柜清单,无法确认报关完成' }
|
||||
return
|
||||
}
|
||||
customsCompleting.value = true; customsCompletionError.value = null
|
||||
try {
|
||||
await ElMessageBox.confirm('请确认装柜清单的国内出口报关协同已经完成。附件上传与确认报关完成是两个步骤;上传附件本身不代表正式报关放行。', '确认报关完成', { type: 'warning', confirmButtonText: '确认完成' })
|
||||
} catch { customsCompleting.value = false; return }
|
||||
try {
|
||||
await completeLoadingDeclaration(overview.value.loadingPlanId, detail.value.warehouseId, document.version, '货柜详情确认国内出口报关完成')
|
||||
await refreshCustomsFacts()
|
||||
ElMessage.success('已确认装柜清单报关协同完成;正式报关放行仍由报关业务事实核验')
|
||||
} catch (cause) {
|
||||
const details = getApiErrorDetails(cause, '确认报关完成失败')
|
||||
if (details.code === 'A0409') {
|
||||
try { await refreshCustomsFacts() } catch { /* 保留原并发错误证据 */ }
|
||||
}
|
||||
customsCompletionError.value = details
|
||||
} finally { customsCompleting.value = false }
|
||||
}
|
||||
async function saveBlob(blob: Blob, fileName: string) { const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = fileName || '货柜资料'; anchor.click(); URL.revokeObjectURL(url) }
|
||||
async function downloadDocument(item: ContainerOverviewDocument) {
|
||||
if (!['DELIVERY_ORDER', 'PICKUP_ORDER'].includes(item.documentType)) return
|
||||
@@ -232,7 +261,8 @@ onMounted(async () => { await load(); if (route.query.action === 'documents') op
|
||||
<template v-else-if="detail">
|
||||
<section class="detail-fields surface-panel"><h2>运输与单证快照</h2><dl><div><dt>真实订舱</dt><dd>{{ detail.oceanBookingId ? (detail.billOfLadingNo || '已关联') : '未关联' }}</dd></div><div><dt>异常状态</dt><dd>{{ detail.exceptionActive ? '异常' : '正常' }}</dd></div><div><dt>目的仓</dt><dd>{{ detail.destinationWarehouseName || detail.destinationWarehouseCode || '—' }}</dd></div><div><dt>船公司</dt><dd>{{ detail.shippingCompanyName || '—' }}</dd></div><div><dt>船名 / 航次</dt><dd>{{ [detail.vesselName, detail.voyageNo].filter(Boolean).join(' / ') || '—' }}</dd></div><div><dt>拖车公司</dt><dd>{{ detail.terminalTruckingCompany || '—' }}</dd></div><div><dt>预计开船</dt><dd>{{ formatTime(detail.estimatedSailingAt) }}</dd></div><div><dt>预计到港</dt><dd>{{ formatTime(detail.estimatedPortArrivalAt) }}</dd></div><div><dt>卸柜完成</dt><dd>{{ formatTime(detail.unloadingCompletedAt) }}</dd></div><div><dt>卸柜照片上传</dt><dd>{{ formatTime(detail.unloadingPhotoUploadedAt) }}</dd></div><div><dt>预计报空</dt><dd>{{ formatTime(detail.estimatedEmptyReportedAt) }}</dd></div><div><dt>实际报空</dt><dd>{{ formatTime(detail.actualEmptyReportedAt) }}</dd></div></dl></section>
|
||||
<LoadErrorState v-if="overviewError" compact title="履约摘要加载失败" :message="overviewError.message" :show-retry="false" />
|
||||
<ContainerOverviewPanel v-else-if="overview" :overview="overview" :can-manage-documents="canEdit" :can-manage-customs="canManageCustoms" :can-manage-exception="canEdit" :document-busy="documentDownloading" @upload-document="openDocument" @download-document="downloadDocument" @manage-exception="openException" />
|
||||
<ContainerOverviewPanel v-else-if="overview" :overview="overview" :can-manage-documents="canEdit" :can-manage-customs="canManageCustoms" :can-manage-exception="canEdit" :document-busy="documentDownloading" :customs-completing="customsCompleting" @upload-document="openDocument" @download-document="downloadDocument" @complete-customs="completeCustomsDeclaration" @manage-exception="openException" />
|
||||
<LoadErrorState v-if="customsCompletionError" compact title="确认报关完成失败" :message="customsCompletionError.message" :show-retry="false"><template #details><ApiErrorMeta :details="customsCompletionError" /></template></LoadErrorState>
|
||||
<section v-if="detail.planStatus === 'COMPLETED' && canViewOutboundDispatch" class="outbound-panel surface-panel" aria-label="国内出仓门禁">
|
||||
<header class="outbound-panel__header"><div><h2>国内出仓门禁</h2><p>按整柜核验全部入库绑定,仅确认一次离仓。</p></div><div v-if="outboundDetail" class="outbound-panel__actions"><el-tag :type="outboundGateSummary.blocked === 0 ? 'success' : 'danger'">{{ outboundGateSummary.blocked === 0 ? '门禁通过' : '门禁阻断' }}</el-tag><StatusBadge :status="outboundStatusText(outboundDetail.aggregate.dispatchStatus)" /><el-button v-if="canConfirmDeparture" type="primary" :loading="outboundConfirming" @click="confirmDeparture">确认离仓</el-button></div></header>
|
||||
<LoadErrorState v-if="outboundError" compact title="国内出仓操作失败" :message="outboundError.message" :loading="outboundLoading" @retry="loadOutboundDispatch"><template #details><ApiErrorMeta :details="outboundError" /></template></LoadErrorState>
|
||||
|
||||
Reference in New Issue
Block a user