- 新增 WorkbenchAiFilePreviewDialog 附件预览对话框及 useWorkbenchAiFilePreview,附件支持点击预览 - 新增 attachmentAssociationJobs/linkedReimbursementDraftJobs 前端服务与对应 composable,接入后台任务轮询与状态展示 - 新增 travelReimbursementDraftBranchModel 草稿分支模型,报销关联门控支持跳过/选择草稿 - PersonalWorkbenchAiMode 及各 composable(expense/document/steward/application-preview/attachment-association)重构适配,WorkbenchAiComposer/FileStrip 样式与交互完善 - DocumentsCenter/ReceiptFolder/TravelReimbursementCreate 等视图及 scripts 重构,风险/差旅规划/审批等工具适配 - 新增/更新前端测试:application-result-card、reimbursement-list-preview-fetch、guided-flow、composer-components 等
191 lines
6.5 KiB
JavaScript
191 lines
6.5 KiB
JavaScript
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
|
||
import {
|
||
applicationDateRangesOverlap,
|
||
normalizeApplicationPreview,
|
||
resolveApplicationDateRange
|
||
} from '../../utils/expenseApplicationPreview.js'
|
||
import { APPLICATION_DUPLICATE_IGNORED_STATUSES } from './travelReimbursementSubmitConstants.js'
|
||
|
||
function normalizeClaimListPayload(payload) {
|
||
if (Array.isArray(payload)) {
|
||
return payload
|
||
}
|
||
return Array.isArray(payload?.items) ? payload.items : []
|
||
}
|
||
|
||
function normalizeClaimRiskFlags(claim) {
|
||
const flags = claim?.risk_flags_json || claim?.riskFlagsJson || claim?.riskFlags || []
|
||
if (Array.isArray(flags)) {
|
||
return flags
|
||
}
|
||
return flags && typeof flags === 'object' ? [flags] : []
|
||
}
|
||
|
||
function extractApplicationDetailFromClaim(claim) {
|
||
return normalizeClaimRiskFlags(claim).reduce((found, item) => {
|
||
if (found || !item || typeof item !== 'object') {
|
||
return found
|
||
}
|
||
const detail = item.application_detail || item.applicationDetail
|
||
return detail && typeof detail === 'object' ? detail : null
|
||
}, null)
|
||
}
|
||
|
||
function isApplicationClaimRecord(claim) {
|
||
const expenseType = String(claim?.expense_type || claim?.expenseType || '').trim().toLowerCase()
|
||
const claimNo = String(claim?.claim_no || claim?.claimNo || '').trim().toUpperCase()
|
||
return (
|
||
expenseType === 'application' ||
|
||
expenseType === 'expense_application' ||
|
||
expenseType.endsWith('_application') ||
|
||
isApplicationDocumentNo(claimNo) ||
|
||
Boolean(extractApplicationDetailFromClaim(claim))
|
||
)
|
||
}
|
||
|
||
function normalizeApplicationExpenseType(value) {
|
||
const text = String(value || '').trim().toLowerCase()
|
||
if (!text) {
|
||
return ''
|
||
}
|
||
if (text === 'travel_application' || /差旅|出差/.test(text)) {
|
||
return 'travel_application'
|
||
}
|
||
if (text === 'purchase_application' || /采购/.test(text)) {
|
||
return 'purchase_application'
|
||
}
|
||
if (text === 'meeting_application' || /会务|会议/.test(text)) {
|
||
return 'meeting_application'
|
||
}
|
||
if (text === 'expense_application' || text === 'application' || text.endsWith('_application')) {
|
||
return text === 'application' ? 'expense_application' : text
|
||
}
|
||
return 'expense_application'
|
||
}
|
||
|
||
function resolveClaimApplicationExpenseType(claim) {
|
||
const detail = extractApplicationDetailFromClaim(claim) || {}
|
||
return normalizeApplicationExpenseType(
|
||
claim?.expense_type ||
|
||
claim?.expenseType ||
|
||
detail.application_type ||
|
||
detail.applicationType ||
|
||
''
|
||
)
|
||
}
|
||
|
||
function isIgnoredApplicationDuplicateStatus(status) {
|
||
return APPLICATION_DUPLICATE_IGNORED_STATUSES.has(String(status || '').trim().toLowerCase())
|
||
}
|
||
|
||
function resolveClaimApplicationDateRange(claim) {
|
||
const detail = extractApplicationDetailFromClaim(claim) || {}
|
||
return (
|
||
resolveApplicationDateRange(
|
||
detail.time ||
|
||
detail.time_range ||
|
||
detail.timeRange ||
|
||
detail.application_time ||
|
||
detail.applicationTime ||
|
||
detail.application_business_time ||
|
||
detail.applicationBusinessTime ||
|
||
detail.application_date ||
|
||
detail.applicationDate,
|
||
detail.days || detail.application_days || detail.applicationDays
|
||
) ||
|
||
resolveApplicationDateRange(claim?.occurred_at || claim?.occurredAt || '')
|
||
)
|
||
}
|
||
|
||
function formatApplicationDateRangeLabel(range) {
|
||
if (!range?.startDate) {
|
||
return '待确认'
|
||
}
|
||
return range.startDate === range.endDate ? range.startDate : `${range.startDate} 至 ${range.endDate}`
|
||
}
|
||
|
||
function findOverlappingApplicationClaim(applicationPreview, claimsPayload) {
|
||
const preview = normalizeApplicationPreview(applicationPreview)
|
||
const fields = preview.fields || {}
|
||
const currentRange = resolveApplicationDateRange(fields.time, fields.days)
|
||
if (!currentRange) {
|
||
return null
|
||
}
|
||
const currentExpenseType = normalizeApplicationExpenseType(fields.applicationType)
|
||
const claims = normalizeClaimListPayload(claimsPayload)
|
||
for (const claim of claims) {
|
||
if (!isApplicationClaimRecord(claim) || isIgnoredApplicationDuplicateStatus(claim?.status)) {
|
||
continue
|
||
}
|
||
const existingExpenseType = resolveClaimApplicationExpenseType(claim)
|
||
if (currentExpenseType && existingExpenseType && currentExpenseType !== existingExpenseType) {
|
||
continue
|
||
}
|
||
const existingRange = resolveClaimApplicationDateRange(claim)
|
||
if (!existingRange || !applicationDateRangesOverlap(currentRange, existingRange)) {
|
||
continue
|
||
}
|
||
return {
|
||
claim,
|
||
currentRange,
|
||
existingRange,
|
||
claimId: String(claim?.id || claim?.claim_id || claim?.claimId || '').trim(),
|
||
claimNo: String(claim?.claim_no || claim?.claimNo || claim?.id || '').trim(),
|
||
status: String(claim?.approval_stage || claim?.approvalStage || claim?.status || '').trim(),
|
||
reason: String(claim?.reason || '').trim(),
|
||
location: String(claim?.location || '').trim()
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
function buildApplicationDateConflictMessage(conflict) {
|
||
const claimNo = conflict?.claimNo || '已有申请'
|
||
return [
|
||
'我先检查了您的申请时间,发现同一天或重叠日期已经存在差旅申请,不能重复创建。',
|
||
'',
|
||
'已有申请:',
|
||
`- **单号**:${claimNo}`,
|
||
`- **申请时间**:${formatApplicationDateRangeLabel(conflict?.existingRange)}`,
|
||
conflict?.location ? `- **地点**:${conflict.location}` : '',
|
||
conflict?.reason ? `- **事由**:${conflict.reason}` : '',
|
||
`- **当前节点**:${conflict?.status || '处理中'}`,
|
||
'',
|
||
`本次识别时间:${formatApplicationDateRangeLabel(conflict?.currentRange)}`,
|
||
'',
|
||
'请先查看已有申请,或修改本次出差时间后再继续。'
|
||
].filter(Boolean).join('\n')
|
||
}
|
||
|
||
function buildApplicationDateConflictActions(conflict) {
|
||
const actions = []
|
||
if (conflict?.claimId) {
|
||
actions.push({
|
||
action_type: 'open_application_detail',
|
||
label: '查看已有申请',
|
||
description: conflict.claimNo ? `进入 ${conflict.claimNo} 单据详情。` : '进入已有申请单据详情。',
|
||
icon: 'mdi mdi-file-search-outline',
|
||
payload: {
|
||
claim_id: conflict.claimId,
|
||
claim_no: conflict.claimNo
|
||
}
|
||
})
|
||
}
|
||
actions.push({
|
||
action_type: 'prefill_composer',
|
||
label: '修改出差时间',
|
||
description: '在输入框中补充新的出差日期后继续。',
|
||
icon: 'mdi mdi-calendar-edit-outline',
|
||
payload: {
|
||
prompt_prefill: '修改出差时间为:'
|
||
}
|
||
})
|
||
return actions
|
||
}
|
||
|
||
export {
|
||
buildApplicationDateConflictActions,
|
||
buildApplicationDateConflictMessage,
|
||
findOverlappingApplicationClaim
|
||
}
|