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 }