diff --git a/backend/migrations/columns.py b/backend/migrations/columns.py index e51a19b..bcd994f 100644 --- a/backend/migrations/columns.py +++ b/backend/migrations/columns.py @@ -105,6 +105,8 @@ def migrate_add_columns(): "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") + _add_column_if_missing(conn, "expense_records", "status", + "ALTER TABLE expense_records ADD COLUMN status VARCHAR(50) NOT NULL DEFAULT '计入费用'") # project_finances 费用明细 JSON 数据 _add_column_if_missing(conn, "project_finances", "expense_data", diff --git a/backend/migrations/seed_data.py b/backend/migrations/seed_data.py index 8d5da9b..b178b06 100644 --- a/backend/migrations/seed_data.py +++ b/backend/migrations/seed_data.py @@ -224,11 +224,12 @@ def seed_db(): incurred = int(amount * random.uniform(0.5, 1.0)) _exec(conn, """ INSERT INTO expense_records - (tenant, expense_type, expense_month, amount, incurred_amount, notes) - VALUES (?,?,?,?,?,?) + (tenant, expense_type, expense_month, amount, incurred_amount, notes, status) + VALUES (?,?,?,?,?,?,?) """, ( tenant, et, month_str, amount, incurred, f"{et} - {random.choice(['季度预算', '紧急支出', '常规费用', '项目费用'])}", + "计入费用", )) print(f"[seed] {tenant}: {len(amounts)} 项目, {op_count} 运营项目, {expense_count} 费用记录") diff --git a/backend/routes.py b/backend/routes.py index 39ca7e3..3334924 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", "expense_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", ["expense_type", "expense_month", "amount", "incurred_amount", "notes", "tenant"]), + "expense": ("expense_records", ["expense_type", "expense_month", "amount", "incurred_amount", "notes", "status", "tenant"]), } # ---------- 鉴权装饰器 ---------- diff --git a/static/modules/expense.js b/static/modules/expense.js index 84d460b..d5fb9a9 100644 --- a/static/modules/expense.js +++ b/static/modules/expense.js @@ -5,6 +5,12 @@ function renderExpense() { 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.expenseSort) state.expenseSort = { key: 'expense_month', asc: true }; + var sortKey = state.expenseSort.key; + var sortAsc = state.expenseSort.asc ? 1 : -1; // 筛选状态 var view = state.expenseView || 'total'; @@ -35,30 +41,41 @@ function renderExpense() { }); } + // 排序 + 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 = ''; + 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 += ''; + 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 += ''; + quarterOpts += ''; } // 月份/季度日期选择器 var dateSelect = ''; if (view === 'monthly') { - dateSelect = '月份:'; + dateSelect = '月份:'; } else if (view === 'quarterly') { - dateSelect = '季度:'; + dateSelect = '季度:'; } // Toolbar @@ -68,21 +85,36 @@ function renderExpense() { }); toolbar += '' + dateSelect; + // 排序箭头 + var sortArrow = function(key) { + var arrow = key === sortKey ? (sortAsc === 1 ? ' ↑' : ' ↓') : ''; + return '' + arrow + ''; + }; + // 表格行 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) || '—') + ''; + 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 + ''; + 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 = ''; + modal += '
'; var target = document.querySelector("#financeExpense") || document.querySelector("#financeTabExpense") || document.querySelector("#expense"); if (!target) return; @@ -94,12 +126,20 @@ function renderExpense() { '
已发生金额' + money(totalIncurred) + '
' + '' + modal + - '
' + tableRows + '
费用类型月份金额已发生金额费用说明
' + + '
' + tableRows + '
月份' + sortArrow('expense_month') + '费用类型' + sortArrow('expense_type') + '状态' + sortArrow('status') + '金额' + sortArrow('amount') + '已发生金额' + sortArrow('incurred_amount') + '费用说明
' + ''; if (window.lucide) window.lucide.createIcons(); } +window.toggleExpenseSort = function(key) { + var cur = state.expenseSort || {}; + if (cur.key === key) { cur.asc = !cur.asc; } + else { cur.key = key; cur.asc = true; } + state.expenseSort = cur; + renderExpense(); +}; + window.setExpenseView = function(v) { state.expenseView = v; renderExpense(); }; window.setExpenseFilter = function(v) { state.expenseFilter = v; renderExpense(); }; window.setExpenseMonth = function(v) { state.expenseMonth = v; renderExpense(); }; @@ -109,6 +149,7 @@ window.openExpenseModal = function() { var form = document.querySelector("#expenseModal form"); form.reset(); form.querySelector('[name="expense_id"]').value = ""; + form.querySelector('[name="status"]').value = "计入费用"; document.querySelector("#expenseModalTitle").textContent = "新增费用"; document.querySelector("#expenseDeleteBtn").classList.add("hidden"); document.querySelector("#expenseModal").classList.remove("hidden"); @@ -130,6 +171,7 @@ window.openExpenseEdit = function(id) { setVal("amount", r.amount); setVal("incurred_amount", r.incurred_amount); setVal("notes", r.notes); + setVal("status", r.status || "计入费用"); document.querySelector("#expenseModalTitle").textContent = "编辑费用"; document.querySelector("#expenseDeleteBtn").classList.remove("hidden"); document.querySelector("#expenseModal").classList.remove("hidden"); @@ -139,9 +181,9 @@ window.saveExpense = async function(event) { event.preventDefault(); var form = event.target; var data = Object.fromEntries(new FormData(form).entries()); - // 数字字段空值转 0,避免线上 MySQL 严格模式报错 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; diff --git a/static/modules/finance.js b/static/modules/finance.js index bfdb0b7..9c0dc85 100644 --- a/static/modules/finance.js +++ b/static/modules/finance.js @@ -304,7 +304,9 @@ function renderFinance() { const tdRow = (pf, rev, payment, cost, paid, gross) => { const cashflow = payment - paid; const exe = rev && pf.sign_amount ? Math.round(rev / pf.sign_amount * 100) + '%' : ''; - const payR = rev && payment ? Math.round(payment / rev * 100) + '%' : ''; + const payRVal = rev && payment ? Math.round(payment / rev * 100) : null; + const payRCls = payRVal === null ? '' : payRVal < 30 ? 'text-red-600' : payRVal < 80 ? 'text-amber-600' : 'text-green-600'; + const payR = payRVal !== null ? '' + payRVal + '%' : ''; const paidR = cost && paid ? Math.round(paid / cost * 100) + '%' : ''; const grossR = rev && gross ? Math.round(gross / rev * 100) + '%' : ''; const st = pf.status === '已签约' ? '已签约' : '待签约'; diff --git a/static/modules/home.js b/static/modules/home.js index 09cf137..ce693bd 100644 --- a/static/modules/home.js +++ b/static/modules/home.js @@ -4,6 +4,15 @@ function renderHome() { const { summary, financeMonthly } = state.data; const m = summary.metrics; const moneyInt = (v) => `${Math.round(Number(v || 0)).toLocaleString("zh-CN")} 元`; + // 回款提醒 + const yrRev = m.revenue_annual || 0, yrPay = m.payment_annual || 0, yrUnpaid = yrRev - yrPay; + const yrPayRate = yrRev > 0 ? Math.round(yrPay / yrRev * 100) : 0; + const yrUnpaidWan = Math.round(yrUnpaid / 10000); + const payRateCls = yrPayRate < 30 ? 'text-red-600' : yrPayRate < 80 ? 'text-amber-600' : 'text-green-600'; + const payReminder = yrRev > 0 ? '
' + + '' + + '回款提醒:本年度确收 ' + moneyInt(yrRev) + ',回款 ' + moneyInt(yrPay) + ',你还有 ' + yrUnpaidWan + '万 未回款,当前回款率:' + yrPayRate + '%' + + '
' : ''; const rows1 = [ ["年度累计", moneyInt(m.signed_annual || m.signed_amount)], ["本季度累计", moneyInt(m.signed_q2 || 0)], @@ -113,6 +122,7 @@ function renderHome() { ["产品迭代", m.upcoming_products, "products"], ].map(([label, value, tab]) => ``).join("")} `} + ${payReminder} ${card(`

部门经营情况

${(() => { var d = [ diff --git a/templates/index.html b/templates/index.html index 6ddb3df..f8bcc11 100644 --- a/templates/index.html +++ b/templates/index.html @@ -76,7 +76,7 @@
-

OPC Manager v1.0.0.4

+

OPC Manager v1.0.0.5

科普 OPC 工作台

指标年度累计上季度累计上月累计本季度累计季环比本月累计月环比