fix: align disposal flow with administrative role

This commit is contained in:
WuYongJie 2026-08-11 21:49:55 +08:00
parent 02b637064c
commit 843babd381
9 changed files with 55 additions and 32 deletions

View File

@ -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<string, any> | undefined
let actionData: Record<string, any> | 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
}
})

View File

@ -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

View File

@ -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))
})

View File

@ -52,7 +52,6 @@ const ROLE_PERMISSIONS: Record<UserRole, readonly Permission[]> = {
PERMISSIONS.ASSETS_MAINTENANCE,
PERMISSIONS.ASSETS_MOUNT,
PERMISSIONS.ASSETS_PRINT_LABELS,
PERMISSIONS.DISPOSALS_APPLY,
PERMISSIONS.INVENTORY_MANAGE,
PERMISSIONS.NOTIFICATIONS_VIEW,
PERMISSIONS.REPORTS_VIEW

View File

@ -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
]
}
},
{

View File

@ -12,15 +12,14 @@ const ACTIONS_BY_ROLE: Record<UserRole, DashboardRoleAction[]> = {
{ 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' },

View File

@ -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))

View File

@ -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<Array<{ value: DisposalScope; label: string }>>(() => {
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

View File

@ -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)