feat: 同步报销流程与工作台改动

This commit is contained in:
caoxiaozhu
2026-06-09 08:32:00 +00:00
parent e124e4bbcb
commit 25724c354f
64 changed files with 6518 additions and 687 deletions

View File

@@ -1,6 +1,6 @@
import { computed, reactive, ref } from 'vue'
import { fetchExpenseClaims } from '../services/reimbursements.js'
import { fetchAllExpenseClaims } from '../services/reimbursements.js'
import { filterActionableRiskFlags, normalizeRiskFlagTone } from '../utils/riskFlags.js'
const EXPENSE_TYPE_LABELS = {
@@ -72,7 +72,6 @@ const APPLICATION_PROGRESS_LABELS = [
'创建申请',
'直属领导审批',
'预算管理者审批',
'审批完成',
APPLICATION_LINK_STATUS_STEP_LABEL,
ARCHIVED_STEP_LABEL
]
@@ -80,7 +79,6 @@ const APPLICATION_PROGRESS_LABELS = [
const APPLICATION_PROGRESS_LABELS_WITHOUT_BUDGET = [
'创建申请',
'直属领导审批',
'审批完成',
APPLICATION_LINK_STATUS_STEP_LABEL,
ARCHIVED_STEP_LABEL
]
@@ -595,17 +593,17 @@ function resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode) {
const normalizedNode = String(workflowNode || '').trim()
if (approvalMeta.key === 'completed') {
return normalizedNode.includes(APPLICATION_ARCHIVE_STAGE_LABEL) ? 4 : 3
return normalizedNode.includes(APPLICATION_ARCHIVE_STAGE_LABEL) ? 3 : 2
}
if (normalizedNode.includes(APPLICATION_ARCHIVE_STAGE_LABEL) || normalizedNode.includes(ARCHIVED_STEP_LABEL)) {
return 4
return 3
}
if (normalizedNode.includes(APPLICATION_LINK_STATUS_STEP_LABEL)) {
return 3
return 2
}
if (normalizedNode.includes('审批完成') || normalizedNode.includes('申请完成')) {
return 3
return 2
}
if (normalizedNode.includes('预算')) {
return 2
@@ -693,6 +691,36 @@ function resolveApplicationApproverName(claim) {
) || '直属领导'
}
function resolveReimbursementApproverName(claim, label) {
const stepLabel = normalizeText(label)
if (stepLabel === '直属领导审批') {
return resolveDisplayName(
claim?.manager_name,
claim?.managerName,
claim?.profile_manager,
claim?.profileManager,
claim?.direct_manager_name,
claim?.directManagerName
) || '直属领导'
}
if (stepLabel === '财务审批') {
const routeEvent = findReimbursementFinanceRouteEvent(claim)
return resolveDisplayName(
claim?.finance_approver_name,
claim?.financeApproverName,
routeEvent?.next_approver_name,
routeEvent?.nextApproverName,
routeEvent?.finance_approver_name,
routeEvent?.financeApproverName,
claim?.finance_owner_name,
claim?.financeOwnerName
) || '财务'
}
return stepLabel.replace(/审批$/, '') || '审批人'
}
function resolveApplicationBudgetApproverName(claim) {
const routeEvent = findApprovalEventForStep(claim, '直属领导审批')
return resolveDisplayName(
@@ -708,6 +736,15 @@ function resolveApplicationBudgetApproverName(claim) {
function resolveProgressDisplayLabel(label, documentTypeCode, claim, approvalMeta) {
const normalizedLabel = normalizeText(label)
const workflowNode = normalizeText(claim?.approval_stage || claim?.workflowNode)
if (
documentTypeCode !== DOCUMENT_TYPE_APPLICATION
&& approvalMeta.key !== 'completed'
&& (normalizedLabel === '直属领导审批' || normalizedLabel === '财务审批')
&& workflowNode.includes(normalizedLabel.replace(/审批$/, ''))
) {
return `等待 ${resolveReimbursementApproverName(claim, normalizedLabel)} 批复`
}
if (
documentTypeCode === DOCUMENT_TYPE_APPLICATION
&& approvalMeta.key !== 'completed'
@@ -796,6 +833,24 @@ function findApprovalEventForStep(claim, label) {
return getLatestEvent(events)
}
function findReimbursementFinanceRouteEvent(claim) {
return getLatestEvent(
getRiskFlags(claim).filter((flag) => {
if (!flag || typeof flag !== 'object') {
return false
}
const source = normalizeText(flag.source)
if (!['manual_approval', 'budget_approval'].includes(source)) {
return false
}
const nextStage = normalizeText(flag.next_approval_stage || flag.nextApprovalStage)
return nextStage.includes('财务')
})
)
}
function findLatestReturnEvent(claim) {
return getLatestEvent(
getRiskFlags(claim).filter((flag) => (
@@ -1066,13 +1121,39 @@ function findMergedApplicationBudgetApprovalEvent(claim) {
source === 'manual_approval'
&& eventType === 'expense_application_approval'
&& previousStage.includes('直属领导')
&& nextStage.includes('审批完成')
&& (
nextStage.includes('审批完成')
|| nextStage.includes(APPLICATION_LINK_STATUS_STEP_LABEL)
|| nextStage.includes('申请完成')
)
&& mergedFlag
)
})
)
}
function resolveBudgetRouteResult(flag, routeDecision = {}) {
if (routeDecision && typeof routeDecision === 'object') {
const routeBudgetResult = routeDecision.budget_result || routeDecision.budgetResult
if (routeBudgetResult && typeof routeBudgetResult === 'object') {
return routeBudgetResult
}
}
const flagBudgetResult = flag?.budget_result || flag?.budgetResult
return flagBudgetResult && typeof flagBudgetResult === 'object' ? flagBudgetResult : {}
}
function applicationBudgetRouteMeetsThreshold(flag, routeDecision = {}) {
const budgetResult = resolveBudgetRouteResult(flag, routeDecision)
const metrics = budgetResult.metrics && typeof budgetResult.metrics === 'object' ? budgetResult.metrics : {}
const overBudgetAmount = parseNumber(metrics.over_budget_amount ?? metrics.overBudgetAmount)
const afterUsageRate = parseNumber(metrics.after_usage_rate ?? metrics.afterUsageRate)
const claimAmountRatio = parseNumber(metrics.claim_amount_ratio ?? metrics.claimAmountRatio)
return overBudgetAmount > 0 || Math.max(afterUsageRate, claimAmountRatio) >= 90
}
function applicationRequiresBudgetReviewStep(claim, workflowNode) {
const node = normalizeText(workflowNode || claim?.approval_stage || claim?.workflowNode)
if (node.includes('预算')) {
@@ -1087,24 +1168,22 @@ function applicationRequiresBudgetReviewStep(claim, workflowNode) {
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)
const routeDecision = flag.route_decision || flag.routeDecision || {}
if (source === 'approval_routing' && flag.requires_budget_review === true) {
return true
return applicationBudgetRouteMeetsThreshold(flag, flag)
}
if (
routeDecision
&& typeof routeDecision === 'object'
&& routeDecision.requires_budget_review === true
) {
return true
return applicationBudgetRouteMeetsThreshold(flag, routeDecision)
}
return (
source === 'budget_approval'
|| eventType === 'expense_application_budget_approval'
|| previousStage.includes('预算')
|| nextStage.includes('预算')
)
})
}
@@ -1273,7 +1352,7 @@ function buildProgressSteps(approvalMeta, workflowNode, claim = {}, options = {}
? hasApplicationReturnStep
? ['创建申请', '直属领导审批', '退回', '待提交']
: hasMergedApplicationBudgetApproval
? ['创建申请', '直属领导审批', '审批完成', APPLICATION_LINK_STATUS_STEP_LABEL, ARCHIVED_STEP_LABEL]
? ['创建申请', '直属领导审批', APPLICATION_LINK_STATUS_STEP_LABEL, ARCHIVED_STEP_LABEL]
: shouldShowApplicationBudgetStep
? APPLICATION_PROGRESS_LABELS
: APPLICATION_PROGRESS_LABELS_WITHOUT_BUDGET
@@ -1512,6 +1591,8 @@ export function mapExpenseClaimToRequest(claim) {
employeePosition: String(claim?.employee_position || '').trim(),
employeeGrade: String(claim?.employee_grade || '').trim(),
managerName: resolveDisplayName(claim?.manager_name),
financeApproverName: resolveDisplayName(claim?.finance_approver_name, claim?.financeApproverName),
financeOwnerName: resolveDisplayName(claim?.finance_owner_name, claim?.financeOwnerName),
budgetApproverName: resolveDisplayName(claim?.budget_approver_name, claim?.budgetApproverName),
budgetApproverGrade: String(claim?.budget_approver_grade || claim?.budgetApproverGrade || '').trim(),
budgetApproverRoleCode: String(claim?.budget_approver_role_code || claim?.budgetApproverRoleCode || '').trim(),
@@ -1665,19 +1746,26 @@ export function useRequests() {
})
})
async function reload() {
loading.value = true
error.value = ''
async function reload(options = {}) {
const silent = Boolean(options?.silent)
if (!silent) {
loading.value = true
error.value = ''
}
try {
const payload = await fetchExpenseClaims()
const payload = await fetchAllExpenseClaims()
requests.value = Array.isArray(payload) ? payload.map((item) => mapExpenseClaimToRequest(item)) : []
loaded.value = true
} catch (nextError) {
requests.value = []
if (!silent) {
requests.value = []
}
error.value = nextError instanceof Error ? nextError.message : '个人报销列表加载失败。'
} finally {
loading.value = false
if (!silent) {
loading.value = false
}
}
}