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 + '
' +
+ '| 月份' + sortArrow('expense_month') + ' | 费用类型' + sortArrow('expense_type') + ' | 状态' + sortArrow('status') + ' | 金额' + sortArrow('amount') + ' | 已发生金额' + sortArrow('incurred_amount') + ' | 费用说明 |
' + tableRows + '
' +
'';
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