- 完善 user_agent_application 申请差旅报销预审槽位与消息组装 - 增强预算助理报告与风险建议卡片交互 - 重构登录页视觉样式与移动端响应式适配 - 优化个人工作台、文档中心、政策中心、员工管理等页面布局 - 拆分 travelRequestDetailPreReviewModel 为 advice/submit 模型 - 补充报销草稿、风险复核、Item Sync 与模板执行器测试覆盖
120 lines
4.1 KiB
JavaScript
120 lines
4.1 KiB
JavaScript
function normalizeText(value) {
|
|
return String(value || '').trim()
|
|
}
|
|
|
|
function uniqueTexts(values) {
|
|
return [...new Set(values.map((item) => normalizeText(item)).filter(Boolean))]
|
|
}
|
|
|
|
function isPlaceholderValue(value) {
|
|
const text = normalizeText(value)
|
|
if (!text) {
|
|
return true
|
|
}
|
|
return ['待补充', '暂无', '无', '未知', '处理中'].includes(text.replace(/\s+/g, ''))
|
|
}
|
|
|
|
function isApplicationDocumentRequest(requestModel) {
|
|
const documentType = normalizeText(
|
|
requestModel?.documentTypeCode
|
|
|| requestModel?.document_type_code
|
|
|| requestModel?.documentType
|
|
|| requestModel?.document_type
|
|
).toLowerCase()
|
|
const claimNo = normalizeText(requestModel?.claimNo || requestModel?.claim_no || requestModel?.documentNo).toUpperCase()
|
|
return documentType === 'application' || claimNo.startsWith('AP-') || claimNo.startsWith('APP-')
|
|
}
|
|
|
|
function isHotelExpenseItem(item) {
|
|
const text = [
|
|
item?.itemType,
|
|
item?.typeCode,
|
|
item?.name,
|
|
item?.category,
|
|
item?.desc,
|
|
item?.itemReason
|
|
].map((value) => normalizeText(value)).join(' ')
|
|
return /hotel_ticket|hotel|住宿|酒店|水单/.test(text)
|
|
}
|
|
|
|
export function buildTravelReceiptMaterialPrompts(requestModel, items) {
|
|
if (isApplicationDocumentRequest(requestModel)) {
|
|
return []
|
|
}
|
|
|
|
const normalizedItems = Array.isArray(items) ? items : []
|
|
const missingHotelItems = normalizedItems.filter(
|
|
(item) => !item?.isSystemGenerated && isHotelExpenseItem(item) && isPlaceholderValue(item.invoiceId)
|
|
)
|
|
if (!missingHotelItems.length) {
|
|
return []
|
|
}
|
|
|
|
return [
|
|
`当前包含 ${missingHotelItems.length} 条住宿费用明细,但暂未关联住宿发票或酒店水单。请补充住宿材料,避免后续被退回补件。`
|
|
]
|
|
}
|
|
|
|
function profileMetric(profile, key) {
|
|
const profiles = Array.isArray(profile?.profiles) ? profile.profiles : []
|
|
for (const item of profiles) {
|
|
const metrics = item?.metrics && typeof item.metrics === 'object' ? item.metrics : {}
|
|
const value = Number(metrics[key])
|
|
if (Number.isFinite(value) && value > 0) {
|
|
return value
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function profileReviewSuggestionTexts(profile) {
|
|
const suggestions = Array.isArray(profile?.review_suggestions)
|
|
? profile.review_suggestions
|
|
: Array.isArray(profile?.reviewSuggestions)
|
|
? profile.reviewSuggestions
|
|
: []
|
|
return suggestions
|
|
.map((item) => normalizeText(item?.message || item?.title || item?.label))
|
|
.filter(Boolean)
|
|
}
|
|
|
|
function profileRiskTagTexts(profile) {
|
|
const tags = Array.isArray(profile?.profile_tags)
|
|
? profile.profile_tags
|
|
: Array.isArray(profile?.profileTags)
|
|
? profile.profileTags
|
|
: []
|
|
return tags
|
|
.filter((tag) => normalizeText(tag?.polarity) === 'risk')
|
|
.map((tag) => normalizeText(tag?.reason || tag?.display_label || tag?.label))
|
|
.filter(Boolean)
|
|
}
|
|
|
|
export function buildEmployeeProfileAdviceItems(profile) {
|
|
if (!profile || typeof profile !== 'object') {
|
|
return []
|
|
}
|
|
|
|
const returnCount = profileMetric(profile, 'return_count')
|
|
const missingAttachmentCount = profileMetric(profile, 'missing_attachment_count')
|
|
const invoiceMismatchCount = profileMetric(profile, 'invoice_mismatch_count')
|
|
const missingContextCount = profileMetric(profile, 'missing_business_context_count')
|
|
const items = []
|
|
|
|
if (returnCount > 0) {
|
|
items.push(`历史退单建议:近 90 天存在 ${returnCount} 次退单或退回记录,提交前重点复核退回原因对应的票据、事由和说明,避免重复被退。`)
|
|
}
|
|
if (missingAttachmentCount > 0 || missingContextCount > 0) {
|
|
items.push(`材料完整性建议:历史材料或业务上下文缺失累计 ${missingAttachmentCount + missingContextCount} 项,本次提交前请重点核对附件、事由、地点和补充说明。`)
|
|
}
|
|
if (invoiceMismatchCount > 0) {
|
|
items.push(`票据一致性建议:历史存在 ${invoiceMismatchCount} 次票据不一致记录,本次请重点核对票据日期、城市、金额和费用明细。`)
|
|
}
|
|
|
|
return uniqueTexts([
|
|
...items,
|
|
...profileReviewSuggestionTexts(profile),
|
|
...profileRiskTagTexts(profile)
|
|
]).slice(0, 4)
|
|
}
|