feat: preview container inbound plans
This commit is contained in:
@@ -212,6 +212,64 @@ export interface ConvertContainerInboundPlanPayload {
|
||||
sourceContainerBookingId: string
|
||||
}
|
||||
|
||||
export type ContainerInboundPlanPreviewReadiness = 'READY' | 'PLANNED' | 'BLOCKED'
|
||||
|
||||
export interface ContainerInboundPlanPreviewOrderDto {
|
||||
customerId: string
|
||||
customerName: string | null
|
||||
orderId: string
|
||||
businessOrderNo: string
|
||||
inboundCode: string | null
|
||||
orderType: string
|
||||
destinationType: 'FBA' | 'ONE_CLICK_DROPSHIP' | 'PRIVATE_ADDRESS'
|
||||
destinationWarehouseId: string | null
|
||||
destinationWarehouseCode: string | null
|
||||
destinationWarehouseName: string | null
|
||||
packageCount: number
|
||||
volumeCbm: number
|
||||
suggestedPalletCount: number | null
|
||||
plannedPalletCount: number | null
|
||||
existingPlanNo: string | null
|
||||
planStatus: string | null
|
||||
orderVersion: number
|
||||
readiness: ContainerInboundPlanPreviewReadiness
|
||||
blockers: string[]
|
||||
}
|
||||
|
||||
export interface ContainerInboundPlanPreviewGroupDto {
|
||||
destinationType: 'FBA' | 'ONE_CLICK_DROPSHIP' | 'PRIVATE_ADDRESS'
|
||||
destinationWarehouseId: string | null
|
||||
destinationWarehouseCode: string | null
|
||||
destinationWarehouseName: string | null
|
||||
orderCount: number
|
||||
pendingOrderCount: number
|
||||
pendingPalletCount: number
|
||||
existingPlannedPalletCount: number
|
||||
suggestedPalletCount: number
|
||||
plannedPalletCount: number
|
||||
orders: ContainerInboundPlanPreviewOrderDto[]
|
||||
}
|
||||
|
||||
export interface ContainerInboundPlanPreviewDto {
|
||||
sourceContainerBookingId: string
|
||||
containerNo: string | null
|
||||
overseasWarehouseId: string | null
|
||||
overseasWarehouseCode: string | null
|
||||
overseasWarehouseName: string | null
|
||||
snapshotVersion: number
|
||||
snapshotHash: string
|
||||
totalOrderCount: number
|
||||
readyOrderCount: number
|
||||
blockedOrderCount: number
|
||||
pendingOrderCount: number
|
||||
pendingPalletCount: number
|
||||
existingPlannedPalletCount: number
|
||||
totalSuggestedPalletCount: number
|
||||
totalPlannedPalletCount: number
|
||||
ready: boolean
|
||||
groups: ContainerInboundPlanPreviewGroupDto[]
|
||||
}
|
||||
|
||||
export type ManualNodeCode =
|
||||
| 'DOMESTIC_CUSTOMS_COMPLETED'
|
||||
| 'ESTIMATED_DEPARTURE'
|
||||
@@ -394,6 +452,14 @@ export async function changeContainerReceivingWarehouse(
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function getContainerInboundPlanPreview(sourceContainerBookingId: string) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<ContainerInboundPlanPreviewDto>, ApiEnvelope<ContainerInboundPlanPreviewDto>>(
|
||||
'/platform/v1/last-leg/inbound-plans/container-preview',
|
||||
{ params: { sourceContainerBookingId } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function convertLastMileContainerToInboundPlans(payload: ConvertContainerInboundPlanPayload) {
|
||||
const response = await overseasHttp.post<ApiEnvelope<InboundPlanDetailDto[]>, ApiEnvelope<InboundPlanDetailDto[]>>(
|
||||
'/platform/v1/last-leg/inbound-plans/convert-container', payload,
|
||||
|
||||
@@ -13,7 +13,7 @@ import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, downloadLastMileOrders, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode } from '../api/lastMileOrders'
|
||||
import { changeContainerReceivingWarehouse, createManualInboundPlan, downloadLastMileOrders, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode } from '../api/lastMileOrders'
|
||||
import OrderReferenceText from '../components/OrderReferenceText.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
@@ -72,6 +72,9 @@ const containerOptions = ref<ContainerPlanProgressSummaryDto[]>([])
|
||||
const containerOptionsLoading = ref(false)
|
||||
const containerLoadError = ref<ApiErrorDetails | null>(null)
|
||||
const selectedContainerBookingId = ref('')
|
||||
const containerPreview = ref<ContainerInboundPlanPreviewDto | null>(null)
|
||||
const containerPreviewLoading = ref(false)
|
||||
const containerPreviewError = ref('')
|
||||
const receivingWarehouseOpen = ref(false)
|
||||
const receivingWarehouseLoading = ref(false)
|
||||
const receivingWarehouseError = ref<ApiErrorDetails | null>(null)
|
||||
@@ -543,6 +546,8 @@ function openContainerConversion() {
|
||||
selectedContainerBookingId.value = ''
|
||||
containerOptions.value = []
|
||||
containerLoadError.value = null
|
||||
containerPreview.value = null
|
||||
containerPreviewError.value = ''
|
||||
actionError.value = null
|
||||
containerConvertOpen.value = true
|
||||
void loadContainerOptions()
|
||||
@@ -551,31 +556,69 @@ function openContainerConversionFor(container: ContainerPlanProgressSummaryDto)
|
||||
selectedContainerBookingId.value = container.sourceContainerBookingId
|
||||
containerOptions.value = [container]
|
||||
containerLoadError.value = null
|
||||
containerPreview.value = null
|
||||
containerPreviewError.value = ''
|
||||
actionError.value = null
|
||||
containerConvertOpen.value = true
|
||||
void loadContainerPreview(container.sourceContainerBookingId)
|
||||
}
|
||||
async function convertContainerInboundPlans() {
|
||||
const container = selectedContainer.value
|
||||
if (!container?.sourceContainerBookingId) { ElMessage.warning('请选择一个待分配柜'); return }
|
||||
if (!canGenerateContainerPlans(container)) {
|
||||
actionError.value = { message: container.blockingReason || '所选柜当前不满足生成资格' }
|
||||
return
|
||||
}
|
||||
actionLoading.value = true
|
||||
actionError.value = null
|
||||
async function loadContainerPreview(sourceContainerBookingId = selectedContainerBookingId.value) {
|
||||
containerPreview.value = null
|
||||
containerPreviewError.value = ''
|
||||
if (!sourceContainerBookingId) return
|
||||
containerPreviewLoading.value = true
|
||||
try {
|
||||
const plans = await convertLastMileContainerToInboundPlans({ sourceContainerBookingId: container.sourceContainerBookingId })
|
||||
containerConvertOpen.value = false
|
||||
selectedContainerBookingId.value = ''
|
||||
ElMessage.success(`已按柜生成 ${plans.length} 个独立入库计划`)
|
||||
await load()
|
||||
containerPreview.value = await getContainerInboundPlanPreview(sourceContainerBookingId)
|
||||
} catch (error) {
|
||||
actionError.value = getApiErrorDetails(error, '生成整柜入库计划失败,请重试')
|
||||
containerPreviewError.value = getApiErrorDetails(error, '加载整柜入库计划预览失败,请重试').message
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
containerPreviewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function previewGroupTitle(group: ContainerInboundPlanPreviewGroupDto) {
|
||||
if (group.destinationType === 'FBA') return `${group.destinationWarehouseName || group.destinationWarehouseCode || '未配置目的仓'} FBA仓`
|
||||
return group.destinationType === 'ONE_CLICK_DROPSHIP' ? '一件代发' : '私人地址'
|
||||
}
|
||||
|
||||
function destinationText(order: { destinationWarehouseCode?: string | null; destinationWarehouseName?: string | null }) {
|
||||
return [order.destinationWarehouseCode, order.destinationWarehouseName].filter(Boolean).join(' / ') || '—'
|
||||
}
|
||||
|
||||
function orderTypeText(orderType: string) {
|
||||
const labels: Record<string, string> = {
|
||||
FIRST_LEG: '头程订单',
|
||||
RELABEL_APPLICATION: '换标下单',
|
||||
WAREHOUSE_TRANSFER: '仓储转运',
|
||||
LOCAL_LOOSEBOARD: '本土散板',
|
||||
RETURN_RELABEL: '退货换标',
|
||||
ONE_CLICK_DROPSHIP: '一件代发',
|
||||
}
|
||||
return labels[orderType] || orderType
|
||||
}
|
||||
|
||||
function readinessText(readiness: ContainerInboundPlanPreviewOrderDto['readiness']) {
|
||||
return readiness === 'READY' ? '可生成' : readiness === 'PLANNED' ? '已有计划' : '已阻断'
|
||||
}
|
||||
|
||||
function readinessType(readiness: ContainerInboundPlanPreviewOrderDto['readiness']) {
|
||||
return readiness === 'READY' ? 'success' : readiness === 'PLANNED' ? 'info' : 'danger'
|
||||
}
|
||||
|
||||
function blockerText(blockers: string[]) {
|
||||
const labels: Record<string, string> = {
|
||||
MISSING_INBOUND_CODE: '缺少入仓码',
|
||||
ORDER_DETAINED: '订单已暂扣',
|
||||
ORDER_STATUS_NOT_READY: '订单状态未就绪',
|
||||
CONTAINER_NO_INCONSISTENT: '货柜号不一致',
|
||||
RECEIVING_WAREHOUSE_INCONSISTENT: '收货仓不一致',
|
||||
DESTINATION_NOT_READY: '目的地未就绪',
|
||||
CARGO_FACT_INVALID: '货物数据无效',
|
||||
DROPSHIP_PALLET_SNAPSHOT_MISSING: '缺少一件代发托盘快照',
|
||||
}
|
||||
return blockers.map((code) => labels[code] || code).join(';') || '—'
|
||||
}
|
||||
|
||||
function openDetail(source: object) {
|
||||
const row = source as LastMileOrderListItem
|
||||
void router.push({ name: 'overseas-last-mile-order-detail', params: { orderId: row.id } })
|
||||
@@ -660,19 +703,52 @@ onMounted(() => { void load() })
|
||||
<el-form-item label="备注"><el-input v-model="transferForm.remark" type="textarea" maxlength="500" placeholder="请输入转仓备注(选填)" /></el-form-item>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
<AppDialog v-model="containerConvertOpen" title="按柜批量生成入库计划" width="640px" confirm-text="确认按柜生成" :loading="containerOptionsLoading" :confirm-loading="actionLoading" :confirm-disabled="!selectedContainer || !canGenerateContainerPlans(selectedContainer)" @confirm="convertContainerInboundPlans">
|
||||
<p class="container-convert-hint">将为所选柜内所有待分配尾程订单分别生成独立入库计划。</p>
|
||||
<AppDialog v-model="containerConvertOpen" title="按柜生成入库计划预览" width="min(1240px, calc(100vw - 80px))" confirm-text="确认生成(接口待就绪)" :loading="containerOptionsLoading || containerPreviewLoading" :confirm-disabled="true">
|
||||
<p class="container-convert-hint">先读取服务端全柜预览,确认分组、托数和阻断原因。当前仅开放预览,确认接口就绪前不会提交生成请求。</p>
|
||||
<LoadErrorState v-if="containerLoadError" title="待分配柜加载失败" :message="containerLoadError.message" compact @retry="loadContainerOptions()">
|
||||
<template #details><ApiErrorMeta :details="containerLoadError" /></template>
|
||||
</LoadErrorState>
|
||||
<el-form v-else label-position="top">
|
||||
<el-form-item label="选择待分配柜" required>
|
||||
<el-select v-model="selectedContainerBookingId" filterable remote clearable :remote-method="loadContainerOptions" :loading="containerOptionsLoading" placeholder="输入柜号搜索并选择">
|
||||
<el-option v-for="container in containerOptions" :key="container.sourceContainerBookingId" :value="container.sourceContainerBookingId" :disabled="!canGenerateContainerPlans(container)" :label="`${container.containerNo || '柜号缺失'} · ${container.totalOrderCount} 票${canGenerateContainerPlans(container) ? '' : ` · ${container.blockingReason || '不可生成'}`}`" />
|
||||
<el-select v-model="selectedContainerBookingId" filterable remote clearable :remote-method="loadContainerOptions" :loading="containerOptionsLoading" placeholder="输入柜号搜索并选择" @change="loadContainerPreview">
|
||||
<el-option v-for="container in containerOptions" :key="container.sourceContainerBookingId" :value="container.sourceContainerBookingId" :label="`${container.containerNo || '柜号缺失'} · ${container.totalOrderCount} 票`" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-alert v-if="selectedContainer" :title="canGenerateContainerPlans(selectedContainer) ? `确认后将为该柜剩余 ${pendingInboundCodeCount(selectedContainer)} 个入仓码分别生成独立入库计划` : selectedContainer.blockingReason || '所选柜不可生成'" :type="canGenerateContainerPlans(selectedContainer) ? 'info' : 'error'" :closable="false" show-icon />
|
||||
</el-form>
|
||||
<el-alert v-if="containerPreviewError" class="container-convert-alert" type="error" :closable="false" show-icon :title="containerPreviewError" />
|
||||
<div v-if="containerPreview" class="container-preview">
|
||||
<div class="container-preview__context">
|
||||
<div class="container-preview__identity text-ellipsis" :title="containerPreview.containerNo || containerPreview.sourceContainerBookingId">{{ containerPreview.containerNo || '未录入货柜号' }}</div>
|
||||
<div class="container-preview__warehouse text-ellipsis" :title="[containerPreview.overseasWarehouseCode, containerPreview.overseasWarehouseName].filter(Boolean).join(' / ') || '未关联海外仓'">{{ [containerPreview.overseasWarehouseCode, containerPreview.overseasWarehouseName].filter(Boolean).join(' / ') || '未关联海外仓' }}</div>
|
||||
</div>
|
||||
<div class="container-preview__metrics">
|
||||
<div class="container-preview__metric container-preview__metric--primary"><span>本次新增托盘码</span><strong>{{ containerPreview.pendingPalletCount }}</strong></div>
|
||||
<div class="container-preview__metric"><span>本次新增计划</span><strong>{{ containerPreview.pendingOrderCount }}</strong></div>
|
||||
<div class="container-preview__metric"><span>已有计划托盘</span><strong>{{ containerPreview.existingPlannedPalletCount }}</strong></div>
|
||||
<div class="container-preview__metric" :class="{ 'container-preview__metric--danger': containerPreview.blockedOrderCount > 0 }"><span>阻断订单</span><strong>{{ containerPreview.blockedOrderCount }}</strong></div>
|
||||
</div>
|
||||
<el-alert v-if="containerPreview.blockedOrderCount > 0" class="container-convert-alert" type="error" :closable="false" show-icon title="存在阻断订单,暂不可确认生成;请按下方原因处理后重新预览。" />
|
||||
<el-alert v-else class="container-convert-alert" type="warning" :closable="false" show-icon title="预览已就绪;确认接口完成接入前,暂不开放提交。" />
|
||||
<div class="container-preview__scroll">
|
||||
<section v-for="group in containerPreview.groups" :key="`${group.destinationType}-${group.destinationWarehouseId || group.destinationWarehouseCode || 'default'}`" class="container-preview__group">
|
||||
<header class="container-preview__group-header">
|
||||
<strong class="text-ellipsis" :title="previewGroupTitle(group)">{{ previewGroupTitle(group) }}:{{ group.pendingPalletCount }}托</strong>
|
||||
<span>本次 {{ group.pendingOrderCount }} 票 / 已有 {{ group.existingPlannedPalletCount }} 托 / 共 {{ group.orderCount }} 票</span>
|
||||
</header>
|
||||
<el-table :data="group.orders" size="small" border>
|
||||
<el-table-column label="入仓码" min-width="148" show-overflow-tooltip><template #default="{ row }">{{ row.inboundCode || '—' }}</template></el-table-column>
|
||||
<el-table-column prop="businessOrderNo" label="订单号" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="客户" min-width="140" show-overflow-tooltip><template #default="{ row }">{{ row.customerName || '—' }}</template></el-table-column>
|
||||
<el-table-column label="业务类型" min-width="112" show-overflow-tooltip><template #default="{ row }">{{ orderTypeText(row.orderType) }}</template></el-table-column>
|
||||
<el-table-column label="目的地" min-width="190" show-overflow-tooltip><template #default="{ row }">{{ destinationText(row) }}</template></el-table-column>
|
||||
<el-table-column label="建议托数" width="94" align="right"><template #default="{ row }">{{ row.suggestedPalletCount ?? '—' }}</template></el-table-column>
|
||||
<el-table-column label="已有计划托数" width="118" align="right"><template #default="{ row }">{{ row.plannedPalletCount ?? '—' }}</template></el-table-column>
|
||||
<el-table-column label="可生成性" width="104"><template #default="{ row }"><el-tag :type="readinessType(row.readiness)" effect="light">{{ readinessText(row.readiness) }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="阻断原因" min-width="210" show-overflow-tooltip><template #default="{ row }">{{ blockerText(row.blockers) }}</template></el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</AppDialog>
|
||||
<AppDialog v-model="containerDetailOpen" :title="`柜子详情 · ${containerDetail?.summary.containerNo || '加载中'}`" width="min(1120px, calc(100vw - 40px))" :loading="containerDetailLoading" :show-footer="false">
|
||||
<LoadErrorState v-if="containerDetailError" title="柜级详情加载失败" :message="containerDetailError.message" compact>
|
||||
@@ -717,7 +793,7 @@ onMounted(() => { void load() })
|
||||
<AppDialog v-model="inboundPlanOpen" title="入库计划" width="min(720px, calc(100vw - 28px))" confirm-text="保存并生成入库单" :confirm-loading="actionLoading" @confirm="submitInboundPlan">
|
||||
<el-alert title="保存后生成入库单;清关完成后入库计划不可更改。" type="info" :closable="false" show-icon />
|
||||
<el-form class="command-form" label-position="top">
|
||||
<div class="command-form__grid"><el-form-item label="计划入库仓"><el-input :model-value="inboundPlanWarehouseLabel" disabled placeholder="当前柜接柜仓" /><small>接柜仓按柜统一;如需变更,请在柜子工作台执行“变更接柜仓”。</small></el-form-item><el-form-item label="预计到仓日期" required><el-date-picker v-model="inboundPlanForm.plannedArrivalDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期" /></el-form-item><el-form-item label="入仓码"><el-input :model-value="inboundPlanForm.inboundCode" disabled placeholder="订单冻结入仓码" /></el-form-item><el-form-item label="托数" required><el-input-number v-model="inboundPlanForm.palletCount" :min="1" :max="999" controls-position="right" placeholder="请输入托数" /></el-form-item><el-form-item label="托盘类型"><el-select v-model="inboundPlanForm.palletType" placeholder="请选择托盘类型"><el-option label="标准托盘" value="STANDARD" /><el-option label="木托盘" value="WOOD" /><el-option label="纸托盘" value="PAPER" /></el-select></el-form-item><el-form-item label="是否分配"><el-switch v-model="inboundPlanForm.assigned" inline-prompt active-text="是" inactive-text="否" /></el-form-item><el-form-item label="是否合托"><el-switch v-model="inboundPlanForm.mergeRequired" inline-prompt active-text="是" inactive-text="否" /></el-form-item><el-form-item label="是否加急"><el-switch v-model="inboundPlanForm.urgent" inline-prompt active-text="是" inactive-text="否" /></el-form-item></div>
|
||||
<div class="command-form__grid"><el-form-item label="计划入库仓"><el-input :model-value="inboundPlanWarehouseLabel" disabled placeholder="当前柜接柜仓" /><small>接柜仓按柜统一;如需变更,请在柜子工作台执行“变更接柜仓”。</small></el-form-item><el-form-item label="预计到仓日期" required><el-date-picker v-model="inboundPlanForm.plannedArrivalDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期" /></el-form-item><el-form-item label="入仓码"><el-input :model-value="inboundPlanForm.inboundCode" disabled placeholder="订单冻结入仓码" /></el-form-item><el-form-item label="托数" required><el-input-number v-model="inboundPlanForm.palletCount" :min="1" :max="999" controls-position="right" placeholder="请输入托数" /></el-form-item><el-form-item label="托盘类型"><el-select v-model="inboundPlanForm.palletType" placeholder="请选择托盘类型"><el-option label="标准托盘" value="STANDARD" /><el-option label="木托盘" value="WOOD" /><el-option label="纸托盘" value="PAPER" /></el-select></el-form-item><el-form-item label="是否分配"><el-switch v-model="inboundPlanForm.assigned" inline-prompt active-text="是" inactive-text="否" /></el-form-item><el-form-item label="允许合托"><el-switch v-model="inboundPlanForm.mergeRequired" inline-prompt active-text="是" inactive-text="否" /><small>仅作为现场可选提示,不强制工人合托。</small></el-form-item><el-form-item label="是否加急"><el-switch v-model="inboundPlanForm.urgent" inline-prompt active-text="是" inactive-text="否" /></el-form-item></div>
|
||||
<el-form-item label="备注"><el-input v-model="inboundPlanForm.remark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="请输入备注(选填)" /></el-form-item>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
@@ -747,6 +823,23 @@ onMounted(() => { void load() })
|
||||
.manual-node-table__row :deep(.el-date-editor) { width: 100%; }
|
||||
.last-mile-toolbar :deep(.query-toolbar) { margin-top: 12px; padding: 12px 0 16px; border: 0; border-top: 1px solid var(--cc-border); border-radius: 0; }
|
||||
.container-convert-hint { margin: 0 0 16px; color: var(--cc-muted); line-height: 1.6; }
|
||||
.container-convert-alert { margin-top: 12px; }
|
||||
.container-preview { margin-top: 16px; }
|
||||
.container-preview__context, .container-preview__group-header { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
|
||||
.container-preview__identity { color: var(--cc-text); font-size: 18px; font-weight: 700; }
|
||||
.container-preview__warehouse, .container-preview__group-header span { color: var(--cc-muted); font-size: 13px; }
|
||||
.container-preview__metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 14px 0; }
|
||||
.container-preview__metric { display: flex; min-height: 72px; padding: 12px 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: var(--cc-surface); flex-direction: column; justify-content: center; }
|
||||
.container-preview__metric span { color: var(--cc-muted); font-size: 13px; }
|
||||
.container-preview__metric strong { margin-top: 4px; color: var(--cc-text); font-size: 24px; line-height: 1; }
|
||||
.container-preview__metric--primary { border-color: var(--cc-primary); background: #f3f6ff; }
|
||||
.container-preview__metric--primary strong { color: var(--cc-primary); }
|
||||
.container-preview__metric--danger strong { color: var(--el-color-danger); }
|
||||
.container-preview__scroll { max-height: 48vh; margin-top: 14px; padding-right: 6px; overflow-y: auto; }
|
||||
.container-preview__group + .container-preview__group { margin-top: 16px; }
|
||||
.container-preview__group-header { min-height: 42px; padding: 0 14px; border: 1px solid var(--cc-border); border-bottom: 0; border-radius: 8px 8px 0 0; background: var(--cc-surface); }
|
||||
.container-preview__group-header strong { min-width: 0; color: var(--cc-text); }
|
||||
.text-ellipsis { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.container-detail-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px 16px; margin-bottom: 12px; padding: 12px 14px; border: 1px solid var(--cc-border); border-radius: 6px; background: var(--cc-surface); font-size: 13px; }
|
||||
.container-detail-summary > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.container-workbench { padding: 18px; }
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const apiSource = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/api/lastMileOrders.ts'), 'utf8')
|
||||
const pageSource = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages/LastMileOrdersView.vue'), 'utf8')
|
||||
|
||||
describe('container inbound plan preview contract', () => {
|
||||
it('uses the backend preview endpoint with the stable container booking id', () => {
|
||||
expect(apiSource).toContain("'/platform/v1/last-leg/inbound-plans/container-preview'")
|
||||
expect(apiSource).toContain('{ params: { sourceContainerBookingId } }')
|
||||
})
|
||||
|
||||
it('renders backend groups, pallet totals, readiness and blockers without local grouping or sorting', () => {
|
||||
expect(pageSource).toContain('containerPreview.pendingPalletCount')
|
||||
expect(pageSource).toContain('containerPreview.existingPlannedPalletCount')
|
||||
expect(pageSource).toContain('v-for="group in containerPreview.groups"')
|
||||
expect(pageSource).toContain('readinessText(row.readiness)')
|
||||
expect(pageSource).toContain('blockerText(row.blockers)')
|
||||
expect(pageSource).not.toContain('containerPreview.groups.sort(')
|
||||
})
|
||||
|
||||
it('requests one preview per selection and does not submit the legacy conversion command', () => {
|
||||
expect(pageSource.match(/await getContainerInboundPlanPreview\(sourceContainerBookingId\)/g)).toHaveLength(1)
|
||||
expect(pageSource).not.toContain('convertLastMileContainerToInboundPlans')
|
||||
expect(pageSource).toContain(':confirm-disabled="true"')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user