import { countClaimRisks, resolveArchiveRiskTone } from './archiveCenterListFilters.js' import { isNewDocument } from './documentCenterNewState.js' import { isArchivedDocumentRow } from './documentCenterRows.js' import { sortDocumentRowsByLatestTime } from './documentCenterSort.js' import { extractDateText, formatDocumentListTime, resolveDocumentSortTime, resolveDocumentStayTimeDisplay } from './documentCenterTime.js' import { normalizeRequestForUi } from './requestViewModel.js' export const DOCUMENT_TYPE_ALL = 'all' export const DOCUMENT_TYPE_APPLICATION = 'application' export const DOCUMENT_TYPE_REIMBURSEMENT = 'reimbursement' export const SCENE_ALL = 'all' export const DOCUMENT_SCOPE_ALL = '全部' export const DOCUMENT_SCOPE_APPLICATION = '申请单' export const DOCUMENT_SCOPE_REIMBURSEMENT = '报销单' export const DOCUMENT_SCOPE_REVIEW = '审核单' export const DOCUMENT_SCOPE_ARCHIVE = '归档' export const scopeTabs = [ DOCUMENT_SCOPE_ALL, DOCUMENT_SCOPE_APPLICATION, DOCUMENT_SCOPE_REIMBURSEMENT, DOCUMENT_SCOPE_REVIEW, DOCUMENT_SCOPE_ARCHIVE ] export const DOCUMENT_LOADING_MIN_VISIBLE_MS = 720 export const DOCUMENT_CENTER_QUERY_KEYS = new Set([ 'dc_page', 'dc_page_size', 'dc_scope', 'dc_status', 'dc_doc_type', 'dc_scene', 'dc_q', 'dc_start', 'dc_end' ]) export const riskLevelTabs = ['全部', '高风险', '中风险', '低风险', '无风险'] export const RISK_TONE_META = { high: { label: '高风险', tone: 'high' }, medium: { label: '中风险', tone: 'medium' }, low: { label: '低风险', tone: 'low' }, none: { label: '无风险', tone: 'none' } } export const FILTER_CONFIG_BY_SCOPE = { [DOCUMENT_SCOPE_ALL]: { searchPlaceholder: '搜索单号、事项、费用场景...', sceneFallbackLabel: '单据场景', dateLabel: '单据时间', statusTitle: '风险等级', statusTabs: riskLevelTabs, showDocumentType: true }, [DOCUMENT_SCOPE_APPLICATION]: { searchPlaceholder: '搜索申请单号、申请事项、申请场景...', sceneFallbackLabel: '申请场景', dateLabel: '申请时间', statusTitle: '风险等级', statusTabs: riskLevelTabs, showDocumentType: false }, [DOCUMENT_SCOPE_REIMBURSEMENT]: { searchPlaceholder: '搜索报销单号、报销事由、费用场景...', sceneFallbackLabel: '费用场景', dateLabel: '报销时间', statusTitle: '风险等级', statusTabs: riskLevelTabs, showDocumentType: false }, [DOCUMENT_SCOPE_REVIEW]: { searchPlaceholder: '搜索审核单号、事项、当前环节...', sceneFallbackLabel: '审核场景', dateLabel: '审核时间', statusTitle: '风险等级', statusTabs: riskLevelTabs, showDocumentType: false }, [DOCUMENT_SCOPE_ARCHIVE]: { searchPlaceholder: '搜索归档单号、事项、费用场景...', sceneFallbackLabel: '归档场景', dateLabel: '归档时间', statusTitle: '风险等级', statusTabs: riskLevelTabs, showDocumentType: false } } export const pageSizeOptions = [10, 20, 50].map((size) => ({ label: `${size} 条/页`, value: size })) export const pageSizeValues = pageSizeOptions.map((item) => item.value) export const documentTypeOptions = [ { value: DOCUMENT_TYPE_ALL, label: '单据类型' }, { value: DOCUMENT_TYPE_APPLICATION, label: '申请单' }, { value: DOCUMENT_TYPE_REIMBURSEMENT, label: '报销单' } ] export function routeQueryEquals(left, right) { const leftEntries = Object.entries(left || {}).map(([key, value]) => [ key, Array.isArray(value) ? value.join(',') : String(value ?? '') ]) const rightEntries = Object.entries(right || {}).map(([key, value]) => [ key, Array.isArray(value) ? value.join(',') : String(value ?? '') ]) if (leftEntries.length !== rightEntries.length) return false const rightMap = new Map(rightEntries) return leftEntries.every(([key, value]) => rightMap.get(key) === value) } export function buildDocumentRow(request, options = {}) { const normalized = normalizeRequestForUi(request) if (!normalized) { return null } const archived = Boolean(options.archived) const source = options.source || 'owned' const statusGroup = resolveStatusGroup(normalized, archived) const statusLabel = archived ? resolveArchivedStatusLabel(normalized) : resolveStatusLabel(normalized, statusGroup) const riskMeta = buildDocumentRiskMeta(normalized, options.currentUser) const documentNo = normalized.documentNo || normalized.id || normalized.claimId || '待生成' const claimId = normalized.claimId || normalized.id || documentNo const createdAtSource = normalized.createdAt || normalized.submittedAt || normalized.applyTime || normalized.updatedAt const updatedAtSource = normalized.updatedAt || normalized.submittedAt || normalized.createdAt || normalized.applyTime const createdSortTime = resolveDocumentSortTime(createdAtSource) const updatedSortTime = resolveDocumentSortTime(updatedAtSource) const documentTypeCode = normalized.documentTypeCode || DOCUMENT_TYPE_REIMBURSEMENT const documentTypeLabel = normalized.documentTypeLabel || (documentTypeCode === DOCUMENT_TYPE_APPLICATION ? '申请单' : '报销单') const initiatorName = String( normalized.person || normalized.employeeName || normalized.profileName || normalized.applicant || request?.employee_name || request?.employeeName || request?.person || '' ).trim() || '待补充' return { ...normalized, rawRequest: request, documentKey: `${source}:${claimId || documentNo}`, documentTypeCode, documentTypeLabel, claimId, documentNo, initiatorName, node: archived ? resolveArchivedDocumentNode(normalized, documentTypeCode) : (normalized.node || normalized.workflowNode || '待提交'), statusGroup, statusLabel, statusTone: archived ? 'archived' : resolveStatusTone(normalized, statusGroup), riskTone: riskMeta.tone, riskLabel: riskMeta.label, riskCount: riskMeta.count, riskTags: riskMeta.tags, source, archived, createdAtDisplay: formatDocumentListTime(createdAtSource), stayTimeDisplay: resolveDocumentStayTimeDisplay(normalized), isNewDocument: archived ? false : isNewDocument({ ...normalized, source, claimId, documentNo }, options.viewedDocumentKeys || []), updatedAtDisplay: formatDocumentListTime(updatedAtSource), createdSortTime, updatedSortTime, sortTime: Math.max(createdSortTime, updatedSortTime) } } export function buildDocumentRiskMeta(row, currentUser = null) { const riskFlags = resolveDocumentRiskFlags(row) const riskSummary = row?.riskSummary || row?.risk // 列表风险标签按当前查看者可见性过滤,与详情页口径一致。 const viewerOptions = currentUser ? { request: row || {}, currentUser } : null const count = countClaimRisks(riskFlags, riskSummary, viewerOptions) if (!count) { const meta = RISK_TONE_META.none return { ...meta, count: 0, tags: [{ ...meta }] } } const tone = resolveArchiveRiskTone(riskFlags, riskSummary, viewerOptions) const meta = RISK_TONE_META[tone] || RISK_TONE_META.medium return { ...meta, count, tags: [{ tone: meta.tone, label: `${meta.label} ${count}项` }] } } export function filterDocumentRows(rows, filters = {}) { const keyword = String(filters.keyword || '').trim().toLowerCase() return sortDocumentRowsByLatestTime((rows || []).filter((row) => { const matchesKeyword = !keyword || [ row.documentNo, row.documentTypeLabel, row.typeLabel, row.initiatorName, row.reason, row.node, row.statusLabel, row.riskLabel ].filter(Boolean).join('').toLowerCase().includes(keyword) const matchesDocumentType = !filters.showDocumentTypeFilter || filters.activeDocumentType === DOCUMENT_TYPE_ALL || row.documentTypeCode === filters.activeDocumentType const matchesScene = filters.activeScene === SCENE_ALL || row.typeCode === filters.activeScene const matchesRiskLevel = matchesRiskLevelTab(row, filters.activeStatusTab, filters.activeScopeTab) const matchesDateRange = matchesAppliedDateRange(row, filters.appliedStart, filters.appliedEnd) return matchesKeyword && matchesDocumentType && matchesScene && matchesRiskLevel && matchesDateRange })) } export function matchesRiskLevelTab(row, tab, activeScopeTab = DOCUMENT_SCOPE_ALL) { if (activeScopeTab !== DOCUMENT_SCOPE_ARCHIVE && isArchivedDocumentRow(row)) { return false } if (tab === '全部') return true if (tab === '高风险') return row.riskTone === 'high' if (tab === '中风险') return row.riskTone === 'medium' if (tab === '低风险') return row.riskTone === 'low' if (tab === '无风险') return row.riskTone === 'none' return true } export function matchesAppliedDateRange(row, start, end) { if (!start || !end) { return true } const date = extractDateText(row.updatedAt || row.submittedAt || row.createdAt || row.applyTime) return Boolean(date) && date >= start && date <= end } export function mergeDocumentRows(rows) { const rowMap = new Map() rows.filter(Boolean).forEach((row) => { const key = row.claimId || row.documentNo || row.documentKey const current = rowMap.get(key) if (!current || resolveSourcePriority(row) >= resolveSourcePriority(current)) { rowMap.set(key, row) } }) return sortDocumentRowsByLatestTime(Array.from(rowMap.values())) } export function hasDocumentCenterActiveFilters(filters = {}) { return Boolean( String(filters.listKeyword || '').trim() || filters.activeStatusTab !== '全部' || (filters.showDocumentTypeFilter && filters.activeDocumentType !== DOCUMENT_TYPE_ALL) || filters.activeScene !== SCENE_ALL || filters.appliedStart || filters.appliedEnd ) } export function buildDocumentCenterEmptyState(options = {}) { const filtered = Boolean(options.hasActiveFilters) const activeScopeTab = options.activeScopeTab || DOCUMENT_SCOPE_ALL if ( activeScopeTab === DOCUMENT_SCOPE_APPLICATION || options.activeDocumentType === DOCUMENT_TYPE_APPLICATION ) { return { eyebrow: '申请单', title: '当前还没有申请单数据', desc: '费用申请功能接入后,差旅、会务、办公采购等前置申请会统一汇总到这里。', icon: 'mdi mdi-file-sign-outline', actionLabel: '', actionIcon: '', tone: 'theme', artLabel: 'APPLY', tips: ['申请、报销、审批与归档统一在此查看', '申请批准后可继续发起报销'] } } return { eyebrow: filtered ? '筛选结果为空' : '单据中心', title: filtered ? '没有符合当前条件的单据' : `“${activeScopeTab}”里暂时没有单据`, desc: filtered ? '可以清空当前分类下的筛选条件后再看看。' : '当前视角暂无可展示单据,可以切换其他视角或发起一笔报销。', icon: filtered ? 'mdi mdi-magnify-scan' : 'mdi mdi-file-document-multiple-outline', actionLabel: '', actionIcon: '', tone: 'theme', artLabel: filtered ? 'FILTER' : 'DOCS', tips: ['单据中心已接入当前报销单据', '归档视角会同步已归档数据'] } } function resolveArchivedDocumentNode(normalized, documentTypeCode) { if (documentTypeCode === DOCUMENT_TYPE_APPLICATION) { return '申请归档' } if (normalized.status === 'paid' || normalized.approvalStatus === '已付款') { return '已付款' } return normalized.node || normalized.workflowNode || '财务归档' } function resolveArchivedStatusLabel(normalized) { if (normalized.status === 'paid' || normalized.approvalStatus === '已付款' || normalized.node === '已付款') { return '已付款' } return '已归档' } function resolveStatusGroup(row, archived) { if (archived) return 'completed' if (row.approvalKey === 'draft') return 'draft' if (row.approvalKey === 'supplement' && row.status === 'returned') return 'pending_submit' if (row.approvalKey === 'supplement') return 'supplement' if (row.approvalKey === 'pending_payment') return 'pending_payment' if (row.approvalKey === 'in_progress') return 'in_progress' if (row.approvalKey === 'completed') return 'completed' return 'other' } function resolveStatusLabel(row, statusGroup) { if (statusGroup === 'pending_submit') return '待提交' if (statusGroup === 'pending_payment') return '待付款' return row.approval || row.approvalStatus || '处理中' } function resolveStatusTone(row, statusGroup) { if (statusGroup === 'pending_submit') return 'warning' return row.approvalTone || 'neutral' } function resolveDocumentRiskFlags(row) { if (Array.isArray(row?.riskFlags)) { return row.riskFlags } if (Array.isArray(row?.risk_flags_json)) { return row.risk_flags_json } return [] } function resolveSourcePriority(row) { if (row.archived) return 3 if (row.source === 'approval') return 2 return 1 }