import { EXPENSE_TYPE_LABELS, formatAmountDisplay } from './travelReimbursementReviewModel.js' export const EXPENSE_QUERY_PAGE_SIZE = 5 export const ASSOCIATABLE_CLAIM_STATUSES = new Set(['draft', 'supplement', 'returned']) const EXPENSE_STATUS_LABELS = { draft: '草稿', supplement: '待补充', returned: '已退回', submitted: '已提交', review: '审批中', approved: '已审核', paid: '已入账' } export function normalizeExpenseQueryStatusGroup(item) { if (!item || typeof item !== 'object') { return null } const rawCount = Number(item.count || 0) return { key: String(item.key || 'other').trim() || 'other', label: String(item.label || '其他状态').trim() || '其他状态', count: Number.isFinite(rawCount) ? Math.max(0, rawCount) : 0 } } export function normalizeExpenseQueryRecord(item) { if (!item || typeof item !== 'object') { return null } const amount = Number(item.amount || 0) const amountValue = Number.isFinite(amount) ? amount : 0 const expenseTypeLabel = String(item.expense_type_label || item.expense_type || '报销').trim() || '报销' const reason = String(item.reason || '').trim() const documentDate = String(item.document_date || '').trim() const occurredAt = String(item.occurred_at || '').trim() return { claimId: String(item.claim_id || '').trim(), claimNo: String(item.claim_no || '').trim() || '未编号', employeeName: String(item.employee_name || '').trim(), expenseType: String(item.expense_type || '').trim(), expenseTypeLabel, amount: amountValue, amountDisplay: formatAmountDisplay(amountValue), status: String(item.status || '').trim(), statusLabel: String(item.status_label || '处理中').trim() || '处理中', statusGroup: String(item.status_group || 'other').trim() || 'other', statusGroupLabel: String(item.status_group_label || '其他状态').trim() || '其他状态', approvalStage: String(item.approval_stage || '').trim(), documentDate, occurredAt, reason, location: String(item.location || '').trim(), summary: reason || `${expenseTypeLabel}报销`, dateDisplay: documentDate || occurredAt || '待补充日期' } } export function resolveExpenseStatusGroup(status) { const normalized = String(status || '').trim() if (['draft', 'supplement', 'returned'].includes(normalized)) { return { key: 'draft', label: normalized === 'draft' ? '草稿' : '待完善' } } if (['submitted', 'review'].includes(normalized)) { return { key: 'in_progress', label: '审批中' } } if (['approved', 'paid'].includes(normalized)) { return { key: 'completed', label: '已完成' } } return { key: 'other', label: '其他状态' } } export function formatQueryRecordDate(value) { const text = String(value || '').trim() if (!text) return '' return text.includes('T') ? text.split('T')[0] : text.slice(0, 10) } export function buildQueryRecordFromClaim(claim) { if (!claim || typeof claim !== 'object') { return null } const claimId = String(claim.id || claim.claim_id || '').trim() if (!claimId) { return null } const status = String(claim.status || '').trim() const statusGroup = resolveExpenseStatusGroup(status) return { claim_id: claimId, claim_no: String(claim.claim_no || claim.claimNo || '').trim() || '未编号', employee_name: String(claim.employee_name || claim.employeeName || '').trim(), expense_type: String(claim.expense_type || claim.expenseType || '').trim(), expense_type_label: EXPENSE_TYPE_LABELS[String(claim.expense_type || claim.expenseType || '').trim()] || String(claim.expense_type || claim.expenseType || '报销').trim(), amount: Number(claim.amount || 0), status, status_label: EXPENSE_STATUS_LABELS[status] || statusGroup.label, status_group: statusGroup.key, status_group_label: statusGroup.label, approval_stage: String(claim.approval_stage || claim.approvalStage || '').trim(), document_date: formatQueryRecordDate(claim.submitted_at || claim.submittedAt || claim.created_at || claim.createdAt || claim.occurred_at || claim.occurredAt), occurred_at: formatQueryRecordDate(claim.occurred_at || claim.occurredAt), reason: String(claim.reason || '').trim(), location: String(claim.location || '').trim() } } export function buildDraftAssociationQueryPayload(claims) { const records = (Array.isArray(claims) ? claims : []) .filter((claim) => ASSOCIATABLE_CLAIM_STATUSES.has(String(claim?.status || '').trim())) .map(buildQueryRecordFromClaim) .filter(Boolean) const statusGroups = records.reduce((groups, record) => { const key = String(record.status_group || 'other') const existing = groups.get(key) || { key, label: String(record.status_group_label || '其他状态'), count: 0 } existing.count += 1 groups.set(key, existing) return groups }, new Map()) return normalizeExpenseQueryPayload({ result_type: 'expense_claim_list', title: '选择关联草稿', scope_label: '可关联草稿', selection_mode: 'draft_association', empty_text: '当前没有可关联的草稿单据。', recent_window_applied: false, record_count: records.length, preview_count: records.length, total_amount: records.reduce((sum, record) => sum + Number(record.amount || 0), 0), status_groups: Array.from(statusGroups.values()), records }) } export function normalizeExpenseQueryPayload(payload) { if (!payload || typeof payload !== 'object') { return null } const resultType = String(payload.result_type || '').trim() if (resultType && resultType !== 'expense_claim_list') { return null } const records = (Array.isArray(payload.records) ? payload.records : []) .map(normalizeExpenseQueryRecord) .filter(Boolean) const statusGroups = (Array.isArray(payload.status_groups) ? payload.status_groups : []) .map(normalizeExpenseQueryStatusGroup) .filter(Boolean) const rawRecordCount = Number(payload.record_count || 0) const rawPreviewCount = Number(payload.preview_count || records.length) const rawOlderRecordCount = Number(payload.older_record_count || 0) const totalAmount = Number(payload.total_amount || 0) const rawWindowDays = Number(payload.window_days || 0) const windowStartDate = String(payload.window_start_date || '').trim() const windowEndDate = String(payload.window_end_date || '').trim() return { resultType: 'expense_claim_list', scopeLabel: String(payload.scope_label || '报销单').trim() || '报销单', selectionMode: String(payload.selection_mode || payload.selectionMode || '').trim(), selectionLocked: Boolean(payload.selection_locked || payload.selectionLocked), selectedClaimId: String(payload.selected_claim_id || payload.selectedClaimId || '').trim(), title: String(payload.title || '').trim(), emptyText: String(payload.empty_text || payload.emptyText || '').trim(), recentWindowApplied: Boolean(payload.recent_window_applied), windowDays: payload.window_days === null || payload.window_days === undefined || payload.window_days === '' ? null : (Number.isFinite(rawWindowDays) ? Math.max(1, rawWindowDays) : null), windowStartDate: windowStartDate || '', windowEndDate: windowEndDate || '', recordCount: Number.isFinite(rawRecordCount) ? Math.max(0, rawRecordCount) : 0, previewCount: Number.isFinite(rawPreviewCount) ? Math.max(0, rawPreviewCount) : records.length, olderRecordCount: Number.isFinite(rawOlderRecordCount) ? Math.max(0, rawOlderRecordCount) : 0, hasMoreInWindow: Boolean(payload.has_more_in_window || payload.has_more), totalAmount: Number.isFinite(totalAmount) ? totalAmount : 0, statusGroups, records, currentPage: 1 } } export function buildExpenseQueryWindowLabel(queryPayload) { if (!queryPayload) { return '' } if (queryPayload.selectionMode === 'draft_association') { return '先选择要关联的草稿,确认后我再识别附件并归集到该单据。' } if (queryPayload.windowStartDate && queryPayload.windowEndDate) { return `${queryPayload.windowStartDate} 至 ${queryPayload.windowEndDate}` } if (queryPayload.recentWindowApplied && queryPayload.windowDays) { return `近 ${queryPayload.windowDays} 日内` } return '当前条件下' } export function getExpenseQueryTotalPages(queryPayload) { const recordCount = Array.isArray(queryPayload?.records) ? queryPayload.records.length : 0 return Math.max(1, Math.ceil(recordCount / EXPENSE_QUERY_PAGE_SIZE)) } export function getExpenseQueryActivePage(queryPayload) { const totalPages = getExpenseQueryTotalPages(queryPayload) const rawPage = Number(queryPayload?.currentPage || 1) if (!Number.isFinite(rawPage)) { return 1 } return Math.min(Math.max(1, Math.round(rawPage)), totalPages) } export function getExpenseQueryVisibleRecords(queryPayload) { const records = Array.isArray(queryPayload?.records) ? queryPayload.records : [] const activePage = getExpenseQueryActivePage(queryPayload) const start = (activePage - 1) * EXPENSE_QUERY_PAGE_SIZE return records.slice(start, start + EXPENSE_QUERY_PAGE_SIZE) } export function buildExpenseQueryHint(queryPayload) { if (!queryPayload) { return '' } if (queryPayload.selectionMode === 'draft_association') { if (queryPayload.selectionLocked && queryPayload.selectedClaimId) { return '已选择关联草稿,附件将按该单据继续识别和归集。' } return '如果这些都不是本次要关联的单据,可以补充单号或先到个人报销列表新建草稿。' } const parts = [] const windowText = buildExpenseQueryWindowLabel(queryPayload) if (Array.isArray(queryPayload.records) && queryPayload.records.length > EXPENSE_QUERY_PAGE_SIZE) { parts.push(`当前共整理 ${queryPayload.records.length} 笔单据,可左右切换查看`) } if (queryPayload.hasMoreInWindow && queryPayload.previewCount < queryPayload.recordCount) { parts.push(`${windowText}共 ${queryPayload.recordCount} 笔,当前先整理最近 ${queryPayload.previewCount} 笔`) } if (queryPayload.olderRecordCount > 0 && queryPayload.windowDays) { parts.push(`另有 ${queryPayload.olderRecordCount} 笔超过 ${queryPayload.windowDays} 日的单据,请前往个人报销中心查看`) } return parts.join('。') }