feat: 报销审批流重构与管家计划全链路贯通
- 重构报销状态注册表、审批流路由与平台风险标记 - 完善管家意图规划器与模型计划构建器全链路 - 新增 OCR Worker 脚本、数据库会话管理与通知状态 - 优化文档中心、日志视图、预算中心与员工管理交互 - 增强工作台摘要、图标资源与全局主题样式 - 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
@@ -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} 张票据` : '无'),
|
||||
|
||||
Reference in New Issue
Block a user