import { computed, ref } from 'vue' import { ElDropdown, ElDropdownItem, ElDropdownMenu } from 'element-plus/es/components/dropdown/index.mjs' import EnterpriseListPage from '../../components/shared/EnterpriseListPage.vue' import { mapExpenseClaimToRequest } from '../../composables/useRequests.js' import { REIMBURSEMENT_LIST_PREVIEW_PARAMS, extractExpenseClaimItems, fetchArchivedExpenseClaims } from '../../services/reimbursements.js' import { ARCHIVE_FILTER_ALL, applyArchiveListFilters, buildArchiveMonthFilterOptions, buildDepartmentFilterOptions, buildTypeFilterOptions, countClaimRisks, extractArchiveMonth, formatArchiveMonthLabel, formatArchiveRiskCountLabel, hasActiveArchiveListFilters, resolveArchiveRiskTone } from '../../utils/archiveCenterListFilters.js' import { normalizeRequestForUi } from '../../utils/requestViewModel.js' import TravelRequestDetailView from '../TravelRequestDetailView.vue' const ARCHIVE_TAB_ALL = '全部归档' const ARCHIVE_TAB_APPLICATION = '申请归档' const ARCHIVE_TAB_REIMBURSEMENT = '报销归档' const ARCHIVE_TYPE_APPLICATION = '申请' const ARCHIVE_TYPE_APPLICATION_CODE = 'application' const ARCHIVE_TYPE_REIMBURSEMENT = '报销' const ARCHIVE_TYPE_REIMBURSEMENT_CODE = 'reimbursement' const tabs = [ARCHIVE_TAB_ALL, ARCHIVE_TAB_APPLICATION, ARCHIVE_TAB_REIMBURSEMENT] const RISK_FILTER_OPTIONS = [ { value: ARCHIVE_FILTER_ALL, label: '全部风险' }, { value: 'has', label: '有风险' }, { value: 'none', label: '无风险' }, { value: 'high', label: '高风险' }, { value: 'medium', label: '中风险' }, { value: 'low', label: '低风险' } ] function formatCurrency(value) { const amount = Number(value) return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY', minimumFractionDigits: 0, maximumFractionDigits: Number.isInteger(amount) ? 0 : 2 }).format(Number.isFinite(amount) ? amount : 0) } function buildArchiveRow(request) { const normalized = normalizeRequestForUi(request) const riskCount = countClaimRisks(normalized.riskFlags, normalized.riskSummary) const riskTone = riskCount > 0 ? resolveArchiveRiskTone(normalized.riskFlags, normalized.riskSummary) : 'none' const hasRisk = riskCount > 0 const isApplicationDocument = normalized.documentTypeCode === 'application' const archiveMonth = extractArchiveMonth( normalized.updatedAt, normalized.submittedAt, normalized.createdAt, normalized.occurredAt, normalized.applyTime ) return { ...normalized, applicant: normalized.person, avatar: String(normalized.person || '?').trim().slice(0, 1) || '?', department: normalized.dept, type: normalized.typeLabel, amount: formatCurrency(normalized.amountValue), time: normalized.applyTime, archivedAt: normalized.updatedAt || normalized.applyTime, archiveMonth, archiveMonthLabel: formatArchiveMonthLabel(archiveMonth), archiveType: isApplicationDocument ? ARCHIVE_TYPE_APPLICATION : ARCHIVE_TYPE_REIMBURSEMENT, archiveTypeCode: isApplicationDocument ? ARCHIVE_TYPE_APPLICATION_CODE : ARCHIVE_TYPE_REIMBURSEMENT_CODE, node: isApplicationDocument ? '申请归档' : (normalized.workflowNode || '已付款'), hasRisk, riskCount, risk: formatArchiveRiskCountLabel(riskCount), riskTone, status: '已归档', statusTone: 'archived', archiveTab: isApplicationDocument ? ARCHIVE_TAB_APPLICATION : ARCHIVE_TAB_REIMBURSEMENT } } function resolveFilterLabel(options, activeValue, fallbackLabel) { return options.find((item) => item.value === activeValue)?.label || fallbackLabel } export default { name: 'ArchiveCenterView', components: { ElDropdown, ElDropdownItem, ElDropdownMenu, EnterpriseListPage, TravelRequestDetailView }, setup() { const activeTab = ref(ARCHIVE_TAB_ALL) const activeRiskFilter = ref(ARCHIVE_FILTER_ALL) const activeTypeFilter = ref(ARCHIVE_FILTER_ALL) const activeDepartmentFilter = ref(ARCHIVE_FILTER_ALL) const activeArchiveMonthFilter = ref(ARCHIVE_FILTER_ALL) const selectedClaimId = ref('') const listKeyword = ref('') const rows = ref([]) const loading = ref(false) const error = ref('') const typeFilterOptions = computed(() => buildTypeFilterOptions(rows.value)) const departmentFilterOptions = computed(() => buildDepartmentFilterOptions(rows.value)) const archiveMonthFilterOptions = computed(() => buildArchiveMonthFilterOptions(rows.value)) const riskFilterLabel = computed(() => resolveFilterLabel(RISK_FILTER_OPTIONS, activeRiskFilter.value, '全部风险')) const typeFilterLabel = computed(() => resolveFilterLabel(typeFilterOptions.value, activeTypeFilter.value, '归档类型')) const departmentFilterLabel = computed(() => resolveFilterLabel(departmentFilterOptions.value, activeDepartmentFilter.value, '所属部门')) const archiveMonthFilterLabel = computed(() => resolveFilterLabel(archiveMonthFilterOptions.value, activeArchiveMonthFilter.value, '归档月份')) const filterMenus = computed(() => [ { key: 'risk', label: riskFilterLabel.value, options: RISK_FILTER_OPTIONS, activeValue: activeRiskFilter.value }, { key: 'type', label: typeFilterLabel.value, options: typeFilterOptions.value, activeValue: activeTypeFilter.value }, { key: 'department', label: departmentFilterLabel.value, options: departmentFilterOptions.value, activeValue: activeDepartmentFilter.value }, { key: 'archiveMonth', label: archiveMonthFilterLabel.value, options: archiveMonthFilterOptions.value, activeValue: activeArchiveMonthFilter.value } ]) const selectedRow = computed({ get() { return rows.value.find((row) => row.claimId === selectedClaimId.value) || null }, set(value) { selectedClaimId.value = value?.claimId || '' } }) const visibleRows = computed(() => applyArchiveListFilters(rows.value, { tab: activeTab.value, risk: activeRiskFilter.value, type: activeTypeFilter.value, department: activeDepartmentFilter.value, archiveMonth: activeArchiveMonthFilter.value, keyword: listKeyword.value })) const showEmpty = computed(() => !loading.value && !error.value && visibleRows.value.length === 0) const archiveEmptyState = computed(() => { if (!rows.value.length) { return { eyebrow: '归档中心', title: '当前还没有已归档单据', desc: '财务确认付款后进入「已付款」状态的报销单会自动汇总到这里,形成公司级财务归档库。', icon: 'mdi mdi-archive-check-outline', actionLabel: null, actionIcon: null, tone: 'slate', artLabel: 'ARCHIVE', tips: ['仅展示已付款或已归档的单据', '申请人仍可在报销中心查看自己的归档记录'] } } const filtersActive = hasActiveArchiveListFilters({ tab: activeTab.value, risk: activeRiskFilter.value, type: activeTypeFilter.value, department: activeDepartmentFilter.value, archiveMonth: activeArchiveMonthFilter.value, keyword: listKeyword.value }) return { eyebrow: filtersActive ? '筛选结果为空' : '归档中心', title: filtersActive ? '没有符合当前筛选条件的归档单据' : `“${activeTab.value}”里暂时没有归档单据`, desc: filtersActive ? '可以调整风险、归档类型、部门或归档月份筛选,也可以修改搜索关键词后重试。' : '可以切换到其他归档分类查看,或调整筛选条件后重新检索。', icon: 'mdi mdi-archive-outline', actionLabel: null, actionIcon: null, tone: 'sky', artLabel: filtersActive ? 'FILTER' : 'ARCHIVE', tips: ['归档中心保存全公司归档数据', '非申请人无法在报销中心查看他人归档单'] } }) function resetListFilters() { activeTab.value = ARCHIVE_TAB_ALL activeRiskFilter.value = ARCHIVE_FILTER_ALL activeTypeFilter.value = ARCHIVE_FILTER_ALL activeDepartmentFilter.value = ARCHIVE_FILTER_ALL activeArchiveMonthFilter.value = ARCHIVE_FILTER_ALL listKeyword.value = '' } function handleEmptyAction() { if (!rows.value.length) { void reload() return } resetListFilters() } function selectFilterValue(key, value) { if (key === 'risk') { activeRiskFilter.value = value } else if (key === 'type') { activeTypeFilter.value = value } else if (key === 'department') { activeDepartmentFilter.value = value } else if (key === 'archiveMonth') { activeArchiveMonthFilter.value = value } } function closeSelectedDetail() { selectedClaimId.value = '' } async function reload() { loading.value = true error.value = '' try { const payload = await fetchArchivedExpenseClaims(REIMBURSEMENT_LIST_PREVIEW_PARAMS) const mappedRows = extractExpenseClaimItems(payload) .map((item) => mapExpenseClaimToRequest(item)) .filter(Boolean) .map((item) => buildArchiveRow(item)) rows.value = mappedRows if (!mappedRows.some((item) => item.claimId === selectedClaimId.value)) { selectedClaimId.value = '' } } catch (nextError) { rows.value = [] selectedClaimId.value = '' error.value = nextError instanceof Error ? nextError.message : '归档中心加载失败。' } finally { loading.value = false } } void reload() return { activeTab, archiveEmptyState, closeSelectedDetail, error, filterMenus, handleEmptyAction, listKeyword, loading, reload, resetListFilters, rows, selectFilterValue, selectedRow, showEmpty, tabs, visibleRows } } }