import { EXPENSE_TYPE_LABELS, formatAmountDisplay } from './travelReimbursementReviewModel.js' export const EXPENSE_QUERY_PAGE_SIZE = 5 export const EXPENSE_CENTER_HREF = '/app/documents' export const ASSOCIATABLE_CLAIM_STATUSES = new Set(['draft', 'supplement', 'returned']) const EXPENSE_STATUS_LABELS = { draft: '草稿', supplement: '待补充', returned: '已退回', submitted: '已提交', review: '审批中', approved: '已审核', pending_payment: '待付款', paid: '已付款' } const EXPENSE_RISK_LEVEL_LABELS = { high: '高风险', medium: '中风险', warning: '中风险', low: '低风险', info: '提示' } export function normalizeExpenseQueryRiskItem(item, index = 0) { if (!item || typeof item !== 'object') { return null } const rawLevel = String(item.level || item.severity || '').trim().toLowerCase() const level = EXPENSE_RISK_LEVEL_LABELS[rawLevel] ? rawLevel : 'medium' const summary = String(item.summary || item.message || item.content || '').trim() const detail = String(item.detail || item.description || summary).trim() if (!summary && !detail) { return null } return { key: String(item.key || `${level}-${index}`).trim() || `${level}-${index}`, level, levelLabel: String(item.level_label || item.levelLabel || EXPENSE_RISK_LEVEL_LABELS[level]).trim() || EXPENSE_RISK_LEVEL_LABELS[level], title: String(item.title || item.label || EXPENSE_RISK_LEVEL_LABELS[level]).trim() || EXPENSE_RISK_LEVEL_LABELS[level], summary: summary || detail, detail: detail || summary } } 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(), riskItems: (Array.isArray(item.risk_flags) ? item.risk_flags : []) .map((riskItem, index) => normalizeExpenseQueryRiskItem(riskItem, index)) .filter(Boolean), 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 (normalized === 'pending_payment') { return { key: 'pending_payment', label: '待付款' } } if (normalized === 'paid') { return { key: 'completed', label: '已付款' } } if (['approved'].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 rawPreviewLimit = Number(payload.preview_limit || EXPENSE_QUERY_PAGE_SIZE) 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, previewLimit: Number.isFinite(rawPreviewLimit) ? Math.max(1, rawPreviewLimit) : EXPENSE_QUERY_PAGE_SIZE, 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 previewLimit = Math.max(1, Number(queryPayload.previewLimit || EXPENSE_QUERY_PAGE_SIZE)) const totalCount = Math.max(0, Number(queryPayload.recordCount || 0)) if (totalCount > previewLimit) { parts.push(`我只会筛选出最近的 ${previewLimit} 条记录;如果想查询全部的单据,请点击 [**这里**](${EXPENSE_CENTER_HREF}) 跳转到单据中心查看。`) } else if (totalCount > 0) { parts.push(`当前已展示本次筛选命中的全部记录;如果想进入单据中心继续筛选,请点击 [**这里**](${EXPENSE_CENTER_HREF})。`) } return parts.join('。') }