diff --git a/backend/src/controllers/__tests__/disposalController.state-machine.test.ts b/backend/src/controllers/__tests__/disposalController.state-machine.test.ts index 4e280a9..b7cd3f2 100644 --- a/backend/src/controllers/__tests__/disposalController.state-machine.test.ts +++ b/backend/src/controllers/__tests__/disposalController.state-machine.test.ts @@ -290,19 +290,36 @@ test('并发审批 CAS 失败时返回 409,不追加动作也不改资产', as } }) -test('行政不能审批自己发起的报废申请', async () => { +test('行政可以审批自己发起的报废申请', async () => { const originalTransaction = prisma.$transaction - let updateCount = 0 + const originalLogCreate = prisma.operationLog.create + let updatedData: Record | undefined + let actionData: Record | undefined ;(prisma.$transaction as any) = async (callback: any) => callback({ disposalRecord: { - findUnique: async () => ({ id: 94, applied_by: 6, items: [] }), - updateMany: async () => { - updateCount += 1 + findUnique: async () => ({ + id: 94, + equipment_id: 10, + equipment_code: rootEquipment.equipment_code, + applied_by: 6, + status: 'pending', + items: [] + }), + updateMany: async (args: any) => { + updatedData = args.data return { count: 1 } } - } + }, + disposalAction: { + create: async (args: any) => { + actionData = args.data + return { id: 1 } + } + }, + user: { findMany: async () => [] } }) + ;(prisma.operationLog.create as any) = async () => ({ id: 1 }) try { const { res, state } = createResponse('administrative') @@ -311,11 +328,15 @@ test('行政不能审批自己发起的报废申请', async () => { res as any ) - assert.equal(state.statusCode, 403) - assert.match(state.body.message, /不能审批自己发起/) - assert.equal(updateCount, 0) + assert.equal(state.statusCode, 200) + assert.equal(state.body.message, '审批通过') + assert.equal(updatedData?.status, 'approved') + assert.equal(updatedData?.approved_by, 6) + assert.equal(actionData?.action_type, 'approved') + assert.equal(actionData?.operator_id, 6) } finally { ;(prisma.$transaction as any) = originalTransaction + ;(prisma.operationLog.create as any) = originalLogCreate } }) diff --git a/backend/src/controllers/disposalController.ts b/backend/src/controllers/disposalController.ts index a002244..2e0381b 100644 --- a/backend/src/controllers/disposalController.ts +++ b/backend/src/controllers/disposalController.ts @@ -563,10 +563,6 @@ export const approveDisposal = async (req: Request, res: Response) => { include: { items: { include: { equipment: true } } } }) if (!disposal) throw Object.assign(new Error('报废记录不存在'), { status: 404 }) - const currentUser = getCurrentUser(res) - if (currentUser.role === 'administrative' && disposal.applied_by === userId) { - throw Object.assign(new Error('行政不能审批自己发起的报废申请,请由其他行政或管理员处理'), { status: 403 }) - } const changedItem = disposal.items.find((item) => item.equipment.status !== '待报废' || item.equipment.parent_id !== item.parent_equipment_id diff --git a/backend/src/utils/__tests__/permissions.test.ts b/backend/src/utils/__tests__/permissions.test.ts index fdeaadf..364cf3d 100644 --- a/backend/src/utils/__tests__/permissions.test.ts +++ b/backend/src/utils/__tests__/permissions.test.ts @@ -10,27 +10,29 @@ test('管理员拥有完整权限集合', () => { assert.ok(permissions.includes(PERMISSIONS.ASSETS_VIEW_OWN)) assert.ok(permissions.includes(PERMISSIONS.ASSETS_PRINT_LABELS)) }) -test('行政可以登记、发起报废和审批,但不能借还、调拨或挂载', () => { +test('行政可以登记并负责报废全流程,但不能借还、调拨或挂载', () => { const permissions = getPermissionsForRole('administrative') assert.ok(permissions.includes(PERMISSIONS.ASSETS_CREATE)) assert.ok(permissions.includes(PERMISSIONS.ASSETS_CORRECT)) assert.ok(permissions.includes(PERMISSIONS.ASSETS_PRINT_LABELS)) assert.ok(permissions.includes(PERMISSIONS.DISPOSALS_APPLY)) assert.ok(permissions.includes(PERMISSIONS.DISPOSALS_APPROVE)) + assert.ok(permissions.includes(PERMISSIONS.DISPOSALS_COMPLETE)) assert.ok(!permissions.includes(PERMISSIONS.ASSETS_BORROW)) assert.ok(!permissions.includes(PERMISSIONS.ASSETS_TRANSFER)) assert.ok(!permissions.includes(PERMISSIONS.ASSETS_MOUNT)) }) -test('IT可以直接维护资产资料、流转、发起报废和盘点,但不能审批或管理用户', () => { +test('IT可以直接维护资产资料、借还、调拨和盘点,但不能处理报废或管理用户', () => { const permissions = getPermissionsForRole('it') assert.ok(permissions.includes(PERMISSIONS.ASSETS_CORRECT)) assert.ok(permissions.includes(PERMISSIONS.ASSETS_BORROW)) assert.ok(permissions.includes(PERMISSIONS.ASSETS_TRANSFER)) assert.ok(permissions.includes(PERMISSIONS.ASSETS_PRINT_LABELS)) - assert.ok(permissions.includes(PERMISSIONS.DISPOSALS_APPLY)) + assert.ok(!permissions.includes(PERMISSIONS.DISPOSALS_APPLY)) assert.ok(permissions.includes(PERMISSIONS.INVENTORY_MANAGE)) assert.ok(!permissions.includes(PERMISSIONS.DISPOSALS_APPROVE)) + assert.ok(!permissions.includes(PERMISSIONS.DISPOSALS_COMPLETE)) assert.ok(!permissions.includes(PERMISSIONS.USERS_MANAGE)) }) diff --git a/backend/src/utils/permissions.ts b/backend/src/utils/permissions.ts index 80c7184..3af8a40 100644 --- a/backend/src/utils/permissions.ts +++ b/backend/src/utils/permissions.ts @@ -52,7 +52,6 @@ const ROLE_PERMISSIONS: Record = { PERMISSIONS.ASSETS_MAINTENANCE, PERMISSIONS.ASSETS_MOUNT, PERMISSIONS.ASSETS_PRINT_LABELS, - PERMISSIONS.DISPOSALS_APPLY, PERMISSIONS.INVENTORY_MANAGE, PERMISSIONS.NOTIFICATIONS_VIEW, PERMISSIONS.REPORTS_VIEW diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 0183627..159167c 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -108,7 +108,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('../views/workflows/DisposalList.vue'), meta: { title: '报废流程', - anyPermissions: [PERMISSIONS.DISPOSALS_APPLY, PERMISSIONS.DISPOSALS_APPROVE, PERMISSIONS.DISPOSALS_COMPLETE] + anyPermissions: [ + PERMISSIONS.DISPOSALS_APPLY, + PERMISSIONS.DISPOSALS_APPROVE, + PERMISSIONS.DISPOSALS_COMPLETE, + PERMISSIONS.NOTIFICATIONS_VIEW + ] } }, { @@ -117,7 +122,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('../views/workflows/DisposalDetail.vue'), meta: { title: '报废详情', - anyPermissions: [PERMISSIONS.DISPOSALS_APPLY, PERMISSIONS.DISPOSALS_APPROVE, PERMISSIONS.DISPOSALS_COMPLETE] + anyPermissions: [ + PERMISSIONS.DISPOSALS_APPLY, + PERMISSIONS.DISPOSALS_APPROVE, + PERMISSIONS.DISPOSALS_COMPLETE, + PERMISSIONS.NOTIFICATIONS_VIEW + ] } }, { diff --git a/frontend/src/views/dashboard/dashboardRoleConfig.ts b/frontend/src/views/dashboard/dashboardRoleConfig.ts index 7dc61b2..40473de 100644 --- a/frontend/src/views/dashboard/dashboardRoleConfig.ts +++ b/frontend/src/views/dashboard/dashboardRoleConfig.ts @@ -12,15 +12,14 @@ const ACTIONS_BY_ROLE: Record = { { id: 'asset-create', label: '资产入库', description: '登记单台资产和基础资料', to: '/equipment/add' }, { id: 'asset-import', label: '批量导入', description: '在资产管理中导入入库表格', to: '/equipment' }, { id: 'inventory', label: '盘点任务', description: '创建任务并跟进盘点结果', to: '/inventory' }, - { id: 'disposal-approval', label: '报废审批', description: '处理 IT 发起的报废申请', to: '/workflows/disposals?scope=pending' } + { id: 'disposal-approval', label: '报废处理', description: '审批并完成报废;申请请从资产列表发起', to: '/workflows/disposals?scope=pending' } ], it: [ { id: 'borrow', label: '资产借出', description: '扫码选择使用者并完成出库', to: '/borrow/scan' }, { id: 'return', label: '资产归还', description: '扫码归还资产及随借配件', to: '/return/scan' }, { id: 'transfer', label: '调拨与闲置', description: '在资产管理中选择资产后调拨', to: '/equipment' }, { id: 'inventory', label: '发起盘点', description: '按人员或组织派发手机盘点任务', to: '/inventory' }, - { id: 'maintenance', label: '维修管理', description: '登记、完成或取消维修', to: '/maintenance' }, - { id: 'disposal-apply', label: '发起报废', description: '在资产详情中选择整机及配件', to: '/equipment' } + { id: 'maintenance', label: '维修管理', description: '登记、完成或取消维修', to: '/maintenance' } ], admin: [ { id: 'system', label: '系统设置', description: '管理用户、角色、同步和系统配置', to: '/settings' }, diff --git a/frontend/src/views/equipment/EquipmentForm.vue b/frontend/src/views/equipment/EquipmentForm.vue index bf27ce0..aa45c5a 100644 --- a/frontend/src/views/equipment/EquipmentForm.vue +++ b/frontend/src/views/equipment/EquipmentForm.vue @@ -638,7 +638,8 @@ const canApplyDisposal = computed(() => authStore.can(PERMISSIONS.DISPOSALS_APPL const canViewDisposals = computed(() => authStore.canAny([ PERMISSIONS.DISPOSALS_APPLY, PERMISSIONS.DISPOSALS_APPROVE, - PERMISSIONS.DISPOSALS_COMPLETE + PERMISSIONS.DISPOSALS_COMPLETE, + PERMISSIONS.NOTIFICATIONS_VIEW ])) const canPrintLabels = computed(() => authStore.can(PERMISSIONS.ASSETS_PRINT_LABELS)) const canOverrideCorrectionRestrictions = computed(() => authStore.can(PERMISSIONS.SYSTEM_SETTINGS)) diff --git a/frontend/src/views/settings/components/DisposalManagement.vue b/frontend/src/views/settings/components/DisposalManagement.vue index 0cfb8b8..1ae6b13 100644 --- a/frontend/src/views/settings/components/DisposalManagement.vue +++ b/frontend/src/views/settings/components/DisposalManagement.vue @@ -337,10 +337,11 @@ const authStore = useAuthStore() const canApprove = computed(() => authStore.can(PERMISSIONS.DISPOSALS_APPROVE)) const canComplete = computed(() => authStore.can(PERMISSIONS.DISPOSALS_COMPLETE)) const canApply = computed(() => authStore.can(PERMISSIONS.DISPOSALS_APPLY)) +const canViewHistory = computed(() => authStore.can(PERMISSIONS.NOTIFICATIONS_VIEW)) const availableScopes = computed>(() => { const scopes: Array<{ value: DisposalScope; label: string }> = [] if (canApprove.value) scopes.push({ value: 'pending', label: '待我审批' }) - if (canApply.value) scopes.push({ value: 'mine', label: '我发起的' }) + if (canApply.value || canViewHistory.value) scopes.push({ value: 'mine', label: '我发起的' }) if (canApprove.value || canComplete.value) scopes.push({ value: 'processed', label: '已处理' }) if (authStore.can(PERMISSIONS.SYSTEM_SETTINGS)) scopes.push({ value: 'all', label: '全部' }) return scopes @@ -414,10 +415,7 @@ const openDetail = (item: DisposalRecord) => router.push(`/workflows/disposals/$ const hasMobileSecondaryActions = (item: DisposalRecord) => (item.status === 'pending' && canApproveItem(item)) || (item.status === 'approved' && canComplete.value) -const canApproveItem = (item: DisposalRecord) => !( - !canApprove.value || - (authStore.user?.role === 'administrative' && Number(item.applied_by) === Number(authStore.user?.id)) -) +const canApproveItem = (_item: DisposalRecord) => canApprove.value const handleMobileOpenDetail = () => { const item = mobileActionItem.value diff --git a/frontend/src/views/workflows/DisposalDetail.vue b/frontend/src/views/workflows/DisposalDetail.vue index 2bbbeb2..b1d95c0 100644 --- a/frontend/src/views/workflows/DisposalDetail.vue +++ b/frontend/src/views/workflows/DisposalDetail.vue @@ -159,10 +159,7 @@ const completeForm = ref({ actual_value: 0, completion_notes: '' }) const canApprove = computed(() => authStore.can(PERMISSIONS.DISPOSALS_APPROVE)) const canComplete = computed(() => authStore.can(PERMISSIONS.DISPOSALS_COMPLETE)) -const canApproveRecord = computed(() => !( - !canApprove.value || - (authStore.user?.role === 'administrative' && Number(record.value?.applied_by) === Number(authStore.user?.id)) -)) +const canApproveRecord = computed(() => canApprove.value) const showActions = computed(() => (record.value?.status === 'pending' && canApproveRecord.value) || (record.value?.status === 'approved' && canComplete.value)