2026-06-22 11:58:53 +08:00
|
|
|
|
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 [
|
2026-06-24 10:42:50 +08:00
|
|
|
|
'我先检查了您的申请时间,发现同一天或重叠日期已经存在差旅申请,不能重复创建。',
|
2026-06-22 11:58:53 +08:00
|
|
|
|
'',
|
|
|
|
|
|
'已有申请:',
|
|
|
|
|
|
`- **单号**:${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
|
|
|
|
|
|
}
|