feat: 完善审批退回流程与报销申请关联
后端优化报销单访问策略和常量定义,增强退回原因和审批状态 流转,前端完善退回对话框和审批交互组件,新增报销申请关联 模型,优化文档中心行数据和审批收件箱工具函数,增强引导 流程和会话模型,补充单元测试覆盖。
This commit is contained in:
@@ -4,6 +4,14 @@ import {
|
||||
buildApplicationTemplatePreview,
|
||||
buildLocalApplicationPreviewMessage
|
||||
} from '../../utils/expenseApplicationPreview.js'
|
||||
import { fetchExpenseClaims } from '../../services/reimbursements.js'
|
||||
import {
|
||||
buildRequiredApplicationActions,
|
||||
buildRequiredApplicationMissingText,
|
||||
buildRequiredApplicationSelectionText,
|
||||
filterRequiredApplicationCandidates,
|
||||
requiresApplicationBeforeReimbursement
|
||||
} from './travelReimbursementApplicationLinkModel.js'
|
||||
import {
|
||||
GUIDED_ACTION_START_APPLICATION,
|
||||
GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW,
|
||||
@@ -11,6 +19,7 @@ import {
|
||||
GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR,
|
||||
GUIDED_ACTION_PROCESS_INTERRUPTION,
|
||||
GUIDED_ACTION_SELECT_EXPENSE_TYPE,
|
||||
GUIDED_ACTION_SELECT_REQUIRED_APPLICATION,
|
||||
GUIDED_ACTION_SELECT_QUERY_MODE,
|
||||
GUIDED_ACTION_SELECT_QUERY_STATUS,
|
||||
GUIDED_ACTION_START_REIMBURSEMENT,
|
||||
@@ -41,8 +50,10 @@ import {
|
||||
resolveGuidedExpenseTypeFromText,
|
||||
resolveGuidedQueryModeFromText,
|
||||
selectGuidedExpenseType,
|
||||
selectGuidedRequiredApplication,
|
||||
selectGuidedQueryMode,
|
||||
shouldConfirmGuidedInterruption
|
||||
shouldConfirmGuidedInterruption,
|
||||
waitForGuidedApplicationSelection
|
||||
} from './travelReimbursementGuidedFlowModel.js'
|
||||
|
||||
function normalizeText(value) {
|
||||
@@ -211,7 +222,98 @@ export function useTravelReimbursementGuidedFlow({
|
||||
})
|
||||
}
|
||||
|
||||
function handleReimbursementAnswer(answerText, files) {
|
||||
async function selectExpenseTypeForGuidedReimbursement(currentState, expenseType, options = {}) {
|
||||
const nextState = options.pendingSceneSelection
|
||||
? {
|
||||
...currentState,
|
||||
values: {
|
||||
...currentState.values,
|
||||
pending_scene_original_message: normalizeText(options.pendingSceneSelection.originalMessage),
|
||||
pending_scene_expense_type_label: normalizeText(options.pendingSceneSelection.expenseTypeLabel)
|
||||
}
|
||||
}
|
||||
: currentState
|
||||
|
||||
if (!requiresApplicationBeforeReimbursement(expenseType)) {
|
||||
guidedFlowState.value = selectGuidedExpenseType(nextState, expenseType)
|
||||
pushNextReimbursementPrompt()
|
||||
return
|
||||
}
|
||||
|
||||
let claimsPayload = null
|
||||
try {
|
||||
claimsPayload = await fetchExpenseClaims()
|
||||
} catch (error) {
|
||||
console.warn('Fetch reimbursement applications failed:', error)
|
||||
pushAssistant('查询可关联申请单时出现异常,请稍后再试。为避免直接报销,我先暂停当前流程。', {
|
||||
meta: ['申请单查询失败']
|
||||
})
|
||||
toast?.('申请单查询失败,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
const applications = filterRequiredApplicationCandidates(claimsPayload, expenseType, currentUser?.value || {})
|
||||
if (!applications.length) {
|
||||
guidedFlowState.value = createGuidedReimbursementState()
|
||||
pushAssistant(buildRequiredApplicationMissingText(expenseType), {
|
||||
meta: ['缺少可关联申请单'],
|
||||
suggestedActions: buildGuidedExpenseTypeActions()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
guidedFlowState.value = waitForGuidedApplicationSelection(nextState, expenseType, applications)
|
||||
pushAssistant(buildRequiredApplicationSelectionText(expenseType, applications), {
|
||||
meta: ['等待关联申请单'],
|
||||
suggestedActions: buildRequiredApplicationActions(applications, GUIDED_ACTION_SELECT_REQUIRED_APPLICATION)
|
||||
})
|
||||
}
|
||||
|
||||
function buildPendingSceneSubmitOptions(state) {
|
||||
const current = normalizeGuidedFlowState(state)
|
||||
const originalMessage = normalizeText(current.values.pending_scene_original_message)
|
||||
const expenseTypeLabel = normalizeText(current.values.pending_scene_expense_type_label)
|
||||
const applicationNo = normalizeText(current.values.application_claim_no)
|
||||
const applicationId = normalizeText(current.values.application_claim_id)
|
||||
if (!originalMessage || !expenseTypeLabel || !applicationNo) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rawText = [
|
||||
originalMessage,
|
||||
`用户选择报销场景:${expenseTypeLabel}`,
|
||||
`关联申请单:${applicationNo}`
|
||||
].join('\n')
|
||||
|
||||
return {
|
||||
rawText,
|
||||
userText: `关联申请单 ${applicationNo}`,
|
||||
pendingText: `已关联申请单,正在按${expenseTypeLabel}识别...`,
|
||||
systemGenerated: true,
|
||||
skipUserMessage: true,
|
||||
extraContext: {
|
||||
draft_claim_id: '',
|
||||
user_input_text: originalMessage,
|
||||
expense_scene_selection: {
|
||||
expense_type: current.expenseType || 'other',
|
||||
expense_type_label: expenseTypeLabel,
|
||||
original_message: originalMessage,
|
||||
application_claim_id: applicationId,
|
||||
application_claim_no: applicationNo
|
||||
},
|
||||
review_form_values: {
|
||||
expense_type: expenseTypeLabel,
|
||||
application_claim_id: applicationId,
|
||||
application_claim_no: applicationNo,
|
||||
application_reason: current.values.application_reason || '',
|
||||
application_location: current.values.application_location || '',
|
||||
application_amount: current.values.application_amount || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReimbursementAnswer(answerText, files) {
|
||||
const currentState = normalizeGuidedFlowState(guidedFlowState.value)
|
||||
const currentStep = getCurrentGuidedStep(currentState)
|
||||
const fileNames = buildFileNames(files)
|
||||
@@ -225,8 +327,18 @@ export function useTravelReimbursementGuidedFlow({
|
||||
})
|
||||
return
|
||||
}
|
||||
guidedFlowState.value = selectGuidedExpenseType(currentState, expenseType)
|
||||
pushNextReimbursementPrompt()
|
||||
await selectExpenseTypeForGuidedReimbursement(currentState, expenseType)
|
||||
return
|
||||
}
|
||||
|
||||
if (currentState.stepKey === 'application_selection') {
|
||||
pushAssistant('请先点击上方列出的申请单完成关联。关联后,我再继续询问报销依据。', {
|
||||
meta: ['等待关联申请单'],
|
||||
suggestedActions: buildRequiredApplicationActions(
|
||||
currentState.applicationCandidates,
|
||||
GUIDED_ACTION_SELECT_REQUIRED_APPLICATION
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -338,7 +450,7 @@ export function useTravelReimbursementGuidedFlow({
|
||||
}
|
||||
|
||||
if (currentState.mode === GUIDED_FLOW_MODE_REIMBURSEMENT) {
|
||||
handleReimbursementAnswer(answerText, files)
|
||||
await handleReimbursementAnswer(answerText, files)
|
||||
clearComposerRuntime()
|
||||
persistAndScroll()
|
||||
return true
|
||||
@@ -361,6 +473,7 @@ export function useTravelReimbursementGuidedFlow({
|
||||
}
|
||||
const guidedActionTypes = new Set([
|
||||
GUIDED_ACTION_SELECT_EXPENSE_TYPE,
|
||||
GUIDED_ACTION_SELECT_REQUIRED_APPLICATION,
|
||||
GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW,
|
||||
GUIDED_ACTION_CONTINUE_FILLING,
|
||||
GUIDED_ACTION_PROCESS_INTERRUPTION,
|
||||
@@ -380,8 +493,23 @@ export function useTravelReimbursementGuidedFlow({
|
||||
if (actionType === GUIDED_ACTION_SELECT_EXPENSE_TYPE) {
|
||||
const expenseType = normalizeText(action?.payload?.expense_type)
|
||||
const expenseTypeLabel = normalizeText(action?.payload?.expense_type_label || action?.label)
|
||||
guidedFlowState.value = selectGuidedExpenseType(guidedFlowState.value, expenseType)
|
||||
pushUser(`选择${expenseTypeLabel || '报销类型'}`)
|
||||
await selectExpenseTypeForGuidedReimbursement(guidedFlowState.value, expenseType)
|
||||
persistAndScroll()
|
||||
return true
|
||||
}
|
||||
|
||||
if (actionType === GUIDED_ACTION_SELECT_REQUIRED_APPLICATION) {
|
||||
const applicationNo = normalizeText(action?.payload?.application_claim_no || action?.label)
|
||||
pushUser(`关联申请单 ${applicationNo || ''}`.trim())
|
||||
guidedFlowState.value = selectGuidedRequiredApplication(guidedFlowState.value, action?.payload || {})
|
||||
const pendingSceneSubmitOptions = buildPendingSceneSubmitOptions(guidedFlowState.value)
|
||||
if (pendingSceneSubmitOptions) {
|
||||
resetGuidedFlowState()
|
||||
persistAndScroll()
|
||||
await submitExistingComposer(pendingSceneSubmitOptions)
|
||||
return true
|
||||
}
|
||||
pushNextReimbursementPrompt()
|
||||
persistAndScroll()
|
||||
return true
|
||||
@@ -450,10 +578,42 @@ export function useTravelReimbursementGuidedFlow({
|
||||
return false
|
||||
}
|
||||
|
||||
async function handleSceneSelectionApplicationGate(message, action) {
|
||||
const actionType = normalizeText(action?.action_type)
|
||||
if (actionType !== 'select_expense_type') {
|
||||
return false
|
||||
}
|
||||
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
||||
const expenseType = normalizeText(actionPayload.expense_type)
|
||||
if (!requiresApplicationBeforeReimbursement(expenseType)) {
|
||||
return false
|
||||
}
|
||||
const expenseTypeLabel = normalizeText(actionPayload.expense_type_label || action?.label)
|
||||
const originalMessage = normalizeText(actionPayload.original_message || message?.text)
|
||||
if (!expenseTypeLabel || !originalMessage) {
|
||||
return false
|
||||
}
|
||||
if (!lockSuggestedActionMessage(message, action)) {
|
||||
return true
|
||||
}
|
||||
|
||||
guidedPendingFiles.value = []
|
||||
pushUser(`选择${expenseTypeLabel}`)
|
||||
await selectExpenseTypeForGuidedReimbursement(createGuidedReimbursementState(), expenseType, {
|
||||
pendingSceneSelection: {
|
||||
originalMessage,
|
||||
expenseTypeLabel
|
||||
}
|
||||
})
|
||||
persistAndScroll()
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
handleGuidedShortcut,
|
||||
handleGuidedComposerSubmit,
|
||||
handleGuidedSuggestedAction,
|
||||
handleSceneSelectionApplicationGate,
|
||||
resetGuidedFlowState
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user