feat: 完善审批退回流程与报销申请关联
后端优化报销单访问策略和常量定义,增强退回原因和审批状态 流转,前端完善退回对话框和审批交互组件,新增报销申请关联 模型,优化文档中心行数据和审批收件箱工具函数,增强引导 流程和会话模型,补充单元测试覆盖。
This commit is contained in:
@@ -40,6 +40,21 @@ function normalizeRoleCode(value) {
|
||||
return roleCode === 'auditor' ? 'budget_monitor' : roleCode
|
||||
}
|
||||
|
||||
function normalizeComparableText(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function collectIdentityNames(...values) {
|
||||
return values
|
||||
.map((value) => normalizeComparableText(value))
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function identityIntersects(leftValues, rightValues) {
|
||||
const rightSet = new Set(rightValues)
|
||||
return leftValues.some((item) => rightSet.has(item))
|
||||
}
|
||||
|
||||
function hasPlatformAdminIdentity(user) {
|
||||
if (!user) {
|
||||
return false
|
||||
@@ -111,10 +126,53 @@ export function canApproveLeaderExpenseClaims(user) {
|
||||
if (isPlatformAdminUser(user)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return normalizedRoleCodes(user).some((roleCode) => CLAIM_LEADER_APPROVAL_ROLE_CODES.has(roleCode))
|
||||
}
|
||||
|
||||
|
||||
return normalizedRoleCodes(user).some((roleCode) => CLAIM_LEADER_APPROVAL_ROLE_CODES.has(roleCode))
|
||||
}
|
||||
|
||||
export function isCurrentRequestApplicant(request, user) {
|
||||
const applicantNames = collectIdentityNames(
|
||||
request?.person,
|
||||
request?.employeeName,
|
||||
request?.employee_name,
|
||||
request?.profileName,
|
||||
request?.applicant
|
||||
)
|
||||
const currentNames = collectIdentityNames(
|
||||
user?.name,
|
||||
user?.username,
|
||||
user?.email,
|
||||
user?.employeeNo,
|
||||
user?.employee_no
|
||||
)
|
||||
|
||||
return applicantNames.length > 0 && identityIntersects(applicantNames, currentNames)
|
||||
}
|
||||
|
||||
export function isCurrentDirectManagerForRequest(request, user) {
|
||||
if (isCurrentRequestApplicant(request, user)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const managerNames = collectIdentityNames(
|
||||
request?.profileManager,
|
||||
request?.managerName,
|
||||
request?.manager_name,
|
||||
request?.directManagerName,
|
||||
request?.direct_manager_name,
|
||||
request?.manager
|
||||
)
|
||||
const currentNames = collectIdentityNames(
|
||||
user?.name,
|
||||
user?.username,
|
||||
user?.email,
|
||||
user?.employeeNo,
|
||||
user?.employee_no
|
||||
)
|
||||
|
||||
return managerNames.length > 0 && identityIntersects(managerNames, currentNames)
|
||||
}
|
||||
|
||||
export function canAccessAppView(user, viewId) {
|
||||
if (!viewId || !user) {
|
||||
return false
|
||||
|
||||
@@ -51,27 +51,86 @@ function getLatestEvent(events) {
|
||||
return sortedEvents.length ? sortedEvents[sortedEvents.length - 1] : null
|
||||
}
|
||||
|
||||
export function findLeaderApprovalEvent(request) {
|
||||
return getLatestEvent(
|
||||
getRiskFlags(request).filter((flag) => {
|
||||
const source = normalizeText(flag?.source)
|
||||
const eventType = normalizeText(flag?.event_type || flag?.eventType)
|
||||
const previousStage = normalizeText(flag?.previous_approval_stage || flag?.previousApprovalStage)
|
||||
const nextStage = normalizeText(flag?.next_approval_stage || flag?.nextApprovalStage)
|
||||
return (
|
||||
source === 'manual_approval'
|
||||
&& (
|
||||
eventType === 'expense_application_approval'
|
||||
|| previousStage.includes('直属领导')
|
||||
|| previousStage.includes('领导审批')
|
||||
|| nextStage.includes('财务')
|
||||
|| nextStage.includes('审批完成')
|
||||
)
|
||||
)
|
||||
})
|
||||
function isLeaderApprovalEvent(flag) {
|
||||
const source = normalizeText(flag?.source)
|
||||
const eventType = normalizeText(flag?.event_type || flag?.eventType)
|
||||
const previousStage = normalizeText(flag?.previous_approval_stage || flag?.previousApprovalStage)
|
||||
const nextStage = normalizeText(flag?.next_approval_stage || flag?.nextApprovalStage)
|
||||
return (
|
||||
source === 'manual_approval'
|
||||
&& (
|
||||
eventType === 'expense_application_approval'
|
||||
|| previousStage.includes('直属领导')
|
||||
|| previousStage.includes('领导审批')
|
||||
|| nextStage.includes('财务')
|
||||
|| nextStage.includes('审批完成')
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function isLeaderReturnEvent(flag) {
|
||||
const source = normalizeText(flag?.source)
|
||||
const eventType = normalizeText(flag?.event_type || flag?.eventType)
|
||||
const returnStage = normalizeText(flag?.return_stage || flag?.returnStage || flag?.previous_approval_stage)
|
||||
const stageKey = normalizeText(flag?.return_stage_key || flag?.returnStageKey)
|
||||
return (
|
||||
source === 'manual_return'
|
||||
&& (
|
||||
eventType === 'expense_application_return'
|
||||
|| stageKey === 'direct_manager'
|
||||
|| returnStage.includes('直属领导')
|
||||
|| returnStage.includes('领导审批')
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function findLeaderApprovalEvent(request) {
|
||||
return getLatestEvent(getRiskFlags(request).filter(isLeaderApprovalEvent))
|
||||
}
|
||||
|
||||
export function buildLeaderApprovalEvents(request) {
|
||||
return getRiskFlags(request)
|
||||
.filter((flag) => isLeaderApprovalEvent(flag) || isLeaderReturnEvent(flag))
|
||||
.map((event) => {
|
||||
const returned = isLeaderReturnEvent(event)
|
||||
const rawTime = event.created_at || event.createdAt
|
||||
const operator = resolveDisplayName(
|
||||
event.operator,
|
||||
event.operator_name,
|
||||
event.operatorName,
|
||||
request?.profileManager,
|
||||
request?.managerName
|
||||
) || '直属领导'
|
||||
const time = formatDateTime(rawTime)
|
||||
const opinion = normalizeText(event.opinion)
|
||||
|| normalizeText(event.leader_opinion || event.leaderOpinion)
|
||||
|| normalizeText(event.reason)
|
||||
|| normalizeText(event.message)
|
||||
|| (returned ? '已退回申请,请申请人补充后重新提交。' : '已审批通过。')
|
||||
const returnCount = Number(event.return_count || event.returnCount || 0)
|
||||
return {
|
||||
id: normalizeText(event.return_event_id || event.returnEventId || event.approval_event_id || event.approvalEventId)
|
||||
|| `${returned ? 'return' : 'approval'}-${event.created_at || event.createdAt || opinion}`,
|
||||
type: returned ? 'returned' : 'approved',
|
||||
tone: returned ? 'danger' : 'success',
|
||||
title: returned ? '领导退回' : '领导审批通过',
|
||||
operator,
|
||||
time,
|
||||
sortAt: rawTime,
|
||||
opinion,
|
||||
returnCount,
|
||||
meta: [operator ? `${operator}${returned ? '退回' : '通过'}` : '', time].filter(Boolean).join(' · ')
|
||||
}
|
||||
})
|
||||
.sort((left, right) => {
|
||||
const leftDate = toDate(left.sortAt)
|
||||
const rightDate = toDate(right.sortAt)
|
||||
if (!leftDate || !rightDate) return 0
|
||||
return leftDate.getTime() - rightDate.getTime()
|
||||
})
|
||||
.map(({ sortAt, ...event }) => event)
|
||||
}
|
||||
|
||||
export function buildLeaderApprovalInfo(request) {
|
||||
const event = findLeaderApprovalEvent(request)
|
||||
if (!event) {
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { mapExpenseClaimToRequest } from '../composables/useRequests.js'
|
||||
import {
|
||||
canApproveLeaderExpenseClaims,
|
||||
canManageExpenseClaims,
|
||||
isCurrentDirectManagerForRequest,
|
||||
isCurrentRequestApplicant,
|
||||
isFinanceUser
|
||||
} from './accessControl.js'
|
||||
|
||||
export function canProcessApprovalRequest(request, currentUser) {
|
||||
const node = String(request?.workflowNode || '').trim()
|
||||
const currentName = String(currentUser?.name || '').trim()
|
||||
const applicantName = String(request?.person || request?.employeeName || '').trim()
|
||||
|
||||
if (currentName && applicantName && currentName === applicantName) {
|
||||
if (isCurrentRequestApplicant(request, currentUser)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (canManageExpenseClaims(currentUser)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (isFinanceUser(currentUser) && node.includes('财务')) {
|
||||
return true
|
||||
}
|
||||
@@ -29,7 +24,11 @@ export function canProcessApprovalRequest(request, currentUser) {
|
||||
|| node.includes('负责人审批')
|
||||
)
|
||||
|
||||
return canApproveLeaderExpenseClaims(currentUser) && isLeaderApprovalNode
|
||||
return (
|
||||
canApproveLeaderExpenseClaims(currentUser)
|
||||
&& isLeaderApprovalNode
|
||||
&& isCurrentDirectManagerForRequest(request, currentUser)
|
||||
)
|
||||
}
|
||||
|
||||
export function listPendingApprovalRequests(claimsPayload, currentUser) {
|
||||
|
||||
@@ -45,3 +45,22 @@ export function isArchivedDocumentRow(row) {
|
||||
export function excludeArchivedDocumentRows(rows) {
|
||||
return (Array.isArray(rows) ? rows : []).filter((row) => !isArchivedDocumentRow(row))
|
||||
}
|
||||
|
||||
export function isApplicationApprovalRow(row) {
|
||||
if (!row) {
|
||||
return false
|
||||
}
|
||||
|
||||
const statusGroup = String(row.statusGroup || '').trim()
|
||||
return statusGroup === 'in_progress' && isApplicationRequestLike(row.rawRequest || row)
|
||||
}
|
||||
|
||||
export function filterApplicationScopeNewRows(rows) {
|
||||
return (Array.isArray(rows) ? rows : []).filter((row) => !isApplicationApprovalRow(row))
|
||||
}
|
||||
|
||||
export function prepareApplicationScopeRows(rows) {
|
||||
return (Array.isArray(rows) ? rows : [])
|
||||
.filter((row) => isApplicationRequestLike(row.rawRequest || row))
|
||||
.map((row) => (isApplicationApprovalRow(row) ? { ...row, isNewDocument: false } : row))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user