优化
This commit is contained in:
@@ -16,7 +16,12 @@ vi.mock('@/shared/api/http', () => ({
|
||||
}))
|
||||
|
||||
const container = { id: 9007199254740993, planId: 9007199254740994, warehouseId: 9007199254740995, shippingCompanyId: 9007199254740996, sourceType: 'SIMULATION', recordStatus: 'ACTIVE', mergedIntoContainerBookingId: null, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', version: '2', capacityCbm: '76', maxWeightKg: '28000' }
|
||||
const plan = { id: 9007199254740993, bookingId: 9007199254740994, version: '3', status: 'EDITING', items: [{ id: 9007199254740995, loadingPlanId: 9007199254740993, accountId: 1, inboundId: 2, inboundDetailId: 3, orderId: 4, destinationWarehouseId: 5, packageCount: '6', version: '1' }] }
|
||||
const plan = {
|
||||
id: 9007199254740993, bookingId: 9007199254740994, version: '3', status: 'LOADING',
|
||||
canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED',
|
||||
pdaTaskId: 9007199254740996, pdaTaskStatus: 'LOADING', pdaTaskVersion: '4',
|
||||
items: [{ id: 9007199254740995, loadingPlanId: 9007199254740993, accountId: 1, inboundId: 2, inboundDetailId: 3, orderId: 4, destinationWarehouseId: 5, packageCount: '6', version: '1', pickedPackageCount: '2', minPackageCount: '2', canAdjust: true, canRemove: false }],
|
||||
}
|
||||
|
||||
describe('platform container loading API', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
@@ -71,12 +76,13 @@ describe('platform container loading API', () => {
|
||||
vi.mocked(http.post).mockResolvedValue({ code: '200', data: plan })
|
||||
vi.mocked(http.patch).mockResolvedValue({ code: '200', data: plan })
|
||||
vi.mocked(http.delete).mockResolvedValue({ code: '200', data: plan })
|
||||
await createSmartLoadingPlan('7', null)
|
||||
const normalized = await createSmartLoadingPlan('7', null)
|
||||
await assignSmartLoadingItem('10', { inboundId: '2', inboundDetailId: '3', packageCount: 4, expectedVersion: 5 })
|
||||
await adjustSmartLoadingItem('10', '11', 6, 7, 8)
|
||||
await removeSmartLoadingItem('10', '11', 9, 10)
|
||||
await pushSmartLoadingPreload('10', 11)
|
||||
expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/first-leg/smart-loading/plans', { bookingId: '7', remark: null })
|
||||
expect(normalized).toMatchObject({ id: String(plan.id), version: 3, canAssign: true, canEdit: true, pdaTaskId: String(plan.pdaTaskId), pdaTaskVersion: 4, items: [{ version: 1, pickedPackageCount: 2, minPackageCount: 2, canAdjust: true, canRemove: false }] })
|
||||
expect(vi.mocked(http.post).mock.calls[1]?.[1]).toMatchObject({ expectedVersion: 5, operationId: expect.stringContaining('assign-') })
|
||||
expect(vi.mocked(http.patch).mock.calls[0]?.[1]).toMatchObject({ expectedPlanVersion: 7, expectedItemVersion: 8, operationId: expect.stringContaining('adjust-') })
|
||||
expect(vi.mocked(http.delete).mock.calls[0]?.[1]).toMatchObject({ data: { expectedPlanVersion: 9, expectedItemVersion: 10, operationId: expect.stringContaining('remove-') } })
|
||||
|
||||
@@ -89,6 +89,9 @@ function item(raw: SmartLoadingItem): SmartLoadingItem {
|
||||
inboundId: id(raw.inboundId), inboundDetailId: id(raw.inboundDetailId), orderId: id(raw.orderId),
|
||||
destinationWarehouseId: optionalId(raw.destinationWarehouseId), version: numberValue(raw.version),
|
||||
packageCount: numberValue(raw.packageCount),
|
||||
pickedPackageCount: numberValue(raw.pickedPackageCount),
|
||||
minPackageCount: numberValue(raw.minPackageCount, 1),
|
||||
canAdjust: Boolean(raw.canAdjust), canRemove: Boolean(raw.canRemove),
|
||||
productQuantity: raw.productQuantity == null ? null : numberValue(raw.productQuantity),
|
||||
grossWeightKg: raw.grossWeightKg == null ? null : numberValue(raw.grossWeightKg),
|
||||
volumeCbm: raw.volumeCbm == null ? null : numberValue(raw.volumeCbm),
|
||||
@@ -96,7 +99,15 @@ function item(raw: SmartLoadingItem): SmartLoadingItem {
|
||||
}
|
||||
|
||||
function plan(raw: SmartLoadingPlan): SmartLoadingPlan {
|
||||
return { ...raw, id: id(raw.id), bookingId: id(raw.bookingId), version: numberValue(raw.version), items: (raw.items || []).map(item) }
|
||||
return {
|
||||
...raw,
|
||||
id: id(raw.id), bookingId: id(raw.bookingId), version: numberValue(raw.version),
|
||||
canAssign: Boolean(raw.canAssign), canEdit: Boolean(raw.canEdit),
|
||||
blockedReason: raw.blockedReason || null,
|
||||
pdaTaskId: optionalId(raw.pdaTaskId),
|
||||
pdaTaskVersion: raw.pdaTaskVersion == null ? null : numberValue(raw.pdaTaskVersion),
|
||||
items: (raw.items || []).map(item),
|
||||
}
|
||||
}
|
||||
|
||||
export async function listContainers(query: ContainerQuery): Promise<ContainerPage> {
|
||||
|
||||
@@ -50,7 +50,9 @@ const container = {
|
||||
}
|
||||
const plan = {
|
||||
id: '99', bookingId: container.id, planNo: 'LP-99', status: 'EDITING' as const, containerNo: 'MSCU-1', voyageNo: null, version: 2,
|
||||
items: [{ id: '101', loadingPlanId: '99', accountId: '1', inboundId: '2', inboundDetailId: '3', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', customerName: '示例企业', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', skuCode: 'SKU-1', productNameZh: '服装', packageCount: 6, productQuantity: 6, grossWeightKg: 60, volumeCbm: 1.2, loadSequence: 1, version: 1 }],
|
||||
canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED' as const,
|
||||
pdaTaskId: null, pdaTaskStatus: null, pdaTaskVersion: null,
|
||||
items: [{ id: '101', loadingPlanId: '99', accountId: '1', inboundId: '2', inboundDetailId: '3', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', customerName: '示例企业', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', skuCode: 'SKU-1', productNameZh: '服装', packageCount: 6, productQuantity: 6, grossWeightKg: 60, volumeCbm: 1.2, loadSequence: 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true }],
|
||||
}
|
||||
|
||||
describe('GlobalSmartLoadingView', () => {
|
||||
@@ -191,7 +193,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.text()).not.toContain('是否退税')
|
||||
})
|
||||
|
||||
it('prefers an editable container on first load so pushed plans do not show a false editing error', async () => {
|
||||
it('prefers an editing container on first load while keeping loading containers available', async () => {
|
||||
const pushed = { ...container, id: '9007199254740995', planStatus: 'LOADING' as const }
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [pushed, container], page: 1, size: 100, total: 2, pages: 1 })
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
@@ -224,16 +226,19 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(waitingOption.text()).not.toContain('拣货中')
|
||||
})
|
||||
|
||||
it('excludes picking containers from the empty-container workbench', async () => {
|
||||
it('keeps picking containers available when the plan capability allows increments', async () => {
|
||||
const pushed = { ...container, id: '9007199254740995', planId: '9007199254740996', cabinetNo: 'CB-2', planStatus: 'LOADING' as const }
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [pushed], page: 1, size: 100, total: 1, pages: 1 })
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...plan, bookingId: pushed.id, status: 'LOADING', pdaTaskId: '700', pdaTaskStatus: 'LOADING', pdaTaskVersion: 4 })
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(getSmartLoadingPlanByBooking).not.toHaveBeenCalled()
|
||||
expect(listSmartLoadingCandidates).not.toHaveBeenCalled()
|
||||
expect(wrapper.findAll('.container-option')).toHaveLength(0)
|
||||
expect(wrapper.text()).not.toContain('CB-2')
|
||||
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(pushed.id)
|
||||
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(pushed.id, pushed.warehouseId)
|
||||
expect(wrapper.findAll('.container-option')).toHaveLength(1)
|
||||
expect(wrapper.text()).toContain('CB-2')
|
||||
expect(wrapper.text()).toContain('已下发 PDA,仍可维护未完成的装柜明细')
|
||||
expect(wrapper.text()).toContain('刷新 PDA 明细与版本')
|
||||
})
|
||||
|
||||
it('hides completed containers from the smart loading workbench', async () => {
|
||||
@@ -267,7 +272,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
|
||||
expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 2)
|
||||
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledTimes(2)
|
||||
expect(warningMessage).toHaveBeenCalledWith('方案已被其他人修改,已刷新最新版本,请重新确认后提交')
|
||||
expect(warningMessage).toHaveBeenCalledWith('计划或 PDA 明细已变化,已刷新最新版本并保留筛选条件,请重新操作')
|
||||
expect(wrapper.text()).not.toContain('装柜操作失败')
|
||||
expect(wrapper.text()).not.toContain('request-1')
|
||||
})
|
||||
@@ -307,7 +312,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
const removeAllButton = wrapper.findAll('button').find((button) => button.text().includes('全部移除'))
|
||||
const removeAllButton = wrapper.findAll('button').find((button) => button.text().includes('移除未拣'))
|
||||
expect(removeAllButton).toBeDefined()
|
||||
await removeAllButton!.trigger('click')
|
||||
await flushPromises()
|
||||
@@ -317,6 +322,43 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.text()).toContain('当前柜次暂无已装货物')
|
||||
})
|
||||
|
||||
it('uses item capabilities to protect picked quantities and removal', async () => {
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
|
||||
...plan,
|
||||
status: 'LOADING',
|
||||
pdaTaskId: '700',
|
||||
pdaTaskStatus: 'LOADING',
|
||||
pdaTaskVersion: 4,
|
||||
items: [{ ...plan.items[0], pickedPackageCount: 3, minPackageCount: 3, canAdjust: true, canRemove: false }],
|
||||
})
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('已拣 3 箱')
|
||||
expect(wrapper.findAll('.row-actions .el-button--danger')).toHaveLength(0)
|
||||
const quantityInput = wrapper.findAllComponents(ElInputNumber).find((input) => input.props('modelValue') === 6)
|
||||
expect(quantityInput?.props('min')).toBe(3)
|
||||
expect(wrapper.findAll('button').some((button) => button.text().includes('移除未拣'))).toBe(false)
|
||||
})
|
||||
|
||||
it('renders final completion capability as read-only with the server reason', async () => {
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
|
||||
...plan,
|
||||
canAssign: false,
|
||||
canEdit: false,
|
||||
blockedReason: '正在校验装柜证据',
|
||||
finalCompletionStatus: 'IN_PROGRESS',
|
||||
})
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('正在最终确认,装柜方案暂时只读')
|
||||
expect(wrapper.text()).toContain('正在校验装柜证据')
|
||||
expect(wrapper.text()).not.toContain('请先完成拖车')
|
||||
expect(wrapper.findAll('.row-actions button')).toHaveLength(0)
|
||||
expect(wrapper.findAll('button').some((button) => button.text().includes('加入'))).toBe(false)
|
||||
})
|
||||
|
||||
it('limits adjusted package count to the assigned count plus remaining availability', async () => {
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '2', inboundDetailId: '3', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '示例企业', goodsName: '服装', availablePackageCount: 4, unitWeightKg: 10, unitVolumeCbm: 0.2 }])
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
@@ -360,7 +402,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(source).not.toContain('channel.containerCount')
|
||||
expect(source).toContain('class="compact-summary" aria-label="当前渠道装柜统计"')
|
||||
expect(source).toContain('<dt>候选货物</dt>')
|
||||
expect(source).toContain('<dt>空柜</dt>')
|
||||
expect(source).toContain('<dt>柜次</dt>')
|
||||
expect(source).toContain('<dt>已装</dt>')
|
||||
expect(source).toContain('<dt>总重量</dt>')
|
||||
expect(source).toContain('<dt>总体积</dt>')
|
||||
|
||||
@@ -63,7 +63,7 @@ const selectedContainer = computed(() => containers.value.find((item) => item.id
|
||||
const channelTabs = computed(() => {
|
||||
const channels = new Map<string, { code: string; name: string }>()
|
||||
for (const item of containers.value) {
|
||||
if (!['WAIT_LOADING', 'EDITING'].includes(item.planStatus) || !item.channelCode) continue
|
||||
if (!['WAIT_LOADING', 'EDITING', 'LOADING'].includes(item.planStatus) || !item.channelCode) continue
|
||||
if (!channels.has(item.channelCode)) channels.set(item.channelCode, {
|
||||
code: item.channelCode,
|
||||
name: item.channelName || item.channelCode,
|
||||
@@ -72,7 +72,8 @@ const channelTabs = computed(() => {
|
||||
return [...channels.values()]
|
||||
})
|
||||
const activeChannel = computed(() => channelTabs.value.find((item) => item.code === activeChannelCode.value) || null)
|
||||
const editable = computed(() => plan.value?.status === 'EDITING')
|
||||
const editable = computed(() => plan.value?.canEdit === true)
|
||||
const assignable = computed(() => plan.value?.canAssign === true)
|
||||
const selectedPlanStatus = computed(() => selectedContainer.value?.planStatus || null)
|
||||
const assignedDetailIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundDetailId)))
|
||||
const totalPackages = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.packageCount || 0), 0))
|
||||
@@ -90,7 +91,7 @@ const queryFields = computed<QueryField[]>(() => [
|
||||
])
|
||||
|
||||
const filteredContainers = computed(() => containers.value.filter((item) => (
|
||||
['WAIT_LOADING', 'EDITING'].includes(item.planStatus)
|
||||
['WAIT_LOADING', 'EDITING', 'LOADING'].includes(item.planStatus)
|
||||
&& Boolean(activeChannelCode.value)
|
||||
&& item.channelCode === activeChannelCode.value
|
||||
)))
|
||||
@@ -106,6 +107,7 @@ const filteredCandidates = computed(() => {
|
||||
function preferredContainerId() {
|
||||
const visible = filteredContainers.value
|
||||
return visible.find((item) => item.planStatus === 'EDITING')?.id
|
||||
|| visible.find((item) => item.planStatus === 'LOADING')?.id
|
||||
|| visible.find((item) => item.planStatus === 'WAIT_LOADING')?.id || visible[0]?.id || ''
|
||||
}
|
||||
|
||||
@@ -136,7 +138,7 @@ function itemRow(source: object) { return source as SmartLoadingItem }
|
||||
function adjustmentMax(source: object) {
|
||||
const item = itemRow(source)
|
||||
const remaining = candidates.value.find((candidate) => candidate.inboundDetailId === item.inboundDetailId)?.availablePackageCount || 0
|
||||
return Math.max(1, item.packageCount + remaining)
|
||||
return Math.max(item.minPackageCount, item.packageCount + remaining)
|
||||
}
|
||||
function displayCabinetNo(container: ContainerBookingResource | null | undefined) {
|
||||
if (!container) return '待生成'
|
||||
@@ -300,7 +302,10 @@ async function adjust(source: object) {
|
||||
const item = itemRow(source)
|
||||
if (!plan.value) return
|
||||
const count = Number(quantityDrafts[item.id])
|
||||
if (!Number.isInteger(count) || count < 1) { ElMessage.warning('分配箱数必须为正整数'); return }
|
||||
if (!Number.isInteger(count) || count < item.minPackageCount) {
|
||||
ElMessage.warning(`分配箱数不能低于已拣下限 ${item.minPackageCount} 箱`)
|
||||
return
|
||||
}
|
||||
const max = adjustmentMax(item)
|
||||
if (count > max) { ElMessage.warning(`该货物最多可分配 ${max} 箱`); return }
|
||||
await mutate(async () => {
|
||||
@@ -311,7 +316,7 @@ async function adjust(source: object) {
|
||||
}
|
||||
async function remove(source: object) {
|
||||
const item = itemRow(source)
|
||||
if (!plan.value) return
|
||||
if (!plan.value || !item.canRemove) return
|
||||
const confirmed = await ElMessageBox.confirm(`确认从当前柜次移除“${item.productNameZh || item.inboundCode || '该货物'}”吗?`, '移除装柜明细', {
|
||||
type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消',
|
||||
}).catch(() => false)
|
||||
@@ -323,22 +328,23 @@ async function remove(source: object) {
|
||||
}, '移除装柜明细失败', { pickingConflict: true })
|
||||
}
|
||||
async function removeAll() {
|
||||
if (!plan.value?.items.length) return
|
||||
const itemCount = plan.value.items.length
|
||||
const confirmed = await ElMessageBox.confirm(`确认移除当前方案的全部 ${itemCount} 条装柜明细吗?`, '全部移除', {
|
||||
type: 'warning', confirmButtonText: '全部移除', cancelButtonText: '取消',
|
||||
const removableItems = plan.value?.items.filter((item) => item.canRemove) || []
|
||||
if (!removableItems.length) return
|
||||
const itemCount = removableItems.length
|
||||
const confirmed = await ElMessageBox.confirm(`确认移除当前方案的 ${itemCount} 条未拣明细吗?已拣明细会保留。`, '移除未拣明细', {
|
||||
type: 'warning', confirmButtonText: '确认移除', cancelButtonText: '取消',
|
||||
}).catch(() => false)
|
||||
if (!confirmed || !plan.value) return
|
||||
await mutate(async () => {
|
||||
let current = plan.value!
|
||||
for (const original of [...current.items]) {
|
||||
for (const original of removableItems) {
|
||||
const item = current.items.find((value) => value.id === original.id)
|
||||
if (!item) continue
|
||||
current = await removeSmartLoadingItem(current.id, item.id, current.version, item.version)
|
||||
acceptPlan(current)
|
||||
}
|
||||
await loadSelectedData()
|
||||
ElMessage.success(`已移除全部 ${itemCount} 条装柜明细`)
|
||||
ElMessage.success(`已移除 ${itemCount} 条未拣明细`)
|
||||
}, '全部移除失败', { pickingConflict: true })
|
||||
}
|
||||
async function pushPreload() {
|
||||
@@ -368,7 +374,7 @@ async function mutate(
|
||||
const pickingMessage = recovery.pickingConflict && details.message.includes('拣货')
|
||||
? '该订单在拣货中不许删除'
|
||||
: null
|
||||
const message = pickingMessage || '方案已被其他人修改,已刷新最新版本,请重新确认后提交'
|
||||
const message = pickingMessage || '计划或 PDA 明细已变化,已刷新最新版本并保留筛选条件,请重新操作'
|
||||
if (recovery.preserveAssign) assignOpen.value = true
|
||||
if (pickingMessage) ElMessage.error(pickingMessage)
|
||||
else ElMessage.warning(message)
|
||||
@@ -392,7 +398,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
</el-tabs>
|
||||
<dl class="compact-summary" aria-label="当前渠道装柜统计">
|
||||
<div><dt>候选货物</dt><dd><strong>{{ filteredCandidates.length }}</strong> 条</dd></div>
|
||||
<div><dt>空柜</dt><dd><strong>{{ filteredContainers.length }}</strong> 个</dd></div>
|
||||
<div><dt>柜次</dt><dd><strong>{{ filteredContainers.length }}</strong> 个</dd></div>
|
||||
<div><dt>已装</dt><dd><strong>{{ totalPackages }}</strong> 箱</dd></div>
|
||||
<div><dt>总重量</dt><dd><strong>{{ formatNumber(totalWeight, 1) }}</strong> kg</dd></div>
|
||||
<div><dt>总体积</dt><dd><strong>{{ formatNumber(totalVolume) }}</strong> m³</dd></div>
|
||||
@@ -426,7 +432,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
>
|
||||
<template #actions="{ row }">
|
||||
<el-button
|
||||
v-if="canAssign && editable"
|
||||
v-if="canAssign && assignable"
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
@@ -441,7 +447,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
</section>
|
||||
|
||||
<aside class="dispatch-pane container-pane" aria-label="货柜列表">
|
||||
<header class="pane-header"><div><strong>当前渠道空柜</strong><small>仅显示待装柜和预装编辑中的柜次</small></div></header>
|
||||
<header class="pane-header"><div><strong>当前渠道柜次</strong><small>显示待装柜、预装编辑和尚未最终完成的 PDA 柜次</small></div></header>
|
||||
<div v-loading="loading" class="container-list">
|
||||
<el-empty v-if="!filteredContainers.length" description="暂无符合条件的货柜" :image-size="72" />
|
||||
<button
|
||||
@@ -472,8 +478,8 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<div><strong>{{ hasRealContainerNo(selectedContainer) ? displayActualContainerNo(selectedContainer) : displayCabinetNo(selectedContainer) }} 装柜方案</strong><small>{{ selectedContainer ? `柜次号:${displayCabinetNo(selectedContainer)} · 集装箱号:${displayActualContainerNo(selectedContainer)}` : '尚未选择柜次' }}</small></div>
|
||||
<div class="plan-actions">
|
||||
<el-tooltip content="刷新当前柜次" placement="top"><el-button aria-label="刷新当前柜次" :loading="selectionLoading" @click="loadSelectedData()"><RefreshCw :size="16" /></el-button></el-tooltip>
|
||||
<el-button v-if="canAssign && editable" type="primary" plain :loading="mutating" @click="recommend"><Sparkles :size="16" />智能推荐</el-button>
|
||||
<el-button v-if="canDelete && editable && plan?.items.length" type="danger" plain :loading="mutating" @click="removeAll"><Trash2 :size="16" />全部移除</el-button>
|
||||
<el-button v-if="canAssign && assignable && plan?.status === 'EDITING'" type="primary" plain :loading="mutating" @click="recommend"><Sparkles :size="16" />智能推荐</el-button>
|
||||
<el-button v-if="canDelete && editable && plan?.items.some((item) => item.canRemove)" type="danger" plain :loading="mutating" @click="removeAll"><Trash2 :size="16" />移除未拣</el-button>
|
||||
<el-button v-if="canPush && plan?.status === 'EDITING'" type="primary" :disabled="!plan.items.length" :loading="mutating" @click="pushPreload"><Upload :size="16" />推送预装</el-button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -492,6 +498,24 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<el-button v-if="canCreate" type="primary" :loading="mutating" @click="createPlan"><Plus :size="16" />创建装柜方案</el-button>
|
||||
</el-empty>
|
||||
<template v-else-if="plan">
|
||||
<el-alert
|
||||
v-if="plan.status === 'LOADING' && plan.canEdit"
|
||||
class="preload-readiness-alert"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="已下发 PDA,仍可维护未完成的装柜明细"
|
||||
description="新增、增加或移除未拣明细会刷新 PDA 明细与版本;PDA 端需要刷新后继续操作。"
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="!plan.canEdit"
|
||||
class="preload-readiness-alert"
|
||||
:type="plan.finalCompletionStatus === 'COMPLETED' ? 'success' : 'info'"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="plan.finalCompletionStatus === 'IN_PROGRESS' ? '正在最终确认,装柜方案暂时只读' : plan.finalCompletionStatus === 'COMPLETED' ? '装柜已完成,方案永久只读' : '当前装柜方案只读'"
|
||||
:description="plan.blockedReason || '请刷新计划获取最新作业状态。'"
|
||||
/>
|
||||
<el-alert
|
||||
class="preload-readiness-alert"
|
||||
type="info"
|
||||
@@ -505,10 +529,10 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<div><div class="meter-title"><span>重量</span><strong>{{ formatNumber(totalWeight, 1) }} / {{ formatNumber(weightLimit, 1) }} kg</strong></div><el-progress :percentage="Math.min(displayedWeightPercent, 100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div>
|
||||
</div>
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="selectionLoading || mutating" stripe preference-key="global-smart-loading-items" column-settings-mode="hide" empty-text="当前柜次暂无已装货物">
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable" size="small" type="primary" plain :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存</el-button><el-button v-if="canDelete && editable" size="small" type="danger" plain @click="remove(row)"><Trash2 :size="15" />移除</el-button></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable && row.canAdjust" size="small" type="primary" plain :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存</el-button><el-button v-if="canDelete && editable && row.canRemove" size="small" type="danger" plain @click="remove(row)"><Trash2 :size="15" />移除</el-button><small v-if="row.pickedPackageCount > 0">已拣 {{ row.pickedPackageCount }} 箱</small></div></template>
|
||||
<template #order="{ row }"><strong>{{ row.inboundCode || '—' }}</strong></template>
|
||||
<template #goods="{ row }"><strong>{{ row.productNameZh || '未命名货物' }}</strong><small>{{ row.skuCode || '无 SKU' }}</small></template>
|
||||
<template #packageCount="{ row }"><el-input-number v-model="quantityDrafts[row.id]" :min="1" :max="adjustmentMax(row)" :step="1" :disabled="!canAssign || !editable" controls-position="right" /></template>
|
||||
<template #packageCount="{ row }"><el-input-number v-model="quantityDrafts[row.id]" :min="row.minPackageCount" :max="adjustmentMax(row)" :step="1" :disabled="!canAssign || !editable || !row.canAdjust" controls-position="right" /></template>
|
||||
<template #volume="{ row }">{{ formatNumber(Number(row.volumeCbm || 0)) }} m³</template>
|
||||
<template #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0), 1) }} kg</template>
|
||||
</DataTable>
|
||||
|
||||
@@ -25,13 +25,18 @@ const container = {
|
||||
terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null,
|
||||
estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null,
|
||||
}
|
||||
const planResponse = {
|
||||
id: '99', bookingId: container.id, planNo: 'LP-99', status: 'EDITING' as const, containerNo: 'MSCU-1', voyageNo: null, version: 2,
|
||||
canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED' as const,
|
||||
pdaTaskId: null, pdaTaskStatus: null, pdaTaskVersion: null, items: [],
|
||||
}
|
||||
|
||||
describe('SmartLoadingWorkbenchView', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(getContainer).mockResolvedValue(container)
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ id: '99', bookingId: container.id, planNo: 'LP-99', status: 'EDITING', containerNo: 'MSCU-1', voyageNo: null, version: 2, items: [] })
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(planResponse)
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '2', inboundDetailId: '3', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '示例企业', goodsName: '服装', availablePackageCount: 12, unitWeightKg: 10, unitVolumeCbm: 0.2 }])
|
||||
})
|
||||
|
||||
@@ -50,26 +55,26 @@ describe('SmartLoadingWorkbenchView', () => {
|
||||
expect(wrapper.text()).toContain('110% 容量上限')
|
||||
})
|
||||
|
||||
it('keeps picking containers out of intelligent loading and links to the preload list', async () => {
|
||||
it('uses loading plan capabilities to keep PDA increments editable', async () => {
|
||||
vi.mocked(getContainer).mockResolvedValue({ ...container, planStatus: 'LOADING' })
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ id: '99', bookingId: container.id, planNo: 'LP-99', status: 'LOADING', containerNo: 'MSCU-1', voyageNo: null, version: 2, items: [] })
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...planResponse, status: 'LOADING', pdaTaskId: '700', pdaTaskStatus: 'LOADING', pdaTaskVersion: 4 })
|
||||
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/container-loading/:containerId/smart-loading', component: SmartLoadingWorkbenchView }] })
|
||||
await router.push(`/container-loading/${container.id}/smart-loading`); await router.isReady()
|
||||
const wrapper = mount(SmartLoadingWorkbenchView, { global: { plugins: [ElementPlus, router] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(getSmartLoadingPlanByBooking).not.toHaveBeenCalled()
|
||||
expect(listSmartLoadingCandidates).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('该柜次已进入拣货流程')
|
||||
expect(wrapper.text()).toContain('查看预装清单')
|
||||
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
|
||||
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(container.id, container.warehouseId)
|
||||
expect(wrapper.text()).toContain('已下发 PDA,仍可维护未完成的装柜明细')
|
||||
expect(wrapper.text()).toContain('刷新 PDA 明细与版本')
|
||||
expect(wrapper.text()).not.toContain('智能推荐')
|
||||
expect(wrapper.text()).not.toContain('ORD-1')
|
||||
expect(wrapper.text()).toContain('IN-1')
|
||||
})
|
||||
|
||||
it('refreshes the latest plan after A0409', async () => {
|
||||
vi.mocked(getSmartLoadingPlanByBooking)
|
||||
.mockResolvedValueOnce({ id: '99', bookingId: container.id, planNo: 'LP-99', status: 'EDITING', containerNo: 'MSCU-1', voyageNo: null, version: 2, items: [] })
|
||||
.mockResolvedValueOnce({ id: '99', bookingId: container.id, planNo: 'LP-99', status: 'EDITING', containerNo: 'MSCU-1', voyageNo: null, version: 3, items: [] })
|
||||
.mockResolvedValueOnce(planResponse)
|
||||
.mockResolvedValueOnce({ ...planResponse, version: 3 })
|
||||
vi.mocked(recommendSmartLoading).mockRejectedValue(new AxiosError(
|
||||
'conflict', 'ERR_BAD_REQUEST', undefined, undefined,
|
||||
{ data: { code: 'A0409', message: '数据已被修改', requestId: 'request-1', traceId: 'trace-1' }, status: 200, statusText: 'OK', headers: {}, config: {} as never },
|
||||
|
||||
@@ -39,7 +39,8 @@ const canAssign = computed(() => access.can('platform:firstleg:smart-loading:ass
|
||||
const canDelete = computed(() => access.can('platform:firstleg:smart-loading:delete'))
|
||||
const canPush = computed(() => access.can('platform:firstleg:smart-loading:push'))
|
||||
const canViewLoadingList = computed(() => access.can('platform:firstleg:loading-list:list'))
|
||||
const editable = computed(() => plan.value?.status === 'EDITING')
|
||||
const editable = computed(() => plan.value?.canEdit === true)
|
||||
const assignable = computed(() => plan.value?.canAssign === true)
|
||||
const assignedDetailIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundDetailId)))
|
||||
const totalVolume = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0))
|
||||
const totalWeight = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.grossWeightKg || 0), 0))
|
||||
@@ -91,14 +92,17 @@ function displayContainerNo(value: ContainerBookingResource | null | undefined)
|
||||
const containerNo = String(value?.containerNo || '').trim()
|
||||
return containerNo && !/^(?:MAN|SPLIT)-/i.test(containerNo) ? containerNo : '待录入'
|
||||
}
|
||||
function syncDrafts() { for (const item of plan.value?.items || []) quantityDrafts[item.id] = item.packageCount }
|
||||
function acceptPlan(next: SmartLoadingPlan) { plan.value = next; noDraft.value = false; syncDrafts() }
|
||||
function syncDrafts(preserved: Record<string, number> = {}) {
|
||||
for (const key of Object.keys(quantityDrafts)) delete quantityDrafts[key]
|
||||
for (const item of plan.value?.items || []) quantityDrafts[item.id] = preserved[item.id] ?? item.packageCount
|
||||
}
|
||||
function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> = {}) { plan.value = next; noDraft.value = false; syncDrafts(preserved) }
|
||||
|
||||
async function load() {
|
||||
loading.value = true; loadError.value = null; noDraft.value = false
|
||||
try {
|
||||
container.value = await getContainer(containerId.value)
|
||||
if (['LOADING', 'COMPLETED'].includes(container.value.planStatus)) {
|
||||
if (container.value.planStatus === 'COMPLETED') {
|
||||
plan.value = null
|
||||
candidates.value = []
|
||||
return
|
||||
@@ -109,7 +113,9 @@ async function load() {
|
||||
if (detail.code === 'A0404') { plan.value = null; noDraft.value = true }
|
||||
else throw error
|
||||
}
|
||||
candidates.value = await listSmartLoadingCandidates(container.value.id, container.value.warehouseId)
|
||||
candidates.value = plan.value?.canAssign
|
||||
? await listSmartLoadingCandidates(container.value.id, container.value.warehouseId)
|
||||
: []
|
||||
} catch (error) { loadError.value = getApiErrorDetails(error, '智能排柜工作台加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
@@ -150,7 +156,7 @@ async function adjust(source: object) {
|
||||
const item = itemRow(source)
|
||||
if (!plan.value) return
|
||||
const count = Number(quantityDrafts[item.id])
|
||||
if (!Number.isInteger(count) || count < 1) { ElMessage.warning('分配箱数必须为正整数'); return }
|
||||
if (!Number.isInteger(count) || count < item.minPackageCount) { ElMessage.warning(`分配箱数不能低于已拣下限 ${item.minPackageCount} 箱`); return }
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await adjustSmartLoadingItem(plan.value.id, item.id, count, plan.value.version, item.version)); ElMessage.success('箱数已调整') }
|
||||
@@ -159,7 +165,7 @@ async function adjust(source: object) {
|
||||
}
|
||||
async function remove(source: object) {
|
||||
const item = itemRow(source)
|
||||
if (!plan.value) return
|
||||
if (!plan.value || !item.canRemove) return
|
||||
await ElMessageBox.confirm(`确认从计划移除“${item.productNameZh || item.inboundCode || '该货物'}”吗?`, '移除装柜明细', { type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消' }).catch(() => false).then(async (confirmed) => {
|
||||
if (!confirmed || !plan.value) return
|
||||
mutating.value = true
|
||||
@@ -184,14 +190,12 @@ async function handleMutationError(error: unknown, fallback: string, pickingConf
|
||||
if (details.code === 'A0409') {
|
||||
const preserved = { ...quantityDrafts }
|
||||
await load()
|
||||
for (const item of plan.value?.items || []) {
|
||||
if (preserved[item.id] != null) quantityDrafts[item.id] = preserved[item.id]
|
||||
}
|
||||
syncDrafts(preserved)
|
||||
}
|
||||
const pickingMessage = pickingConflict && details.code === 'A0409' && details.message.includes('拣货')
|
||||
? '该订单在拣货中不许删除'
|
||||
: null
|
||||
mutationError.value = { ...details, message: pickingMessage || (details.code === 'A0409' ? '计划已被其他人修改,已刷新最新版本并保留本次输入,请重新确认后提交' : details.message) }
|
||||
mutationError.value = { ...details, message: pickingMessage || (details.code === 'A0409' ? '计划或 PDA 明细已变化,已刷新最新版本并保留本次输入,请重新操作' : details.message) }
|
||||
ElMessage.error(mutationError.value.message)
|
||||
}
|
||||
|
||||
@@ -215,7 +219,7 @@ onMounted(load)
|
||||
<div class="header-actions">
|
||||
<el-button text aria-label="返回货柜列表" @click="router.back()"><ArrowLeft :size="18" />返回</el-button>
|
||||
<el-tooltip content="刷新" placement="top"><el-button :loading="loading" aria-label="刷新智能排柜" @click="load"><RefreshCw :size="16" /></el-button></el-tooltip>
|
||||
<el-button v-if="canAssign && editable" type="primary" plain :loading="mutating" @click="recommend"><Sparkles :size="16" />智能推荐</el-button>
|
||||
<el-button v-if="canAssign && assignable && plan?.status === 'EDITING'" type="primary" plain :loading="mutating" @click="recommend"><Sparkles :size="16" />智能推荐</el-button>
|
||||
<el-button v-if="canPush && plan?.status === 'EDITING'" type="primary" :disabled="!plan.items.length" :loading="mutating" @click="pushPreload"><Upload :size="16" />推送预装</el-button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -228,27 +232,24 @@ onMounted(load)
|
||||
<p>智能装柜方案已锁定,不能继续调整。请进入装柜清单查看完整记录。</p>
|
||||
<el-button v-if="canViewLoadingList" type="primary" @click="openOperationalList"><ListChecks :size="16" />查看装柜清单</el-button>
|
||||
</section>
|
||||
<section v-else-if="container?.planStatus === 'LOADING'" class="readonly-status-card" aria-label="已进入拣货流程">
|
||||
<strong>该柜次已进入拣货流程</strong>
|
||||
<p>智能装柜仅处理待装柜和预装编辑中的空柜,请在预装清单继续查看。</p>
|
||||
<el-button v-if="canViewLoadingList" type="primary" @click="openOperationalList"><ListChecks :size="16" />查看预装清单</el-button>
|
||||
</section>
|
||||
<template v-else>
|
||||
<section v-if="container" class="summary-band"><div><span>柜型</span><strong>{{ container.containerType }}</strong></div><div><span>装柜仓库</span><strong>{{ container.warehouseName || '—' }}</strong></div></section>
|
||||
<section v-if="container" class="capacity-grid"><div><div class="meter-title"><span>体积占用(含 110% 上限)</span><strong>{{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</strong></div><el-progress :percentage="Math.min(displayedVolumePercent,100)" :status="volumePercent > 100 ? 'exception' : undefined" /></div><div><div class="meter-title"><span>重量占用(含 110% 上限)</span><strong>{{ formatNumber(totalWeight,1) }} / {{ formatNumber(weightLimit,1) }} kg</strong></div><el-progress :percentage="Math.min(displayedWeightPercent,100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div></section>
|
||||
<el-empty v-if="noDraft" description="该货柜尚未创建排柜计划"><el-button v-if="canCreate" type="primary" :loading="mutating" @click="createPlan"><Plus :size="16" />创建排柜计划</el-button></el-empty>
|
||||
<template v-else-if="plan">
|
||||
<el-alert v-if="plan.status === 'LOADING' && plan.canEdit" type="warning" :closable="false" show-icon title="已下发 PDA,仍可维护未完成的装柜明细" description="新增、增加或移除未拣明细会刷新 PDA 明细与版本;PDA 端需要刷新后继续操作。" />
|
||||
<el-alert v-else-if="!plan.canEdit" :type="plan.finalCompletionStatus === 'COMPLETED' ? 'success' : 'info'" :closable="false" show-icon :title="plan.finalCompletionStatus === 'IN_PROGRESS' ? '正在最终确认,装柜方案暂时只读' : plan.finalCompletionStatus === 'COMPLETED' ? '装柜已完成,方案永久只读' : '当前装柜方案只读'" :description="plan.blockedReason || '请刷新计划获取最新作业状态。'" />
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="loading || mutating" stripe preference-key="smart-loading-items" column-settings-mode="hide" empty-text="暂无已分配货物">
|
||||
<template #toolbar-start><div><h2>{{ displayContainerNo(container) }} 装柜方案</h2><p>智能建议必须经人工核对。</p></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable" size="small" type="primary" plain :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存箱数</el-button><el-button v-if="canDelete && editable" size="small" type="danger" plain @click="remove(row)"><Trash2 :size="15" />移除</el-button></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable && row.canAdjust" size="small" type="primary" plain :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存箱数</el-button><el-button v-if="canDelete && editable && row.canRemove" size="small" type="danger" plain @click="remove(row)"><Trash2 :size="15" />移除</el-button><small v-if="row.pickedPackageCount > 0">已拣 {{ row.pickedPackageCount }} 箱</small></div></template>
|
||||
<template #order="{ row }"><strong>{{ row.inboundCode || '—' }}</strong></template>
|
||||
<template #goods="{ row }"><strong>{{ row.productNameZh || '未命名货物' }}</strong><small>{{ row.skuCode || '无 SKU' }}</small></template>
|
||||
<template #packageCount="{ row }"><el-input-number v-model="quantityDrafts[row.id]" :min="1" :step="1" :disabled="!canAssign || !editable" controls-position="right" /></template>
|
||||
<template #packageCount="{ row }"><el-input-number v-model="quantityDrafts[row.id]" :min="row.minPackageCount" :step="1" :disabled="!canAssign || !editable || !row.canAdjust" controls-position="right" /></template>
|
||||
<template #volume="{ row }">{{ formatNumber(Number(row.volumeCbm || 0)) }} m³</template><template #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0),1) }} kg</template>
|
||||
</DataTable>
|
||||
<DataTable :data="candidates" :columns="candidateColumns" :loading="loading || mutating" stripe preference-key="smart-loading-candidates" column-settings-mode="hide" empty-text="当前仓库暂无可排柜货物">
|
||||
<template #toolbar-start><div><h2>候选货物</h2><p>系统基于装柜仓库、物流渠道、可用箱数及 110% 容量上限校验。</p></div></template>
|
||||
<template #actions="{ row }"><el-button v-if="canAssign && editable" size="small" type="primary" plain :disabled="assignedDetailIds.has(row.inboundDetailId) || row.availablePackageCount < 1" @click="openAssign(row)"><Plus :size="15" />分配</el-button></template>
|
||||
<template #actions="{ row }"><el-button v-if="canAssign && assignable" size="small" type="primary" plain :disabled="assignedDetailIds.has(row.inboundDetailId) || row.availablePackageCount < 1" @click="openAssign(row)"><Plus :size="15" />分配</el-button></template>
|
||||
<template #order="{ row }"><strong>{{ row.inboundCode || '—' }}</strong></template>
|
||||
<template #unitVolume="{ row }">{{ formatNumber(Number(row.unitVolumeCbm || 0)) }} m³</template><template #unitWeight="{ row }">{{ formatNumber(Number(row.unitWeightKg || 0),1) }} kg</template>
|
||||
</DataTable>
|
||||
|
||||
@@ -243,6 +243,10 @@ export interface SmartLoadingItem {
|
||||
volumeCbm: number | null
|
||||
loadSequence: number | null
|
||||
version: number
|
||||
pickedPackageCount: number
|
||||
minPackageCount: number
|
||||
canAdjust: boolean
|
||||
canRemove: boolean
|
||||
}
|
||||
|
||||
export interface SmartLoadingPlan {
|
||||
@@ -253,5 +257,12 @@ export interface SmartLoadingPlan {
|
||||
containerNo: string | null
|
||||
voyageNo: string | null
|
||||
version: number
|
||||
canAssign: boolean
|
||||
canEdit: boolean
|
||||
blockedReason: string | null
|
||||
finalCompletionStatus: 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED'
|
||||
pdaTaskId: string | null
|
||||
pdaTaskStatus: 'LOADING' | 'COMPLETED' | null
|
||||
pdaTaskVersion: number | null
|
||||
items: SmartLoadingItem[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user