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

@@ -11,6 +11,8 @@ const CREATE_APPLICATION = '\u521b\u5efa\u7533\u8bf7'
const DIRECT_MANAGER_APPROVAL = '\u76f4\u5c5e\u9886\u5bfc\u5ba1\u6279'
const BUDGET_MANAGER_APPROVAL = '\u9884\u7b97\u7ba1\u7406\u8005\u5ba1\u6279'
const APPROVAL_COMPLETED = '\u5ba1\u6279\u5b8c\u6210'
const APPLICATION_LINK_STATUS = '\u5173\u8054\u5355\u636e\u72b6\u6001'
const APPLICATION_ARCHIVE = '\u7533\u8bf7\u5f52\u6863'
const RETURNED = '\u9000\u56de'
const WAIT_SUBMIT = '\u5f85\u63d0\u4ea4'
const LINKED_APPLICATION = '\u5173\u8054\u5355\u636e'
@@ -156,7 +158,7 @@ test('application claims are mapped as application documents', () => {
assert.equal(request.expenseTableSummary, '预计金额已随申请提交')
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, WAIT_LEADER_LI_APPROVAL, APPROVAL_COMPLETED]
[CREATE_APPLICATION, WAIT_LEADER_LI_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.some((step) => step.label === 'AI预审'), false)
assert.equal(request.progressSteps.some((step) => step.label === '财务审批'), false)
@@ -250,7 +252,7 @@ test('application claims wait for department P8 budget monitor after leader appr
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_ZHAO_APPROVAL, APPROVAL_COMPLETED]
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_ZHAO_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.find((step) => step.label === WAIT_BUDGET_ZHAO_APPROVAL)?.current, true)
assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, 'Leader Li通过')
@@ -293,7 +295,7 @@ test('application budget wait label uses claim-level budget approver snapshot',
assert.equal(request.budgetApproverName, 'P8 Executive')
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_P8_EXECUTIVE_APPROVAL, APPROVAL_COMPLETED]
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_P8_EXECUTIVE_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.find((step) => step.label === WAIT_BUDGET_P8_EXECUTIVE_APPROVAL)?.current, true)
})
@@ -386,14 +388,16 @@ test('approved application claims complete after budget approval', () => {
})
assert.equal(request.documentTypeCode, 'application')
assert.equal(request.workflowNode, '审批完成')
assert.equal(request.workflowNode, APPLICATION_LINK_STATUS)
assert.deepEqual(
request.progressSteps.map((step) => step.label),
['创建申请', '直属领导审批', '预算管理者审批', '审批完成']
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, BUDGET_MANAGER_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.every((step) => step.done), true)
assert.equal(request.progressSteps.find((step) => step.label === '直属领导审批')?.time, '李经理通过')
assert.equal(request.progressSteps.find((step) => step.label === '预算管理者审批')?.time, '赵预算通过')
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.current, true)
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.time, '未关联')
assert.equal(request.progressSteps.find((step) => step.label === ARCHIVED)?.done, false)
assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, '李经理通过')
assert.equal(request.progressSteps.find((step) => step.label === BUDGET_MANAGER_APPROVAL)?.time, '赵预算通过')
})
test('application claims hide budget step when leader approval also covers budget approval', () => {
@@ -430,9 +434,10 @@ test('application claims hide budget step when leader approval also covers budge
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED]
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.every((step) => step.done), true)
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.current, true)
assert.equal(request.progressSteps.find((step) => step.label === ARCHIVED)?.done, false)
assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false)
assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, '李预算经理通过')
})
@@ -481,13 +486,92 @@ test('approved application claims hide budget step when dynamic route skipped bu
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED]
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.every((step) => step.done), true)
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.current, true)
assert.equal(request.progressSteps.find((step) => step.label === ARCHIVED)?.done, false)
assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false)
assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, 'Leader Li通过')
})
test('approved application claims show linked reimbursement status before archive', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-linked-draft',
claim_no: 'AP-202606050001-LINKED',
employee_name: '张三',
department_name: '交付部',
manager_name: 'Leader Li',
expense_type: 'travel_application',
reason: 'Project onsite support',
location: 'Shanghai',
amount: 500,
invoice_count: 0,
occurred_at: '2026-06-05T00:00:00.000Z',
submitted_at: '2026-06-05T02:00:00.000Z',
created_at: '2026-06-05T01:30:00.000Z',
updated_at: '2026-06-05T03:00:00.000Z',
status: 'approved',
approval_stage: APPLICATION_LINK_STATUS,
risk_flags_json: [
{
source: 'manual_approval',
event_type: 'expense_application_approval',
operator: 'Leader Li',
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
next_approval_stage: APPLICATION_LINK_STATUS,
generated_draft_claim_no: 'RE-202606050001-LINKED',
created_at: '2026-06-05T03:00:00.000Z'
}
],
items: []
})
const linkStep = request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)
assert.equal(request.workflowNode, APPLICATION_LINK_STATUS)
assert.equal(linkStep?.current, true)
assert.equal(linkStep?.time, '关联中 RE-202606050001-LINKED')
assert.equal(request.secondaryStatusValue, '关联中 RE-202606050001-LINKED')
assert.equal(request.progressSteps.find((step) => step.label === ARCHIVED)?.done, false)
})
test('application claims are archived only after linked reimbursement is paid', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-archived',
claim_no: 'AP-202606050001-ARCHIVED',
employee_name: '张三',
department_name: '交付部',
manager_name: 'Leader Li',
expense_type: 'travel_application',
reason: 'Project onsite support',
location: 'Shanghai',
amount: 500,
invoice_count: 0,
occurred_at: '2026-06-05T00:00:00.000Z',
submitted_at: '2026-06-05T02:00:00.000Z',
created_at: '2026-06-05T01:30:00.000Z',
updated_at: '2026-06-07T03:00:00.000Z',
status: 'approved',
approval_stage: APPLICATION_ARCHIVE,
risk_flags_json: [
{
source: 'application_archive_sync',
event_type: 'expense_application_archived_by_reimbursement',
reimbursement_claim_no: 'RE-202606050001-ARCHIVED',
created_at: '2026-06-07T03:00:00.000Z'
}
],
items: []
})
assert.equal(request.workflowNode, APPLICATION_ARCHIVE)
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.every((step) => step.done), true)
assert.equal(request.secondaryStatusValue, '已归档')
})
test('progress steps show approval operator time and current stay duration', () => {
const originalNow = Date.now
Date.now = () => new Date('2026-05-20T05:00:00.000Z').getTime()