diff --git a/src/modules/overseas-warehouse/components/TransportExecutionPanel.vue b/src/modules/overseas-warehouse/components/TransportExecutionPanel.vue index abd8952a..031f644d 100644 --- a/src/modules/overseas-warehouse/components/TransportExecutionPanel.vue +++ b/src/modules/overseas-warehouse/components/TransportExecutionPanel.vue @@ -11,7 +11,7 @@ import { listVehicles, listDrivers, pickupStatusLabels, type VehicleDetail, type import { getTransport, assignTransport, transportOccupancy, cancelTransport, releaseTransport, commandTransport, uploadTransport, downloadTransportEvidence, transportLabels, type TransportDetail } from '../api/transportTasks' import { dispatchActionPermissions as permission } from '../dispatchPresentation' -const props = defineProps<{ dispatchNo: string }>(), emit = defineEmits<{ saved: [] }>() +const props = defineProps<{ dispatchNo: string; pickupApproved?: boolean }>(), emit = defineEmits<{ saved: [] }>() const access = useAccessPreviewStore(), detail = ref(null), error = ref(null), busy = ref(false) const vehicles = ref([]), drivers = ref([]), vehicleId = ref(''), driverId = ref(''), reason = ref('') const occupancy = ref>({}), candidateBusy = ref(false) @@ -109,13 +109,13 @@ onBeforeRouteLeave(() => !locked.value || window.confirm('运输请求尚待核 保存车辆司机
- 确认发车 + 确认发车 取消排车 确认运输完成 刷新运输进度
-

送仓后由仓库独立清点收货,取货数量保留为运输事实。

+

{{ props.pickupApproved ? '两级审核已通过,可发车取货。' : '请维护车辆信息并提交调度审核,经调度和理货员审核通过后发车取货。' }}送仓后由仓库独立清点收货,取货数量保留为运输事实。

{{ file.fileName }}

已确认的运输完成凭证

{{ file.fileName }}
diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrdersView.test.ts b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.test.ts index 4a36f1e8..05791c6a 100644 --- a/src/modules/overseas-warehouse/pages/DeliveryOrdersView.test.ts +++ b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.test.ts @@ -330,7 +330,7 @@ describe('DeliveryOrdersView permissions and commands', () => { const wrapper = mount(DeliveryOrdersView, { global: { plugins: [ElementPlus], stubs: { ...commonPhase5Stubs, DataTable: table, DeliveryDocumentsDialog: true } } }) await flushPromises() expect(wrapper.text()).toContain('1/3 已上传') - expect(wrapper.text()).toContain('签收文件(POD签收/快递签收) · 已上传') + expect(wrapper.text()).not.toContain('签收文件(POD签收/快递签收)') expect(getDeliveryDocuments).not.toHaveBeenCalled() await wrapper.findAll('button').find(button => button.text().includes('管理资料'))!.trigger('click') const dialog = wrapper.findComponent({ name: 'DeliveryDocumentsDialog' }) diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue index 2953f96d..c0913077 100644 --- a/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue @@ -23,7 +23,6 @@ import { overseasWarehousePermissions as permission } from '../permissions' import { listReceivingOverseasWarehouses, type ReceivingOverseasWarehouseDto } from '../api/pallets' import DeliveryDocumentsDialog from '../components/DeliveryDocumentsDialog.vue' -import { deliveryDocumentCategories } from '../api/deliveryDocuments' import { returnSourceKey, returnSourceReference } from '../returnSource' const documentsOpen = ref(false) @@ -193,7 +192,7 @@ onMounted(() => { void load(); void loadWarehouses() }) - +

当前资料

当前状态{{ statusLabels[String(statusRow?.status)] || '—' }}
关联出库{{ statusRow?.outboundNo || '—' }}
@@ -219,4 +218,4 @@ onMounted(() => { void load(); void loadWarehouses() })
- + diff --git a/src/modules/overseas-warehouse/pages/DispatchDetailView.vue b/src/modules/overseas-warehouse/pages/DispatchDetailView.vue index b6929f96..e7c33840 100644 --- a/src/modules/overseas-warehouse/pages/DispatchDetailView.vue +++ b/src/modules/overseas-warehouse/pages/DispatchDetailView.vue @@ -29,7 +29,7 @@ const rejectionError = ref(null) const operation = ref<'ROUTE' | undefined>() const action = ref('EDIT'), actionOpen = ref(false) const detailActions: DispatchAction[] = ['EDIT', 'SUBMIT_REVIEW', 'DISPATCH_REVIEW', 'LOADING_REVIEW', 'RETURN_TO_REVIEW', 'LOADING_COMPLETE', 'CANCEL'] -const visibleActions = computed(() => detailActions.filter(value => canDispatchAction(detail.value?.actions, value, access.can))) +const visibleActions = computed(() => detailActions.filter(value => !(detail.value?.taskType === 'PICKUP' && value === 'EDIT') && canDispatchAction(detail.value?.actions, value, access.can))) const vehicleStatus = computed(() => { const status = detail.value?.vehicleInfo.vehicle?.vehicleStatus return status && Object.hasOwn(vehicleStatusLabels, status) ? vehicleStatusLabels[status as keyof typeof vehicleStatusLabels] : '—' @@ -84,8 +84,8 @@ onBeforeUnmount(() => { generation++ })
- -
{{ dispatchActionLabels[value] }}调整卸货顺序
+ +
{{ dispatchActionLabels[value] }}调整卸货顺序
{{ field[0] }}
{{ field[1] }}
diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.test.ts b/src/modules/overseas-warehouse/pages/DispatchesView.test.ts index 1ffd7517..0cf553d6 100644 --- a/src/modules/overseas-warehouse/pages/DispatchesView.test.ts +++ b/src/modules/overseas-warehouse/pages/DispatchesView.test.ts @@ -154,11 +154,24 @@ it.each(['PICKUP', 'LOCAL_TRUCK'] as const)('opens %s through the existing dispa vi.mocked(listDispatches).mockResolvedValueOnce(page([listRow({ taskType })])) const wrapper = mount(DispatchesView, options); await flushPromises() const row = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')! - expect(row.props('actions')).toEqual([expect.objectContaining({ key: 'detail', label: '运输操作' })]) + expect(row.props('actions')).toEqual([expect.objectContaining({ key: 'detail', label: taskType === 'PICKUP' ? '详情' : '运输操作' })]) row.vm.$emit('action', 'detail'); await flushPromises() expect(push).toHaveBeenCalledWith({ name: 'overseas-dispatch-detail', params: { dispatchNo: 'PC/1' } }) expect(wrapper.text()).toContain('安排到仓取货'); expect(wrapper.text()).toContain('安排本土卡车'); wrapper.unmount() }) +it('keeps pickup workflow actions but hides route ordering and BOL even when editing is allowed', async () => { + const allowed = { allowed: true, blockCode: null, blockReason: null } + vi.mocked(listDispatches).mockResolvedValueOnce(page([listRow({ taskType: 'PICKUP', actions: { EDIT: allowed, SUBMIT_REVIEW: allowed, CANCEL: allowed } })])) + const wrapper = mount(DispatchesView, { global: { ...options.global, stubs: { ...options.global.stubs, TransportExecutionPanel: true, AppDialog: { template: '
' } } } }); await flushPromises() + const row = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')! + expect((row.props('actions') as Array<{ key: string }>).map(item => item.key)).toEqual(['EDIT', 'SUBMIT_REVIEW', 'CANCEL', 'detail']) + row.vm.$emit('action', 'EDIT'); await flushPromises() + expect(wrapper.getComponent({ name: 'TransportExecutionPanel' }).props('dispatchNo')).toBe('PC/1') + expect(wrapper.getComponent({ name: 'DispatchWorkflowDialog' }).props('modelValue')).toBe(false) + row.vm.$emit('action', 'SUBMIT_REVIEW'); await flushPromises() + expect(wrapper.getComponent({ name: 'DispatchWorkflowDialog' }).props()).toMatchObject({ modelValue: true, action: 'SUBMIT_REVIEW', dispatchNo: 'PC/1' }) + wrapper.unmount() +}) it('reads one BOL on demand and preserves popup failure handling', async () => { vi.stubGlobal('open', vi.fn().mockReturnValue(null)); const wrapper = mount(DispatchesView, options); await flushPromises(); wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!.vm.$emit('action', 'bol'); await flushPromises(); expect(getDispatchBolPdf).not.toHaveBeenCalled(); wrapper.unmount() }) diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.vue b/src/modules/overseas-warehouse/pages/DispatchesView.vue index dfd58040..88666fad 100644 --- a/src/modules/overseas-warehouse/pages/DispatchesView.vue +++ b/src/modules/overseas-warehouse/pages/DispatchesView.vue @@ -22,6 +22,8 @@ import { dispatchStages, reviewLabels, stageLabel, type BeforeLoadingSection, ty import { canDispatchAction, dispatchActionLabels, dispatchActionPermissions, dispatchRejectionReason } from '../dispatchPresentation' import { formatOverseasDateTime as time } from '../lastMilePresentation' import TransportTaskCreateDialog from '../components/TransportTaskCreateDialog.vue' +import AppDialog from '@/shared/components/AppDialog.vue' +import TransportExecutionPanel from '../components/TransportExecutionPanel.vue' import { transportLabels, type TransportType } from '../api/transportTasks' import SmartDispatchDialog from '../components/SmartDispatchDialog.vue' import DispatchWorkflowDialog from '../components/DispatchWorkflowDialog.vue' @@ -33,6 +35,7 @@ const rows = ref([]), statistics = ref(null), mutationError = ref(null) const rejectionError = ref(null) const transportOpen = ref(false), transportType = ref('PICKUP') +const transportEditOpen = ref(false), transportEditNo = ref('') function createTransport(type: TransportType) { transportType.value = type; transportOpen.value = true } const page = ref(1) const pageSize = ref(10) @@ -84,7 +87,7 @@ function rowActions(row: DispatchListRow): RowActionItem[] { const review = section.value return [...(canDispatchAction(row.actions, review, access.can) ? [{ key: review, label: '审核', ariaLabel: dispatchActionLabels[review], priority: 600 }] : []), { key: 'detail', label: '详情', priority: 500 }] } - if (row.taskType && row.taskType !== 'DELIVERY') return [{ key: 'detail', label: '运输操作', priority: 500 }] + if (row.taskType === 'LOCAL_TRUCK') return [{ key: 'detail', label: '运输操作', priority: 500 }] const available = rowActionOrder.filter(value => canDispatchAction(row.actions, value, access.can)) const primaryAction = available.find(value => value === 'EDIT') ?? available[0] return [...available.map((value, index) => ({ @@ -93,7 +96,7 @@ function rowActions(row: DispatchListRow): RowActionItem[] { ariaLabel: dispatchActionLabels[value], priority: value === primaryAction ? 600 : 400 - index * 10, tone: ['CANCEL', 'MARK_EXCEPTION'].includes(value) ? 'danger' as const : undefined, - })), { key: 'detail', label: '详情', priority: 500 }, ...(canDispatchAction(row.actions, 'EDIT', access.can) ? [{ key: 'route-order', label: '调整卸货顺序', priority: 190 }] : []), { key: 'bol', label: '下载BOL', priority: 100, loading: bolLoadingNo.value === row.dispatchNo }] + })), { key: 'detail', label: '详情', priority: 500 }, ...(row.taskType === 'PICKUP' ? [] : [...(canDispatchAction(row.actions, 'EDIT', access.can) ? [{ key: 'route-order', label: '调整卸货顺序', priority: 190 }] : []), { key: 'bol', label: '下载BOL', priority: 100, loading: bolLoadingNo.value === row.dispatchNo }])] } function handleAction(value: string, row: DispatchListRow) { if (value === 'detail') return openDetail(row) @@ -105,6 +108,7 @@ function handleAction(value: string, row: DispatchListRow) { if (value === 'ADD_CARGO' || value === 'REMOVE_CARGO') return const requested = value as DispatchAction if (!canDispatchAction(row.actions, requested, access.can)) return + if (row.taskType === 'PICKUP' && requested === 'EDIT') { transportEditNo.value = row.dispatchNo; transportEditOpen.value = true; return } action.value = requested; actionNo.value = row.dispatchNo; actionOpen.value = true } async function handleRejection(details: ApiErrorDetails) { rejectionError.value = details; await load() } @@ -150,6 +154,7 @@ onBeforeUnmount(() => { sequence++ })
+ diff --git a/src/modules/overseas-warehouse/pages/InventoryCustomerName.behavior.test.ts b/src/modules/overseas-warehouse/pages/InventoryCustomerName.behavior.test.ts index 5330dd45..7e6a2522 100644 --- a/src/modules/overseas-warehouse/pages/InventoryCustomerName.behavior.test.ts +++ b/src/modules/overseas-warehouse/pages/InventoryCustomerName.behavior.test.ts @@ -74,21 +74,12 @@ describe('inventory customer-name snapshots', () => { expect(wrapper.text()).not.toContain(balance.id) }) - it('keeps the formal customer and carton list when piece projection fails', async () => { - vi.mocked(getInventoryPieceProjection).mockRejectedValueOnce(new Error('projection unavailable')) + it('keeps carton totals without the removed SKU and piece columns or their projection request', async () => { const wrapper = mount(InventoryView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } }) await flushPromises() expect(wrapper.text()).toContain('华远贸易') - expect(wrapper.text()).toContain('可用件数及箱内SKU加载失败') - expect(wrapper.text()).not.toContain('库存数据加载失败') - }) - - it('renders every mixed-carton SKU and real piece count alongside carton totals', async () => { - vi.mocked(getInventoryPieceProjection).mockResolvedValueOnce({ availablePieceCount: '12', knownAvailablePieceCount: '12', unknownCartonCount: '0', availableCartonCount: '2', availablePalletCount: '1', groups: [{ customerId, warehouseId: '200', destinationCode: null, anchorSkuCode: 'SKU-1', availablePieceCount: '12', knownAvailablePieceCount: '12', unknownCartonCount: '0', availableCartonCount: '2', availablePalletCount: '1', skuContents: [{ skuCode: 'SKU-1', quantity: '5' }, { skuCode: 'SKU-SECOND', quantity: '7' }], cartons: [] }] }) - const wrapper = mount(InventoryView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } }) - await flushPromises() - expect(wrapper.text()).toContain('华远贸易') - expect(wrapper.text()).toContain('SKU-1 × 5 件;SKU-SECOND × 7 件') + expect(wrapper.text()).not.toContain('可用件数') + expect(getInventoryPieceProjection).not.toHaveBeenCalled() expect(wrapper.find('[aria-label="可用库存件箱托汇总"]').exists()).toBe(false) expect(wrapper.get('[aria-label="库存汇总"]').text()).toContain('可用库存(箱)10') }) diff --git a/src/modules/overseas-warehouse/pages/InventoryView.vue b/src/modules/overseas-warehouse/pages/InventoryView.vue index 7c235301..95f25c59 100644 --- a/src/modules/overseas-warehouse/pages/InventoryView.vue +++ b/src/modules/overseas-warehouse/pages/InventoryView.vue @@ -13,7 +13,7 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v import ListPageLayout from '@/shared/components/ListPageLayout.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' -import { getInventorySummary, getInventoryPieceProjection, listInventoryBalances, type InventoryDashboardDto, type InventoryPieceGroup } from '../api/inventory' +import { getInventorySummary, listInventoryBalances, type InventoryDashboardDto } from '../api/inventory' import { ElMessage } from 'element-plus' import { listReceivingOverseasWarehouses, type ReceivingOverseasWarehouseDto } from '../api/pallets' import { toInventoryListItem } from '../inventoryPresentation' @@ -26,8 +26,6 @@ const access = useAccessPreviewStore() const warehouses = ref([]) const rows = ref([]) const summary = ref(null) -const rowPieces = ref>({}) -const pieceError = ref('') const loading = ref(false) const contextLoading = ref(false) const loadError = ref(null) @@ -53,8 +51,6 @@ const columns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' }, { key: 'owner', prop: 'owner', label: '客户', minWidth: 160 }, { key: 'warehouse', prop: 'warehouse', label: '海外仓', minWidth: 170 }, - { key: 'sku', prop: 'sku', label: '箱内SKU / 可用件数', minWidth: 220, slotName: 'skuContents', singleLine: false, className: 'inventory-sku-contents' }, - { key: 'availablePieces', label: '可用件数', minWidth: 130, slotName: 'availablePieces' }, { key: 'productName', prop: 'productName', label: '中文品名', minWidth: 180 }, { key: 'location', prop: 'location', label: '库位', minWidth: 140, defaultVisible: false }, { key: 'onHand', prop: 'onHand', label: '在库总量', width: 105, align: 'right' }, @@ -79,17 +75,13 @@ function query() { async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model) - loading.value = true; loadError.value = null; rowPieces.value = {}; pieceError.value = '' + loading.value = true; loadError.value = null try { const currentQuery = query() const [pageResult, summaryResult] = await Promise.all([listInventoryBalances(currentQuery), getInventorySummary(currentQuery)]) rows.value = pageResult.records.map(toInventoryListItem); total.value = pageResult.total; summary.value = summaryResult - try { - const pieceResult = await getInventoryPieceProjection(currentQuery) - rowPieces.value = Object.fromEntries(pageResult.records.map(dto => [String(dto.id), pieceResult.groups.find(group => String(group.customerId) === String(dto.customerId) && String(group.warehouseId) === String(dto.overseasWarehouseId) && (group.destinationCode ?? '') === (dto.destinationWarehouseCode ?? '') && (group.anchorSkuCode ?? '') === (dto.skuCode ?? ''))])) - } catch { pieceError.value = '可用件数及箱内SKU加载失败,请重新查询;箱数列表仍可查看。' } } - catch (error) { rows.value = []; total.value = 0; summary.value = null; rowPieces.value = {}; loadError.value = getApiErrorDetails(error, '库存数据加载失败,请重试') } + catch (error) { rows.value = []; total.value = 0; summary.value = null; loadError.value = getApiErrorDetails(error, '库存数据加载失败,请重试') } finally { loading.value = false } } @@ -107,20 +99,17 @@ onMounted(() => { void loadWarehouses(); void load() })
- + - -