feat: 完善文档中心与报销申请交互及侧边栏重构

后端优化编排器报销查询和本体检测精度,增强报销单草稿保
存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导
航,完善文档中心状态筛选和详情提示,报销创建和审批详情
页优化会话管理和费用明细交互,新增助手应用服务和预设动
作工具函数,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-25 13:35:39 +08:00
parent 50b1c3f9a9
commit d0e946cf47
59 changed files with 5117 additions and 416 deletions

View File

@@ -5,6 +5,10 @@ import { filterActionableRiskFlags } from '../utils/riskFlags.js'
const EXPENSE_TYPE_LABELS = {
travel: '差旅费',
travel_application: '差旅费用申请',
expense_application: '费用申请',
purchase_application: '采购费用申请',
meeting_application: '会务费用申请',
train_ticket: '火车票',
flight_ticket: '机票',
ship_ticket: '轮船票',
@@ -36,6 +40,8 @@ const SYSTEM_GENERATED_EXPENSE_TYPES = new Set(['travel_allowance'])
const LONG_DISTANCE_TRAVEL_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket'])
const ROUTE_DESCRIPTION_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket', 'ship_ticket', 'ferry_ticket', 'ride_ticket'])
const HOTEL_DESCRIPTION_EXPENSE_TYPES = new Set(['hotel_ticket'])
const DOCUMENT_TYPE_APPLICATION = 'application'
const DOCUMENT_TYPE_REIMBURSEMENT = 'reimbursement'
const REIMBURSEMENT_PROGRESS_LABELS = [
'创建单据',
@@ -46,6 +52,12 @@ const REIMBURSEMENT_PROGRESS_LABELS = [
'归档入账'
]
const APPLICATION_PROGRESS_LABELS = [
'创建申请',
'直属领导审批',
'审批完成'
]
function parseNumber(value) {
const nextValue = Number(value)
return Number.isFinite(nextValue) ? nextValue : 0
@@ -123,6 +135,28 @@ function resolveTypeLabel(typeCode) {
return EXPENSE_TYPE_LABELS[String(typeCode || '').trim()] || EXPENSE_TYPE_LABELS.other
}
function resolveDocumentTypeMeta(claim, typeCode) {
const explicitType = String(
claim?.document_type_code
|| claim?.documentTypeCode
|| claim?.document_type
|| claim?.documentType
|| ''
).trim()
const claimNo = String(claim?.claim_no || claim?.claimNo || '').trim().toUpperCase()
const normalizedType = String(typeCode || '').trim()
const isApplication =
explicitType === DOCUMENT_TYPE_APPLICATION
|| explicitType === 'expense_application'
|| claimNo.startsWith('APP-')
|| normalizedType === 'application'
|| normalizedType.endsWith('_application')
return isApplication
? { documentTypeCode: DOCUMENT_TYPE_APPLICATION, documentTypeLabel: '申请单' }
: { documentTypeCode: DOCUMENT_TYPE_REIMBURSEMENT, documentTypeLabel: '报销单' }
}
function normalizeExpenseType(typeCode) {
return String(typeCode || '').trim() || 'other'
}
@@ -237,7 +271,7 @@ function resolveApprovalMeta(status) {
return { key: 'in_progress', label: '审批中', tone: 'info' }
}
function resolveWorkflowNode(claim, approvalMeta) {
function resolveWorkflowNode(claim, approvalMeta, isApplicationDocument = false) {
if (String(claim?.status || '').trim().toLowerCase() === 'returned') {
return '待提交'
}
@@ -259,10 +293,10 @@ function resolveWorkflowNode(claim, approvalMeta) {
}
if (approvalMeta.key === 'completed') {
return '归档入账'
return isApplicationDocument ? '审批完成' : '归档入账'
}
return 'AI预审'
return isApplicationDocument ? '直属领导审批' : 'AI预审'
}
function stringifyRiskFlag(value) {
@@ -345,6 +379,31 @@ function resolveProgressCurrentIndex(approvalMeta, workflowNode) {
return 2
}
function resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode) {
const normalizedNode = String(workflowNode || '').trim()
if (approvalMeta.key === 'completed') {
return 2
}
if (normalizedNode.includes('审批完成') || normalizedNode.includes('申请完成')) {
return 2
}
if (
normalizedNode.includes('直属领导')
|| normalizedNode.includes('领导审批')
|| normalizedNode.includes('部门负责人')
|| normalizedNode.includes('负责人审批')
) {
return 1
}
if (approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' || approvalMeta.key === 'rejected') {
return 0
}
return 1
}
function normalizeText(value) {
return String(value || '').trim()
}
@@ -438,9 +497,9 @@ function buildCompletedStepMeta(claim, label) {
const stepLabel = normalizeText(label)
const employeeName = normalizeText(claim?.employee_name) || '申请人'
if (stepLabel === '创建单据') {
if (stepLabel === '创建单据' || stepLabel === '创建申请') {
const createdAt = formatDateTime(claim?.created_at)
return buildProgressStepMeta(`${employeeName}创建`, createdAt)
return buildProgressStepMeta(stepLabel === '创建申请' ? `${employeeName}发起申请` : `${employeeName}创建`, createdAt)
}
if (stepLabel === '待提交') {
@@ -477,12 +536,17 @@ function buildCompletedStepMeta(claim, label) {
return buildProgressStepMeta('归档入账', archivedAt)
}
if (stepLabel === '审批完成') {
const completedAt = formatDateTime(claim?.updated_at)
return buildProgressStepMeta('审批完成', completedAt)
}
return buildProgressStepMeta('已完成')
}
function resolveCurrentStepStartedAt(claim, label) {
const stepLabel = normalizeText(label)
if (stepLabel === '创建单据') {
if (stepLabel === '创建单据' || stepLabel === '创建申请') {
return claim?.created_at
}
if (stepLabel === '待提交') {
@@ -499,14 +563,22 @@ function resolveCurrentStepStartedAt(claim, label) {
const leaderApprovalEvent = findApprovalEventForStep(claim, '直属领导审批')
return leaderApprovalEvent?.created_at || leaderApprovalEvent?.createdAt || claim?.updated_at || claim?.submitted_at
}
if (stepLabel === '归档入账') {
if (stepLabel === '归档入账' || stepLabel === '审批完成') {
return claim?.updated_at || claim?.submitted_at
}
return ''
}
function buildProgressSteps(approvalMeta, workflowNode, claim = {}) {
const currentIndex = resolveProgressCurrentIndex(approvalMeta, workflowNode)
function buildProgressSteps(approvalMeta, workflowNode, claim = {}, options = {}) {
const documentTypeCode = String(options.documentTypeCode || '').trim()
const progressLabels =
documentTypeCode === DOCUMENT_TYPE_APPLICATION
? APPLICATION_PROGRESS_LABELS
: REIMBURSEMENT_PROGRESS_LABELS
const currentIndex =
documentTypeCode === DOCUMENT_TYPE_APPLICATION
? resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode)
: resolveProgressCurrentIndex(approvalMeta, workflowNode)
const currentTime =
approvalMeta.key === 'completed'
? '已完成'
@@ -516,7 +588,7 @@ function buildProgressSteps(approvalMeta, workflowNode, claim = {}) {
? '已退回'
: '进行中'
return REIMBURSEMENT_PROGRESS_LABELS.map((label, index) => {
return progressLabels.map((label, index) => {
if (approvalMeta.key === 'completed') {
const stepMeta = buildCompletedStepMeta(claim, label)
return {
@@ -636,8 +708,10 @@ function buildExpenseItems(claim, riskSummary) {
export function mapExpenseClaimToRequest(claim) {
const typeCode = String(claim?.expense_type || '').trim() || 'other'
const typeLabel = resolveTypeLabel(typeCode)
const documentTypeMeta = resolveDocumentTypeMeta(claim, typeCode)
const isApplicationDocument = documentTypeMeta.documentTypeCode === DOCUMENT_TYPE_APPLICATION
const approvalMeta = resolveApprovalMeta(claim?.status)
const workflowNode = resolveWorkflowNode(claim, approvalMeta)
const workflowNode = resolveWorkflowNode(claim, approvalMeta, isApplicationDocument)
const invoiceCount = Math.max(0, parseNumber(claim?.invoice_count))
const riskSummary = buildRiskSummary(claim?.risk_flags_json)
const expenseItems = buildExpenseItems(claim, riskSummary)
@@ -659,8 +733,9 @@ export function mapExpenseClaimToRequest(claim) {
entity: '',
typeCode,
typeLabel,
detailVariant: typeCode === 'travel' ? 'travel' : 'general',
title: String(claim?.reason || '').trim() || `${typeLabel}报销`,
...documentTypeMeta,
detailVariant: typeCode === 'travel' || typeCode === 'travel_application' ? 'travel' : 'general',
title: String(claim?.reason || '').trim() || (isApplicationDocument ? typeLabel : `${typeLabel}报销`),
sceneLabel: typeLabel,
sceneTarget: String(claim?.location || '').trim() || '待补充',
location: String(claim?.location || '').trim() || '待补充',
@@ -678,18 +753,24 @@ export function mapExpenseClaimToRequest(claim) {
approvalKey: approvalMeta.key,
approvalStatus: approvalMeta.label,
approvalTone: approvalMeta.tone,
secondaryStatusLabel: typeCode === 'travel' ? '行程状态' : '票据状态',
secondaryStatusValue: invoiceCount > 0 ? `已关联 ${invoiceCount} 张票据` : '待上传票据',
secondaryStatusTone: invoiceCount > 0 ? 'success' : 'warning',
secondaryStatusLabel: isApplicationDocument ? '申请材料' : (typeCode === 'travel' ? '行程状态' : '票据状态'),
secondaryStatusValue: isApplicationDocument
? '已进入审批流程'
: (invoiceCount > 0 ? `已关联 ${invoiceCount} 张票据` : '待上传票据'),
secondaryStatusTone: isApplicationDocument ? 'success' : (invoiceCount > 0 ? 'success' : 'warning'),
riskSummary,
attachmentSummary: invoiceCount > 0 ? `${invoiceCount} 张票据` : '无',
expenseTableSummary: expenseItems.length
attachmentSummary: isApplicationDocument ? '申请单' : (invoiceCount > 0 ? `${invoiceCount} 张票据` : '无'),
expenseTableSummary: isApplicationDocument
? '预计金额已纳入预算管理口径'
: expenseItems.length
? (invoiceCount > 0
? `${expenseItems.length} 条费用明细,已关联 ${invoiceCount} 张票据`
: `${expenseItems.length} 条费用明细,待补充票据`)
: '暂无费用明细',
note: String(claim?.reason || '').trim(),
progressSteps: buildProgressSteps(approvalMeta, workflowNode, claim),
progressSteps: buildProgressSteps(approvalMeta, workflowNode, claim, {
documentTypeCode: documentTypeMeta.documentTypeCode
}),
expenseItems
}
}