feat: 报销审批流重构与管家计划全链路贯通

- 重构报销状态注册表、审批流路由与平台风险标记
- 完善管家意图规划器与模型计划构建器全链路
- 新增 OCR Worker 脚本、数据库会话管理与通知状态
- 优化文档中心、日志视图、预算中心与员工管理交互
- 增强工作台摘要、图标资源与全局主题样式
- 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-06 17:19:07 +08:00
parent f60cebadb8
commit e124e4bbcb
162 changed files with 9161 additions and 1941 deletions

View File

@@ -54,6 +54,8 @@ const DOCUMENT_BACKED_EXPENSE_TYPES = new Set([
const DOCUMENT_TYPE_APPLICATION = 'application'
const DOCUMENT_TYPE_REIMBURSEMENT = 'reimbursement'
const RELATED_APPLICATION_STEP_LABEL = '关联单据'
const APPLICATION_LINK_STATUS_STEP_LABEL = '关联单据状态'
const APPLICATION_ARCHIVE_STAGE_LABEL = '申请归档'
const ARCHIVED_STEP_LABEL = '已归档'
const REIMBURSEMENT_PROGRESS_LABELS = [
@@ -70,13 +72,17 @@ const APPLICATION_PROGRESS_LABELS = [
'创建申请',
'直属领导审批',
'预算管理者审批',
'审批完成'
'审批完成',
APPLICATION_LINK_STATUS_STEP_LABEL,
ARCHIVED_STEP_LABEL
]
const APPLICATION_PROGRESS_LABELS_WITHOUT_BUDGET = [
'创建申请',
'直属领导审批',
'审批完成'
'审批完成',
APPLICATION_LINK_STATUS_STEP_LABEL,
ARCHIVED_STEP_LABEL
]
function parseNumber(value) {
@@ -425,6 +431,17 @@ function resolveWorkflowNode(claim, approvalMeta, isApplicationDocument = false)
const rawNode = String(claim?.approval_stage || '').trim()
if (rawNode) {
if (
isApplicationDocument
&& approvalMeta.key === 'completed'
&& (
rawNode === '审批完成'
|| rawNode.includes('审批完成')
|| rawNode.includes('申请完成')
)
) {
return APPLICATION_LINK_STATUS_STEP_LABEL
}
if (rawNode === '审批流转' || rawNode.includes('AI预审') || rawNode.includes('AI验审')) {
return approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' ? '待提交' : '直属领导审批'
}
@@ -444,7 +461,7 @@ function resolveWorkflowNode(claim, approvalMeta, isApplicationDocument = false)
if (approvalMeta.key === 'completed') {
const normalizedStatus = String(claim?.status || '').trim().toLowerCase()
return isApplicationDocument ? '审批完成' : normalizedStatus === 'paid' ? '已付款' : '归档入账'
return isApplicationDocument ? APPLICATION_LINK_STATUS_STEP_LABEL : normalizedStatus === 'paid' ? '已付款' : '归档入账'
}
return '直属领导审批'
@@ -578,9 +595,15 @@ function resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode) {
const normalizedNode = String(workflowNode || '').trim()
if (approvalMeta.key === 'completed') {
return 3
return normalizedNode.includes(APPLICATION_ARCHIVE_STAGE_LABEL) ? 4 : 3
}
if (normalizedNode.includes(APPLICATION_ARCHIVE_STAGE_LABEL) || normalizedNode.includes(ARCHIVED_STEP_LABEL)) {
return 4
}
if (normalizedNode.includes(APPLICATION_LINK_STATUS_STEP_LABEL)) {
return 3
}
if (normalizedNode.includes('审批完成') || normalizedNode.includes('申请完成')) {
return 3
}
@@ -602,6 +625,44 @@ function resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode) {
return 1
}
function isApplicationArchivedWorkflow(claim, workflowNode) {
const normalizedNode = normalizeText(workflowNode || claim?.approval_stage || claim?.workflowNode)
if (normalizedNode.includes(APPLICATION_ARCHIVE_STAGE_LABEL) || normalizedNode.includes(ARCHIVED_STEP_LABEL)) {
return true
}
return getRiskFlags(claim).some((flag) => (
flag
&& typeof flag === 'object'
&& normalizeText(flag.source) === 'application_archive_sync'
))
}
function resolveApplicationLinkedReimbursementNo(claim) {
for (const flag of [...getRiskFlags(claim)].reverse()) {
if (!flag || typeof flag !== 'object') {
continue
}
const generatedNo = normalizeText(
flag.generated_draft_claim_no
|| flag.generatedDraftClaimNo
|| flag.reimbursement_claim_no
|| flag.reimbursementClaimNo
)
if (generatedNo) {
return generatedNo
}
}
return ''
}
function buildApplicationLinkStatusStepMeta(claim) {
const reimbursementNo = resolveApplicationLinkedReimbursementNo(claim)
const updatedAt = formatDateTime(claim?.updated_at)
return reimbursementNo
? buildProgressStepMeta(`关联中 ${reimbursementNo}`, updatedAt)
: buildProgressStepMeta('未关联', updatedAt)
}
function normalizeText(value) {
return String(value || '').trim()
}
@@ -1069,6 +1130,10 @@ function buildCompletedStepMeta(claim, label) {
return buildProgressStepMeta('待核对关联单据', createdAt)
}
if (stepLabel === APPLICATION_LINK_STATUS_STEP_LABEL) {
return buildApplicationLinkStatusStepMeta(claim)
}
if (stepLabel === '创建单据' || stepLabel === '创建申请') {
const createdAt = formatDateTime(claim?.created_at)
return buildProgressStepMeta(stepLabel === '创建申请' ? `${employeeName}发起申请` : `${employeeName}创建`, createdAt)
@@ -1201,24 +1266,32 @@ function buildProgressSteps(approvalMeta, workflowNode, claim = {}, options = {}
&& !hasMergedApplicationBudgetApproval
&& applicationRequiresBudgetReviewStep(claim, workflowNode)
)
const isApplicationDocument = documentTypeCode === DOCUMENT_TYPE_APPLICATION
const applicationArchived = isApplicationDocument && isApplicationArchivedWorkflow(claim, workflowNode)
const progressLabels =
documentTypeCode === DOCUMENT_TYPE_APPLICATION
isApplicationDocument
? hasApplicationReturnStep
? ['创建申请', '直属领导审批', '退回', '待提交']
: hasMergedApplicationBudgetApproval
? ['创建申请', '直属领导审批', '审批完成']
? ['创建申请', '直属领导审批', '审批完成', APPLICATION_LINK_STATUS_STEP_LABEL, ARCHIVED_STEP_LABEL]
: shouldShowApplicationBudgetStep
? APPLICATION_PROGRESS_LABELS
: APPLICATION_PROGRESS_LABELS_WITHOUT_BUDGET
: REIMBURSEMENT_PROGRESS_LABELS
const applicationLinkIndex = progressLabels.indexOf(APPLICATION_LINK_STATUS_STEP_LABEL)
const applicationArchiveIndex = progressLabels.indexOf(ARCHIVED_STEP_LABEL)
const currentIndex =
documentTypeCode === DOCUMENT_TYPE_APPLICATION
isApplicationDocument
? hasApplicationReturnStep
? 3
: Math.min(
resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode),
Math.max(0, progressLabels.length - 1)
)
: applicationArchived && applicationArchiveIndex >= 0
? applicationArchiveIndex
: approvalMeta.key === 'completed' && applicationLinkIndex >= 0
? applicationLinkIndex
: Math.min(
resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode),
Math.max(0, progressLabels.length - 1)
)
: resolveProgressCurrentIndex(approvalMeta, workflowNode)
const currentTime =
approvalMeta.key === 'completed'
@@ -1233,7 +1306,7 @@ function buildProgressSteps(approvalMeta, workflowNode, claim = {}, options = {}
return progressLabels.map((label, index) => {
const displayLabel = resolveProgressDisplayLabel(label, documentTypeCode, claim, approvalMeta)
if (approvalMeta.key === 'completed') {
if (approvalMeta.key === 'completed' && (!isApplicationDocument || applicationArchived)) {
const stepMeta = buildCompletedStepMeta(claim, label)
return {
index: index + 1,
@@ -1264,6 +1337,20 @@ function buildProgressSteps(approvalMeta, workflowNode, claim = {}, options = {}
}
if (index === currentIndex) {
if (isApplicationDocument && label === APPLICATION_LINK_STATUS_STEP_LABEL) {
const stepMeta = buildApplicationLinkStatusStepMeta(claim)
return {
index: index + 1,
label: displayLabel,
rawLabel: label,
time: stepMeta.time,
detail: stepMeta.detail,
title: stepMeta.title,
done: false,
active: true,
current: true
}
}
const stayDuration = formatDurationFrom(resolveCurrentStepStartedAt(claim, label))
return {
index: index + 1,
@@ -1385,6 +1472,11 @@ export function mapExpenseClaimToRequest(claim) {
const isApplicationDocument = documentTypeMeta.documentTypeCode === DOCUMENT_TYPE_APPLICATION
const approvalMeta = resolveApprovalMeta(claim?.status)
const workflowNode = resolveWorkflowNode(claim, approvalMeta, isApplicationDocument)
const applicationArchived = isApplicationDocument && isApplicationArchivedWorkflow(claim, workflowNode)
const applicationLinkedReimbursementNo = isApplicationDocument ? resolveApplicationLinkedReimbursementNo(claim) : ''
const applicationLinkStatusText = applicationLinkedReimbursementNo
? `关联中 ${applicationLinkedReimbursementNo}`
: '未关联'
const invoiceCount = Math.max(0, parseNumber(claim?.invoice_count))
const riskMeta = buildRiskMeta(claim?.risk_flags_json)
const riskSummary = riskMeta.summary
@@ -1453,10 +1545,18 @@ export function mapExpenseClaimToRequest(claim) {
secondaryStatusValue: isApplicationDocument
? approvalMeta.key === 'supplement'
? '领导已退回,待重新提交'
: '已进入审批流程'
: applicationArchived
? '已归档'
: approvalMeta.key === 'completed'
? applicationLinkStatusText
: '已进入审批流程'
: (invoiceCount > 0 ? `已关联 ${invoiceCount} 张票据` : '待上传票据'),
secondaryStatusTone: isApplicationDocument
? approvalMeta.key === 'supplement' ? 'warning' : 'success'
? approvalMeta.key === 'supplement'
? 'warning'
: approvalMeta.key === 'completed' && !applicationArchived && !applicationLinkedReimbursementNo
? 'warning'
: 'success'
: (invoiceCount > 0 ? 'success' : 'warning'),
riskSummary,
attachmentSummary: isApplicationDocument ? '申请单' : (invoiceCount > 0 ? `${invoiceCount} 张票据` : '无'),