// expense.js — 费用管理模块 function renderPlanExpense() { var records = state.data.planExpense || []; var money = function(v) { return '¥ ' + (Number(v || 0)).toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); }; var esc = function(s) { return (s || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); }; var EXPENSE_TYPES = ['通用费用','平台采购','人力成本','研发费用','奖金','其他']; var STATUS_OPTIONS = ['计入费用','暂不计入']; // 排序状态 if (!state.planExpSort) state.planExpSort = { key: 'expense_month', asc: true }; var sortKey = state.planExpSort.key; var sortAsc = state.planExpSort.asc ? 1 : -1; // 筛选状态 var view = state.planExpView || 'total'; var typeFilter = state.planExpFilter || '全部'; var filtered = typeFilter === '全部' ? records : records.filter(function(r) { return r.expense_type === typeFilter; }); // 视图数据预处理 var now = new Date(); var thisYear = now.getFullYear(); var defaultMonth = thisYear + '-' + String(now.getMonth() + 1).padStart(2, '0'); if (!state.planExpMonth) state.planExpMonth = defaultMonth; if (state.planExpQuarter === '') state.planExpQuarter = String(Math.floor(now.getMonth() / 3)); // 月度筛选 if (view === 'monthly') { filtered = filtered.filter(function(r) { return r.expense_month === state.planExpMonth; }); } // 季度筛选 if (view === 'quarterly') { var q = parseInt(state.planExpQuarter) || 0; var qRanges = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]]; var qRange = qRanges[q]; filtered = filtered.filter(function(r) { var m = parseInt((r.expense_month || '0').substring(5)) || 0; return qRange.indexOf(m) >= 0; }); } // 排序 filtered.sort(function(a, b) { var va = a[sortKey] || '', vb = b[sortKey] || ''; if (sortKey === 'amount' || sortKey === 'incurred_amount') { va = parseFloat(va) || 0; vb = parseFloat(vb) || 0; } if (va < vb) return -1 * sortAsc; if (va > vb) return 1 * sortAsc; return 0; }); var totalAmount = filtered.reduce(function(s, r) { return s + (r.amount || 0); }, 0); var totalIncurred = filtered.reduce(function(s, r) { return s + (r.incurred_amount || 0); }, 0); // 月份下拉选项(去年/今年/明年) var monthOpts = ''; for (var yr = thisYear - 1; yr <= thisYear + 1; yr++) for (var m = 1; m <= 12; m++) { var mv = yr + '-' + String(m).padStart(2, '0'); monthOpts += ''; } // 季度下拉 var qLabels = ['Q1 (1-3月)','Q2 (4-6月)','Q3 (7-9月)','Q4 (10-12月)']; var quarterOpts = ''; for (var qi = 0; qi < 4; qi++) { quarterOpts += ''; } // 月份/季度日期选择器 var dateSelect = ''; if (view === 'monthly') { dateSelect = '月份:'; } else if (view === 'quarterly') { dateSelect = '季度:'; } // Toolbar var toolbar = '视图:类型:' + dateSelect; // 排序箭头 var sortArrow = function(key) { var arrow = key === sortKey ? (sortAsc === 1 ? ' ↑' : ' ↓') : ''; return '' + arrow + ''; }; // 表格行 var tableRows = ''; if (filtered.length) { filtered.forEach(function(r) { var statusCls = r.status === '暂不计入' ? 'text-amber-600' : 'text-green-600'; tableRows += '' + (r.expense_month || '—') + '' + esc(r.expense_type) + '' + esc(r.status || '计入费用') + '' + money(r.amount) + '' + money(r.incurred_amount) + '' + (esc(r.notes) || '—') + ''; }); } else { var emptyMsg = view === 'monthly' ? '该月份暂无费用记录' : view === 'quarterly' ? '该季度暂无费用记录' : '暂无费用记录'; tableRows = '' + emptyMsg + ''; } // 模态框 monthOpts for form (with current value preselected) var modalMonthOpts = ''; for (var yr2 = thisYear - 1; yr2 <= thisYear + 1; yr2++) for (var m2 = 1; m2 <= 12; m2++) { var mv2 = yr2 + '-' + String(m2).padStart(2, '0'); modalMonthOpts += ''; } var statusOpts = STATUS_OPTIONS.map(function(s) { return ''; }).join(''); var modal = ''; var target = document.querySelector("#planExpenseModule") || document.querySelector("#planExpenseModule") || document.querySelector("#planExpenseContent"); if (!target) return; target.innerHTML = '
' + '
' + toolbar + '
' + '
' + '
费用记录' + filtered.length + '
' + '
总费用' + money(totalAmount) + '
' + '
已发生金额' + money(totalIncurred) + '
' + '
' + modal + '
' + tableRows + '
月份' + sortArrow('expense_month') + '费用类型' + sortArrow('expense_type') + '状态' + sortArrow('status') + '金额' + sortArrow('amount') + '已发生金额' + sortArrow('incurred_amount') + '费用说明
' + '
'; if (window.lucide) window.lucide.createIcons(); } window.togglePlanExpSort = function(key) { var cur = state.planExpSort || {}; if (cur.key === key) { cur.asc = !cur.asc; } else { cur.key = key; cur.asc = true; } state.planExpSort = cur; renderPlanExpense(); }; window.setPlanExpView = function(v) { state.planExpView = v; renderPlanExpense(); }; window.setPlanExpFilter = function(v) { state.planExpFilter = v; renderPlanExpense(); }; window.setPlanExpMonth = function(v) { state.planExpMonth = v; renderPlanExpense(); }; window.setPlanExpQuarter = function(v) { state.planExpQuarter = v; renderPlanExpense(); }; window.openPlanExpModal = function() { var form = document.querySelector("#planExpenseModal form"); form.reset(); form.querySelector('[name="expense_id"]').value = ""; form.querySelector('[name="status"]').value = "计入费用"; document.querySelector("#planExpenseModalTitle").textContent = "新增费用"; document.querySelector("#planExpDeleteBtn").classList.add("hidden"); document.querySelector("#planExpenseModal").classList.remove("hidden"); }; window.closePlanExpModal = function() { document.querySelector("#planExpenseModal").classList.add("hidden"); }; window.openPlanExpEdit = function(id) { var r = (state.data.planExpense || []).find(function(x) { return x.id === id; }); if (!r) return; var form = document.querySelector("#planExpenseModal form"); form.reset(); var setVal = function(name, val) { var el = form.querySelector('[name="' + name + '"]'); if (el) el.value = val || ""; }; setVal("expense_id", r.id); setVal("expense_type", r.expense_type); setVal("expense_month", r.expense_month); setVal("amount", r.amount); setVal("incurred_amount", r.incurred_amount); setVal("notes", r.notes); setVal("status", r.status || "计入费用"); document.querySelector("#planExpenseModalTitle").textContent = "编辑费用"; document.querySelector("#planExpDeleteBtn").classList.remove("hidden"); document.querySelector("#planExpenseModal").classList.remove("hidden"); }; window.savePlanExpense = async function(event) { event.preventDefault(); var form = event.target; var data = Object.fromEntries(new FormData(form).entries()); if (data.amount === '' || data.amount === undefined) data.amount = 0; if (data.incurred_amount === '' || data.incurred_amount === undefined) data.incurred_amount = 0; if (!data.status) data.status = '计入费用'; var isEdit = !!data.expense_id; delete data.expense_id; var id = form.querySelector('[name="expense_id"]').value; var url = isEdit ? "/api/planExpense/" + id : "/api/planExpense"; var method = isEdit ? "PUT" : "POST"; var resp = await fetch(url, { method: method, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: Object.assign({}, data, { tenant: state.tenant }) }) }); if (!resp.ok) { var err = await resp.text(); alert("保存失败:" + err); return; } var bResp = await fetch("/api/bootstrap?tenant=" + encodeURIComponent(state.tenant)); if (bResp.ok) { var bd = await bResp.json(); state.data = bd; applyUserTenants(); renderPlanExpense(); } closePlanExpModal(); }; window.deletePlanExpItem = async function() { var id = document.querySelector("#planExpenseModal form [name='expense_id']").value; if (!id || !confirm("确定删除?")) return; var resp = await fetch("/api/planExpense/" + id, { method: "DELETE" }); if (!resp.ok) { alert("删除失败"); return; } var bResp = await fetch("/api/bootstrap?tenant=" + encodeURIComponent(state.tenant)); if (bResp.ok) { var bd = await bResp.json(); state.data = bd; applyUserTenants(); renderPlanExpense(); } };