feat: 本体字段治理与风险规则模板执行器重构

- 新增本体字段注册表与字段治理审计脚本
- 重构风险规则模板执行器、DSL 验证与清单分类器
- 完善票据夹服务与差旅请求详情页交互
- 优化趋势图表与总览页数据展示
- 增强报销平台风险分级与模拟公司筛选
- 补充本体字段、风险规则生成与票据夹服务测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-03 15:46:56 +08:00
parent e12b140508
commit 34457f9c3e
81 changed files with 4858 additions and 1073 deletions

View File

@@ -1,4 +1,4 @@
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue'
import { useSystemState } from '../../composables/useSystemState.js'
import { useToast } from '../../composables/useToast.js'
@@ -94,6 +94,223 @@ import {
} from './travelRequestDetailAdviceModel.js'
import { useTravelRequestPaymentFlow } from './travelRequestDetailPaymentFlow.js'
const SMART_ENTRY_RECOGNITION_TASK_RETENTION_MS = 10 * 60 * 1000
const smartEntryRecognitionTasks = new Map()
let smartEntryRecognitionTaskSeq = 0
function normalizeSmartEntryClaimId(claimId) {
return String(claimId || '').trim()
}
function buildRecognizedExpenseItemPatch(payload, fileName = '') {
const recognizedItemAmount = Number(payload?.item_amount ?? payload?.itemAmount)
const recognizedItemDate = normalizeIsoDateValue(payload?.item_date ?? payload?.itemDate)
const recognizedItemType = String(payload?.item_type ?? payload?.itemType ?? '').trim()
const recognizedItemReason = String(payload?.item_reason ?? payload?.itemReason ?? '').trim()
const recognizedItemLocation = String(payload?.item_location ?? payload?.itemLocation ?? '').trim()
const itemPatch = {
invoiceId: String(payload?.invoice_id || '').trim(),
attachmentHint: String(payload?.attachment?.file_name || fileName || '').trim()
}
if (recognizedItemDate) {
itemPatch.itemDate = recognizedItemDate
}
if (recognizedItemType) {
itemPatch.itemType = recognizedItemType
}
if (recognizedItemReason) {
itemPatch.itemReason = recognizedItemReason
}
if (recognizedItemLocation) {
itemPatch.itemLocation = recognizedItemLocation
}
if (Number.isFinite(recognizedItemAmount) && recognizedItemAmount > 0) {
itemPatch.itemAmount = recognizedItemAmount
itemPatch.amount = formatCurrency(recognizedItemAmount)
}
return itemPatch
}
function buildSmartEntryRecognitionSnapshot(task) {
if (!task) {
return null
}
return {
id: task.id,
claimId: task.claimId,
busy: task.busy,
total: task.total,
current: task.current,
completed: task.completed,
successCount: task.successCount,
failedCount: task.failedCount,
uploadingItemId: task.uploadingItemId,
fileName: task.fileName,
status: task.status,
payloads: [...task.payloads],
errors: [...task.errors]
}
}
function notifySmartEntryRecognitionTask(task) {
const snapshot = buildSmartEntryRecognitionSnapshot(task)
task.listeners.forEach((listener) => {
try {
listener(snapshot)
} catch (error) {
console.error('同步附件识别状态失败', error)
}
})
}
function scheduleSmartEntryRecognitionTaskCleanup(task) {
if (task.cleanupTimer) {
clearTimeout(task.cleanupTimer)
}
task.cleanupTimer = globalThis.setTimeout(() => {
const currentTask = smartEntryRecognitionTasks.get(task.claimId)
if (currentTask?.id === task.id && !currentTask.busy) {
smartEntryRecognitionTasks.delete(task.claimId)
}
}, SMART_ENTRY_RECOGNITION_TASK_RETENTION_MS)
}
function getSmartEntryRecognitionTask(claimId) {
return smartEntryRecognitionTasks.get(normalizeSmartEntryClaimId(claimId)) || null
}
function subscribeSmartEntryRecognitionTask(claimId, listener) {
const task = getSmartEntryRecognitionTask(claimId)
if (!task) {
listener(null)
return () => {}
}
task.listeners.add(listener)
listener(buildSmartEntryRecognitionSnapshot(task))
return () => {
task.listeners.delete(listener)
}
}
function resolveSmartEntryTaskAvailableItems(itemSnapshots) {
return (Array.isArray(itemSnapshots) ? itemSnapshots : [])
.filter((item) => item && !item.isSystemGenerated && !item.invoiceId)
.map((item) => ({ id: String(item.id || '').trim() }))
.filter((item) => item.id)
}
async function resolveSmartEntryRecognitionTaskItem(task) {
const availableItem = task.availableItems.shift()
if (availableItem?.id) {
return { id: availableItem.id, createdItem: null }
}
const claim = await createExpenseClaimItem(task.claimId, {})
const items = Array.isArray(claim?.items) ? claim.items : []
const createdItem = items.find((entry) => {
const itemId = String(entry?.id || '').trim()
return itemId && !task.knownItemIds.has(itemId)
})
if (!createdItem) {
throw new Error('新增费用明细失败,请稍后重试。')
}
const itemId = String(createdItem.id || '').trim()
task.knownItemIds.add(itemId)
return { id: itemId, createdItem }
}
async function runSmartEntryRecognitionTask(task, files) {
notifySmartEntryRecognitionTask(task)
for (let index = 0; index < files.length; index += 1) {
const file = files[index]
const fileName = String(file?.name || `${index + 1} 张附件`).trim()
task.current = index + 1
task.fileName = fileName
task.uploadingItemId = ''
notifySmartEntryRecognitionTask(task)
try {
const targetItem = await resolveSmartEntryRecognitionTaskItem(task)
task.uploadingItemId = targetItem.id
notifySmartEntryRecognitionTask(task)
const payload = await uploadExpenseClaimItemAttachment(task.claimId, targetItem.id, file)
task.successCount += 1
task.payloads.push({
id: `${task.id}:${index}:${targetItem.id}`,
itemId: targetItem.id,
fileName,
payload,
createdItem: targetItem.createdItem
})
} catch (error) {
task.failedCount += 1
task.errors.push({
fileName,
message: error?.message || '附件识别失败,请稍后重试。'
})
} finally {
task.completed = index + 1
task.uploadingItemId = ''
notifySmartEntryRecognitionTask(task)
}
}
task.busy = false
task.current = task.total
task.fileName = ''
task.status = task.failedCount
? task.successCount
? 'partial'
: 'failed'
: 'completed'
notifySmartEntryRecognitionTask(task)
scheduleSmartEntryRecognitionTaskCleanup(task)
}
function startSmartEntryRecognitionTask({ claimId, files, itemSnapshots }) {
const normalizedClaimId = normalizeSmartEntryClaimId(claimId)
const pendingFiles = Array.isArray(files) ? files.filter(Boolean) : []
if (!normalizedClaimId || !pendingFiles.length) {
return { task: null, reused: false }
}
const existingTask = getSmartEntryRecognitionTask(normalizedClaimId)
if (existingTask?.busy) {
return { task: existingTask, reused: true }
}
const sourceItems = Array.isArray(itemSnapshots) ? itemSnapshots : []
const task = {
id: `smart-entry-${Date.now()}-${smartEntryRecognitionTaskSeq += 1}`,
claimId: normalizedClaimId,
busy: true,
total: pendingFiles.length,
current: 0,
completed: 0,
successCount: 0,
failedCount: 0,
uploadingItemId: '',
fileName: '',
status: 'running',
payloads: [],
errors: [],
availableItems: resolveSmartEntryTaskAvailableItems(sourceItems),
knownItemIds: new Set(sourceItems.map((item) => String(item?.id || '').trim()).filter(Boolean)),
listeners: new Set(),
cleanupTimer: null
}
smartEntryRecognitionTasks.set(normalizedClaimId, task)
void runSmartEntryRecognitionTask(task, pendingFiles)
return { task, reused: false }
}
/*
* 以下片段仅用于兼容现有源码正则测试。
* 运行时实现位于 travelRequestDetailExpenseModel.js。
@@ -388,6 +605,8 @@ export default {
const riskOverrideDialogOpen = ref(false)
const riskOverrideBusy = ref(false)
const riskOverrideIndex = ref(0)
const highlightedRiskCardId = ref('')
let highlightedRiskCardTimer = 0
const riskOverrideReasons = reactive({})
const deleteBusy = ref(false)
const deleteDialogOpen = ref(false)
@@ -397,6 +616,16 @@ export default {
const approveConfirmDialogOpen = ref(false)
const leaderOpinion = ref('')
const expenseUploadInput = ref(null)
const smartEntryUploadInput = ref(null)
const smartEntryUploadDialogOpen = ref(false)
const smartEntrySelectedFiles = ref([])
const smartEntryRecognitionBusy = ref(false)
const smartEntryRecognitionTotal = ref(0)
const smartEntryRecognitionCompleted = ref(0)
const smartEntryRecognitionCurrent = ref(0)
const appliedSmartEntryRecognitionPayloadIds = new Set()
const notifiedSmartEntryRecognitionTaskIds = new Set()
let stopSmartEntryRecognitionTask = null
const expenseAttachmentMeta = reactive({})
const attachmentPreviewOpen = ref(false)
const attachmentPreviewLoading = ref(false)
@@ -411,6 +640,7 @@ export default {
itemReason: '',
itemLocation: '',
itemAmount: '',
itemNote: '',
invoiceId: ''
})
const detailNoteEditor = ref('')
@@ -669,6 +899,7 @@ export default {
|| approveBusy.value
|| payBusy.value
|| creatingExpense.value
|| smartEntryRecognitionBusy.value
|| Boolean(uploadingExpenseId.value)
|| Boolean(deletingAttachmentId.value)
|| Boolean(deletingExpenseId.value)
@@ -773,7 +1004,7 @@ export default {
const uploadedExpenseCount = computed(() => expenseItems.value.filter((item) => item.attachments.length).length)
const expenseTableColumnCount = computed(
() => 6 + (isEditableRequest.value ? 1 : 0)
() => 7 + (isEditableRequest.value ? 1 : 0)
)
const canEditDetailNote = computed(() => isDraftRequest.value)
const stripDetailNoteRiskTags = (value) =>
@@ -821,12 +1052,42 @@ export default {
() => request.value.claimId,
() => {
riskFlagPreviewSnapshot.value = null
}
appliedSmartEntryRecognitionPayloadIds.clear()
bindSmartEntryRecognitionTask()
},
{ immediate: true }
)
const draftBlockingIssues = computed(() =>
isEditableRequest.value ? buildDraftBlockingIssues(request.value, expenseItems.value) : []
)
const canSubmit = computed(() => isEditableRequest.value && !actionBusy.value)
const smartEntryRecognitionText = computed(() => {
const total = smartEntryRecognitionTotal.value
if (!total) {
return '附件识别准备中,请稍候。识别完成前暂不可编辑费用明细。'
}
const current = Math.min(Math.max(smartEntryRecognitionCurrent.value || 1, 1), total)
return `附件识别中(${current}/${total}),请稍候。识别完成前暂不可编辑费用明细。`
})
const smartEntrySelectedFileCount = computed(() => smartEntrySelectedFiles.value.length)
const smartEntrySelectedFileNames = computed(() =>
smartEntrySelectedFiles.value
.map((file) => String(file?.name || '').trim())
.filter(Boolean)
)
const smartEntrySelectedFileSummary = computed(() => {
const names = smartEntrySelectedFileNames.value
if (!names.length) {
return ''
}
if (names.length === 1) {
return names[0]
}
return `已选择 ${names.length} 张附件`
})
const smartEntryUploadBusy = computed(() =>
smartEntryUploadDialogOpen.value && (creatingExpense.value || Boolean(uploadingExpenseId.value))
)
const attachmentPreviewEntries = computed(() =>
expenseItems.value
.filter((item) => canPreviewAttachment(item))
@@ -929,6 +1190,102 @@ export default {
return `${label}${summary}`
}
function resetSmartEntryRecognitionState() {
smartEntryRecognitionBusy.value = false
smartEntryRecognitionTotal.value = 0
smartEntryRecognitionCompleted.value = 0
smartEntryRecognitionCurrent.value = 0
if (!pendingUploadExpenseId.value) {
uploadingExpenseId.value = ''
}
}
function ensureSmartEntryRecognitionItem(entry, patch) {
const itemId = String(entry?.itemId || '').trim()
if (!itemId) {
return null
}
const existingItem = expenseItems.value.find((item) => item.id === itemId)
if (existingItem) {
return existingItem
}
const rawItem = entry?.createdItem || {
id: itemId,
invoice_id: patch.invoiceId,
item_date: patch.itemDate,
item_type: patch.itemType,
item_reason: patch.itemReason,
item_location: patch.itemLocation,
item_amount: patch.itemAmount,
attachment_hint: patch.attachmentHint
}
const nextItem = buildExpenseItemViewModel(rawItem, expenseItems.value.length, request.value)
expenseItems.value = rebuildExpenseItems([...expenseItems.value, nextItem], request.value)
return nextItem
}
function applySmartEntryRecognitionPayload(entry) {
const payloadId = String(entry?.id || '').trim()
const itemId = String(entry?.itemId || '').trim()
if (!payloadId || !itemId || appliedSmartEntryRecognitionPayloadIds.has(payloadId)) {
return
}
const itemPatch = buildRecognizedExpenseItemPatch(entry.payload, entry.fileName)
const item = ensureSmartEntryRecognitionItem(entry, itemPatch)
if (!item) {
return
}
applyClaimRiskFlagsPayload(entry.payload)
if (entry.payload?.attachment) {
expenseAttachmentMeta[itemId] = entry.payload.attachment
}
applyLocalExpenseItemPatch(itemId, itemPatch)
if (editingExpenseId.value === itemId) {
populateExpenseEditor({ ...item, ...itemPatch })
}
appliedSmartEntryRecognitionPayloadIds.add(payloadId)
emit('request-updated', { claimId: request.value.claimId })
}
function syncSmartEntryRecognitionSnapshot(snapshot) {
if (!snapshot) {
resetSmartEntryRecognitionState()
return
}
smartEntryRecognitionBusy.value = Boolean(snapshot.busy)
smartEntryRecognitionTotal.value = snapshot.total || 0
smartEntryRecognitionCompleted.value = snapshot.completed || 0
smartEntryRecognitionCurrent.value = snapshot.current || 0
uploadingExpenseId.value = snapshot.uploadingItemId || ''
snapshot.payloads.forEach((entry) => applySmartEntryRecognitionPayload(entry))
if (!snapshot.busy && snapshot.status && !notifiedSmartEntryRecognitionTaskIds.has(snapshot.id)) {
notifiedSmartEntryRecognitionTaskIds.add(snapshot.id)
if (snapshot.failedCount && snapshot.successCount) {
toast(`已完成 ${snapshot.successCount} 张附件识别,${snapshot.failedCount} 张识别失败。`)
} else if (snapshot.failedCount) {
toast('附件识别失败,请稍后重试。')
} else if (snapshot.total > 1) {
toast(`已完成 ${snapshot.successCount} 张附件的智能录入。`)
}
}
}
function bindSmartEntryRecognitionTask(claimId = request.value.claimId) {
if (stopSmartEntryRecognitionTask) {
stopSmartEntryRecognitionTask()
stopSmartEntryRecognitionTask = null
}
stopSmartEntryRecognitionTask = subscribeSmartEntryRecognitionTask(claimId, syncSmartEntryRecognitionSnapshot)
}
async function refreshExpenseAttachmentMeta(itemId) {
if (!request.value.claimId || !itemId) {
return null
@@ -1048,10 +1405,19 @@ export default {
return normalizeRiskTone(resolveExpenseRiskState(item)?.tone) === 'high'
}
function hasExpenseRiskOrAbnormal(item) {
const state = resolveExpenseRiskState(item)
return Boolean(
String(item?.itemNote || '').trim()
|| normalizeRiskTone(state?.tone) !== 'low'
|| item?.tone === 'bad'
)
}
function resolveExpenseRiskIndicatorTitle(item) {
const state = resolveExpenseRiskState(item)
const summary = String(state?.summary || state?.headline || '').trim()
return summary ? `重大风险示:${summary}` : '重大风险示'
return summary ? `查看风险示:${summary}` : '查看风险示'
}
function applyClaimRiskFlagsPayload(payload) {
@@ -1198,6 +1564,62 @@ export default {
|| (!isEditableRequest.value && canViewApprovalRiskAdvice.value && aiAdvice.value.riskCards.length > 0)
|| (!isEditableRequest.value && isCurrentApplicant.value && !isApplicationDocument.value && hasVisibleRiskCards.value)
))
function normalizeRiskDomId(value) {
return String(value || '').trim().replace(/[^A-Za-z0-9_-]/g, '-') || 'unknown'
}
function resolveRiskCardDomId(card) {
return `detail-risk-card-${normalizeRiskDomId(card?.id)}`
}
function isHighlightedRiskCard(card) {
return Boolean(card?.id) && String(card.id) === highlightedRiskCardId.value
}
function resolveExpenseRiskTargetCard(item) {
const itemId = String(item?.id || '').trim()
const invoiceId = String(item?.invoiceId || '').trim()
const itemIndex = expenseItems.value.findIndex((entry) => entry.id === item?.id) + 1
const cards = Array.isArray(aiAdvice.value?.riskCards) ? aiAdvice.value.riskCards : []
const actionableCards = cards.filter((card) => ['medium', 'high'].includes(normalizeRiskTone(card?.tone)))
return actionableCards.find((card) => String(card?.itemId || card?.item_id || '').trim() === itemId)
|| actionableCards.find((card) => invoiceId && String(card?.invoiceId || card?.invoice_id || '').trim() === invoiceId)
|| actionableCards.find((card) => Number(card?.itemIndex || card?.item_index || 0) === itemIndex)
|| actionableCards.find((card) => itemIndex > 0 && String(card?.title || '').includes(`${itemIndex}`))
|| null
}
function hasExpenseRiskIndicator(item) {
return Boolean(resolveExpenseRiskTargetCard(item))
}
async function focusExpenseRisk(item) {
const card = resolveExpenseRiskTargetCard(item)
const riskSection = document.querySelector('.validation-section--risk')
if (!card && !riskSection) {
toast('当前费用明细暂无可定位的风险点。')
return
}
highlightedRiskCardId.value = card?.id ? String(card.id) : ''
await nextTick()
const target = card
? document.getElementById(resolveRiskCardDomId(card))
: riskSection
target?.scrollIntoView({ behavior: 'smooth', block: 'center' })
if (highlightedRiskCardTimer) {
window.clearTimeout(highlightedRiskCardTimer)
}
highlightedRiskCardTimer = window.setTimeout(() => {
highlightedRiskCardId.value = ''
highlightedRiskCardTimer = 0
}, 1800)
}
const aiAdviceTitle = computed(() => {
if (!isEditableRequest.value && isCurrentApplicant.value && !isApplicationDocument.value) {
return '报销风险提示'
@@ -1375,6 +1797,7 @@ export default {
expenseEditor.itemLocation =
item.itemLocation || (isSyntheticLocationDisplay(item.detail, item.itemType) ? '' : item.detail)
expenseEditor.itemAmount = item.itemAmount ? String(item.itemAmount) : ''
expenseEditor.itemNote = item.itemNote || ''
expenseEditor.invoiceId = item.invoiceId || ''
}
@@ -1416,14 +1839,10 @@ export default {
return ''
}
async function handleAddExpenseItem() {
if (!isEditableRequest.value || actionBusy.value) {
return
}
async function createDraftExpenseItem({ openEditor = true } = {}) {
if (!request.value.claimId) {
toast('当前草稿缺少 claimId暂时无法新增费用明细。')
return
return null
}
creatingExpense.value = true
@@ -1441,15 +1860,108 @@ export default {
const nextItem = buildExpenseItemViewModel(createdItem, expenseItems.value.length, request.value)
expenseItems.value = rebuildExpenseItems([...expenseItems.value, nextItem], request.value)
creatingExpense.value = false
startExpenseEdit(nextItem)
toast('已新增一条费用明细,请继续填写。')
if (openEditor) {
startExpenseEdit(nextItem)
toast('已新增一条费用明细,请继续填写。')
}
return nextItem
} catch (error) {
toast(error?.message || '新增费用明细失败,请稍后重试。')
return null
} finally {
creatingExpense.value = false
}
}
async function handleAddExpenseItem() {
if (!isEditableRequest.value || actionBusy.value) {
return
}
await createDraftExpenseItem({ openEditor: true })
}
function triggerSmartEntryUpload() {
if (!isEditableRequest.value || actionBusy.value) {
return
}
if (!request.value.claimId) {
toast('当前草稿缺少 claimId暂时无法上传单据。')
return
}
smartEntrySelectedFiles.value = []
smartEntryUploadDialogOpen.value = true
}
function closeSmartEntryUploadDialog() {
if (smartEntryUploadBusy.value) {
return
}
smartEntryUploadDialogOpen.value = false
clearSmartEntryFile()
}
function chooseSmartEntryFile() {
if (smartEntryUploadBusy.value) {
return
}
if (smartEntryUploadInput.value) {
smartEntryUploadInput.value.value = ''
smartEntryUploadInput.value.click()
}
}
function clearSmartEntryFile() {
smartEntrySelectedFiles.value = []
if (smartEntryUploadInput.value) {
smartEntryUploadInput.value.value = ''
}
}
function handleSmartEntryFileChange(event) {
const target = event?.target
const fileList = target?.files
const files = Array.from(fileList || [])
if (target) {
target.value = ''
}
if (!files.length) {
return
}
smartEntrySelectedFiles.value = files
}
async function confirmSmartEntryUpload() {
if (smartEntryUploadBusy.value) {
return
}
const files = [...smartEntrySelectedFiles.value]
if (!files.length) {
toast('请先选择需要智能录入的附件。')
return
}
smartEntryUploadDialogOpen.value = false
clearSmartEntryFile()
const { task, reused } = startSmartEntryRecognitionTask({
claimId: request.value.claimId,
files,
itemSnapshots: expenseItems.value
})
if (!task) {
toast('当前草稿缺少 claimId暂时无法识别附件。')
return
}
bindSmartEntryRecognitionTask(request.value.claimId)
toast(reused ? '当前单据已有附件识别任务,请等待识别完成。' : '附件已转入后台识别,费用明细将在识别完成后自动更新。')
}
function triggerExpenseUpload(item) {
if (!isEditableRequest.value || actionBusy.value) {
return
@@ -1570,31 +2082,7 @@ export default {
const payload = await uploadExpenseClaimItemAttachment(request.value.claimId, item.id, file)
applyClaimRiskFlagsPayload(payload)
expenseAttachmentMeta[item.id] = payload?.attachment || null
const recognizedItemAmount = Number(payload?.item_amount ?? payload?.itemAmount)
const recognizedItemDate = normalizeIsoDateValue(payload?.item_date ?? payload?.itemDate)
const recognizedItemType = String(payload?.item_type ?? payload?.itemType ?? '').trim()
const recognizedItemReason = String(payload?.item_reason ?? payload?.itemReason ?? '').trim()
const recognizedItemLocation = String(payload?.item_location ?? payload?.itemLocation ?? '').trim()
const itemPatch = {
invoiceId: String(payload?.invoice_id || '').trim(),
attachmentHint: String(payload?.attachment?.file_name || file.name || '').trim()
}
if (recognizedItemDate) {
itemPatch.itemDate = recognizedItemDate
}
if (recognizedItemType) {
itemPatch.itemType = recognizedItemType
}
if (recognizedItemReason) {
itemPatch.itemReason = recognizedItemReason
}
if (recognizedItemLocation) {
itemPatch.itemLocation = recognizedItemLocation
}
if (Number.isFinite(recognizedItemAmount) && recognizedItemAmount > 0) {
itemPatch.itemAmount = recognizedItemAmount
itemPatch.amount = formatCurrency(recognizedItemAmount)
}
const itemPatch = buildRecognizedExpenseItemPatch(payload, file.name)
applyLocalExpenseItemPatch(item.id, {
...itemPatch
})
@@ -1603,8 +2091,10 @@ export default {
emit('request-updated', { claimId: request.value.claimId })
const riskNotice = buildAttachmentRiskNotice(payload?.attachment)
toast(riskNotice || payload?.message || `${file.name} 已关联到当前费用明细。`)
return true
} catch (error) {
toast(error?.message || '附件上传失败,请稍后重试。')
return false
} finally {
uploadingExpenseId.value = ''
}
@@ -1693,6 +2183,7 @@ export default {
expenseEditor.itemReason = ''
expenseEditor.itemLocation = ''
expenseEditor.itemAmount = ''
expenseEditor.itemNote = ''
expenseEditor.invoiceId = ''
}
if (pendingUploadExpenseId.value === item.id) {
@@ -1736,6 +2227,7 @@ export default {
item_type: expenseEditor.itemType,
item_reason: expenseEditor.itemReason.trim(),
item_location: preservedLocation,
item_note: expenseEditor.itemNote.trim(),
item_amount: nextAmount,
invoice_id: nextInvoiceId
}
@@ -1748,6 +2240,7 @@ export default {
itemType: expenseEditor.itemType,
itemReason: expenseEditor.itemReason.trim(),
itemLocation: preservedLocation,
itemNote: expenseEditor.itemNote.trim(),
itemAmount: nextAmount,
invoiceId: nextInvoiceId
})
@@ -1788,11 +2281,6 @@ export default {
return
}
if (submitRiskWarnings.value.length && !hasRiskOverrideExplanation.value) {
openRiskOverrideDialog()
return
}
submitConfirmDialogOpen.value = true
}
@@ -1823,12 +2311,6 @@ export default {
return
}
if (submitRiskWarnings.value.length && !hasRiskOverrideExplanation.value) {
submitConfirmDialogOpen.value = false
openRiskOverrideDialog()
return
}
submitBusy.value = true
try {
const payload = await submitExpenseClaim(request.value.claimId)
@@ -2007,26 +2489,6 @@ export default {
}
}
function openAiEntry() {
if (!canOpenAiEntry.value) {
return
}
const claimId = String(request.value?.claimId || '').trim()
emit('openAssistant', {
source: 'detail',
prompt: '',
request: request.value,
restoreLatestConversation: false,
scope: claimId
? {
type: 'claim',
claimId
}
: null
})
}
function buildApplicationEditPreview() {
const factEntries = applicationDetailFactItems.value
.map((item) => [String(item?.label || '').trim(), String(item?.value || '').trim()])
@@ -2098,6 +2560,14 @@ export default {
}
onBeforeUnmount(() => {
if (highlightedRiskCardTimer) {
window.clearTimeout(highlightedRiskCardTimer)
highlightedRiskCardTimer = 0
}
if (stopSmartEntryRecognitionTask) {
stopSmartEntryRecognitionTask()
stopSmartEntryRecognitionTask = null
}
closeAttachmentPreview()
})
@@ -2112,9 +2582,10 @@ export default {
canNavigateAttachmentPreview,
canModifyReturnedApplication, canOpenAiEntry, canApproveRequest, canPayRequest, canReturnRequest, canSubmit, canPreviewAttachment,
closeApproveConfirmDialog, closeDeleteDialog, closeAttachmentPreview, closePayConfirmDialog, closeSubmitConfirmDialog,
closeRiskOverrideDialog,
closeRiskOverrideDialog, closeSmartEntryUploadDialog,
closeReturnDialog, confirmApproveRequest, confirmDeleteRequest, confirmSubmitRequest, confirmReturnRequest,
confirmPayRequest, confirmRiskOverrideReasons,
confirmPayRequest, confirmRiskOverrideReasons, confirmSmartEntryUpload,
chooseSmartEntryFile, clearSmartEntryFile,
currentAttachmentPreviewInsight, currentAttachmentPreviewRiskCards, currentProgressRingMotion,
currentSubmitRiskWarning,
canEditDetailNote, deleteActionLabel, deleteBusy, deleteDialogDescription, deleteDialogOpen,
@@ -2123,20 +2594,27 @@ export default {
expenseItems, expenseTableColumnCount, expenseTotal, expenseUploadInput,
expenseTypeOptions: EXPENSE_TYPE_OPTIONS,
goToNextSubmitRisk, goToPreviousSubmitRisk,
handleAddExpenseItem, handleApproveRequest, handleDeleteRequest, handleExpenseFileChange,
focusExpenseRisk,
handleAddExpenseItem, handleApproveRequest, handleDeleteRequest, handleExpenseFileChange, handleSmartEntryFileChange,
handleModifyApplication,
handlePayRequest,
handleReturnRequest, handleSubmit, heroFactItems, isApplicationDocument, isDraftRequest, isEditableRequest, isTravelRequest,
isMajorExpenseRisk,
openAiEntry, openAttachmentPreview, goToNextAttachmentPreview, goToPreviousAttachmentPreview,
hasExpenseRiskIndicator,
hasExpenseRiskOrAbnormal,
triggerSmartEntryUpload, openAttachmentPreview, goToNextAttachmentPreview, goToPreviousAttachmentPreview,
payBusy, payConfirmDialogOpen, profile, progressSteps, request, leaderOpinion, removeExpenseAttachment, removeExpenseItem,
hasLeaderApprovalEvents, hasSingleLeaderApprovalEvent, leaderApprovalEvents, leaderApprovalReadonlyMeta,
resolveExpenseRiskIndicatorTitle,
resetDetailNote, resolveAttachmentDisplayName, resolveAttachmentPreviewTitle, resolveAttachmentRecognition,
resolveExpenseReasonHelper, resolveExpenseReasonPlaceholder, resolveExpenseRiskState, resolveExpenseIssues,
resolveRiskCardDomId, isHighlightedRiskCard,
returnBusy, returnDialogDescription, returnDialogOpen, riskOverrideBusy, riskOverrideDialogOpen, riskOverrideIndexLabel,
requiresApprovalOpinion,
riskOverrideReasons, saveDetailNote, savingDetailNote, savingExpenseId,
smartEntrySelectedFileCount, smartEntrySelectedFileNames, smartEntrySelectedFileSummary,
smartEntryRecognitionBusy, smartEntryRecognitionText,
smartEntryUploadBusy, smartEntryUploadDialogOpen, smartEntryUploadInput,
showAiAdvicePanel, showApplicationLeaderOpinion,
showBudgetAnalysis, showStageRiskAdvice,
showExpenseRisk, startExpenseEdit, submitActionIcon, submitActionLabel, submitBusy,