diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.test.ts b/src/modules/overseas-warehouse/api/lastMileOrders.test.ts index 25cf9cd9..f865432f 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.test.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' -import { reviewCargoDiscard } from './lastMileOrders' +import { reviewCargoDiscard, updateLastMileOrderPalletMerge } from './lastMileOrders' import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, getContainerPlanProgress, getLastMileOrder, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes } from './lastMileOrders' vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal()), http: { get: vi.fn(), put: vi.fn(), post: vi.fn() } })) @@ -148,3 +148,12 @@ describe('last-mile order overseas API', () => { ) }) }) + +it('sets and cancels a merge group with string IDs and explicit customer scope', async () => { + vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: null }) + for (const required of [true, false]) { + await updateLastMileOrderPalletMerge(['2092873463568596994'], '2092618717861048321', required) + expect(http.put).toHaveBeenLastCalledWith('/platform/v1/last-leg/orders/pallet-merge-required/batch', + { orderIds: ['2092873463568596994'], palletMergeRequired: required }, { params: { customerId: '2092618717861048321' } }) + } +}) diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.ts b/src/modules/overseas-warehouse/api/lastMileOrders.ts index 23ba0e59..cd9a2afa 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.ts @@ -427,6 +427,15 @@ export async function transferLastMileOrders(payload: TransferDestinationPayload return unwrapApiResponse(response) } +export async function updateLastMileOrderPalletMerge(orderIds: string[], customerId: string, palletMergeRequired: boolean) { + const response = await http.put, ApiEnvelope>( + '/platform/v1/last-leg/orders/pallet-merge-required/batch', + { orderIds, palletMergeRequired }, + { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + export async function listContainerPlanProgress(query: ContainerPlanProgressQuery): Promise> { const response = await http.get>, ApiEnvelope>>( '/platform/v1/last-leg/orders/container-summaries', diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts index 2cd7ef0d..25c76e1a 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts @@ -21,6 +21,7 @@ import { listLastMileOrders, reviewCargoDiscard, transferLastMileOrders, + updateLastMileOrderPalletMerge, getLastMileOrderManualNodes, updateLastMileOrderHold, updateLastMileOrderManualNodes, @@ -38,7 +39,7 @@ vi.mock('../api/lastMileOrders', () => ({ changeContainerReceivingWarehouse: vi.fn(), createManualInboundPlan: vi.fn(), convertLastMileContainerToInboundPlans: vi.fn(), getContainerInboundPlanPreview: vi.fn(), transferLastMileOrders: vi.fn(), getLastMileOrderManualNodes: vi.fn(), updateLastMileOrderHold: vi.fn(), updateLastMileOrderManualNodes: vi.fn(), - reviewCargoDiscard: vi.fn(), + reviewCargoDiscard: vi.fn(), updateLastMileOrderPalletMerge: vi.fn(), })) class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} } @@ -1189,3 +1190,40 @@ describe('LastMileOrdersView pending order and container conversion', () => { expect(routerPush).toHaveBeenCalledWith({ name: 'overseas-return-inventory', query: expect.objectContaining({ packing: '1', returnNos: 'RETURN-1' }) }) }) }) + + describe('tail-leg pallet merge toolbar', () => { + beforeEach(() => { + canAccess.mockImplementation(() => true) + vi.mocked(listLastMileOrders).mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 }) + vi.mocked(updateLastMileOrderPalletMerge).mockReset().mockResolvedValue(null) + }) + it('submits the selected group and cancellation with the customer scope', async () => { + vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as Awaited>) + const wrapper = await mountView() + const row = { id: '2092873463568596994', customerId, currentStatusCode: 'PENDING_ASSIGNMENT', palletMergeFlag: '' } + expect(wrapper.findAll('button').find(b => b.text() === '合托')!.attributes('disabled')).toBeDefined() + await selectRows(wrapper, [row]) + await wrapper.findAll('button').find(b => b.text() === '合托')!.trigger('click') + await flushPromises() + expect(updateLastMileOrderPalletMerge).toHaveBeenLastCalledWith([row.id], customerId, true) + await selectRows(wrapper, [{ ...row, palletMergeFlag: 'REQUIRED' }]) + await wrapper.findAll('button').find(b => b.text() === '取消合托')!.trigger('click') + await flushPromises() + expect(updateLastMileOrderPalletMerge).toHaveBeenLastCalledWith([row.id], customerId, false) + }) + it('blocks completed and mixed-customer selections and hides unauthorized actions', async () => { + const wrapper = await mountView() + for (const rows of [ + [{ id: '1', customerId, currentStatusCode: 'INBOUND_COMPLETED', palletMergeFlag: 'REQUIRED' }], + [{ id: '1', customerId, palletMergeFlag: 'MERGED' }], + [{ id: '1', customerId }, { id: '2', customerId: 'other' }], + ]) { + await selectRows(wrapper, rows) + expect(wrapper.findAll('button').find(b => b.text() === '合托')!.attributes('disabled')).toBeDefined() + expect(wrapper.findAll('button').find(b => b.text() === '取消合托')!.attributes('disabled')).toBeDefined() + } + canAccess.mockImplementation(() => false) + const denied = await mountView() + expect(denied.findAll('button').some(b => b.text() === '合托' || b.text() === '取消合托')).toBe(false) + }) +}) diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue index f73473c0..f33eab86 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue @@ -16,7 +16,7 @@ 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 RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue' -import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, reviewDirectRelabelOrder, reviewReturnOrder, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode, type LastMileOrderQuery } from '../api/lastMileOrders' +import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, reviewDirectRelabelOrder, reviewReturnOrder, transferLastMileOrders, updateLastMileOrderPalletMerge, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode, type LastMileOrderQuery } from '../api/lastMileOrders' import { businessOrderLink } from '../businessOrderLink' import OrderReferenceText from '../components/OrderReferenceText.vue' import { reviewCargoDiscard } from '../api/lastMileOrders' @@ -64,6 +64,33 @@ function openReturnPackingPlan() { const exportLoading = ref(false) const actionLoading = ref(false) async function exportOrders() { exportLoading.value = true; try { await submitExport('LAST_MILE_ORDER_LIST', '尾程订单', orderQuery()) } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '尾程订单导出失败,请重试').message) } finally { exportLoading.value = false } } +const palletMergeSelectionEditable = computed(() => selectedRows.value.length > 0 + && selectedRows.value.every(row => row.customerId && row.currentStatusCode !== 'INBOUND_COMPLETED' && row.palletMergeFlag !== 'MERGED') + && new Set(selectedRows.value.map(row => row.customerId)).size === 1) +const canCancelPalletMerge = computed(() => palletMergeSelectionEditable.value + && selectedRows.value.every(row => row.palletMergeFlag === 'REQUIRED')) +async function submitPalletMerge(required: boolean) { + if (actionLoading.value || !access.can(permission.lastMileOrderPalletMerge) + || !(required ? palletMergeSelectionEditable.value : canCancelPalletMerge.value)) return + const selection = [...selectedRows.value] + const label = required ? '合托' : '取消合托' + try { + await ElMessageBox.confirm(required + ? `将勾选的 ${selection.length} 票订单标记为同一合托组?所选订单须属于同一客户、同一目的仓,PDA 将提示现场合托。` + : `取消勾选的 ${selection.length} 票订单的合托标记和 PDA 提示?`, label, + { customClass: 'admin-density-confirm', confirmButtonText: `确认${label}`, cancelButtonText: '返回', type: 'warning' }) + } catch { return } + actionLoading.value = true + actionError.value = null + try { + await updateLastMileOrderPalletMerge(selection.map(row => row.id), selection[0]!.customerId, required) + ElMessage.success(required ? '已标记合托' : '已取消合托标记') + selectedRows.value = [] + await load() + } catch (cause) { + actionError.value = getApiErrorDetails(cause, `${label}失败,请重试`) + } finally { actionLoading.value = false } +} const actionError = ref(null) const actionTarget = ref(null) const inboundPlanOpen = ref(false) @@ -1108,7 +1135,7 @@ onMounted(() => { void load() }) - + diff --git a/src/modules/overseas-warehouse/permissions.ts b/src/modules/overseas-warehouse/permissions.ts index 64c2523b..8dcd5cf5 100644 --- a/src/modules/overseas-warehouse/permissions.ts +++ b/src/modules/overseas-warehouse/permissions.ts @@ -2,6 +2,7 @@ export const overseasWarehousePermissions = { lastMileOrderList: 'platform:overseas-warehouse:last-mile-order:list', lastMileOrderExport: 'platform:overseas-warehouse:last-mile-order:export', lastMileOrderTransfer: 'platform:overseas-warehouse:last-mile-order:edit', + lastMileOrderPalletMerge: 'platform:overseas-warehouse:last-mile-order:edit', warehouseTransferApprove: 'platform:overseas-warehouse:warehouse-transfer:approve', cargoDiscardApprove: 'lastleg:cargo-discard:approve', inboundOrderList: 'platform:overseas-warehouse:inbound-order:list',