fix: handle risk explanation standard adjustment
This commit is contained in:
@@ -10,7 +10,9 @@ import TravelRequestDeleteDialog from '../../components/travel/TravelRequestDele
|
||||
import StageRiskAdviceCard from '../../components/travel/StageRiskAdviceCard.vue'
|
||||
import TravelRequestReturnDialog from '../../components/travel/TravelRequestReturnDialog.vue'
|
||||
import {
|
||||
acceptExpenseClaimStandardAdjustment,
|
||||
approveExpenseClaim,
|
||||
calculateTravelReimbursement,
|
||||
createExpenseClaimItem,
|
||||
deleteExpenseClaimItem,
|
||||
deleteExpenseClaimItemAttachment,
|
||||
@@ -88,6 +90,13 @@ import {
|
||||
resolveSubmitConfirmDescription,
|
||||
resolveSubmitConfirmText
|
||||
} from './travelRequestDetailSubmitModel.js'
|
||||
import {
|
||||
buildCurrentStandardAdjustmentMap,
|
||||
buildStandardAdjustmentPayload as buildStandardAdjustmentPayloadModel,
|
||||
filterSubmitterStandardAdjustedRiskCards as filterSubmitterStandardAdjustedRiskCardsModel,
|
||||
isRiskCardMissingExpenseNote as isRiskCardMissingExpenseNoteModel,
|
||||
resolveExpenseItemForRiskCard as resolveExpenseItemForRiskCardModel
|
||||
} from './travelRequestDetailStandardAdjustment.js'
|
||||
import {
|
||||
buildEmployeeProfileAdviceItems,
|
||||
buildTravelReceiptMaterialPrompts
|
||||
@@ -994,9 +1003,16 @@ export default {
|
||||
}
|
||||
|
||||
const expenseTotal = computed(() => {
|
||||
const total = expenseItems.value.reduce((sum, item) => sum + Number(item.itemAmount || 0), 0)
|
||||
const total = expenseItems.value.reduce((sum, item) => {
|
||||
const adjustedAmount = Number(item.reimbursableAmount)
|
||||
const originalAmount = Number(item.itemAmount || 0)
|
||||
return sum + (Number.isFinite(adjustedAmount) ? adjustedAmount : originalAmount)
|
||||
}, 0)
|
||||
return formatCurrency(total)
|
||||
})
|
||||
const submitConfirmAmountDisplay = computed(() =>
|
||||
isApplicationDocument.value ? (request.value.amountDisplay || expenseTotal.value) : expenseTotal.value
|
||||
)
|
||||
const applicationDetailFactItems = computed(() => buildApplicationDetailFactItems(request.value))
|
||||
const relatedApplicationFactItems = computed(() => buildRelatedApplicationFactItems(request.value))
|
||||
|
||||
@@ -1155,6 +1171,57 @@ export default {
|
||||
return requestFlags
|
||||
}
|
||||
|
||||
function resolveCurrentStandardAdjustmentMap() {
|
||||
return buildCurrentStandardAdjustmentMap(request.value, resolveClaimRiskFlags())
|
||||
}
|
||||
|
||||
function resolveExpenseItemForRiskCard(card) {
|
||||
return resolveExpenseItemForRiskCardModel(card, expenseItems.value)
|
||||
}
|
||||
|
||||
function filterSubmitterStandardAdjustedRiskCards(cards, businessStage) {
|
||||
return filterSubmitterStandardAdjustedRiskCardsModel({
|
||||
cards,
|
||||
businessStage,
|
||||
isCurrentApplicant: isCurrentApplicant.value,
|
||||
expenseItems: expenseItems.value,
|
||||
standardAdjustmentMap: resolveCurrentStandardAdjustmentMap()
|
||||
})
|
||||
}
|
||||
|
||||
function isRiskCardMissingExpenseNote(card) {
|
||||
return isRiskCardMissingExpenseNoteModel(card, expenseItems.value)
|
||||
}
|
||||
|
||||
async function buildStandardAdjustmentPayload() {
|
||||
return buildStandardAdjustmentPayloadModel({
|
||||
warnings: submitRiskWarnings.value,
|
||||
expenseItems: expenseItems.value,
|
||||
request: request.value,
|
||||
calculateTravelReimbursement
|
||||
})
|
||||
}
|
||||
|
||||
function applyStandardAdjustmentResponse(payload = {}) {
|
||||
const flags = Array.isArray(payload?.risk_flags_json)
|
||||
? payload.risk_flags_json
|
||||
: Array.isArray(payload?.riskFlags)
|
||||
? payload.riskFlags
|
||||
: resolveClaimRiskFlags()
|
||||
riskFlagPreviewSnapshot.value = {
|
||||
claimId: request.value.claimId,
|
||||
riskFlags: flags
|
||||
}
|
||||
const sourceItems = Array.isArray(payload?.items) && payload.items.length
|
||||
? payload.items
|
||||
: expenseItems.value
|
||||
expenseItems.value = rebuildExpenseItems(sourceItems, {
|
||||
...request.value,
|
||||
riskFlags: flags,
|
||||
risk_flags_json: flags
|
||||
})
|
||||
}
|
||||
|
||||
function resolveAttachmentDisplayName(item) {
|
||||
const metadata = resolveAttachmentMeta(item)
|
||||
return String(metadata?.file_name || item.attachmentHint || '').trim()
|
||||
@@ -1530,7 +1597,7 @@ export default {
|
||||
: []
|
||||
const scopedRiskCards = [
|
||||
...(hasActionableRiskCards ? [] : summaryRiskCards),
|
||||
...directRiskCards
|
||||
...filterSubmitterStandardAdjustedRiskCards(directRiskCards, currentBusinessStage)
|
||||
]
|
||||
const riskCards = filterRiskCardsForVisibility(scopedRiskCards, riskViewerContext.value)
|
||||
|
||||
@@ -1652,7 +1719,8 @@ export default {
|
||||
|
||||
const submitRiskWarnings = computed(() =>
|
||||
aiAdvice.value.riskCards
|
||||
.filter((card) => normalizeRiskTone(card?.tone) === 'high')
|
||||
.filter((card) => ['medium', 'high'].includes(normalizeRiskTone(card?.tone)))
|
||||
.filter((card) => isRiskCardMissingExpenseNote(card))
|
||||
.map((card, index) => ({
|
||||
...card,
|
||||
id: String(card.id || `submit-risk-${index}`),
|
||||
@@ -1663,7 +1731,6 @@ export default {
|
||||
const riskOverrideIndexLabel = computed(() =>
|
||||
submitRiskWarnings.value.length ? `${riskOverrideIndex.value + 1} / ${submitRiskWarnings.value.length}` : ''
|
||||
)
|
||||
const hasRiskOverrideExplanation = computed(() => detailNoteTags.value.includes('#high_risk'))
|
||||
|
||||
function resetDetailNote() {
|
||||
detailNoteEditor.value = detailNoteSource.value
|
||||
@@ -1722,6 +1789,18 @@ export default {
|
||||
riskOverrideDialogOpen.value = false
|
||||
}
|
||||
|
||||
function resizeExpenseNoteInput(event) {
|
||||
const target = event?.target
|
||||
if (!target || typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
const style = window.getComputedStyle(target)
|
||||
const lineHeight = Number.parseFloat(style.lineHeight) || 18
|
||||
const maxHeight = lineHeight * 3 + 18
|
||||
target.style.height = 'auto'
|
||||
target.style.height = `${Math.min(target.scrollHeight, maxHeight)}px`
|
||||
}
|
||||
|
||||
function goToPreviousSubmitRisk() {
|
||||
if (!submitRiskWarnings.value.length) {
|
||||
return
|
||||
@@ -1737,17 +1816,6 @@ export default {
|
||||
riskOverrideIndex.value = (riskOverrideIndex.value + 1) % submitRiskWarnings.value.length
|
||||
}
|
||||
|
||||
function buildRiskOverrideAppendix() {
|
||||
return submitRiskWarnings.value
|
||||
.map((risk, index) => {
|
||||
const reason = String(riskOverrideReasons[risk.id] || '').trim()
|
||||
const tags = resolveRiskTags(risk).join(' ')
|
||||
const title = String(risk.title || risk.label || '重大风险').trim()
|
||||
return `超标说明:${tags} 第${index + 1}条 ${title}:${reason}`
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
function mergeDetailNoteWithRiskOverride(appendix) {
|
||||
const baseNote = detailNoteEditor.value.trim() || detailNoteSource.value
|
||||
return [baseNote, appendix].map((item) => String(item || '').trim()).filter(Boolean).join('\n')
|
||||
@@ -1760,28 +1828,91 @@ export default {
|
||||
const missingIndex = submitRiskWarnings.value.findIndex((risk) => !String(riskOverrideReasons[risk.id] || '').trim())
|
||||
if (missingIndex >= 0) {
|
||||
riskOverrideIndex.value = missingIndex
|
||||
toast('请为每一条重大风险填写违规提交原因。')
|
||||
toast('请为每一条风险填写异常说明。')
|
||||
return
|
||||
}
|
||||
|
||||
const appendix = buildRiskOverrideAppendix()
|
||||
const nextNote = mergeDetailNoteWithRiskOverride(appendix)
|
||||
if (nextNote.length > 500) {
|
||||
toast('附加说明最多 500 字,请精简风险原因后再继续提交。')
|
||||
return
|
||||
}
|
||||
const itemNoteGroups = new Map()
|
||||
const claimLevelRisks = []
|
||||
submitRiskWarnings.value.forEach((risk, index) => {
|
||||
const reason = String(riskOverrideReasons[risk.id] || '').trim()
|
||||
const item = resolveExpenseItemForRiskCard(risk)
|
||||
if (item?.id) {
|
||||
const currentGroup = itemNoteGroups.get(item.id) || { item, reasons: [] }
|
||||
currentGroup.reasons.push(reason)
|
||||
itemNoteGroups.set(item.id, currentGroup)
|
||||
} else {
|
||||
const title = String(risk.title || risk.label || '风险').trim()
|
||||
claimLevelRisks.push(`异常说明:第${index + 1}条 ${title}:${reason}`)
|
||||
}
|
||||
})
|
||||
|
||||
riskOverrideBusy.value = true
|
||||
try {
|
||||
await updateExpenseClaim(request.value.claimId, {
|
||||
reason: nextNote
|
||||
await Promise.all(
|
||||
[...itemNoteGroups.entries()].map(([itemId, group]) => {
|
||||
const existingNote = String(group.item?.itemNote || '').trim()
|
||||
const nextNote = [
|
||||
existingNote,
|
||||
...group.reasons.filter((reason) => existingNote !== reason && !existingNote.includes(reason))
|
||||
].filter(Boolean).join('\n')
|
||||
return updateExpenseClaimItem(request.value.claimId, itemId, {
|
||||
item_note: nextNote
|
||||
})
|
||||
})
|
||||
)
|
||||
itemNoteGroups.forEach((group, itemId) => {
|
||||
const existingNote = String(group.item?.itemNote || '').trim()
|
||||
const nextNote = [
|
||||
existingNote,
|
||||
...group.reasons.filter((reason) => existingNote !== reason && !existingNote.includes(reason))
|
||||
].filter(Boolean).join('\n')
|
||||
applyLocalExpenseItemPatch(itemId, {
|
||||
itemNote: nextNote
|
||||
})
|
||||
})
|
||||
detailNoteEditor.value = nextNote
|
||||
if (claimLevelRisks.length) {
|
||||
const appendix = claimLevelRisks.join('\n')
|
||||
const nextNote = mergeDetailNoteWithRiskOverride(appendix)
|
||||
if (nextNote.length > 500) {
|
||||
toast('附加说明最多 500 字,请精简风险原因后再继续提交。')
|
||||
return
|
||||
}
|
||||
await updateExpenseClaim(request.value.claimId, {
|
||||
reason: nextNote
|
||||
})
|
||||
detailNoteEditor.value = nextNote
|
||||
}
|
||||
riskOverrideDialogOpen.value = false
|
||||
submitConfirmDialogOpen.value = true
|
||||
toast('违规提交原因已写入附加说明。')
|
||||
toast('异常说明已保存,可继续提交审批。')
|
||||
emit('request-updated', { claimId: request.value.claimId })
|
||||
} catch (error) {
|
||||
toast(error?.message || '风险原因保存失败,请稍后重试。')
|
||||
toast(error?.message || '异常说明保存失败,请稍后重试。')
|
||||
} finally {
|
||||
riskOverrideBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmStandardAdjustment() {
|
||||
if (riskOverrideBusy.value) {
|
||||
return
|
||||
}
|
||||
riskOverrideBusy.value = true
|
||||
try {
|
||||
const payload = await buildStandardAdjustmentPayload()
|
||||
if (!payload.risks.length) {
|
||||
toast('当前风险暂未匹配到可重算的费用明细,请先补充异常说明。')
|
||||
return
|
||||
}
|
||||
const response = await acceptExpenseClaimStandardAdjustment(request.value.claimId, payload)
|
||||
applyStandardAdjustmentResponse(response)
|
||||
riskOverrideDialogOpen.value = false
|
||||
submitConfirmDialogOpen.value = true
|
||||
toast('已按职级最高报销标准重算实际报销金额。')
|
||||
emit('request-updated', { claimId: request.value.claimId })
|
||||
} catch (error) {
|
||||
toast(error?.message || '按职级标准重算失败,请稍后重试。')
|
||||
} finally {
|
||||
riskOverrideBusy.value = false
|
||||
}
|
||||
@@ -1809,6 +1940,10 @@ export default {
|
||||
}
|
||||
|
||||
populateExpenseEditor(item)
|
||||
void nextTick(() => {
|
||||
const textarea = document.querySelector('.risk-note-editor-textarea')
|
||||
resizeExpenseNoteInput({ target: textarea })
|
||||
})
|
||||
}
|
||||
|
||||
function validateExpenseEditor() {
|
||||
@@ -2237,6 +2372,11 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (submitRiskWarnings.value.length) {
|
||||
openRiskOverrideDialog()
|
||||
return
|
||||
}
|
||||
|
||||
submitConfirmDialogOpen.value = true
|
||||
}
|
||||
|
||||
@@ -2540,7 +2680,7 @@ export default {
|
||||
closeApproveConfirmDialog, closeDeleteDialog, closeAttachmentPreview, closePayConfirmDialog, closeSubmitConfirmDialog,
|
||||
closeRiskOverrideDialog, closeSmartEntryUploadDialog,
|
||||
closeReturnDialog, confirmApproveRequest, confirmDeleteRequest, confirmSubmitRequest, confirmReturnRequest,
|
||||
confirmPayRequest, confirmRiskOverrideReasons, confirmSmartEntryUpload,
|
||||
confirmPayRequest, confirmRiskOverrideReasons, confirmStandardAdjustment, confirmSmartEntryUpload,
|
||||
chooseSmartEntryFile, clearSmartEntryFile,
|
||||
currentAttachmentPreviewInsight, currentAttachmentPreviewRiskCards, currentProgressRingMotion,
|
||||
currentSubmitRiskWarning,
|
||||
@@ -2562,7 +2702,7 @@ export default {
|
||||
payBusy, payConfirmDialogOpen, profile, progressSteps, request, leaderOpinion, removeExpenseAttachment, removeExpenseItem,
|
||||
hasLeaderApprovalEvents, hasSingleLeaderApprovalEvent, leaderApprovalEvents, leaderApprovalReadonlyMeta,
|
||||
resolveExpenseRiskIndicatorTitle,
|
||||
resetDetailNote, resolveAttachmentDisplayName, resolveAttachmentPreviewTitle, resolveAttachmentRecognition,
|
||||
resetDetailNote, resizeExpenseNoteInput, resolveAttachmentDisplayName, resolveAttachmentPreviewTitle, resolveAttachmentRecognition,
|
||||
resolveExpenseReasonHelper, resolveExpenseReasonPlaceholder, resolveExpenseRiskState, resolveExpenseIssues,
|
||||
resolveRiskCardDomId, isHighlightedRiskCard,
|
||||
returnBusy, returnDialogDescription, returnDialogOpen, riskOverrideBusy, riskOverrideDialogOpen, riskOverrideIndexLabel,
|
||||
@@ -2574,7 +2714,7 @@ export default {
|
||||
showAiAdvicePanel, showApplicationLeaderOpinion,
|
||||
showBudgetAnalysis, showStageRiskAdvice,
|
||||
showExpenseRisk, startExpenseEdit, submitActionIcon, submitActionLabel, submitBusy,
|
||||
submitConfirmDescription, submitConfirmDialogOpen, submitConfirmText, submitRiskWarnings,
|
||||
submitConfirmAmountDisplay, submitConfirmDescription, submitConfirmDialogOpen, submitConfirmText, submitRiskWarnings,
|
||||
triggerExpenseUpload, uploadedExpenseCount, uploadingExpenseId, saveExpenseEdit
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user