diff --git a/backend/migrations/columns.py b/backend/migrations/columns.py index 8143022..5600acf 100644 --- a/backend/migrations/columns.py +++ b/backend/migrations/columns.py @@ -98,6 +98,13 @@ def migrate_add_columns(): "ALTER TABLE project_finances ADD COLUMN contact_phone VARCHAR(50) NOT NULL DEFAULT ''") _add_column_if_missing(conn, "project_finances", "other_info", "ALTER TABLE project_finances ADD COLUMN other_info VARCHAR(500) NOT NULL DEFAULT ''") + # expense_records 字段迁移(重构为费用类型+月份+金额+已发生金额+费用说明) + _add_column_if_missing(conn, "expense_records", "expense_type", + "ALTER TABLE expense_records ADD COLUMN expense_type VARCHAR(100) NOT NULL DEFAULT ''") + _add_column_if_missing(conn, "expense_records", "expense_month", + "ALTER TABLE expense_records ADD COLUMN expense_month VARCHAR(20) NOT NULL DEFAULT ''") + _add_column_if_missing(conn, "expense_records", "incurred_amount", + "ALTER TABLE expense_records ADD COLUMN incurred_amount DOUBLE NOT NULL DEFAULT 0") conn.commit() print("[migrate] 加列迁移完成") diff --git a/backend/migrations/tables.py b/backend/migrations/tables.py index 5cfb3d6..a8a91ae 100644 --- a/backend/migrations/tables.py +++ b/backend/migrations/tables.py @@ -150,13 +150,10 @@ def migrate_create_tables(): """CREATE TABLE IF NOT EXISTS expense_records ( id INT AUTO_INCREMENT PRIMARY KEY, tenant VARCHAR(100) NOT NULL DEFAULT '科普·无界', - project_name VARCHAR(200) NOT NULL DEFAULT '', - purchase_item VARCHAR(300) NOT NULL DEFAULT '', - supplier VARCHAR(200) NOT NULL DEFAULT '', + expense_type VARCHAR(100) NOT NULL DEFAULT '', + expense_month VARCHAR(20) NOT NULL DEFAULT '', amount DOUBLE NOT NULL DEFAULT 0, - purchase_date VARCHAR(30) NOT NULL DEFAULT '', - status VARCHAR(50) NOT NULL DEFAULT '待审批', - category VARCHAR(100) NOT NULL DEFAULT '', + incurred_amount DOUBLE NOT NULL DEFAULT 0, notes VARCHAR(1000) NOT NULL DEFAULT '', created_at VARCHAR(30) NOT NULL DEFAULT '', updated_at VARCHAR(30) NOT NULL DEFAULT '' diff --git a/backend/routes.py b/backend/routes.py index 3d20fc9..2d06019 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -24,7 +24,7 @@ TABLES = { "finance": ("finance_records", ["month", "project_name", "record_type", "category", "amount", "occurred_date", "notes", "tenant"]), "tasks": ("project_tasks", ["project_id", "phase", "milestone", "task", "owner", "due_date", "blockers", "notes", "status", "sort_order", "priority", "tenant"]), "projectFinances": ("project_finances", ["project_id", "tenant", "business_type", "customer_name", "sign_amount", "sign_month", "status", "sales_person", "owner", "total_rev", "total_gross", "total_payment", "total_cost", "total_paid", "budget_data", "start_date", "end_date", "task_type", "task_count", "service_fee_standard", "project_manager", "task_data", "project_code", "contact_name", "contact_phone", "other_info"]), - "expense": ("expense_records", ["project_name", "purchase_item", "supplier", "amount", "purchase_date", "status", "category", "notes", "tenant"]), + "expense": ("expense_records", ["expense_type", "expense_month", "amount", "incurred_amount", "notes", "tenant"]), } # ---------- 鉴权装饰器 ---------- diff --git a/static/modules/expense.js b/static/modules/expense.js index b7cdebb..e4ff43d 100644 --- a/static/modules/expense.js +++ b/static/modules/expense.js @@ -1,35 +1,110 @@ // expense.js — 费用管理模块 function renderExpense() { - const records = state.data.expense || []; - const money = (v) => `¥ ${(Number(v || 0)).toLocaleString("zh-CN", { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`; - const badge = (s) => { - const m = { "已审批": "green", "已采购": "blue", "已完成": "slate", "待审批": "amber" }; - const c = m[s] || "amber"; - return `${s}`; - }; - const esc = (s) => (s || "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + var records = state.data.expense || []; + 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 = ['通用费用','平台采购','人力成本','研发费用','奖金','其他']; - const totalAmount = records.reduce((s, r) => s + (r.amount || 0), 0); + // 筛选状态 + var view = state.expenseView || 'total'; + var typeFilter = state.expenseFilter || '全部'; + 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.expenseMonth) state.expenseMonth = defaultMonth; + if (state.expenseQuarter === '') state.expenseQuarter = String(Math.floor(now.getMonth() / 3)); + + // 月度筛选 + if (view === 'monthly') { + filtered = filtered.filter(function(r) { return r.expense_month === state.expenseMonth; }); + } + + // 季度筛选 + if (view === 'quarterly') { + var q = parseInt(state.expenseQuarter) || 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; + }); + } + + 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 tableRows = ''; + if (filtered.length) { + filtered.forEach(function(r) { + tableRows += '' + esc(r.expense_type) + '' + (r.expense_month || '—') + '' + money(r.amount) + '' + money(r.incurred_amount) + '' + (esc(r.notes) || '—') + ''; + }); + } else { + var emptyMsg = view === 'monthly' ? '该月份暂无费用记录' : view === 'quarterly' ? '该季度暂无费用记录' : '暂无费用记录'; + tableRows = '' + emptyMsg + ''; + } + + // 模态框 + var modal = ''; + + document.querySelector("#expense").innerHTML = '
' + + '
' + toolbar + '
' + + '
' + + '
费用记录' + filtered.length + '
' + + '
总费用' + money(totalAmount) + '
' + + '
已发生金额' + money(totalIncurred) + '
' + + '
' + + modal + + '
' + tableRows + '
费用类型月份金额已发生金额费用说明
' + + '
'; - document.querySelector("#expense").innerHTML = `
-
-
费用记录${records.length}
-
费用金额${money(totalAmount)}
-
待审批${records.filter(r => r.status === '待审批').length}
-
-
-

费用列表 (${records.length})

- -
- -
${records.length ? records.map(r => ``).join("") : ''}
项目名称采购内容供应商金额采购日期状态分类备注
${esc(r.project_name)}${esc(r.purchase_item)}${esc(r.supplier)}${money(r.amount)}${r.purchase_date || '—'}${badge(r.status)}${esc(r.category)}${esc(r.notes) || '—'}
暂无费用记录
合计(${records.length} 条)${money(totalAmount)}
-
`; if (window.lucide) window.lucide.createIcons(); } -window.openExpenseModal = () => { - const form = document.querySelector("#expenseModal form"); +window.setExpenseView = function(v) { state.expenseView = v; renderExpense(); }; +window.setExpenseFilter = function(v) { state.expenseFilter = v; renderExpense(); }; +window.setExpenseMonth = function(v) { state.expenseMonth = v; renderExpense(); }; +window.setExpenseQuarter = function(v) { state.expenseQuarter = v; renderExpense(); }; + +window.openExpenseModal = function() { + var form = document.querySelector("#expenseModal form"); form.reset(); form.querySelector('[name="expense_id"]').value = ""; document.querySelector("#expenseModalTitle").textContent = "新增费用"; @@ -37,49 +112,44 @@ window.openExpenseModal = () => { document.querySelector("#expenseModal").classList.remove("hidden"); }; -window.closeExpenseModal = () => { +window.closeExpenseModal = function() { document.querySelector("#expenseModal").classList.add("hidden"); }; -window.openExpenseEdit = (id) => { - const r = (state.data.expense || []).find(x => x.id === id); +window.openExpenseEdit = function(id) { + var r = (state.data.expense || []).find(function(x) { return x.id === id; }); if (!r) return; - const form = document.querySelector("#expenseModal form"); + var form = document.querySelector("#expenseModal form"); form.reset(); - const setVal = (name, val) => { const el = form.querySelector(`[name="${name}"]`); if (el) el.value = val || ""; }; + var setVal = function(name, val) { var el = form.querySelector('[name="' + name + '"]'); if (el) el.value = val || ""; }; setVal("expense_id", r.id); - setVal("project_name", r.project_name); - setVal("purchase_item", r.purchase_item); - setVal("supplier", r.supplier); + setVal("expense_type", r.expense_type); + setVal("expense_month", r.expense_month); setVal("amount", r.amount); - setVal("purchase_date", r.purchase_date); - setVal("status", r.status); - setVal("category", r.category); + setVal("incurred_amount", r.incurred_amount); setVal("notes", r.notes); document.querySelector("#expenseModalTitle").textContent = "编辑费用"; document.querySelector("#expenseDeleteBtn").classList.remove("hidden"); document.querySelector("#expenseModal").classList.remove("hidden"); }; -window.saveExpense = async (event) => { +window.saveExpense = async function(event) { event.preventDefault(); - const form = event.target; - const data = Object.fromEntries(new FormData(form).entries()); - const isEdit = !!data.expense_id; + var form = event.target; + var data = Object.fromEntries(new FormData(form).entries()); + var isEdit = !!data.expense_id; delete data.expense_id; - const url = isEdit - ? `/api/expense/${form.querySelector('[name="expense_id"]').value}` - : "/api/expense"; - const method = isEdit ? "PUT" : "POST"; + var id = form.querySelector('[name="expense_id"]').value; + var url = isEdit ? "/api/expense/" + id : "/api/expense"; + var method = isEdit ? "PUT" : "POST"; - const resp = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: { ...data, tenant: state.tenant } }) }); + 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) { alert("保存失败"); return; } - // Refresh data - const bResp = await fetch(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`); + var bResp = await fetch("/api/bootstrap?tenant=" + encodeURIComponent(state.tenant)); if (bResp.ok) { - const bd = await bResp.json(); + var bd = await bResp.json(); state.data = bd; applyUserTenants(); renderExpense(); @@ -87,17 +157,16 @@ window.saveExpense = async (event) => { closeExpenseModal(); }; -window.deleteExpenseItem = async () => { - const id = document.querySelector("#expenseModal form [name='expense_id']").value; +window.deleteExpenseItem = async function() { + var id = document.querySelector("#expenseModal form [name='expense_id']").value; if (!id || !confirm("确定删除?")) return; - const resp = await fetch(`/api/expense/${id}`, { method: "DELETE" }); + var resp = await fetch("/api/expense/" + id, { method: "DELETE" }); if (!resp.ok) { alert("删除失败"); return; } - const bResp = await fetch(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`); + var bResp = await fetch("/api/bootstrap?tenant=" + encodeURIComponent(state.tenant)); if (bResp.ok) { - const bd = await bResp.json(); + var bd = await bResp.json(); state.data = bd; applyUserTenants(); renderExpense(); } - closeExpenseModal(); }; diff --git a/static/modules/finance.js b/static/modules/finance.js index 8a635d4..724cd70 100644 --- a/static/modules/finance.js +++ b/static/modules/finance.js @@ -164,7 +164,7 @@ function renderFinance() {