import { computed, onMounted, ref, watch } from 'vue' import { ElButton } from 'element-plus/es/components/button/index.mjs' import BudgetTrendChart from '../../components/charts/BudgetTrendChart.vue' import EnterpriseDetailCard from '../../components/shared/EnterpriseDetailCard.vue' import EnterpriseDetailPage from '../../components/shared/EnterpriseDetailPage.vue' import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue' import TableEmptyState from '../../components/shared/TableEmptyState.vue' import TableLoadingState from '../../components/shared/TableLoadingState.vue' import { fetchEmployeeMeta } from '../../services/employees.js' import { canEditBudgetCenter, canSwitchBudgetDepartments, isBudgetMonitorUser, isExecutiveUser } from '../../utils/accessControl.js' import { BUDGET_QUARTER_OPTIONS, BUDGET_YEAR_OPTIONS } from '../../utils/budgetOntology.js' import { BUDGET_PAGE_SIZE_OPTIONS, BUDGET_SCOPE_ALL, BUDGET_SCOPE_ARCHIVE, BUDGET_SCOPE_REVIEW, buildBudgetRows, buildBudgetScopeTabs, buildBudgetUsageData, getBudgetStatusOptions, matchesBudgetKeyword } from './budgetCenterListModel.js' const FALLBACK_DEPARTMENTS = [ { code: 'MARKET-DEPT', name: '市场部', costCenter: 'CC-4100' }, { code: 'FINANCE-DEPT', name: '财务部', costCenter: 'CC-2100' }, { code: 'TECH-DEPT', name: '技术部', costCenter: 'CC-6100' }, { code: 'HR-DEPT', name: '人力资源部', costCenter: 'CC-3200' }, { code: 'PRODUCTION-DEPT', name: '生产部', costCenter: 'CC-7200' }, { code: 'PRESIDENT-OFFICE', name: '总裁办', costCenter: 'CC-1000' } ] function mapOptions(values, suffix = '') { return values.map((value) => ({ label: suffix ? `${value}${suffix}` : value, value })) } function resolveBudgetUpdatedAt(row) { return row?.updatedAt || row?.submittedAt || row?.archivedAt || '-' } function resolveBudgetCompiler(row) { return row?.compiler || row?.owner || '-' } function buildBudgetDetailKpis(row) { return [ { label: '编制人', value: resolveBudgetCompiler(row), unit: '', meta: row.scope === BUDGET_SCOPE_REVIEW ? '提交草案' : '预算编制', color: 'var(--theme-primary)' }, { label: '审核人', value: row.reviewer || '-', unit: '', meta: '高级财务审核', color: '#3b82f6' }, { label: '版本', value: row.version || '-', unit: '', meta: row.periodType || '预算版本', color: '#64748b' }, { label: '更新时间', value: resolveBudgetUpdatedAt(row), unit: '', meta: '最近同步', color: '#f59e0b' } ] } export default { name: 'BudgetCenterView', props: { currentUser: { type: Object, default: () => ({}) } }, emits: ['openAssistant', 'detail-open-change', 'detail-topbar-change'], components: { BudgetTrendChart, EnterpriseSelect, EnterpriseDetailCard, EnterpriseDetailPage, TableEmptyState, TableLoadingState, ElButton }, setup(props, { emit }) { const departments = ref(FALLBACK_DEPARTMENTS) const activeBudgetScope = ref(BUDGET_SCOPE_ALL) const budgetKeyword = ref('') const budgetPage = ref(1) const budgetPageSize = ref(8) const budgetLoading = ref(true) const budgetError = ref('') const selectedBudgetId = ref('') const filters = ref({ year: '2026', quarter: 'Q1', status: '全部' }) const canEditBudget = computed(() => canEditBudgetCenter(props.currentUser)) const canAuditBudgetDrafts = computed(() => canEditBudgetCenter(props.currentUser)) const canSwitchDepartments = computed(() => canSwitchBudgetDepartments(props.currentUser)) const isDepartmentBudgetMonitor = computed( () => isBudgetMonitorUser(props.currentUser) && !canSwitchDepartments.value && !isExecutiveUser(props.currentUser) ) const currentUserDepartmentName = computed(() => String(props.currentUser?.departmentName || props.currentUser?.department || '').trim() ) const currentUserCostCenter = computed(() => String(props.currentUser?.costCenter || props.currentUser?.cost_center || '').trim() ) const yearOptions = mapOptions(BUDGET_YEAR_OPTIONS, '年度') const quarterOptions = mapOptions(BUDGET_QUARTER_OPTIONS) const budgetPageSizeOptions = BUDGET_PAGE_SIZE_OPTIONS.map((size) => ({ label: `${size} 条/页`, value: size })) const budgetRowsByScope = computed(() => buildBudgetRows({ departments: departments.value, year: filters.value.year, quarter: filters.value.quarter }) ) const budgetScopeTabs = computed(() => buildBudgetScopeTabs(budgetRowsByScope.value)) const activeScopeRows = computed(() => budgetRowsByScope.value[activeBudgetScope.value] || []) const activeScopeLabel = computed( () => budgetScopeTabs.value.find((item) => item.value === activeBudgetScope.value)?.label || '预算' ) const statusOptions = computed(() => mapOptions(getBudgetStatusOptions(activeBudgetScope.value))) const filteredBudgetRows = computed(() => activeScopeRows.value .filter((row) => filters.value.status === '全部' || row.statusLabel === filters.value.status) .filter((row) => matchesBudgetKeyword(row, budgetKeyword.value)) ) const totalBudgetRows = computed(() => filteredBudgetRows.value.length) const totalBudgetPages = computed(() => Math.max(1, Math.ceil(totalBudgetRows.value / Number(budgetPageSize.value || 8))) ) const currentBudgetPage = computed(() => Math.min(Math.max(1, budgetPage.value), totalBudgetPages.value) ) const budgetPageNumbers = computed(() => Array.from({ length: totalBudgetPages.value }, (_, index) => index + 1) ) const visibleBudgetRows = computed(() => { const pageSize = Number(budgetPageSize.value || 8) const start = (currentBudgetPage.value - 1) * pageSize return filteredBudgetRows.value.slice(start, start + pageSize) }) const selectedBudget = computed(() => activeScopeRows.value.find((row) => row.id === selectedBudgetId.value) || null ) const detailMode = computed(() => Boolean(selectedBudget.value)) const selectedBudgetUsageData = computed(() => buildBudgetUsageData(selectedBudget.value)) const budgetDetailTopBarPayload = computed(() => { const row = selectedBudget.value if (!row) return null return { view: { eyebrow: '预算详情', title: `${row.departmentName} · ${row.periodLabel}`, desc: `${row.budgetNo} / ${row.version} · 仅覆盖差旅、通信、招待费、办公用品` }, alerts: [], kpis: buildBudgetDetailKpis(row) } }) const selectedBudgetStatusNotes = computed(() => { const row = selectedBudget.value if (!row) return [] return [ { label: '预算状态', value: row.statusLabel, tone: row.statusTone || 'ok', desc: row.auditSummary || '当前预算状态已完成同步,可在预算中心继续追踪。' }, { label: '风险状态', value: row.riskLabel, tone: row.riskTone || 'ok', desc: `当前已发生与已占用合计使用率为 ${row.usageRateLabel},系统按四类费用的提醒、告警和风险阈值综合判断。` } ] }) const showTable = computed(() => !budgetLoading.value && !budgetError.value && visibleBudgetRows.value.length > 0) const showEmpty = computed(() => !budgetLoading.value && !budgetError.value && visibleBudgetRows.value.length === 0) const emptyState = computed(() => ({ eyebrow: activeScopeLabel.value, title: `暂无${activeScopeLabel.value}`, desc: '当前筛选条件下没有匹配的预算记录。', icon: 'mdi mdi-database-search-outline', tone: 'blue', artLabel: '预算列表为空', tips: ['可以调整年度、季度、状态或关键词后重试。'] })) const pageSummary = computed(() => `共 ${totalBudgetRows.value} 条,目前第 ${currentBudgetPage.value} 页`) function openBudgetAssistant(prompt = '') { if (!canEditBudget.value) return emit('openAssistant', { source: 'budget', sessionType: 'budget', prompt, files: [], conversation: null }) } function openBudgetReviewAssistant(row) { if (!row || !canAuditBudgetDrafts.value) { openBudgetDetail(row) return } openBudgetAssistant( `请进入预算审核模式,审核${row.departmentName}${row.periodLabel}预算草案,重点看差旅、通信、招待费和办公用品的合理性、风险点和是否可以通过。` ) } function openBudgetDetail(row) { if (!row?.id) return selectedBudgetId.value = row.id } function backToList() { selectedBudgetId.value = '' } function handleRowAction(row) { if (activeBudgetScope.value === BUDGET_SCOPE_REVIEW && canAuditBudgetDrafts.value) { openBudgetReviewAssistant(row) return } openBudgetDetail(row) } function goToBudgetPage(page) { budgetPage.value = Math.min(Math.max(Number(page) || 1, 1), totalBudgetPages.value) } function changeBudgetPageSize(size) { budgetPageSize.value = Number(size) || 8 budgetPage.value = 1 } function resolveScopedDepartments(options) { if (!isDepartmentBudgetMonitor.value) return options const userDepartment = currentUserDepartmentName.value const userCostCenter = currentUserCostCenter.value const scoped = options.filter((item) => { if (userCostCenter && item.costCenter === userCostCenter) return true return userDepartment && item.name === userDepartment }) if (scoped.length) return scoped return [ { id: '', code: userCostCenter || userDepartment || 'CURRENT-DEPARTMENT', name: userDepartment || '当前部门', costCenter: userCostCenter } ] } async function loadDepartments() { budgetLoading.value = true budgetError.value = '' try { const payload = await fetchEmployeeMeta() const options = Array.isArray(payload?.organizationOptions) ? payload.organizationOptions : [] const nextDepartments = options .filter((item) => item?.code && item?.name) .map((item) => ({ id: String(item.id || ''), code: String(item.code), name: String(item.name), costCenter: String(item.costCenter || '') })) const scopedDepartments = resolveScopedDepartments(nextDepartments) if (scopedDepartments.length) { departments.value = scopedDepartments } } catch (error) { console.warn('Failed to load budget departments from employee meta:', error) } finally { budgetLoading.value = false } } onMounted(() => { void loadDepartments() }) watch( () => activeBudgetScope.value, () => { filters.value.status = '全部' budgetPage.value = 1 selectedBudgetId.value = '' } ) watch( [ budgetPageSize, budgetKeyword, () => filters.value.year, () => filters.value.quarter, () => filters.value.status ], () => { budgetPage.value = 1 } ) watch(totalBudgetPages, (pages) => { if (budgetPage.value > pages) { budgetPage.value = pages } }) watch(detailMode, (value) => { emit('detail-open-change', value) }, { immediate: true }) watch(budgetDetailTopBarPayload, (payload) => { emit('detail-topbar-change', payload) }, { immediate: true, deep: true }) return { BUDGET_SCOPE_ALL, BUDGET_SCOPE_ARCHIVE, BUDGET_SCOPE_REVIEW, activeBudgetScope, budgetError, budgetKeyword, budgetLoading, budgetPage: currentBudgetPage, budgetPageNumbers, budgetPageSize, budgetPageSizeOptions, budgetScopeTabs, backToList, canAuditBudgetDrafts, canEditBudget, changeBudgetPageSize, detailMode, emptyState, filters, goToBudgetPage, handleRowAction, openBudgetAssistant, openBudgetDetail, openBudgetReviewAssistant, pageSummary, quarterOptions, selectedBudget, selectedBudgetStatusNotes, selectedBudgetUsageData, showEmpty, showTable, statusOptions, totalBudgetPages, totalBudgetRows, visibleBudgetRows, yearOptions } } }