feat: 完善审批退回流程与报销申请关联
后端优化报销单访问策略和常量定义,增强退回原因和审批状态 流转,前端完善退回对话框和审批交互组件,新增报销申请关联 模型,优化文档中心行数据和审批收件箱工具函数,增强引导 流程和会话模型,补充单元测试覆盖。
This commit is contained in:
@@ -240,7 +240,6 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import EnterpriseSelect from '../components/shared/EnterpriseSelect.vue'
|
||||
import TableEmptyState from '../components/shared/TableEmptyState.vue'
|
||||
import TableLoadingState from '../components/shared/TableLoadingState.vue'
|
||||
@@ -248,9 +247,8 @@ import { mapExpenseClaimToRequest } from '../composables/useRequests.js'
|
||||
import { fetchApprovalExpenseClaims, fetchArchivedExpenseClaims } from '../services/reimbursements.js'
|
||||
import { countNewDocuments, isNewDocument, markDocumentViewed, readDocumentScope, readViewedDocumentKeys, writeDocumentScope } from '../utils/documentCenterNewState.js'
|
||||
import { extractDateText, formatDocumentListTime, resolveDocumentSortTime, resolveDocumentStayTimeDisplay } from '../utils/documentCenterTime.js'
|
||||
import { excludeArchivedDocumentRows, isArchivedDocumentRow } from '../utils/documentCenterRows.js'
|
||||
import { excludeArchivedDocumentRows, filterApplicationScopeNewRows, isArchivedDocumentRow, prepareApplicationScopeRows } from '../utils/documentCenterRows.js'
|
||||
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
|
||||
|
||||
const DOCUMENT_TYPE_ALL = 'all'
|
||||
const DOCUMENT_TYPE_APPLICATION = 'application'
|
||||
const DOCUMENT_TYPE_REIMBURSEMENT = 'reimbursement'
|
||||
@@ -260,7 +258,6 @@ const DOCUMENT_SCOPE_APPLICATION = '申请单'
|
||||
const DOCUMENT_SCOPE_REIMBURSEMENT = '报销单'
|
||||
const DOCUMENT_SCOPE_REVIEW = '审核单'
|
||||
const DOCUMENT_SCOPE_ARCHIVE = '归档'
|
||||
|
||||
const scopeTabs = [DOCUMENT_SCOPE_ALL, DOCUMENT_SCOPE_APPLICATION, DOCUMENT_SCOPE_REIMBURSEMENT, DOCUMENT_SCOPE_REVIEW, DOCUMENT_SCOPE_ARCHIVE]
|
||||
const statusTabs = ['全部', '草稿', '待提交', '审批中', '待补充', '已完成']
|
||||
const FILTER_CONFIG_BY_SCOPE = {
|
||||
@@ -311,14 +308,12 @@ const documentTypeOptions = [
|
||||
{ value: DOCUMENT_TYPE_APPLICATION, label: '申请单' },
|
||||
{ value: DOCUMENT_TYPE_REIMBURSEMENT, label: '报销单' }
|
||||
]
|
||||
|
||||
const props = defineProps({
|
||||
filteredRequests: { type: Array, required: true },
|
||||
hasData: { type: Boolean, default: false },
|
||||
loading: { type: Boolean, default: false },
|
||||
error: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'open-document',
|
||||
'create-request',
|
||||
@@ -326,7 +321,6 @@ const emit = defineEmits([
|
||||
'reload',
|
||||
'summary-change'
|
||||
])
|
||||
|
||||
const activeScopeTab = ref(readDocumentScope(DOCUMENT_SCOPE_ALL, scopeTabs))
|
||||
const activeStatusTab = ref('全部')
|
||||
const activeDocumentType = ref(DOCUMENT_TYPE_ALL)
|
||||
@@ -345,17 +339,13 @@ const approvalRows = ref([])
|
||||
const supportingLoading = ref(false)
|
||||
const supportingError = ref('')
|
||||
const viewedDocumentKeys = ref(readViewedDocumentKeys())
|
||||
|
||||
const activeFilterConfig = computed(() =>
|
||||
FILTER_CONFIG_BY_SCOPE[activeScopeTab.value] || FILTER_CONFIG_BY_SCOPE[DOCUMENT_SCOPE_APPLICATION]
|
||||
)
|
||||
|
||||
const showDocumentTypeFilter = computed(() => Boolean(activeFilterConfig.value.showDocumentType))
|
||||
|
||||
const documentTypeFilterLabel = computed(() =>
|
||||
documentTypeOptions.find((item) => item.value === activeDocumentType.value)?.label || '单据类型'
|
||||
)
|
||||
|
||||
const statusFilterOptions = computed(() =>
|
||||
activeFilterConfig.value.statusTabs.map((tab) => ({
|
||||
value: tab,
|
||||
@@ -380,10 +370,11 @@ const ownedRows = computed(() =>
|
||||
)
|
||||
|
||||
const nonArchivedRows = computed(() => mergeDocumentRows([...ownedRows.value, ...approvalRows.value]))
|
||||
const applicationScopeRows = computed(() => prepareApplicationScopeRows(ownedRows.value))
|
||||
|
||||
const scopeNewCountMap = computed(() => ({
|
||||
[DOCUMENT_SCOPE_ALL]: countNewDocuments(nonArchivedRows.value, viewedDocumentKeys.value),
|
||||
[DOCUMENT_SCOPE_APPLICATION]: countNewDocuments(nonArchivedRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_APPLICATION), viewedDocumentKeys.value),
|
||||
[DOCUMENT_SCOPE_APPLICATION]: countNewDocuments(filterApplicationScopeNewRows(applicationScopeRows.value), viewedDocumentKeys.value),
|
||||
[DOCUMENT_SCOPE_REIMBURSEMENT]: countNewDocuments(ownedRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_REIMBURSEMENT), viewedDocumentKeys.value),
|
||||
[DOCUMENT_SCOPE_REVIEW]: countNewDocuments(approvalRows.value, viewedDocumentKeys.value),
|
||||
[DOCUMENT_SCOPE_ARCHIVE]: countNewDocuments(archiveRows.value, viewedDocumentKeys.value)
|
||||
@@ -401,7 +392,7 @@ const activeScopeRows = computed(() => {
|
||||
if (activeScopeTab.value === DOCUMENT_SCOPE_ALL) return nonArchivedRows.value
|
||||
|
||||
if (activeScopeTab.value === DOCUMENT_SCOPE_APPLICATION) {
|
||||
return nonArchivedRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_APPLICATION)
|
||||
return applicationScopeRows.value
|
||||
}
|
||||
|
||||
if (activeScopeTab.value === DOCUMENT_SCOPE_REIMBURSEMENT) {
|
||||
|
||||
@@ -182,21 +182,26 @@
|
||||
<span><i class="mdi mdi-account-tie-outline"></i>领导意见</span>
|
||||
<strong v-if="leaderApprovalReadonlyMeta">{{ leaderApprovalReadonlyMeta }}</strong>
|
||||
</div>
|
||||
<div v-if="showApplicationLeaderOpinionInput" class="leader-approval-card inline-leader-opinion">
|
||||
<textarea
|
||||
v-model="leaderOpinion"
|
||||
maxlength="500"
|
||||
:required="requiresApprovalOpinion"
|
||||
:placeholder="approvalOpinionPlaceholder"
|
||||
:aria-label="approvalOpinionTitle"
|
||||
></textarea>
|
||||
<div class="leader-opinion-meta">
|
||||
<span>{{ approvalOpinionHint }}</span>
|
||||
<strong>{{ leaderOpinion.length }}/500</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="detail-note readonly application-leader-opinion-display">
|
||||
<p>{{ leaderApprovalReadonlyText }}</p>
|
||||
<div v-if="hasLeaderApprovalEvents" class="application-leader-opinion-timeline" aria-label="领导批复事件流">
|
||||
<article
|
||||
v-for="event in leaderApprovalEvents"
|
||||
:key="event.id"
|
||||
class="application-leader-opinion-event"
|
||||
:class="event.tone"
|
||||
>
|
||||
<div class="application-leader-opinion-event-head">
|
||||
<span>
|
||||
<i :class="event.type === 'returned' ? 'mdi mdi-arrow-u-left-top' : 'mdi mdi-check-circle-outline'"></i>
|
||||
{{ event.title }}
|
||||
</span>
|
||||
<time v-if="event.time">{{ event.time }}</time>
|
||||
</div>
|
||||
<p>{{ event.opinion }}</p>
|
||||
<footer>
|
||||
<span>{{ event.operator }}</span>
|
||||
<span v-if="event.returnCount">第 {{ event.returnCount }} 次退回</span>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -475,20 +480,6 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article v-if="showLeaderApprovalPanel" class="detail-card panel leader-approval-card">
|
||||
<h3>{{ approvalOpinionTitle }}</h3>
|
||||
<textarea
|
||||
v-model="leaderOpinion"
|
||||
maxlength="500"
|
||||
:required="requiresApprovalOpinion"
|
||||
:placeholder="approvalOpinionPlaceholder"
|
||||
:aria-label="approvalOpinionTitle"
|
||||
></textarea>
|
||||
<div class="leader-opinion-meta">
|
||||
<span>{{ approvalOpinionHint }}</span>
|
||||
<strong>{{ leaderOpinion.length }}/500</strong>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -774,7 +765,10 @@
|
||||
:summary-label="approvalConfirmSummaryLabel"
|
||||
:next-stage="approvalNextStage"
|
||||
:opinion-title="approvalOpinionTitle"
|
||||
:opinion="leaderOpinion"
|
||||
v-model:opinion="leaderOpinion"
|
||||
:opinion-placeholder="approvalOpinionPlaceholder"
|
||||
:opinion-hint="approvalOpinionHint"
|
||||
:opinion-required="requiresApprovalOpinion"
|
||||
@close="closeApproveConfirmDialog"
|
||||
@confirm="confirmApproveRequest"
|
||||
/>
|
||||
@@ -784,6 +778,7 @@
|
||||
:title="`确认退回 ${request.id} 吗?`"
|
||||
:description="returnDialogDescription"
|
||||
:busy="returnBusy"
|
||||
:application="isApplicationDocument"
|
||||
@close="closeReturnDialog"
|
||||
@confirm="confirmReturnRequest"
|
||||
/>
|
||||
|
||||
@@ -176,6 +176,7 @@ import {
|
||||
buildWelcomeInsight,
|
||||
createMessage,
|
||||
filterAssistantSessionModes,
|
||||
hasMeaningfulSessionMessages,
|
||||
resolveAssistantSessionMode,
|
||||
resolveKnowledgeRankLabel,
|
||||
resolveKnowledgeRankTone,
|
||||
@@ -718,7 +719,7 @@ export default {
|
||||
return labels[currentInsight.value.intent] ?? 'AI 处理中'
|
||||
})
|
||||
const canDeleteCurrentSession = computed(
|
||||
() => Boolean(conversationId.value) || messages.value.some((item) => item.role === 'user')
|
||||
() => Boolean(conversationId.value) || hasMeaningfulSessionMessages(messages.value)
|
||||
)
|
||||
const latestReviewMessage = computed(() =>
|
||||
[...messages.value].reverse().find((item) =>
|
||||
@@ -1029,6 +1030,7 @@ export default {
|
||||
handleGuidedShortcut,
|
||||
handleGuidedComposerSubmit,
|
||||
handleGuidedSuggestedAction,
|
||||
handleSceneSelectionApplicationGate,
|
||||
resetGuidedFlowState
|
||||
} = useTravelReimbursementGuidedFlow({
|
||||
guidedFlowState,
|
||||
@@ -1470,6 +1472,7 @@ export default {
|
||||
if (message?.suggestedActionsLocked) return
|
||||
if (applySuggestedActionPrefill(action)) return
|
||||
if (await handleGuidedSuggestedAction(message, action)) return
|
||||
if (await handleSceneSelectionApplicationGate(message, action)) return
|
||||
|
||||
if (actionType === ASSISTANT_SCOPE_ACTION_SWITCH) {
|
||||
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
||||
|
||||
@@ -26,9 +26,15 @@ import {
|
||||
canDeleteArchivedExpenseClaims,
|
||||
canManageExpenseClaims,
|
||||
canReturnExpenseClaims,
|
||||
isCurrentDirectManagerForRequest,
|
||||
isCurrentRequestApplicant,
|
||||
isFinanceUser
|
||||
} from '../../utils/accessControl.js'
|
||||
import { buildLeaderApprovalInfo, resolveGeneratedDraftClaimNo } from '../../utils/applicationApproval.js'
|
||||
import {
|
||||
buildLeaderApprovalEvents,
|
||||
buildLeaderApprovalInfo,
|
||||
resolveGeneratedDraftClaimNo
|
||||
} from '../../utils/applicationApproval.js'
|
||||
import { buildApplicationDetailFactItems } from '../../utils/expenseApplicationDetail.js'
|
||||
import { isArchivedRequestView, normalizeRequestForUi } from '../../utils/requestViewModel.js'
|
||||
import {
|
||||
@@ -484,11 +490,26 @@ export default {
|
||||
const node = String(request.value.node || request.value.approvalStage || '').trim()
|
||||
return node === '财务审批'
|
||||
})
|
||||
const canReturnRequest = computed(() =>
|
||||
canReturnExpenseClaims(currentUser.value)
|
||||
&& request.value.approvalKey === 'in_progress'
|
||||
&& Boolean(request.value.claimId)
|
||||
)
|
||||
const isCurrentApplicant = computed(() => isCurrentRequestApplicant(request.value, currentUser.value))
|
||||
const isCurrentDirectManagerApprover = computed(() => (
|
||||
canApproveLeaderExpenseClaims(currentUser.value)
|
||||
&& isCurrentDirectManagerForRequest(request.value, currentUser.value)
|
||||
))
|
||||
const canProcessFinanceApprovalStage = computed(() => (
|
||||
!isApplicationDocument.value
|
||||
&& isFinanceApprovalStage.value
|
||||
&& isFinanceUser(currentUser.value)
|
||||
&& !isCurrentApplicant.value
|
||||
))
|
||||
const canReturnRequest = computed(() => {
|
||||
if (request.value.approvalKey !== 'in_progress' || !request.value.claimId || !canReturnExpenseClaims(currentUser.value)) {
|
||||
return false
|
||||
}
|
||||
if (isDirectManagerApprovalStage.value) {
|
||||
return isCurrentDirectManagerApprover.value
|
||||
}
|
||||
return canProcessFinanceApprovalStage.value
|
||||
})
|
||||
const canApproveRequest = computed(() =>
|
||||
(Boolean(props.approvalMode) || isApplicationDocument.value)
|
||||
&& request.value.approvalKey === 'in_progress'
|
||||
@@ -496,32 +517,16 @@ export default {
|
||||
&& (
|
||||
(
|
||||
isDirectManagerApprovalStage.value
|
||||
&& canApproveLeaderExpenseClaims(currentUser.value)
|
||||
)
|
||||
|| (
|
||||
!isApplicationDocument.value
|
||||
&& isFinanceApprovalStage.value
|
||||
&& isFinanceUser(currentUser.value)
|
||||
&& isCurrentDirectManagerApprover.value
|
||||
)
|
||||
|| canProcessFinanceApprovalStage.value
|
||||
)
|
||||
)
|
||||
const showApplicationLeaderOpinionInput = computed(() => (
|
||||
isApplicationDocument.value
|
||||
&& canApproveRequest.value
|
||||
&& isDirectManagerApprovalStage.value
|
||||
))
|
||||
const leaderApprovalInfo = computed(() => buildLeaderApprovalInfo(request.value))
|
||||
const leaderApprovalReadonlyText = computed(() => {
|
||||
if (leaderApprovalInfo.value.opinion) {
|
||||
return leaderApprovalInfo.value.opinion
|
||||
}
|
||||
return isApplicationDocument.value ? '待直属领导填写审批意见。' : ''
|
||||
})
|
||||
const leaderApprovalEvents = computed(() => buildLeaderApprovalEvents(request.value))
|
||||
const hasLeaderApprovalEvents = computed(() => leaderApprovalEvents.value.length > 0)
|
||||
const leaderApprovalReadonlyMeta = computed(() => {
|
||||
const pieces = [
|
||||
leaderApprovalInfo.value.operator ? `${leaderApprovalInfo.value.operator}确认` : '',
|
||||
leaderApprovalInfo.value.time
|
||||
].filter(Boolean)
|
||||
const pieces = hasLeaderApprovalEvents.value ? [`${leaderApprovalEvents.value.length} 条批复记录`] : []
|
||||
if (leaderApprovalInfo.value.generatedDraftClaimNo) {
|
||||
pieces.push(`已生成报销草稿 ${leaderApprovalInfo.value.generatedDraftClaimNo}`)
|
||||
}
|
||||
@@ -529,12 +534,8 @@ export default {
|
||||
})
|
||||
const showApplicationLeaderOpinion = computed(() => (
|
||||
isApplicationDocument.value
|
||||
&& (
|
||||
showApplicationLeaderOpinionInput.value
|
||||
|| leaderApprovalReadonlyText.value
|
||||
)
|
||||
&& hasLeaderApprovalEvents.value
|
||||
))
|
||||
const showLeaderApprovalPanel = computed(() => canApproveRequest.value && !showApplicationLeaderOpinionInput.value)
|
||||
const requiresApprovalOpinion = computed(() => isDirectManagerApprovalStage.value)
|
||||
const approvalOpinionTitle = computed(() => (isFinanceApprovalStage.value ? '财务意见' : '领导意见'))
|
||||
const approvalOpinionPlaceholder = computed(() => {
|
||||
@@ -1726,11 +1727,6 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (requiresApprovalOpinion.value && !leaderOpinion.value.trim()) {
|
||||
toast('请先填写领导意见,填写后才能确认审核。')
|
||||
return
|
||||
}
|
||||
|
||||
approveConfirmDialogOpen.value = true
|
||||
}
|
||||
|
||||
@@ -1757,7 +1753,6 @@ export default {
|
||||
|
||||
if (requiresApprovalOpinion.value && !leaderOpinion.value.trim()) {
|
||||
toast('请先填写领导意见,填写后才能确认审核。')
|
||||
approveConfirmDialogOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1833,15 +1828,15 @@ export default {
|
||||
isMajorExpenseRisk,
|
||||
openAiEntry, openAttachmentPreview, goToNextAttachmentPreview, goToPreviousAttachmentPreview,
|
||||
profile, progressSteps, request, leaderOpinion, removeExpenseAttachment, removeExpenseItem,
|
||||
leaderApprovalReadonlyMeta, leaderApprovalReadonlyText,
|
||||
hasLeaderApprovalEvents, leaderApprovalEvents, leaderApprovalReadonlyMeta,
|
||||
resolveExpenseRiskIndicatorTitle,
|
||||
resetDetailNote, resolveAttachmentDisplayName, resolveAttachmentPreviewTitle, resolveAttachmentRecognition,
|
||||
resolveExpenseReasonHelper, resolveExpenseReasonPlaceholder, resolveExpenseRiskState, resolveExpenseIssues,
|
||||
returnBusy, returnDialogDescription, returnDialogOpen, riskOverrideBusy, riskOverrideDialogOpen, riskOverrideIndexLabel,
|
||||
requiresApprovalOpinion,
|
||||
riskOverrideReasons, saveDetailNote, savingDetailNote, savingExpenseId,
|
||||
showAiAdvicePanel, showApplicationLeaderOpinion, showApplicationLeaderOpinionInput,
|
||||
showLeaderApprovalPanel, showExpenseRisk, startExpenseEdit, submitBusy, submitConfirmDialogOpen,
|
||||
showAiAdvicePanel, showApplicationLeaderOpinion,
|
||||
showExpenseRisk, startExpenseEdit, submitBusy, submitConfirmDialogOpen,
|
||||
submitRiskWarnings,
|
||||
triggerExpenseUpload, uploadedExpenseCount, uploadingExpenseId, saveExpenseEdit
|
||||
}
|
||||
|
||||
294
web/src/views/scripts/travelReimbursementApplicationLinkModel.js
Normal file
294
web/src/views/scripts/travelReimbursementApplicationLinkModel.js
Normal file
@@ -0,0 +1,294 @@
|
||||
const REQUIRED_APPLICATION_EXPENSE_TYPES = new Set(['travel', 'meal'])
|
||||
|
||||
const APPLICATION_TYPE_ALIASES = {
|
||||
travel: new Set(['travel', 'travel_application']),
|
||||
meal: new Set([
|
||||
'meal',
|
||||
'entertainment',
|
||||
'meal_application',
|
||||
'entertainment_application',
|
||||
'business_entertainment_application',
|
||||
'hospitality_application'
|
||||
])
|
||||
}
|
||||
|
||||
const GENERIC_APPLICATION_TYPES = new Set(['application', 'expense_application'])
|
||||
const BLOCKED_APPLICATION_STATUSES = new Set(['draft', 'returned', 'rejected', 'cancelled', 'canceled', 'deleted'])
|
||||
|
||||
const STATUS_LABELS = {
|
||||
submitted: '审批中',
|
||||
approved: '已审批',
|
||||
completed: '已完成',
|
||||
archived: '已归档',
|
||||
paid: '已入账'
|
||||
}
|
||||
|
||||
const EXPENSE_TYPE_LABELS = {
|
||||
travel: '差旅费',
|
||||
meal: '业务招待费'
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function normalizeLower(value) {
|
||||
return normalizeText(value).toLowerCase()
|
||||
}
|
||||
|
||||
function uniqueValues(values) {
|
||||
return Array.from(new Set((Array.isArray(values) ? values : []).map(normalizeText).filter(Boolean)))
|
||||
}
|
||||
|
||||
function normalizeClaimNo(claim) {
|
||||
return normalizeText(claim?.claim_no || claim?.claimNo).toUpperCase()
|
||||
}
|
||||
|
||||
function normalizeExpenseType(claim) {
|
||||
return normalizeLower(claim?.expense_type || claim?.expenseType || claim?.type_code || claim?.typeCode)
|
||||
}
|
||||
|
||||
function normalizeApplicationStatus(claim) {
|
||||
return normalizeLower(claim?.status || claim?.state || claim?.approval_status || claim?.approvalStatus)
|
||||
}
|
||||
|
||||
function normalizeDocumentType(claim) {
|
||||
return normalizeLower(
|
||||
claim?.document_type_code
|
||||
|| claim?.documentTypeCode
|
||||
|| claim?.document_type
|
||||
|| claim?.documentType
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeApplicationDate(claim) {
|
||||
return normalizeText(
|
||||
claim?.submitted_at
|
||||
|| claim?.submittedAt
|
||||
|| claim?.created_at
|
||||
|| claim?.createdAt
|
||||
|| claim?.occurred_at
|
||||
|| claim?.occurredAt
|
||||
)
|
||||
}
|
||||
|
||||
function toTimestamp(value) {
|
||||
const date = new Date(value)
|
||||
return Number.isNaN(date.getTime()) ? 0 : date.getTime()
|
||||
}
|
||||
|
||||
function formatAmount(value) {
|
||||
const numberValue = Number(String(value ?? '').replace(/[^\d.-]/g, ''))
|
||||
if (!Number.isFinite(numberValue) || numberValue <= 0) {
|
||||
return ''
|
||||
}
|
||||
return `¥${new Intl.NumberFormat('zh-CN', {
|
||||
minimumFractionDigits: Number.isInteger(numberValue) ? 0 : 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(numberValue)}`
|
||||
}
|
||||
|
||||
function includesAny(text, keywords) {
|
||||
const normalized = normalizeText(text)
|
||||
return keywords.some((keyword) => normalized.includes(keyword))
|
||||
}
|
||||
|
||||
function buildApplicationKeywordText(claim) {
|
||||
return [
|
||||
claim?.reason,
|
||||
claim?.business_reason,
|
||||
claim?.title,
|
||||
claim?.summary,
|
||||
claim?.description,
|
||||
claim?.location,
|
||||
claim?.business_location,
|
||||
claim?.expense_type_label,
|
||||
claim?.expenseTypeLabel
|
||||
].map(normalizeText).filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function matchesGenericApplicationByText(claim, expenseType) {
|
||||
const haystack = buildApplicationKeywordText(claim)
|
||||
if (expenseType === 'travel') {
|
||||
return includesAny(haystack, ['差旅', '出差', '住宿', '交通', '行程'])
|
||||
}
|
||||
if (expenseType === 'meal') {
|
||||
return includesAny(haystack, ['招待', '客户', '接待', '宴请', '用餐', '餐饮'])
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function requiresApplicationBeforeReimbursement(expenseType) {
|
||||
return REQUIRED_APPLICATION_EXPENSE_TYPES.has(normalizeLower(expenseType))
|
||||
}
|
||||
|
||||
export function getRequiredApplicationExpenseLabel(expenseType) {
|
||||
return EXPENSE_TYPE_LABELS[normalizeLower(expenseType)] || '报销'
|
||||
}
|
||||
|
||||
export function isExpenseApplicationClaim(claim) {
|
||||
const documentType = normalizeDocumentType(claim)
|
||||
const expenseType = normalizeExpenseType(claim)
|
||||
const claimNo = normalizeClaimNo(claim)
|
||||
|
||||
return documentType === 'application'
|
||||
|| documentType === 'expense_application'
|
||||
|| claimNo.startsWith('AP-')
|
||||
|| claimNo.startsWith('APP-')
|
||||
|| expenseType === 'application'
|
||||
|| expenseType.endsWith('_application')
|
||||
}
|
||||
|
||||
export function matchesRequiredApplicationExpenseType(claim, expenseType) {
|
||||
const normalizedExpenseType = normalizeLower(expenseType)
|
||||
const claimExpenseType = normalizeExpenseType(claim)
|
||||
const aliases = APPLICATION_TYPE_ALIASES[normalizedExpenseType] || new Set()
|
||||
|
||||
if (aliases.has(claimExpenseType)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return GENERIC_APPLICATION_TYPES.has(claimExpenseType)
|
||||
&& matchesGenericApplicationByText(claim, normalizedExpenseType)
|
||||
}
|
||||
|
||||
export function isClaimOwnedByCurrentUser(claim, currentUser = {}) {
|
||||
const userIds = uniqueValues([
|
||||
currentUser.id,
|
||||
currentUser.employeeId,
|
||||
currentUser.employee_id,
|
||||
currentUser.employeeNo,
|
||||
currentUser.employee_no,
|
||||
currentUser.username,
|
||||
currentUser.email
|
||||
])
|
||||
const claimIds = uniqueValues([
|
||||
claim?.employee_id,
|
||||
claim?.employeeId,
|
||||
claim?.employee_no,
|
||||
claim?.employeeNo,
|
||||
claim?.username,
|
||||
claim?.user_id,
|
||||
claim?.userId
|
||||
])
|
||||
if (userIds.length && claimIds.length && claimIds.some((item) => userIds.includes(item))) {
|
||||
return true
|
||||
}
|
||||
|
||||
const userNames = uniqueValues([
|
||||
currentUser.name,
|
||||
currentUser.user_name,
|
||||
currentUser.employeeName,
|
||||
currentUser.employee_name,
|
||||
currentUser.username
|
||||
])
|
||||
const claimNames = uniqueValues([
|
||||
claim?.employee_name,
|
||||
claim?.employeeName,
|
||||
claim?.applicant,
|
||||
claim?.applicant_name,
|
||||
claim?.applicantName
|
||||
])
|
||||
if (userNames.length && claimNames.length) {
|
||||
return claimNames.some((item) => userNames.includes(item))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function isUsableRequiredApplicationClaim(claim) {
|
||||
const status = normalizeApplicationStatus(claim)
|
||||
return !BLOCKED_APPLICATION_STATUSES.has(status)
|
||||
}
|
||||
|
||||
export function normalizeRequiredApplicationCandidate(claim) {
|
||||
const claimNo = normalizeText(claim?.claim_no || claim?.claimNo)
|
||||
const location = normalizeText(claim?.location || claim?.business_location || claim?.businessLocation)
|
||||
const amountText = formatAmount(claim?.amount || claim?.budget_amount || claim?.budgetAmount)
|
||||
const status = normalizeApplicationStatus(claim)
|
||||
|
||||
return {
|
||||
id: normalizeText(claim?.id || claim?.claim_id || claim?.claimId),
|
||||
claim_no: claimNo,
|
||||
expense_type: normalizeExpenseType(claim),
|
||||
reason: normalizeText(claim?.reason || claim?.business_reason || claim?.description || claim?.title),
|
||||
location,
|
||||
amount: normalizeText(claim?.amount || claim?.budget_amount || claim?.budgetAmount),
|
||||
amount_label: amountText,
|
||||
status,
|
||||
status_label: STATUS_LABELS[status] || normalizeText(claim?.approval_stage || claim?.approvalStage || status),
|
||||
application_date: normalizeApplicationDate(claim)
|
||||
}
|
||||
}
|
||||
|
||||
export function filterRequiredApplicationCandidates(claimsPayload, expenseType, currentUser = {}) {
|
||||
const claims = Array.isArray(claimsPayload)
|
||||
? claimsPayload
|
||||
: Array.isArray(claimsPayload?.items)
|
||||
? claimsPayload.items
|
||||
: Array.isArray(claimsPayload?.claims)
|
||||
? claimsPayload.claims
|
||||
: []
|
||||
|
||||
return claims
|
||||
.filter((claim) => (
|
||||
isExpenseApplicationClaim(claim)
|
||||
&& isUsableRequiredApplicationClaim(claim)
|
||||
&& isClaimOwnedByCurrentUser(claim, currentUser)
|
||||
&& matchesRequiredApplicationExpenseType(claim, expenseType)
|
||||
))
|
||||
.map(normalizeRequiredApplicationCandidate)
|
||||
.sort((left, right) => toTimestamp(right.application_date) - toTimestamp(left.application_date))
|
||||
}
|
||||
|
||||
export function buildRequiredApplicationActions(applications, actionType) {
|
||||
return (Array.isArray(applications) ? applications : []).map((application) => {
|
||||
const claimNo = normalizeText(application.claim_no) || '未编号申请单'
|
||||
const description = [
|
||||
application.status_label,
|
||||
application.location && `地点:${application.location}`,
|
||||
application.amount_label && `预算:${application.amount_label}`,
|
||||
application.reason && `事由:${application.reason}`
|
||||
].filter(Boolean).join(' · ')
|
||||
|
||||
return {
|
||||
label: claimNo,
|
||||
description,
|
||||
icon: 'mdi mdi-file-link-outline',
|
||||
action_type: actionType,
|
||||
payload: {
|
||||
application_claim_id: application.id,
|
||||
application_claim_no: application.claim_no,
|
||||
application_expense_type: application.expense_type,
|
||||
application_reason: application.reason,
|
||||
application_location: application.location,
|
||||
application_amount: application.amount,
|
||||
application_amount_label: application.amount_label,
|
||||
application_status: application.status,
|
||||
application_status_label: application.status_label,
|
||||
application_date: application.application_date
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function buildRequiredApplicationSelectionText(expenseType, applications) {
|
||||
const label = getRequiredApplicationExpenseLabel(expenseType)
|
||||
return [
|
||||
`发起“${label}”报销前,需要先关联对应的申请单。`,
|
||||
'',
|
||||
`我查到 ${applications.length} 个可关联申请单,请先选择其中一个。`,
|
||||
'选择后,我再继续向你收集本次报销依据。'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function buildRequiredApplicationMissingText(expenseType) {
|
||||
const label = getRequiredApplicationExpenseLabel(expenseType)
|
||||
return [
|
||||
`发起“${label}”报销前,需要先关联对应的申请单。`,
|
||||
'',
|
||||
`我没有查到你名下可关联的“${label}”申请单,所以当前不能继续这类报销流程。`,
|
||||
'请先切换到申请助手发起相关申请;申请单存在后,再回到报销助手继续。'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
@@ -906,7 +906,10 @@ export function hasMeaningfulSessionMessages(messages) {
|
||||
|| message.reviewPayload
|
||||
|| message.queryPayload
|
||||
|| message.draftPayload
|
||||
|| message.applicationPreview
|
||||
|| message.budgetReport
|
||||
|| message.pendingAttachmentAssociation
|
||||
|| (Array.isArray(message.riskFlags) && message.riskFlags.length)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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