feat: 完善审批退回流程与报销申请关联

后端优化报销单访问策略和常量定义,增强退回原因和审批状态
流转,前端完善退回对话框和审批交互组件,新增报销申请关联
模型,优化文档中心行数据和审批收件箱工具函数,增强引导
流程和会话模型,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-27 14:35:17 +08:00
parent 7d32eae74e
commit cbb98f4469
30 changed files with 1794 additions and 250 deletions

View File

@@ -7,6 +7,7 @@ export const GUIDED_ACTION_START_APPLICATION = 'start_guided_application'
export const GUIDED_ACTION_START_STATUS_QUERY = 'start_guided_status_query'
export const GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR = 'open_travel_calculator'
export const GUIDED_ACTION_SELECT_EXPENSE_TYPE = 'guided_select_expense_type'
export const GUIDED_ACTION_SELECT_REQUIRED_APPLICATION = 'guided_select_required_application'
export const GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW = 'guided_confirm_reimbursement_review'
export const GUIDED_ACTION_CONTINUE_FILLING = 'guided_continue_filling'
export const GUIDED_ACTION_PROCESS_INTERRUPTION = 'guided_process_interruption'
@@ -109,13 +110,36 @@ function normalizeValues(values) {
}, {})
}
function normalizeApplicationCandidates(applications) {
if (!Array.isArray(applications)) {
return []
}
return applications
.map((item) => (item && typeof item === 'object' ? item : null))
.filter(Boolean)
.map((item) => ({
id: normalizeText(item.id || item.application_claim_id),
claim_no: normalizeText(item.claim_no || item.application_claim_no),
expense_type: normalizeText(item.expense_type || item.application_expense_type),
reason: normalizeText(item.reason || item.application_reason),
location: normalizeText(item.location || item.application_location),
amount: normalizeText(item.amount || item.application_amount),
amount_label: normalizeText(item.amount_label || item.application_amount_label),
status: normalizeText(item.status || item.application_status),
status_label: normalizeText(item.status_label || item.application_status_label),
application_date: normalizeText(item.application_date)
}))
.filter((item) => item.id || item.claim_no)
}
export function createEmptyGuidedFlowState() {
return {
mode: GUIDED_FLOW_MODE_NONE,
stepKey: '',
expenseType: '',
values: {},
pendingInterruptionText: ''
pendingInterruptionText: '',
applicationCandidates: []
}
}
@@ -134,7 +158,8 @@ export function normalizeGuidedFlowState(state) {
stepKey: normalizeText(source.stepKey),
expenseType: normalizeText(source.expenseType),
values: normalizeValues(source.values),
pendingInterruptionText: normalizeText(source.pendingInterruptionText)
pendingInterruptionText: normalizeText(source.pendingInterruptionText),
applicationCandidates: normalizeApplicationCandidates(source.applicationCandidates)
}
}
@@ -191,7 +216,44 @@ export function selectGuidedExpenseType(state, expenseType) {
mode: GUIDED_FLOW_MODE_REIMBURSEMENT,
expenseType: type.key,
stepKey: steps[0]?.key || 'summary',
pendingInterruptionText: ''
pendingInterruptionText: '',
applicationCandidates: []
}
}
export function waitForGuidedApplicationSelection(state, expenseType, applications = []) {
const type = getGuidedExpenseType(expenseType)
if (!type) {
return normalizeGuidedFlowState(state)
}
return {
...normalizeGuidedFlowState(state),
mode: GUIDED_FLOW_MODE_REIMBURSEMENT,
expenseType: type.key,
stepKey: 'application_selection',
pendingInterruptionText: '',
applicationCandidates: normalizeApplicationCandidates(applications)
}
}
export function selectGuidedRequiredApplication(state, application = {}) {
const current = normalizeGuidedFlowState(state)
const steps = getGuidedReimbursementSteps(current.expenseType)
return {
...current,
values: normalizeValues({
...current.values,
application_claim_id: application.application_claim_id || application.id || '',
application_claim_no: application.application_claim_no || application.claim_no || '',
application_reason: application.application_reason || application.reason || '',
application_location: application.application_location || application.location || '',
application_amount: application.application_amount || application.amount || '',
application_amount_label: application.application_amount_label || application.amount_label || '',
application_status_label: application.application_status_label || application.status_label || ''
}),
stepKey: steps[0]?.key || 'summary',
pendingInterruptionText: '',
applicationCandidates: []
}
}
@@ -290,6 +352,16 @@ export function buildGuidedReimbursementSummaryText(state) {
'请核查下面的关键信息:'
]
if (current.values.application_claim_no) {
const applicationParts = [
current.values.application_claim_no,
current.values.application_reason,
current.values.application_location,
current.values.application_amount_label
].filter(Boolean)
lines.push(`- 关联申请单:${applicationParts.join(' / ')}`)
}
steps.forEach((step) => {
const value = step.key === 'attachments'
? (current.values.attachment_names?.length
@@ -324,6 +396,9 @@ export function buildGuidedReviewSubmitOptions(state, files = []) {
: values[step.key]
return `${step.summaryLabel}${value || '待补充'}`
})
if (values.application_claim_no) {
fieldLines.unshift(`关联申请单:${values.application_claim_no}`)
}
const rawText = [
`报销类型:${typeLabel}`,
...fieldLines
@@ -340,7 +415,12 @@ export function buildGuidedReviewSubmitOptions(state, files = []) {
time_range: values.time_range || '',
business_time: values.time_range || '',
amount: values.amount || '',
attachment_names: Array.isArray(values.attachment_names) ? values.attachment_names : []
attachment_names: Array.isArray(values.attachment_names) ? values.attachment_names : [],
application_claim_id: values.application_claim_id || '',
application_claim_no: values.application_claim_no || '',
application_reason: values.application_reason || '',
application_location: values.application_location || '',
application_amount: values.application_amount || ''
}
return {
@@ -355,7 +435,9 @@ export function buildGuidedReviewSubmitOptions(state, files = []) {
expense_scene_selection: {
expense_type: type?.key || current.expenseType || 'other',
expense_type_label: typeLabel,
original_message: rawText
original_message: rawText,
application_claim_id: values.application_claim_id || '',
application_claim_no: values.application_claim_no || ''
},
review_form_values: reviewFormValues
}