feat: 新增预算费控模型与报销审批流引擎
后端新增预算费控服务和报销单审批流模块,引入申请人费用画像 算法,优化知识库 RAG 运行时和同步逻辑,完善报销单工作流常 量和明细同步,更新差旅报销规则电子表格,前端新增预算分析 组件和数字员工模型,完善审批对话框和洞察面板交互,优化侧 边栏和顶栏样式,补充单元测试。
This commit is contained in:
@@ -19,9 +19,10 @@ const VIEW_ROLE_RULES = {
|
||||
employees: ['manager'],
|
||||
settings: ['manager']
|
||||
}
|
||||
const CLAIM_MANAGER_ROLE_CODES = new Set(['executive'])
|
||||
const CLAIM_RETURN_ROLE_CODES = new Set(['finance', 'executive', 'manager', 'approver'])
|
||||
const CLAIM_LEADER_APPROVAL_ROLE_CODES = new Set(['manager', 'approver'])
|
||||
const CLAIM_MANAGER_ROLE_CODES = new Set(['executive'])
|
||||
const CLAIM_RETURN_ROLE_CODES = new Set(['finance', 'executive', 'manager', 'approver', 'budget_monitor'])
|
||||
const CLAIM_LEADER_APPROVAL_ROLE_CODES = new Set(['manager', 'approver'])
|
||||
const CLAIM_BUDGET_APPROVAL_GRADE = 'P8'
|
||||
|
||||
function normalizedRoleCodes(user) {
|
||||
if (!user) {
|
||||
@@ -55,6 +56,25 @@ function identityIntersects(leftValues, rightValues) {
|
||||
return leftValues.some((item) => rightSet.has(item))
|
||||
}
|
||||
|
||||
function normalizedGrade(user) {
|
||||
return String(user?.grade || user?.employeeGrade || '').trim().toUpperCase()
|
||||
}
|
||||
|
||||
function departmentIntersects(request, user) {
|
||||
const requestDepartments = collectIdentityNames(
|
||||
request?.dept,
|
||||
request?.departmentName,
|
||||
request?.department_name
|
||||
)
|
||||
const currentDepartments = collectIdentityNames(
|
||||
user?.department,
|
||||
user?.departmentName,
|
||||
user?.department_name
|
||||
)
|
||||
|
||||
return requestDepartments.length > 0 && identityIntersects(requestDepartments, currentDepartments)
|
||||
}
|
||||
|
||||
function hasPlatformAdminIdentity(user) {
|
||||
if (!user) {
|
||||
return false
|
||||
@@ -130,6 +150,25 @@ export function canApproveLeaderExpenseClaims(user) {
|
||||
return normalizedRoleCodes(user).some((roleCode) => CLAIM_LEADER_APPROVAL_ROLE_CODES.has(roleCode))
|
||||
}
|
||||
|
||||
export function canApproveBudgetExpenseApplications(user, request = null) {
|
||||
if (isPlatformAdminUser(user)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const roleCodes = normalizedRoleCodes(user)
|
||||
if (roleCodes.includes('executive')) {
|
||||
return true
|
||||
}
|
||||
if (!roleCodes.includes('budget_monitor')) {
|
||||
return false
|
||||
}
|
||||
if (normalizedGrade(user) !== CLAIM_BUDGET_APPROVAL_GRADE) {
|
||||
return false
|
||||
}
|
||||
|
||||
return request ? departmentIntersects(request, user) : true
|
||||
}
|
||||
|
||||
export function isCurrentRequestApplicant(request, user) {
|
||||
const applicantNames = collectIdentityNames(
|
||||
request?.person,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mapExpenseClaimToRequest } from '../composables/useRequests.js'
|
||||
import {
|
||||
canApproveBudgetExpenseApplications,
|
||||
canApproveLeaderExpenseClaims,
|
||||
isCurrentDirectManagerForRequest,
|
||||
isCurrentRequestApplicant,
|
||||
@@ -17,6 +18,10 @@ export function canProcessApprovalRequest(request, currentUser) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (node.includes('预算')) {
|
||||
return canApproveBudgetExpenseApplications(currentUser, request)
|
||||
}
|
||||
|
||||
const isLeaderApprovalNode = (
|
||||
node.includes('直属领导')
|
||||
|| node.includes('领导审批')
|
||||
|
||||
@@ -115,10 +115,69 @@ export function hasPendingInfo(request) {
|
||||
return false
|
||||
}
|
||||
|
||||
function resolveDetailAlertTone(request) {
|
||||
if (request?.approvalKey === 'completed') return 'success'
|
||||
if (request?.approvalKey === 'rejected') return 'danger'
|
||||
return 'warning'
|
||||
function getRiskFlags(request) {
|
||||
if (Array.isArray(request?.riskFlags)) {
|
||||
return request.riskFlags
|
||||
}
|
||||
if (Array.isArray(request?.risk_flags_json)) {
|
||||
return request.risk_flags_json
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function parseNonNegativeInteger(value) {
|
||||
const nextValue = Number(value)
|
||||
return Number.isFinite(nextValue) && nextValue > 0 ? Math.floor(nextValue) : 0
|
||||
}
|
||||
|
||||
function resolveSlaReminderCount(request) {
|
||||
const directCount = [
|
||||
request?.slaReminderCount,
|
||||
request?.sla_reminder_count,
|
||||
request?.slaUrgeCount,
|
||||
request?.sla_urge_count,
|
||||
request?.urgeCount,
|
||||
request?.urge_count,
|
||||
request?.reminderCount,
|
||||
request?.reminder_count
|
||||
].reduce((max, value) => Math.max(max, parseNonNegativeInteger(value)), 0)
|
||||
|
||||
if (directCount > 0) {
|
||||
return directCount
|
||||
}
|
||||
|
||||
return getRiskFlags(request).reduce((count, flag) => {
|
||||
if (!flag || typeof flag !== 'object') {
|
||||
return count
|
||||
}
|
||||
|
||||
const explicitCount = [
|
||||
flag.slaReminderCount,
|
||||
flag.sla_reminder_count,
|
||||
flag.slaUrgeCount,
|
||||
flag.sla_urge_count,
|
||||
flag.urgeCount,
|
||||
flag.urge_count,
|
||||
flag.reminderCount,
|
||||
flag.reminder_count
|
||||
].reduce((max, value) => Math.max(max, parseNonNegativeInteger(value)), 0)
|
||||
|
||||
if (explicitCount > 0) {
|
||||
return count + explicitCount
|
||||
}
|
||||
|
||||
const signal = [
|
||||
flag.source,
|
||||
flag.event_type,
|
||||
flag.eventType,
|
||||
flag.action,
|
||||
flag.type,
|
||||
flag.label,
|
||||
flag.message
|
||||
].join(' ')
|
||||
|
||||
return /sla|remind|reminder|urge|催单/i.test(signal) ? count + 1 : count
|
||||
}, 0)
|
||||
}
|
||||
|
||||
export function buildDetailAlerts(request) {
|
||||
@@ -127,11 +186,13 @@ export function buildDetailAlerts(request) {
|
||||
}
|
||||
|
||||
const alerts = []
|
||||
const nodeLabel = String(request.node || request.approval || '').trim()
|
||||
const slaReminderCount = resolveSlaReminderCount(request)
|
||||
|
||||
if (nodeLabel) {
|
||||
alerts.push({ label: nodeLabel, tone: resolveDetailAlertTone(request) })
|
||||
}
|
||||
alerts.push({
|
||||
label: `SLA 催单次数 ${slaReminderCount}`,
|
||||
tone: slaReminderCount > 0 ? 'warning' : 'neutral',
|
||||
icon: 'mdi mdi-bell-ring-outline'
|
||||
})
|
||||
|
||||
if (hasMissingAttachment(request)) {
|
||||
alerts.push({ label: '缺少票据', tone: 'warning' })
|
||||
|
||||
Reference in New Issue
Block a user