优化
This commit is contained in:
@@ -17,7 +17,6 @@ import {
|
||||
removeSmartLoadingGroup,
|
||||
removeSmartLoadingItem,
|
||||
} from '../api/platformContainerLoading'
|
||||
import { batchQueryPlatformCoLoadGroups } from '@/modules/unified-orders'
|
||||
|
||||
const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() }))
|
||||
|
||||
@@ -31,9 +30,6 @@ vi.mock('@/core/access/accessPreview', () => ({
|
||||
vi.mock('@/modules/iam', () => ({
|
||||
listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }),
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders', () => ({
|
||||
batchQueryPlatformCoLoadGroups: vi.fn().mockResolvedValue([]),
|
||||
}))
|
||||
vi.mock('../api/platformContainerLoading', () => ({
|
||||
listContainers: vi.fn(),
|
||||
getSmartLoadingPlanByBooking: vi.fn(),
|
||||
@@ -47,6 +43,7 @@ vi.mock('../api/platformContainerLoading', () => ({
|
||||
removeSmartLoadingGroup: vi.fn(),
|
||||
pushSmartLoadingPreload: vi.fn(),
|
||||
}))
|
||||
vi.mock('../smartLoadingPlanGroups', () => ({ loadSmartLoadingPlanCoLoadGroups: vi.fn().mockResolvedValue(new Map()) }))
|
||||
|
||||
const container = {
|
||||
id: '9007199254740993', planId: '9007199254740994', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1',
|
||||
@@ -70,7 +67,6 @@ describe('GlobalSmartLoadingView', () => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [container], page: 1, size: 100, total: 1, pages: 1 })
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(plan)
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([])
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '另一企业', goodsSummary: '鞋类', availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 2.4, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null }])
|
||||
})
|
||||
|
||||
@@ -244,36 +240,6 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(assignSmartLoadingGroup).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('aggregates assigned members into one locked group row and removes the group once', async () => {
|
||||
const baseItems = ['201', '202'].map((inboundId, index) => ({
|
||||
...plan.items[0], id: `70${index}`, accountId: `${index + 1}`, inboundId, inboundDetailId: `80${index}`, orderId: `30${index}`, orderNo: `ORD-${index}`, inboundCode: `NYEXX260829005${index + 1}`, customerName: `客户${index}`, packageCount: 5, grossWeightKg: 50, volumeCbm: 1, loadSequence: index + 1,
|
||||
}))
|
||||
const items = [baseItems[0], { ...baseItems[0], id: '703', inboundId: '203', inboundDetailId: '803', inboundCode: 'NYEXX2608290051-B', packageCount: 3, grossWeightKg: 30, volumeCbm: 0.6 }, baseItems[1]]
|
||||
const groupedPlan = { ...plan, items }
|
||||
const group = {
|
||||
groupId: '2093539779795795969', groupNo: 'CLG-2093539779795795969', status: 'LOCKED' as const, warehouseId: '8', channelCode: 'ZBYHK', lockedPlanId: '99', version: 1,
|
||||
members: items.map((item) => ({ tenantId: item.accountId, orderId: item.orderId, inboundId: item.inboundId, inboundCode: item.inboundCode, fullPackageCount: item.packageCount })), fullPackageCount: 13, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:30:00',
|
||||
}
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValueOnce(groupedPlan).mockResolvedValue({ ...groupedPlan, version: 3, items: [] })
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([...new Set(items.map((item) => item.orderId))].map((orderId) => ({ orderId, group })))
|
||||
vi.mocked(removeSmartLoadingGroup).mockResolvedValue({ ...groupedPlan, version: 3, items: [] })
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('CLG-2093539779795795969 · 2 票 · 3 个入仓片段')
|
||||
expect(wrapper.text()).toContain('ORD-0:NYEXX2608290051、NYEXX2608290051-B')
|
||||
expect(wrapper.text()).toContain('固定 13 箱 · 已锁定')
|
||||
expect(wrapper.text()).toContain('整组固定全箱数,不允许单票调整')
|
||||
expect(wrapper.findAllComponents(ElInputNumber).some((input) => input.props('modelValue') === 13)).toBe(false)
|
||||
const removeButton = wrapper.findAll('button').find((button) => button.text().includes('移除整组'))!
|
||||
await removeButton.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(removeSmartLoadingGroup).toHaveBeenCalledWith(plan.id, group.groupId, plan.version)
|
||||
expect(removeSmartLoadingItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the package count editor inside its table cell without overflow ellipsis', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
|
||||
expect(source).toContain('<div v-else class="package-count-editor"><el-input-number')
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Plus, RefreshCw, Save, Sparkles, Trash2, Upload } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { batchQueryPlatformCoLoadGroups, type CoLoadGroup } from '@/modules/unified-orders'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
@@ -32,6 +31,7 @@ import type {
|
||||
SmartLoadingPlan,
|
||||
} from '../types'
|
||||
import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLoadingItems'
|
||||
import { loadSmartLoadingPlanCoLoadGroups, type SmartLoadingPlanCoLoadGroup } from '../smartLoadingPlanGroups'
|
||||
|
||||
const props = withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
|
||||
|
||||
@@ -55,7 +55,7 @@ const assignOpen = ref(false)
|
||||
const selectedCandidate = ref<SmartLoadingCandidateRow | null>(null)
|
||||
const assignForm = reactive({ packageCount: 1 })
|
||||
const quantityDrafts = reactive<Record<string, number>>({})
|
||||
const planGroupsByInboundId = ref(new Map<string, CoLoadGroup>())
|
||||
const planGroupsByInboundId = ref(new Map<string, SmartLoadingPlanCoLoadGroup>())
|
||||
const query = ref<QueryModel>({ customerId: '', inboundCode: '' })
|
||||
const appliedQuery = ref<QueryModel>({ ...query.value })
|
||||
let selectionRequest = 0
|
||||
@@ -95,10 +95,10 @@ function memberOrderSummaries<T extends { orderId: string; orderNo?: string | nu
|
||||
}
|
||||
return [...orders.values()].map((order) => `${order.label}:${order.inboundCodes.join('、')}`)
|
||||
}
|
||||
function groupOrderCount(group: CoLoadGroup) { return new Set(group.members.map((member) => member.orderId)).size }
|
||||
function groupMemberLabel(group: CoLoadGroup) {
|
||||
const orderCount = groupOrderCount(group)
|
||||
return group.members.length === orderCount ? `${orderCount} 票` : `${orderCount} 票 · ${group.members.length} 个入仓片段`
|
||||
function groupMemberLabel(group: SmartLoadingPlanCoLoadGroup) {
|
||||
return group.inboundFragmentCount === group.memberOrderCount
|
||||
? `${group.memberOrderCount} 票`
|
||||
: `${group.memberOrderCount} 票 · ${group.inboundFragmentCount} 个入仓片段`
|
||||
}
|
||||
const displayedCandidates = computed<SmartLoadingCandidateRow[]>(() => {
|
||||
const rows: SmartLoadingCandidateRow[] = []
|
||||
@@ -128,7 +128,7 @@ const displayedCandidates = computed<SmartLoadingCandidateRow[]>(() => {
|
||||
}
|
||||
return rows
|
||||
})
|
||||
type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: CoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] }
|
||||
type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: SmartLoadingPlanCoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] }
|
||||
const displayedPlanItems = computed<SmartLoadingPlanRow[]>(() => {
|
||||
const inboundRows = groupSmartLoadingItems(plan.value?.items || [])
|
||||
const result: SmartLoadingPlanRow[] = []
|
||||
@@ -261,18 +261,6 @@ function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> =
|
||||
if (syncContainerStatus) container.planStatus = next.status
|
||||
}
|
||||
}
|
||||
async function loadPlanGroups(request = selectionRequest) {
|
||||
planGroupsByInboundId.value = new Map()
|
||||
const currentPlan = plan.value
|
||||
const orderIds = [...new Set((currentPlan?.items || []).map((item) => item.orderId))]
|
||||
if (!currentPlan || !orderIds.length || !canAssign.value) return
|
||||
const groups = await batchQueryPlatformCoLoadGroups(orderIds)
|
||||
if (request !== selectionRequest || plan.value?.id !== currentPlan.id) return
|
||||
const byInbound = new Map<string, CoLoadGroup>()
|
||||
for (const info of groups) for (const member of info.group.members) byInbound.set(member.inboundId, info.group)
|
||||
planGroupsByInboundId.value = byInbound
|
||||
syncDrafts(currentPlan)
|
||||
}
|
||||
function clearSelectionData() {
|
||||
plan.value = null
|
||||
candidates.value = []
|
||||
@@ -303,7 +291,8 @@ async function loadSelectedData(preserved: Record<string, number> = {}) {
|
||||
candidates.value = candidateResult
|
||||
if (planResult.value) {
|
||||
acceptPlan(planResult.value, preserved, false)
|
||||
await loadPlanGroups(request)
|
||||
planGroupsByInboundId.value = await loadSmartLoadingPlanCoLoadGroups(planResult.value)
|
||||
syncDrafts(planResult.value, preserved)
|
||||
}
|
||||
else noDraft.value = planResult.missing
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,14 +8,13 @@ import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import SmartLoadingWorkbenchView from './SmartLoadingWorkbenchView.vue'
|
||||
import { assignSmartLoadingGroup, assignSmartLoadingItem, getContainer, getSmartLoadingPlanByBooking, listSmartLoadingCandidates, recommendSmartLoading, removeSmartLoadingGroup } from '../api/platformContainerLoading'
|
||||
import { batchQueryPlatformCoLoadGroups } from '@/modules/unified-orders'
|
||||
|
||||
vi.mock('../api/platformContainerLoading', () => ({
|
||||
getContainer: vi.fn(), getSmartLoadingPlanByBooking: vi.fn(), listSmartLoadingCandidates: vi.fn(),
|
||||
createSmartLoadingPlan: vi.fn(), assignSmartLoadingItem: vi.fn(), assignSmartLoadingGroup: vi.fn(), recommendSmartLoading: vi.fn(),
|
||||
adjustSmartLoadingItem: vi.fn(), removeSmartLoadingItem: vi.fn(), removeSmartLoadingGroup: vi.fn(), pushSmartLoadingPreload: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders', () => ({ batchQueryPlatformCoLoadGroups: vi.fn().mockResolvedValue([]) }))
|
||||
vi.mock('../smartLoadingPlanGroups', () => ({ loadSmartLoadingPlanCoLoadGroups: vi.fn().mockResolvedValue(new Map()) }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||||
|
||||
const container = {
|
||||
@@ -178,31 +177,4 @@ describe('SmartLoadingWorkbenchView', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('removes a locked group once instead of looping through member items', async () => {
|
||||
const baseItems = ['201', '202'].map((inboundId, index) => ({
|
||||
id: `70${index}`, loadingPlanId: '99', accountId: `${index + 1}`, inboundId, inboundDetailId: `80${index}`, orderId: `30${index}`,
|
||||
orderNo: `ORD-${index}`, inboundCode: `IN-${inboundId}`, customerName: `客户${index}`, destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9',
|
||||
skuCode: `SKU-${index}`, productNameZh: `货物${index}`, packageCount: 5, productQuantity: 50, grossWeightKg: 50, volumeCbm: 1,
|
||||
loadSequence: index + 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true,
|
||||
}))
|
||||
const items = [baseItems[0], { ...baseItems[0], id: '703', inboundId: '203', inboundDetailId: '803', inboundCode: 'IN-203', packageCount: 3, productQuantity: 30, grossWeightKg: 30, volumeCbm: 0.6 }, baseItems[1]]
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...planResponse, items })
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([...new Set(items.map((item) => item.orderId))].map((orderId) => ({ orderId, group: {
|
||||
groupId: '9900', groupNo: 'CLG-9900', status: 'LOCKED', warehouseId: '8', channelCode: 'ZBYHK', lockedPlanId: '99', version: 1,
|
||||
members: items.map((member) => ({ tenantId: member.accountId, orderId: member.orderId, inboundId: member.inboundId, inboundCode: member.inboundCode, fullPackageCount: member.packageCount })), fullPackageCount: 13, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:30:00',
|
||||
} })))
|
||||
vi.mocked(removeSmartLoadingGroup).mockResolvedValue({ ...planResponse, version: 3, items: [] })
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
|
||||
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(wrapper.text()).toContain('CLG-9900 · 2 票 · 3 个入仓片段')
|
||||
expect(wrapper.text()).toContain('ORD-0:IN-201、IN-203')
|
||||
expect(wrapper.text()).toContain('全部入仓片段联动,固定 13 箱')
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('移除整组'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(removeSmartLoadingGroup).toHaveBeenCalledWith('99', '9900', 2)
|
||||
expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.stringContaining('组内所有入仓片段将联动移除'), '移除同柜不可拆组', expect.any(Object))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft, ListChecks, Plus, RefreshCw, Save, Sparkles, Trash2, Upload } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { batchQueryPlatformCoLoadGroups, type CoLoadGroup } from '@/modules/unified-orders'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
} from '../api/platformContainerLoading'
|
||||
import type { ContainerBookingResource, SmartLoadingCandidate, SmartLoadingPlan } from '../types'
|
||||
import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLoadingItems'
|
||||
import { loadSmartLoadingPlanCoLoadGroups, type SmartLoadingPlanCoLoadGroup } from '../smartLoadingPlanGroups'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -35,7 +35,7 @@ const assignOpen = ref(false)
|
||||
const selectedCandidate = ref<SmartLoadingCandidateRow | null>(null)
|
||||
const assignForm = reactive({ packageCount: 1 })
|
||||
const quantityDrafts = reactive<Record<string, number>>({})
|
||||
const planGroupsByInboundId = ref(new Map<string, CoLoadGroup>())
|
||||
const planGroupsByInboundId = ref(new Map<string, SmartLoadingPlanCoLoadGroup>())
|
||||
|
||||
const canCreate = computed(() => access.can('platform:firstleg:smart-loading:create'))
|
||||
const canAssign = computed(() => access.can('platform:firstleg:smart-loading:assign'))
|
||||
@@ -56,10 +56,10 @@ function memberOrderSummaries<T extends { orderId: string; orderNo?: string | nu
|
||||
}
|
||||
return [...orders.values()].map((order) => `${order.label}:${order.inboundCodes.join('、')}`)
|
||||
}
|
||||
function groupOrderCount(group: CoLoadGroup) { return new Set(group.members.map((member) => member.orderId)).size }
|
||||
function groupMemberLabel(group: CoLoadGroup) {
|
||||
const orderCount = groupOrderCount(group)
|
||||
return group.members.length === orderCount ? `${orderCount} 票` : `${orderCount} 票 · ${group.members.length} 个入仓片段`
|
||||
function groupMemberLabel(group: SmartLoadingPlanCoLoadGroup) {
|
||||
return group.inboundFragmentCount === group.memberOrderCount
|
||||
? `${group.memberOrderCount} 票`
|
||||
: `${group.memberOrderCount} 票 · ${group.inboundFragmentCount} 个入仓片段`
|
||||
}
|
||||
const displayedCandidates = computed<SmartLoadingCandidateRow[]>(() => {
|
||||
const rows: SmartLoadingCandidateRow[] = []
|
||||
@@ -89,7 +89,7 @@ const displayedCandidates = computed<SmartLoadingCandidateRow[]>(() => {
|
||||
}
|
||||
return rows
|
||||
})
|
||||
type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: CoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] }
|
||||
type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: SmartLoadingPlanCoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] }
|
||||
const displayedPlanItems = computed<SmartLoadingPlanRow[]>(() => {
|
||||
const inboundRows = groupSmartLoadingItems(plan.value?.items || [])
|
||||
const result: SmartLoadingPlanRow[] = []
|
||||
@@ -187,15 +187,10 @@ function syncDrafts(preserved: Record<string, number> = {}) {
|
||||
for (const item of displayedPlanItems.value) 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 loadPlanGroups() {
|
||||
planGroupsByInboundId.value = new Map()
|
||||
const orderIds = [...new Set((plan.value?.items || []).map((item) => item.orderId))]
|
||||
if (!orderIds.length || !canAssign.value) return
|
||||
const groups = await batchQueryPlatformCoLoadGroups(orderIds)
|
||||
const byInbound = new Map<string, CoLoadGroup>()
|
||||
for (const info of groups) for (const member of info.group.members) byInbound.set(member.inboundId, info.group)
|
||||
planGroupsByInboundId.value = byInbound
|
||||
syncDrafts()
|
||||
async function acceptPlanWithGroups(next: SmartLoadingPlan, preserved: Record<string, number> = {}) {
|
||||
acceptPlan(next, preserved)
|
||||
planGroupsByInboundId.value = await loadSmartLoadingPlanCoLoadGroups(next)
|
||||
syncDrafts(preserved)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
@@ -208,7 +203,7 @@ async function load() {
|
||||
candidates.value = []
|
||||
return
|
||||
}
|
||||
try { acceptPlan(await getSmartLoadingPlanByBooking(containerId.value)); await loadPlanGroups() }
|
||||
try { await acceptPlanWithGroups(await getSmartLoadingPlanByBooking(containerId.value)) }
|
||||
catch (error) {
|
||||
const detail = getApiErrorDetails(error)
|
||||
if (detail.code === 'A0404') { plan.value = null; noDraft.value = true }
|
||||
@@ -230,7 +225,7 @@ function openOperationalList() {
|
||||
async function createPlan() {
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await createSmartLoadingPlan(containerId.value, null)); ElMessage.success('排柜计划已创建') }
|
||||
try { await acceptPlanWithGroups(await createSmartLoadingPlan(containerId.value, null)); ElMessage.success('排柜计划已创建') }
|
||||
catch (error) { await handleMutationError(error, '创建排柜计划失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
@@ -251,8 +246,7 @@ async function assign() {
|
||||
const next = selected.coLoadGroupId
|
||||
? await assignSmartLoadingGroup(plan.value.id, selected.inboundId, plan.value.version)
|
||||
: await assignSmartLoadingItem(plan.value.id, { inboundId: selected.inboundId, packageCount: assignForm.packageCount, expectedVersion: plan.value.version })
|
||||
acceptPlan(next)
|
||||
await loadPlanGroups()
|
||||
await acceptPlanWithGroups(next)
|
||||
assignOpen.value = false
|
||||
ElMessage.success(selected.coLoadGroupId ? `同柜不可拆组 ${selected.groupNo} 已整组分配` : '货物已分配')
|
||||
} catch (error) { await handleMutationError(error, '分配失败') }
|
||||
@@ -269,13 +263,13 @@ async function recommend() {
|
||||
mutationError.value = null
|
||||
try {
|
||||
try {
|
||||
acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version))
|
||||
await acceptPlanWithGroups(await recommendSmartLoading(plan.value.id, plan.value.version))
|
||||
} catch (error) {
|
||||
const details = getApiErrorDetails(error, '智能推荐失败')
|
||||
if (details.code !== 'A0409' || !/(版本|并发|已被修改|已变化)/.test(details.message)) throw error
|
||||
await load()
|
||||
if (!plan.value) throw error
|
||||
acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version))
|
||||
await acceptPlanWithGroups(await recommendSmartLoading(plan.value.id, plan.value.version))
|
||||
}
|
||||
ElMessage.success('智能建议已生成,请人工核对后再推送')
|
||||
}
|
||||
@@ -289,7 +283,7 @@ async function adjust(source: object) {
|
||||
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('箱数已调整') }
|
||||
try { await acceptPlanWithGroups(await adjustSmartLoadingItem(plan.value.id, item.id, count, plan.value.version, item.version)); ElMessage.success('箱数已调整') }
|
||||
catch (error) { await handleMutationError(error, '调整失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
@@ -306,7 +300,7 @@ async function remove(source: object) {
|
||||
mutationError.value = null
|
||||
try {
|
||||
if (row.coLoadGroup) {
|
||||
acceptPlan(await removeSmartLoadingGroup(plan.value!.id, row.coLoadGroup.groupId, plan.value!.version))
|
||||
await acceptPlanWithGroups(await removeSmartLoadingGroup(plan.value!.id, row.coLoadGroup.groupId, plan.value!.version))
|
||||
await load()
|
||||
ElMessage.success(`同柜不可拆组 ${row.coLoadGroup.groupNo} 已整组移除`)
|
||||
return
|
||||
@@ -316,7 +310,7 @@ async function remove(source: object) {
|
||||
const currentItem = current.items.find((value) => value.id === original.id)
|
||||
if (!currentItem) continue
|
||||
current = await removeSmartLoadingItem(current.id, currentItem.id, current.version, currentItem.version)
|
||||
acceptPlan(current)
|
||||
await acceptPlanWithGroups(current)
|
||||
}
|
||||
await load()
|
||||
ElMessage.success('该入仓码已从装柜方案移除')
|
||||
@@ -331,7 +325,7 @@ async function pushPreload() {
|
||||
if (!confirmed || !plan.value) return
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await pushSmartLoadingPreload(plan.value.id, plan.value.version)); ElMessage.success('预装清单已生成,请继续完成集装箱号和预装确认') }
|
||||
try { await acceptPlanWithGroups(await pushSmartLoadingPreload(plan.value.id, plan.value.version)); ElMessage.success('预装清单已生成,请继续完成集装箱号和预装确认') }
|
||||
catch (error) { await handleMutationError(error, '推送失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { loadSmartLoadingPlanCoLoadGroups } from './smartLoadingPlanGroups'
|
||||
import type { SmartLoadingPlan } from './types'
|
||||
|
||||
const item = {
|
||||
id: '701', loadingPlanId: '99', accountId: '1', inboundId: '201', inboundDetailId: '801', orderId: '301',
|
||||
orderNo: 'ORD-1', inboundCode: 'IN-201', customerName: '客户一', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9',
|
||||
skuCode: 'SKU-1', productNameZh: '货物一', packageCount: 5, productQuantity: 50, grossWeightKg: 50, volumeCbm: 1,
|
||||
loadSequence: 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true,
|
||||
}
|
||||
|
||||
const plan = {
|
||||
id: '99', planNo: 'LP-99', status: 'EDITING', bookingId: '88', containerNo: null, voyageNo: null, version: 2,
|
||||
canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED', pdaTaskId: null,
|
||||
pdaTaskStatus: null, pdaTaskVersion: null,
|
||||
items: [item, { ...item, id: '702', inboundId: '202', inboundDetailId: '802', orderId: '302', orderNo: 'ORD-2', inboundCode: 'IN-202', packageCount: 7 }],
|
||||
} satisfies SmartLoadingPlan
|
||||
|
||||
describe('smart loading plan co-load groups', () => {
|
||||
it('rebuilds the locked group from the current inbound association contract', async () => {
|
||||
const lookup = vi.fn(async (inboundId: string) => ({
|
||||
inboundId, inboundCode: `IN-${inboundId}`, loadingPlanId: '99', coLoadGroupId: '9001', coLoadGroupNo: 'CLG-9001', items: [],
|
||||
}))
|
||||
const result = await loadSmartLoadingPlanCoLoadGroups(plan, lookup)
|
||||
expect(lookup).toHaveBeenCalledTimes(2)
|
||||
expect(result.get('201')).toEqual({
|
||||
groupId: '9001', groupNo: 'CLG-9001', lockedPlanId: '99', memberOrderCount: 2, inboundFragmentCount: 2, fullPackageCount: 12,
|
||||
})
|
||||
expect(result.get('202')).toBe(result.get('201'))
|
||||
})
|
||||
|
||||
it('leaves ordinary plan items ungrouped', async () => {
|
||||
const result = await loadSmartLoadingPlanCoLoadGroups(plan, async (inboundId) => ({ inboundId, inboundCode: `IN-${inboundId}`, items: [] }))
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { getLoadingAssociationSummaryByInboundId, type LoadingAssociationSummary } from '@/shared/api/inboundCodeRelations'
|
||||
import { groupSmartLoadingItems } from './smartLoadingItems'
|
||||
import type { SmartLoadingPlan } from './types'
|
||||
|
||||
export interface SmartLoadingPlanCoLoadGroup {
|
||||
groupId: string
|
||||
groupNo: string
|
||||
lockedPlanId: string
|
||||
memberOrderCount: number
|
||||
inboundFragmentCount: number
|
||||
fullPackageCount: number
|
||||
}
|
||||
|
||||
type AssociationLookup = (inboundId: string) => Promise<LoadingAssociationSummary>
|
||||
|
||||
export async function loadSmartLoadingPlanCoLoadGroups(
|
||||
plan: SmartLoadingPlan,
|
||||
lookup: AssociationLookup = getLoadingAssociationSummaryByInboundId,
|
||||
) {
|
||||
const inboundRows = groupSmartLoadingItems(plan.items || [])
|
||||
if (!inboundRows.length) return new Map<string, SmartLoadingPlanCoLoadGroup>()
|
||||
|
||||
const associations = await Promise.all(inboundRows.map(async (row) => ({
|
||||
row,
|
||||
association: await lookup(row.inboundId),
|
||||
})))
|
||||
const membersByGroupId = new Map<string, typeof associations>()
|
||||
for (const entry of associations) {
|
||||
const groupId = String(entry.association.coLoadGroupId || '')
|
||||
if (!groupId) continue
|
||||
membersByGroupId.set(groupId, [...(membersByGroupId.get(groupId) || []), entry])
|
||||
}
|
||||
|
||||
const groupsByInboundId = new Map<string, SmartLoadingPlanCoLoadGroup>()
|
||||
for (const [groupId, members] of membersByGroupId) {
|
||||
const group = {
|
||||
groupId,
|
||||
groupNo: members.find((entry) => entry.association.coLoadGroupNo)?.association.coLoadGroupNo || '同柜不可拆组',
|
||||
lockedPlanId: members.find((entry) => entry.association.loadingPlanId)?.association.loadingPlanId || plan.id,
|
||||
memberOrderCount: new Set(members.map((entry) => entry.row.orderId)).size,
|
||||
inboundFragmentCount: members.length,
|
||||
fullPackageCount: members.reduce((sum, entry) => sum + entry.row.packageCount, 0),
|
||||
} satisfies SmartLoadingPlanCoLoadGroup
|
||||
for (const { row } of members) groupsByInboundId.set(row.inboundId, group)
|
||||
}
|
||||
return groupsByInboundId
|
||||
}
|
||||
Reference in New Issue
Block a user