Files
X-Financial/web/src/views/scripts/travelReimbursementSubmitAttachmentFlow.js

272 lines
9.5 KiB
JavaScript
Raw Normal View History

import { ATTACHMENT_ASSOCIATION_CONFIRM_HREF } from './travelReimbursementAttachmentModel.js'
export function createSubmitAttachmentAssociationFlow({
activeReviewPayload,
buildReviewFormContextFromPayload,
createMessage,
draftClaimId,
emitDraftSaved,
fetchReceiptFolderItems,
isKnowledgeSession,
messages,
nextTick,
persistSessionState,
resetFlowRun,
reviewInlineForm,
scrollToBottom,
sessionSwitchBusy,
submitComposer,
submitting,
toast
}) {
const pendingAttachmentAssociations = new Map()
function createPendingAttachmentAssociationId() {
return `attachment-association-${Date.now()}-${Math.random().toString(16).slice(2)}`
}
function emitSavedDraftRefresh(draftPayload) {
if (!emitDraftSaved || isKnowledgeSession.value || !draftPayload?.claim_no) {
return
}
const draftType = String(draftPayload.draft_type || '').trim()
emitDraftSaved({
claimId: String(draftPayload.claim_id || draftPayload.claimId || '').trim(),
claimNo: String(draftPayload.claim_no || draftPayload.claimNo || '').trim(),
status: String(draftPayload.status || '').trim(),
approvalStage: String(draftPayload.approval_stage || draftPayload.approvalStage || '').trim(),
documentType: draftType === 'expense_application' ? 'application' : 'reimbursement'
})
}
function normalizeRecognizedAttachmentData(data) {
if (!data || typeof data !== 'object') {
return null
}
const documents = Array.isArray(data.ocrDocuments) ? data.ocrDocuments : []
if (!documents.length) {
return null
}
return {
ocrPayload: data.ocrPayload || null,
ocrSummary: String(data.ocrSummary || '').trim(),
ocrDocuments: documents,
ocrFilePreviews: Array.isArray(data.ocrFilePreviews) ? data.ocrFilePreviews : []
}
}
function hasReceiptFolderSourceFile(files) {
return files.some((file) => String(file?.receiptId || '').trim())
}
async function promptUnlinkedReceiptFolderIfNeeded({
detailScopedClaimId,
files,
fileNames,
options,
rawText,
resolvedUploadDisposition,
reviewAction,
systemGenerated,
userText
}) {
if (
isKnowledgeSession.value ||
systemGenerated ||
!files.length ||
detailScopedClaimId ||
resolvedUploadDisposition ||
options.skipReceiptFolderUnlinkedPrompt ||
options.skipDraftAssociationPrompt ||
reviewAction ||
hasReceiptFolderSourceFile(files)
) {
return false
}
let unlinkedReceipts = []
try {
unlinkedReceipts = await fetchReceiptFolderItems('unlinked')
} catch (error) {
console.warn('Failed to load unlinked receipt folder items before attachment upload:', error)
return false
}
const count = Array.isArray(unlinkedReceipts) ? unlinkedReceipts.length : 0
if (!count) {
return false
}
resetFlowRun()
if (!options.skipUserMessage) {
messages.value.push(createMessage('user', userText, fileNames))
}
messages.value.push(createMessage(
'assistant',
`票据夹中还有 ${count} 份未关联票据。建议先处理这些票据再上传新附件,避免重复保存或遗漏关联。`,
[],
{
meta: ['票据夹待关联'],
suggestedActions: [
{
action_type: 'open_receipt_folder',
label: '去票据夹关联',
icon: 'mdi mdi-folder-open-outline',
payload: { target_view: 'receiptFolder' }
},
{
action_type: 'continue_upload_with_unlinked_receipts',
label: '继续上传新附件',
icon: 'mdi mdi-upload-outline',
payload: { raw_text: rawText }
}
]
}
))
nextTick(scrollToBottom)
persistSessionState()
return true
}
function buildConfirmedAssociationText(message) {
return String(message?.text || '')
.replace(`[确认](${ATTACHMENT_ASSOCIATION_CONFIRM_HREF})`, '已确认')
.replace(`[确定](${ATTACHMENT_ASSOCIATION_CONFIRM_HREF})`, '已确定')
}
function resolveReviewPanelScope({
reviewPayload = null,
reviewAction = '',
fileCount = 0,
rawText = ''
} = {}) {
if (!reviewPayload || typeof reviewPayload !== 'object') {
return ''
}
const normalizedAction = String(reviewAction || '').trim()
const documentCount = Array.isArray(reviewPayload.document_cards) ? reviewPayload.document_cards.length : 0
const riskCount = Array.isArray(reviewPayload.risk_briefs) ? reviewPayload.risk_briefs.length : 0
const asksRisk = /风险|隐患|超标|异常|重复|待整改|风险项|高风险|中风险|低风险/.test(String(rawText || ''))
if (fileCount > 0 && documentCount > 0) {
return 'documents'
}
if (riskCount > 0 && (asksRisk || ['next_step', 'submit', 'submit_claim'].includes(normalizedAction))) {
return 'risk'
}
if (!normalizedAction && fileCount === 0) {
return 'overview'
}
return ''
}
async function confirmPendingAttachmentAssociation(message) {
if (submitting.value || sessionSwitchBusy.value) return null
const pending = message?.pendingAttachmentAssociation && typeof message.pendingAttachmentAssociation === 'object'
? message.pendingAttachmentAssociation
: null
const associationId = String(pending?.id || '').trim()
if (!associationId || pending?.status === 'confirmed') {
return null
}
const runtime = pendingAttachmentAssociations.get(associationId)
if (!runtime || !Array.isArray(runtime.files) || !runtime.files.length) {
toast('当前会话里没有可归集的附件原件,请重新上传票据后再确认。')
return null
}
pending.status = 'confirmed'
message.pendingAttachmentAssociation = pending
message.text = buildConfirmedAssociationText(message)
message.meta = ['已确认归集']
persistSessionState()
if (pending.mode === 'save_then_associate') {
const inheritedReviewContext = buildReviewFormContextFromPayload(
activeReviewPayload.value,
reviewInlineForm.value
)
const savePayload = await submitComposer({
rawText: '请先把当前已识别的报销信息保存为草稿,随后继续归集本次上传的附件。',
userText: '',
files: [],
skipUserMessage: true,
pendingText: '正在先保存未保存单据...',
systemGenerated: true,
extraContext: {
...runtime.extraContext,
...inheritedReviewContext,
review_action: 'save_draft'
}
})
const savedClaimId = String(savePayload?.result?.draft_payload?.claim_id || draftClaimId.value || '').trim()
const savedClaimNo = String(savePayload?.result?.draft_payload?.claim_no || '').trim()
if (!savedClaimId) {
toast('当前单据还没有保存成功,请稍后重试。')
return savePayload
}
return submitComposer({
rawText: `确认将本次上传的 ${runtime.fileNames.length} 份票据归集到草稿 ${savedClaimNo || '当前草稿'}`,
userText: `保存草稿并归集 ${runtime.fileNames.length} 份票据`,
files: runtime.files,
uploadDisposition: 'continue_existing',
skipDraftAssociationPrompt: true,
skipUserMessage: true,
appendToCurrentFlow: true,
systemGenerated: true,
pendingText: savedClaimNo
? `草稿 ${savedClaimNo} 已保存,正在识别并归集附件...`
: '草稿已保存,正在识别并归集附件...',
associationConfirmed: true,
extraContext: {
...runtime.extraContext,
review_action: 'link_to_existing_draft',
draft_claim_id: savedClaimId,
selected_claim_id: savedClaimId,
selected_claim_no: savedClaimNo,
attachment_association_confirmed: true
}
})
}
return submitComposer({
rawText: `确认将本次上传的 ${runtime.fileNames.length} 份票据归集到草稿 ${runtime.claimNo || '当前草稿'}`,
userText: `确认归集到草稿 ${runtime.claimNo || '当前草稿'}`,
files: runtime.files,
uploadDisposition: 'continue_existing',
skipDraftAssociationPrompt: true,
pendingText: runtime.claimNo
? `正在将票据归集到草稿 ${runtime.claimNo}...`
: '正在将票据归集到当前草稿...',
associationConfirmed: true,
recognizedAttachmentData: {
ocrPayload: runtime.ocrPayload,
ocrSummary: runtime.ocrSummary,
ocrDocuments: runtime.ocrDocuments,
ocrFilePreviews: runtime.ocrFilePreviews
},
extraContext: {
...runtime.extraContext,
review_action: 'link_to_existing_draft',
draft_claim_id: runtime.claimId,
selected_claim_id: runtime.claimId,
selected_claim_no: runtime.claimNo,
attachment_association_confirmed: true
}
})
}
return {
confirmPendingAttachmentAssociation,
createPendingAttachmentAssociationId,
emitSavedDraftRefresh,
normalizeRecognizedAttachmentData,
pendingAttachmentAssociations,
promptUnlinkedReceiptFolderIfNeeded,
resolveReviewPanelScope
}
}