Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
666f35931c | ||
|
|
8196eb2802 | ||
|
|
1d79160dc2 | ||
|
|
3cb0abcf41 | ||
|
|
c3e0e9a496 | ||
|
|
b626de53ca | ||
|
|
6853b755b8 | ||
|
|
76da3a2106 | ||
|
|
e7e93c06f8 |
@@ -17,7 +17,7 @@ def run_migrations():
|
|||||||
"""
|
"""
|
||||||
from migrations.tables import migrate_create_tables
|
from migrations.tables import migrate_create_tables
|
||||||
from migrations.columns import migrate_add_columns
|
from migrations.columns import migrate_add_columns
|
||||||
from migrations.data_fixes import migrate_fix_task_status, migrate_rename_tenant, migrate_drop_product_fields, migrate_fix_service_fee_standard
|
from migrations.data_fixes import migrate_fix_task_status, migrate_rename_tenant, migrate_drop_product_fields, migrate_fix_service_fee_standard, migrate_fix_expense_status
|
||||||
from migrations.data_split import migrate_data_split
|
from migrations.data_split import migrate_data_split
|
||||||
from migrations.seed import migrate_seed_users, migrate_seed_demo_data
|
from migrations.seed import migrate_seed_users, migrate_seed_demo_data
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ def run_migrations():
|
|||||||
migrate_rename_tenant()
|
migrate_rename_tenant()
|
||||||
migrate_drop_product_fields()
|
migrate_drop_product_fields()
|
||||||
migrate_fix_service_fee_standard()
|
migrate_fix_service_fee_standard()
|
||||||
|
migrate_fix_expense_status()
|
||||||
migrate_data_split()
|
migrate_data_split()
|
||||||
migrate_seed_users()
|
migrate_seed_users()
|
||||||
migrate_seed_demo_data()
|
migrate_seed_demo_data()
|
||||||
|
|||||||
@@ -92,3 +92,23 @@ def migrate_fix_service_fee_standard():
|
|||||||
print(f"[migrate] project_finances: {affected} 条记录 service_fee_standard 修正为 5")
|
print(f"[migrate] project_finances: {affected} 条记录 service_fee_standard 修正为 5")
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_fix_expense_status():
|
||||||
|
"""修正 expense_records 中空的 status 为默认值「计入费用」"""
|
||||||
|
from db import db
|
||||||
|
|
||||||
|
conn = db()
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE expense_records SET status='计入费用' "
|
||||||
|
"WHERE status IS NULL OR status='' OR status='待审批'"
|
||||||
|
)
|
||||||
|
affected = cur.rowcount
|
||||||
|
cur.close()
|
||||||
|
if affected:
|
||||||
|
conn.commit()
|
||||||
|
print(f"[migrate] expense_records: {affected} 条记录 status 修正为 '计入费用'")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ def bootstrap():
|
|||||||
def t_expense_sum(m_range):
|
def t_expense_sum(m_range):
|
||||||
total = 0
|
total = 0
|
||||||
for r in t_expense:
|
for r in t_expense:
|
||||||
|
if (r.get("status") or "") == "暂不计入": continue
|
||||||
m = (r.get("expense_month") or "").strip()
|
m = (r.get("expense_month") or "").strip()
|
||||||
if not m or "-" not in m: continue
|
if not m or "-" not in m: continue
|
||||||
try:
|
try:
|
||||||
@@ -358,6 +359,27 @@ def bootstrap():
|
|||||||
all_metrics[-1]["cashflow_month"] = all_metrics[-1]["payment_month"] - all_metrics[-1]["paid_month"]
|
all_metrics[-1]["cashflow_month"] = all_metrics[-1]["payment_month"] - all_metrics[-1]["paid_month"]
|
||||||
all_metrics[-1]["cashflow_prev_q"] = all_metrics[-1]["payment_prev_q"] - all_metrics[-1]["paid_prev_q"]
|
all_metrics[-1]["cashflow_prev_q"] = all_metrics[-1]["payment_prev_q"] - all_metrics[-1]["paid_prev_q"]
|
||||||
all_metrics[-1]["cashflow_prev_month"] = all_metrics[-1]["payment_prev_month"] - all_metrics[-1]["paid_prev_month"]
|
all_metrics[-1]["cashflow_prev_month"] = all_metrics[-1]["payment_prev_month"] - all_metrics[-1]["paid_prev_month"]
|
||||||
|
def t_expense_paid_sum(m_range):
|
||||||
|
total = 0
|
||||||
|
for r in t_expense:
|
||||||
|
if (r.get("status") or "") == "暂不计入": continue
|
||||||
|
m = (r.get("expense_month") or "").strip()
|
||||||
|
if not m or "-" not in m: continue
|
||||||
|
try:
|
||||||
|
if int(m.split("-")[1]) in m_range:
|
||||||
|
total += float(r.get("incurred_amount") or 0)
|
||||||
|
except: pass
|
||||||
|
return total
|
||||||
|
all_metrics[-1]["expense_paid_annual"] = t_expense_paid_sum(range(1, 13))
|
||||||
|
all_metrics[-1]["expense_paid_q2"] = t_expense_paid_sum(_q_range)
|
||||||
|
all_metrics[-1]["expense_paid_month"] = t_expense_paid_sum([_now_month])
|
||||||
|
all_metrics[-1]["expense_paid_prev_q"] = t_expense_paid_sum(_prev_q_range)
|
||||||
|
all_metrics[-1]["expense_paid_prev_month"] = t_expense_paid_sum([_prev_month]) if _prev_month else 0
|
||||||
|
all_metrics[-1]["dept_cf_annual"] = all_metrics[-1]["cashflow_annual"] - all_metrics[-1]["expense_paid_annual"]
|
||||||
|
all_metrics[-1]["dept_cf_q2"] = all_metrics[-1]["cashflow_q2"] - all_metrics[-1]["expense_paid_q2"]
|
||||||
|
all_metrics[-1]["dept_cf_month"] = all_metrics[-1]["cashflow_month"] - all_metrics[-1]["expense_paid_month"]
|
||||||
|
all_metrics[-1]["dept_cf_prev_q"] = all_metrics[-1]["cashflow_prev_q"] - all_metrics[-1]["expense_paid_prev_q"]
|
||||||
|
all_metrics[-1]["dept_cf_prev_month"] = all_metrics[-1]["cashflow_prev_month"] - all_metrics[-1]["expense_paid_prev_month"]
|
||||||
all_metrics[-1]["profit_annual"] = all_metrics[-1]["gross_annual"] - all_metrics[-1]["proj_expense_annual"]
|
all_metrics[-1]["profit_annual"] = all_metrics[-1]["gross_annual"] - all_metrics[-1]["proj_expense_annual"]
|
||||||
all_metrics[-1]["profit_q2"] = all_metrics[-1]["gross_q2"] - all_metrics[-1]["proj_expense_q2"]
|
all_metrics[-1]["profit_q2"] = all_metrics[-1]["gross_q2"] - all_metrics[-1]["proj_expense_q2"]
|
||||||
all_metrics[-1]["profit_month"] = all_metrics[-1]["monthly_net_profit"] - all_metrics[-1]["proj_expense_month"]
|
all_metrics[-1]["profit_month"] = all_metrics[-1]["monthly_net_profit"] - all_metrics[-1]["proj_expense_month"]
|
||||||
@@ -366,7 +388,7 @@ def bootstrap():
|
|||||||
all_monthly.append(monthly_finance(conn, t))
|
all_monthly.append(monthly_finance(conn, t))
|
||||||
all_recent.extend(rows(conn, "SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 4", [t]))
|
all_recent.extend(rows(conn, "SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 4", [t]))
|
||||||
agg = {}
|
agg = {}
|
||||||
for key in ["total_projects","total_proposals","total_products","upcoming_products","signed_amount","signed_annual","signed_q2","signed_month","signed_prev_q","signed_prev_month","revenue_annual","revenue_q2","monthly_revenue","revenue_prev_q","revenue_prev_month","gross_annual","gross_q2","monthly_net_profit","gross_prev_q","gross_prev_month","payment_annual","payment_q2","payment_month","payment_prev_q","payment_prev_month","cost_annual","cost_q2","cost_month","cost_prev_q","cost_prev_month","expense_annual","expense_q2","expense_month","expense_prev_q","expense_prev_month","paid_annual","paid_q2","paid_month","paid_prev_q","paid_prev_month","cashflow_annual","cashflow_q2","cashflow_month","cashflow_prev_q","cashflow_prev_month","profit_annual","profit_q2","profit_month","profit_prev_q","profit_prev_month","proj_expense_annual","proj_expense_q2","proj_expense_month","proj_expense_prev_q","proj_expense_prev_month"]:
|
for key in ["total_projects","total_proposals","total_products","upcoming_products","signed_amount","signed_annual","signed_q2","signed_month","signed_prev_q","signed_prev_month","revenue_annual","revenue_q2","monthly_revenue","revenue_prev_q","revenue_prev_month","gross_annual","gross_q2","monthly_net_profit","gross_prev_q","gross_prev_month","payment_annual","payment_q2","payment_month","payment_prev_q","payment_prev_month","cost_annual","cost_q2","cost_month","cost_prev_q","cost_prev_month","expense_annual","expense_q2","expense_month","expense_prev_q","expense_prev_month","paid_annual","paid_q2","paid_month","paid_prev_q","paid_prev_month","cashflow_annual","cashflow_q2","cashflow_month","cashflow_prev_q","cashflow_prev_month","expense_paid_annual","expense_paid_q2","expense_paid_month","expense_paid_prev_q","expense_paid_prev_month","dept_cf_annual","dept_cf_q2","dept_cf_month","dept_cf_prev_q","dept_cf_prev_month","profit_annual","profit_q2","profit_month","profit_prev_q","profit_prev_month","proj_expense_annual","proj_expense_q2","proj_expense_month","proj_expense_prev_q","proj_expense_prev_month"]:
|
||||||
agg[key] = sum(m.get(key, 0) for m in all_metrics)
|
agg[key] = sum(m.get(key, 0) for m in all_metrics)
|
||||||
merged_monthly = []
|
merged_monthly = []
|
||||||
for i in range(12):
|
for i in range(12):
|
||||||
@@ -459,6 +481,7 @@ def bootstrap():
|
|||||||
def expense_sum(month_range):
|
def expense_sum(month_range):
|
||||||
total = 0
|
total = 0
|
||||||
for r in expense:
|
for r in expense:
|
||||||
|
if (r.get("status") or "") == "暂不计入": continue
|
||||||
m = (r.get("expense_month") or "").strip()
|
m = (r.get("expense_month") or "").strip()
|
||||||
if not m or "-" not in m: continue
|
if not m or "-" not in m: continue
|
||||||
try:
|
try:
|
||||||
@@ -471,6 +494,22 @@ def bootstrap():
|
|||||||
expense_month_val = expense_sum([_now_month])
|
expense_month_val = expense_sum([_now_month])
|
||||||
expense_prev_q = expense_sum(_prev_q_range)
|
expense_prev_q = expense_sum(_prev_q_range)
|
||||||
expense_prev_month = expense_sum([_prev_month]) if _prev_month else 0
|
expense_prev_month = expense_sum([_prev_month]) if _prev_month else 0
|
||||||
|
def expense_paid_sum(month_range):
|
||||||
|
total = 0
|
||||||
|
for r in expense:
|
||||||
|
if (r.get("status") or "") == "暂不计入": continue
|
||||||
|
m = (r.get("expense_month") or "").strip()
|
||||||
|
if not m or "-" not in m: continue
|
||||||
|
try:
|
||||||
|
if int(m.split("-")[1]) in month_range:
|
||||||
|
total += float(r.get("incurred_amount") or 0)
|
||||||
|
except: pass
|
||||||
|
return total
|
||||||
|
expense_paid_annual = expense_paid_sum(range(1, 13))
|
||||||
|
expense_paid_q2 = expense_paid_sum(_q_range)
|
||||||
|
expense_paid_month = expense_paid_sum([_now_month])
|
||||||
|
expense_paid_prev_q = expense_paid_sum(_prev_q_range)
|
||||||
|
expense_paid_prev_month = expense_paid_sum([_prev_month]) if _prev_month else 0
|
||||||
paid_annual = sum_expense("paid", range(1, 13))
|
paid_annual = sum_expense("paid", range(1, 13))
|
||||||
paid_q2 = sum_expense("paid", _q_range)
|
paid_q2 = sum_expense("paid", _q_range)
|
||||||
paid_month = sum_expense("paid", [_now_month])
|
paid_month = sum_expense("paid", [_now_month])
|
||||||
@@ -486,6 +525,11 @@ def bootstrap():
|
|||||||
cashflow_month = payment_month - paid_month
|
cashflow_month = payment_month - paid_month
|
||||||
cashflow_prev_q = payment_prev_q - paid_prev_q
|
cashflow_prev_q = payment_prev_q - paid_prev_q
|
||||||
cashflow_prev_month = payment_prev_month - paid_prev_month
|
cashflow_prev_month = payment_prev_month - paid_prev_month
|
||||||
|
dept_cf_annual = cashflow_annual - expense_paid_annual
|
||||||
|
dept_cf_q2 = cashflow_q2 - expense_paid_q2
|
||||||
|
dept_cf_month = cashflow_month - expense_paid_month
|
||||||
|
dept_cf_prev_q = cashflow_prev_q - expense_paid_prev_q
|
||||||
|
dept_cf_prev_month = cashflow_prev_month - expense_paid_prev_month
|
||||||
profit_annual = gross_annual - proj_expense_annual
|
profit_annual = gross_annual - proj_expense_annual
|
||||||
profit_q2 = gross_q2 - proj_expense_q2
|
profit_q2 = gross_q2 - proj_expense_q2
|
||||||
profit_month = gross_month - proj_expense_month
|
profit_month = gross_month - proj_expense_month
|
||||||
@@ -557,6 +601,16 @@ def bootstrap():
|
|||||||
"cashflow_month": cashflow_month,
|
"cashflow_month": cashflow_month,
|
||||||
"cashflow_prev_q": cashflow_prev_q,
|
"cashflow_prev_q": cashflow_prev_q,
|
||||||
"cashflow_prev_month": cashflow_prev_month,
|
"cashflow_prev_month": cashflow_prev_month,
|
||||||
|
"expense_paid_annual": expense_paid_annual,
|
||||||
|
"expense_paid_q2": expense_paid_q2,
|
||||||
|
"expense_paid_month": expense_paid_month,
|
||||||
|
"expense_paid_prev_q": expense_paid_prev_q,
|
||||||
|
"expense_paid_prev_month": expense_paid_prev_month,
|
||||||
|
"dept_cf_annual": dept_cf_annual,
|
||||||
|
"dept_cf_q2": dept_cf_q2,
|
||||||
|
"dept_cf_month": dept_cf_month,
|
||||||
|
"dept_cf_prev_q": dept_cf_prev_q,
|
||||||
|
"dept_cf_prev_month": dept_cf_prev_month,
|
||||||
"profit_annual": profit_annual,
|
"profit_annual": profit_annual,
|
||||||
"profit_q2": profit_q2,
|
"profit_q2": profit_q2,
|
||||||
"profit_month": profit_month,
|
"profit_month": profit_month,
|
||||||
|
|||||||
@@ -116,11 +116,11 @@ function renderFinance() {
|
|||||||
const finHeaderBase = `<span class="text-sm text-slate-500">视图:</span><select onchange="setFinView(this.value)" class="text-sm font-medium py-1 px-2.5 cursor-pointer" style="appearance:none;-webkit-appearance:none;background-color:transparent;border:0;outline:none;background:url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2212%22 height=%2212%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%236b7280%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpolyline points=%226 9 12 15 18 9%22%3E%3C/polyline%3E%3C/svg%3E') no-repeat right 4px center;padding-right:22px;min-height:30px"><option value="overview" ${state.finView==='overview'?'selected':''}>总视图</option><option value="quarterly" ${state.finView==='quarterly'?'selected':''}>季度视图</option><option value="monthly" ${state.finView==='monthly'?'selected':''}>月度视图</option></select>${toolDateSelect}`;
|
const finHeaderBase = `<span class="text-sm text-slate-500">视图:</span><select onchange="setFinView(this.value)" class="text-sm font-medium py-1 px-2.5 cursor-pointer" style="appearance:none;-webkit-appearance:none;background-color:transparent;border:0;outline:none;background:url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2212%22 height=%2212%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%236b7280%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpolyline points=%226 9 12 15 18 9%22%3E%3C/polyline%3E%3C/svg%3E') no-repeat right 4px center;padding-right:22px;min-height:30px"><option value="overview" ${state.finView==='overview'?'selected':''}>总视图</option><option value="quarterly" ${state.finView==='quarterly'?'selected':''}>季度视图</option><option value="monthly" ${state.finView==='monthly'?'selected':''}>月度视图</option></select>${toolDateSelect}`;
|
||||||
const finAddBtn = `<button class="btn btn-primary btn-sm" onclick="openFinanceModal()">新增财务项目</button>`;
|
const finAddBtn = `<button class="btn btn-primary btn-sm" onclick="openFinanceModal()">新增财务项目</button>`;
|
||||||
|
|
||||||
const finFilterTabs = `<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0"><button onclick="switchFinFilter('已签约')" class="finance-tab${state.finFilter==='已签约'?' active':''}" style="font-size:14px;font-weight:600">已签约 (${pfs.filter(x=>x.status==='已签约').length})</button><button onclick="switchFinFilter('待签约')" class="finance-tab${state.finFilter==='待签约'?' active':''}" style="font-size:14px;font-weight:600">待签约 (${pfs.filter(x=>x.status==='待签约').length})</button><button onclick="switchFinFilter('expense')" class="finance-tab${state.finFilter==='expense'?' active':''}" style="font-size:14px;font-weight:600">平台费用</button></div>`;
|
const finFilterTabs = `<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0"><button onclick="switchFinFilter('overview')" class="finance-tab${state.finFilter==='overview'?' active':''}" style="font-size:14px;font-weight:600">总览</button><button onclick="switchFinFilter('已签约')" class="finance-tab${state.finFilter==='已签约'?' active':''}" style="font-size:14px;font-weight:600">已签约 (${pfs.filter(x=>x.status==='已签约').length})</button><button onclick="switchFinFilter('待签约')" class="finance-tab${state.finFilter==='待签约'?' active':''}" style="font-size:14px;font-weight:600">待签约 (${pfs.filter(x=>x.status==='待签约').length})</button><button onclick="switchFinFilter('expense')" class="finance-tab${state.finFilter==='expense'?' active':''}" style="font-size:14px;font-weight:600">平台费用</button></div>`;
|
||||||
|
|
||||||
document.querySelector("#finance").innerHTML = `<div class="grid gap-4">
|
document.querySelector("#finance").innerHTML = `<div class="grid gap-4">
|
||||||
${finFilterTabs}
|
${finFilterTabs}
|
||||||
${state.finFilter !== 'expense' ? card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "p-4") : ''}
|
${state.finFilter !== 'expense' && state.finFilter !== 'overview' ? card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "p-4") : ''}
|
||||||
<div id="financeModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closeFinanceModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="financeModalTitle">新增项目财务</h3><p class="text-xs text-slate-400 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="financeDeleteBtn" onclick="deleteFinanceItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeFinanceModal()"><i data-lucide="x"></i></button></div></div>
|
<div id="financeModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closeFinanceModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="financeModalTitle">新增项目财务</h3><p class="text-xs text-slate-400 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="financeDeleteBtn" onclick="deleteFinanceItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeFinanceModal()"><i data-lucide="x"></i></button></div></div>
|
||||||
<div class="finance-tabs">
|
<div class="finance-tabs">
|
||||||
<button class="finance-tab active" data-tab="info" onclick="switchFinanceTab('info')"><i data-lucide="info" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>基本信息</button>
|
<button class="finance-tab active" data-tab="info" onclick="switchFinanceTab('info')"><i data-lucide="info" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>基本信息</button>
|
||||||
@@ -237,6 +237,10 @@ function renderFinance() {
|
|||||||
</div>
|
</div>
|
||||||
</div><div class="flex justify-end gap-3 pt-2 finance-form-actions"><button type="button" class="btn btn-ghost btn-sm px-6" onclick="closeFinanceModal()">取消</button><button type="submit" class="btn btn-primary btn-sm px-8">保存</button></div></form></div></div>
|
</div><div class="flex justify-end gap-3 pt-2 finance-form-actions"><button type="button" class="btn btn-ghost btn-sm px-6" onclick="closeFinanceModal()">取消</button><button type="submit" class="btn btn-primary btn-sm px-8">保存</button></div></form></div></div>
|
||||||
${(() => {
|
${(() => {
|
||||||
|
if (state.finFilter === 'overview') {
|
||||||
|
setTimeout(() => renderFinanceOverview(), 10);
|
||||||
|
return `<div id="financeOverview"></div>`;
|
||||||
|
}
|
||||||
if (state.finFilter === 'expense') {
|
if (state.finFilter === 'expense') {
|
||||||
setTimeout(() => renderExpense(), 10);
|
setTimeout(() => renderExpense(), 10);
|
||||||
return `<div id="financeExpense"></div>`;
|
return `<div id="financeExpense"></div>`;
|
||||||
@@ -876,4 +880,95 @@ window.deleteFinanceItem = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast("删除失败:" + error.message, "error");
|
toast("删除失败:" + error.message, "error");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 总览渲染
|
||||||
|
function renderFinanceOverview() {
|
||||||
|
const { summary, financeMonthly } = state.data;
|
||||||
|
if (!summary) return;
|
||||||
|
const m = summary.metrics;
|
||||||
|
const moneyIntL = (v) => `${Math.round(Number(v || 0)).toLocaleString("zh-CN")} 元`;
|
||||||
|
const moneyWan = (v) => {
|
||||||
|
const n = Number(v || 0);
|
||||||
|
return n >= 10000 ? (n / 10000).toFixed(0) + "万" : n.toLocaleString("zh-CN");
|
||||||
|
};
|
||||||
|
const card = (body, cls) => '<div class="card ' + (cls || 'p-4') + '">' + body + '</div>';
|
||||||
|
|
||||||
|
const rows1 = [["年度累计", moneyIntL(m.signed_annual || m.signed_amount)], ["本季度累计", moneyIntL(m.signed_q2 || 0)], ["本月累计", moneyIntL(m.signed_month || 0)], ["上本季度累计", moneyIntL(m.signed_prev_q || 0)], ["上月累计", moneyIntL(m.signed_prev_month || 0)]];
|
||||||
|
const rows2 = [["年度累计", moneyIntL(m.revenue_annual || 0)], ["本季度累计", moneyIntL(m.revenue_q2 || 0)], ["本月累计", moneyIntL(m.monthly_revenue || 0)], ["上本季度累计", moneyIntL(m.revenue_prev_q || 0)], ["上月累计", moneyIntL(m.revenue_prev_month || 0)]];
|
||||||
|
const rows3 = [["年度累计", moneyIntL(m.gross_annual || 0)], ["本季度累计", moneyIntL(m.gross_q2 || 0)], ["本月累计", moneyIntL(m.monthly_net_profit || 0)], ["上本季度累计", moneyIntL(m.gross_prev_q || 0)], ["上月累计", moneyIntL(m.gross_prev_month || 0)]];
|
||||||
|
const rows4 = [["年度累计", moneyIntL(m.cost_annual || 0)], ["本季度累计", moneyIntL(m.cost_q2 || 0)], ["本月累计", moneyIntL(m.cost_month || 0)], ["上本季度累计", moneyIntL(m.cost_prev_q || 0)], ["上月累计", moneyIntL(m.cost_prev_month || 0)]];
|
||||||
|
const rows5 = [["年度累计", moneyIntL(m.profit_annual || 0)], ["本季度累计", moneyIntL(m.profit_q2 || 0)], ["本月累计", moneyIntL(m.profit_month || 0)], ["上本季度累计", moneyIntL(m.profit_prev_q || 0)], ["上月累计", moneyIntL(m.profit_prev_month || 0)]];
|
||||||
|
const rows7 = [["年度累计", moneyIntL(m.payment_annual || 0)], ["本季度累计", moneyIntL(m.payment_q2 || 0)], ["本月累计", moneyIntL(m.payment_month || 0)], ["上本季度累计", moneyIntL(m.payment_prev_q || 0)], ["上月累计", moneyIntL(m.payment_prev_month || 0)]];
|
||||||
|
const rows8 = [["年度累计", moneyIntL(m.paid_annual || 0)], ["本季度累计", moneyIntL(m.paid_q2 || 0)], ["本月累计", moneyIntL(m.paid_month || 0)], ["上本季度累计", moneyIntL(m.paid_prev_q || 0)], ["上月累计", moneyIntL(m.paid_prev_month || 0)]];
|
||||||
|
const rows9 = [["年度累计", moneyIntL(m.cashflow_annual || 0)], ["本季度累计", moneyIntL(m.cashflow_q2 || 0)], ["本月累计", moneyIntL(m.cashflow_month || 0)], ["上本季度累计", moneyIntL(m.cashflow_prev_q || 0)], ["上月累计", moneyIntL(m.cashflow_prev_month || 0)]];
|
||||||
|
|
||||||
|
const qoq = (cur, prev) => { if (!prev) return '<span class="text-slate-300">—</span>'; const pct = Math.round((cur - prev) / prev * 100); const cls = pct >= 0 ? 'text-green-600' : 'text-red-600'; const sign = pct >= 0 ? '+' : ''; return '<span class="' + cls + '">' + sign + pct + '%</span>'; };
|
||||||
|
const LABELS = ['合同金额','确收金额','确收毛利','成本','项目利润','回款金额','已付','现金流'];
|
||||||
|
const Q_FIELDS = [m.signed_q2||0, m.revenue_q2, m.gross_q2, m.cost_q2||0, m.profit_q2||0, m.payment_q2||0, m.paid_q2||0, m.cashflow_q2||0];
|
||||||
|
const Q_PREV = [m.signed_prev_q||0, m.revenue_prev_q||0, m.gross_prev_q||0, m.cost_prev_q||0, m.profit_prev_q||0, m.payment_prev_q||0, m.paid_prev_q||0, m.cashflow_prev_q||0];
|
||||||
|
const M_FIELDS = [m.signed_month||0, m.monthly_revenue, m.monthly_net_profit, m.cost_month||0, m.profit_month||0, m.payment_month||0, m.paid_month||0, m.cashflow_month||0];
|
||||||
|
const M_PREV = [m.signed_prev_month||0, m.revenue_prev_month||0, m.gross_prev_month||0, m.cost_prev_month||0, m.profit_prev_month||0, m.payment_prev_month||0, m.paid_prev_month||0, m.cashflow_prev_month||0];
|
||||||
|
const ROWS = [rows1, rows2, rows3, rows4, rows5, rows7, rows8, rows9];
|
||||||
|
const CF_RAW = [m.cashflow_annual||0, m.cashflow_q2||0, m.cashflow_month||0, m.cashflow_prev_q||0, m.cashflow_prev_month||0];
|
||||||
|
const PF_RAW = [m.profit_annual||0, m.profit_q2||0, m.profit_month||0, m.profit_prev_q||0, m.profit_prev_month||0];
|
||||||
|
|
||||||
|
var tdVal = function(ri, col, val) {
|
||||||
|
if (ri === 4) return '<td class="py-2 text-right font-semibold ' + (PF_RAW[col] >= 0 ? 'text-green-600' : 'text-red-600') + '">' + val + '</td>';
|
||||||
|
if (ri === 7) return '<td class="py-2 text-right font-semibold ' + (CF_RAW[col] >= 0 ? 'text-green-600' : 'text-red-600') + '">' + val + '</td>';
|
||||||
|
return '<td class="py-2 text-right font-semibold text-slate-800">' + val + '</td>';
|
||||||
|
};
|
||||||
|
var thead = '<tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500">指标</th><th class="py-2 text-right font-semibold text-slate-700">年度累计</th><th class="py-2 text-right font-semibold text-slate-700">上季度累计</th><th class="py-2 text-right font-semibold text-slate-700">上月累计</th><th class="py-2 text-right font-semibold text-slate-700">本季度累计</th><th class="py-2 text-right font-semibold text-slate-400">季环比</th><th class="py-2 text-right font-semibold text-slate-700">本月累计</th><th class="py-2 text-right font-semibold text-slate-400">月环比</th></tr>';
|
||||||
|
var tbody = '';
|
||||||
|
for (var ri = 0; ri < 8; ri++) {
|
||||||
|
var vals = ROWS[ri];
|
||||||
|
tbody += '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left font-semibold text-slate-700">' + LABELS[ri] + '</td>' +
|
||||||
|
tdVal(ri, 0, vals[0][1]) + tdVal(ri, 3, vals[3][1]) + tdVal(ri, 4, vals[4][1]) + tdVal(ri, 1, vals[1][1]) +
|
||||||
|
'<td class="py-2 text-right">' + qoq(Q_FIELDS[ri], Q_PREV[ri]) + '</td>' +
|
||||||
|
tdVal(ri, 2, vals[2][1]) +
|
||||||
|
'<td class="py-2 text-right">' + qoq(M_FIELDS[ri], M_PREV[ri]) + '</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector("#financeOverview").innerHTML = '<div class="grid gap-5">' +
|
||||||
|
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
|
||||||
|
'<div class="grid grid-cols-4 gap-5">' +
|
||||||
|
card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartSign"></canvas></div>', 'p-4') +
|
||||||
|
card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartRev"></canvas></div>', 'p-4') +
|
||||||
|
card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度回款与已付</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartCash"></canvas></div>', 'p-4') +
|
||||||
|
card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度项目利润</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartProfit"></canvas></div>', 'p-4') +
|
||||||
|
'</div></div>';
|
||||||
|
|
||||||
|
if (window.lucide) window.lucide.createIcons();
|
||||||
|
// 渲染图表(复用 home.js 的 moneyTick / chartOptions / monthLabels)
|
||||||
|
if (financeMonthly && window.Chart) {
|
||||||
|
renderOverviewCharts(financeMonthly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOverviewCharts(data) {
|
||||||
|
if (typeof monthLabels !== 'function') return;
|
||||||
|
const labels = monthLabels(data);
|
||||||
|
const baseOpts = typeof chartOptions === 'function' ? chartOptions(moneyTick) : { responsive: true, maintainAspectRatio: false };
|
||||||
|
var iconf = { type: "line", options: baseOpts, data: { labels: labels } };
|
||||||
|
|
||||||
|
var c1 = document.querySelector("#finChartSign");
|
||||||
|
if (c1 && window.Chart) {
|
||||||
|
if (state.finChart1) state.finChart1.destroy();
|
||||||
|
state.finChart1 = new Chart(c1, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "签约金额", data: data.map(function(x) { return x.sign || 0; }), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 }] } }));
|
||||||
|
}
|
||||||
|
var c2 = document.querySelector("#finChartRev");
|
||||||
|
if (c2 && window.Chart) {
|
||||||
|
if (state.finChart2) state.finChart2.destroy();
|
||||||
|
state.finChart2 = new Chart(c2, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "确收", data: data.map(function(x) { return x.revenue || 0; }), borderColor: "#2563eb", backgroundColor: "rgba(37,99,235,0.06)", fill: true, tension: 0.3 }, { label: "毛利", data: data.map(function(x) { return x.gross || 0; }), borderColor: "#059669", backgroundColor: "rgba(5,150,105,0.06)", fill: true, tension: 0.3 }] } }));
|
||||||
|
}
|
||||||
|
var c3 = document.querySelector("#finChartCash");
|
||||||
|
if (c3 && window.Chart) {
|
||||||
|
if (state.finChart3) state.finChart3.destroy();
|
||||||
|
state.finChart3 = new Chart(c3, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "回款", data: data.map(function(x) { return x.payment || 0; }), borderColor: "#d97706", backgroundColor: "rgba(217,119,6,0.06)", fill: true, tension: 0.3 }, { label: "已付", data: data.map(function(x) { return x.cost || 0; }), borderColor: "#7c3aed", backgroundColor: "rgba(124,58,237,0.06)", fill: true, tension: 0.3 }] } }));
|
||||||
|
}
|
||||||
|
var c4 = document.querySelector("#finChartProfit");
|
||||||
|
if (c4 && window.Chart) {
|
||||||
|
if (state.finChart4) state.finChart4.destroy();
|
||||||
|
state.finChart4 = new Chart(c4, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "利润", data: data.map(function(x) { return x.gross || 0; }), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 }] } }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ function renderHome() {
|
|||||||
// 回款提醒
|
// 回款提醒
|
||||||
const yrRev = m.revenue_annual || 0, yrPay = m.payment_annual || 0, yrUnpaid = yrRev - yrPay;
|
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 yrPayRate = yrRev > 0 ? Math.round(yrPay / yrRev * 100) : 0;
|
||||||
const yrUnpaidWan = Math.round(yrUnpaid / 10000);
|
const yrUnpaidWan = Math.round(Math.abs(yrUnpaid) / 10000);
|
||||||
const payRateCls = yrPayRate < 30 ? 'text-red-600' : yrPayRate < 80 ? 'text-amber-600' : 'text-green-600';
|
const payRateCls = yrPayRate < 30 ? 'text-red-600' : yrPayRate < 80 ? 'text-amber-600' : 'text-green-600';
|
||||||
|
const unpaidLabel = yrUnpaid >= 0
|
||||||
|
? '你还有 <strong class=\"text-red-600\">' + yrUnpaidWan + '万</strong> 未回款'
|
||||||
|
: '超额回款 <strong class=\"text-green-600\">' + yrUnpaidWan + '万</strong>';
|
||||||
const payReminder = yrRev > 0 ? '<div class=\"flex items-center gap-3 px-4 py-3 rounded-lg border\" style=\"background:linear-gradient(135deg,#fef3c7,#fde68a);border-color:#f59e0b\">' +
|
const payReminder = yrRev > 0 ? '<div class=\"flex items-center gap-3 px-4 py-3 rounded-lg border\" style=\"background:linear-gradient(135deg,#fef3c7,#fde68a);border-color:#f59e0b\">' +
|
||||||
'<i data-lucide=\"bell-ring\" class=\"text-amber-600 flex-shrink-0\" style=\"width:18px;height:18px\"></i>' +
|
'<i data-lucide=\"bell-ring\" class=\"text-amber-600 flex-shrink-0\" style=\"width:18px;height:18px\"></i>' +
|
||||||
'<span class=\"text-sm text-amber-900\"><strong>回款提醒:</strong>本年度确收 <strong>' + moneyInt(yrRev) + '</strong>,回款 <strong>' + moneyInt(yrPay) + '</strong>,你还有 <strong class=\"text-red-600\">' + yrUnpaidWan + '万</strong> 未回款,当前回款率:<strong class=\"' + payRateCls + '\">' + yrPayRate + '%</strong></span>' +
|
'<span class=\"text-sm text-amber-900\"><strong>回款提醒:</strong>本年度确收 <strong>' + moneyInt(yrRev) + '</strong>,回款 <strong>' + moneyInt(yrPay) + '</strong>,' + unpaidLabel + ',当前回款率:<strong class=\"' + payRateCls + '\">' + yrPayRate + '%</strong></span>' +
|
||||||
'</div>' : '';
|
'</div>' : '';
|
||||||
const rows1 = [
|
const rows1 = [
|
||||||
["年度累计", moneyInt(m.signed_annual || m.signed_amount)],
|
["年度累计", moneyInt(m.signed_annual || m.signed_amount)],
|
||||||
@@ -123,10 +126,9 @@ function renderHome() {
|
|||||||
].map(([label, value, tab]) => `<button class="metric-card" onclick="switchTab('${tab}')"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="gauge"></i>${label}</span><strong class="mt-2 block text-2xl">${value}</strong></button>`).join("")}
|
].map(([label, value, tab]) => `<button class="metric-card" onclick="switchTab('${tab}')"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="gauge"></i>${label}</span><strong class="mt-2 block text-2xl">${value}</strong></button>`).join("")}
|
||||||
</div>`}
|
</div>`}
|
||||||
${payReminder}
|
${payReminder}
|
||||||
${card(`<h3 class="text-sm font-bold text-slate-700 mb-3">部门经营情况</h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500">指标</th><th class="py-2 text-right font-semibold text-slate-700">年度累计</th><th class="py-2 text-right font-semibold text-slate-700">上季度累计</th><th class="py-2 text-right font-semibold text-slate-700">上月累计</th><th class="py-2 text-right font-semibold text-slate-700">本季度累计</th><th class="py-2 text-right font-semibold text-slate-400">季环比</th><th class="py-2 text-right font-semibold text-slate-700">本月累计</th><th class="py-2 text-right font-semibold text-slate-400">月环比</th></tr></thead><tbody>
|
${(() => {
|
||||||
${(() => {
|
|
||||||
var d = [
|
var d = [
|
||||||
['部门毛利', [m.profit_annual||0, m.profit_q2||0, m.profit_month||0, m.profit_prev_q||0, m.profit_prev_month||0],
|
['项目毛利', [m.profit_annual||0, m.profit_q2||0, m.profit_month||0, m.profit_prev_q||0, m.profit_prev_month||0],
|
||||||
[m.profit_q2||0,m.profit_prev_q||0], [m.profit_month||0,m.profit_prev_month||0]],
|
[m.profit_q2||0,m.profit_prev_q||0], [m.profit_month||0,m.profit_prev_month||0]],
|
||||||
['部门费用', [m.expense_annual||0, m.expense_q2||0, (m.expense_month||0), m.expense_prev_q||0, m.expense_prev_month||0],
|
['部门费用', [m.expense_annual||0, m.expense_q2||0, (m.expense_month||0), m.expense_prev_q||0, m.expense_prev_month||0],
|
||||||
[m.expense_q2||0,m.expense_prev_q||0], [(m.expense_month||0),m.expense_prev_month||0]],
|
[m.expense_q2||0,m.expense_prev_q||0], [(m.expense_month||0),m.expense_prev_month||0]],
|
||||||
@@ -137,35 +139,38 @@ function renderHome() {
|
|||||||
(m.profit_q2||0)-(m.expense_q2||0), (m.profit_prev_q||0)-(m.expense_prev_q||0)
|
(m.profit_q2||0)-(m.expense_q2||0), (m.profit_prev_q||0)-(m.expense_prev_q||0)
|
||||||
], [
|
], [
|
||||||
(m.profit_month||0)-(m.expense_month||0), (m.profit_prev_month||0)-(m.expense_prev_month||0)
|
(m.profit_month||0)-(m.expense_month||0), (m.profit_prev_month||0)-(m.expense_prev_month||0)
|
||||||
]]
|
]],
|
||||||
|
['项目现金流', [m.cashflow_annual||0, m.cashflow_q2||0, m.cashflow_month||0, m.cashflow_prev_q||0, m.cashflow_prev_month||0],
|
||||||
|
[m.cashflow_q2||0,m.cashflow_prev_q||0], [m.cashflow_month||0,m.cashflow_prev_month||0]],
|
||||||
|
['部门费用流出', [m.expense_paid_annual||0, m.expense_paid_q2||0, m.expense_paid_month||0, m.expense_paid_prev_q||0, m.expense_paid_prev_month||0],
|
||||||
|
[m.expense_paid_q2||0,m.expense_paid_prev_q||0], [m.expense_paid_month||0,m.expense_paid_prev_month||0]],
|
||||||
|
['部门现金流', [m.dept_cf_annual||0, m.dept_cf_q2||0, m.dept_cf_month||0, m.dept_cf_prev_q||0, m.dept_cf_prev_month||0],
|
||||||
|
[m.dept_cf_q2||0,m.dept_cf_prev_q||0], [m.dept_cf_month||0,m.dept_cf_prev_month||0]]
|
||||||
];
|
];
|
||||||
var h = '';
|
var buildDeptTable = function(title, dataRows) {
|
||||||
var netCls = function(di, raw) { return di === 2 ? (raw >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-800'; };
|
var h = '';
|
||||||
for (var di = 0; di < 3; di++) {
|
for (var di = 0; di < dataRows.length; di++) {
|
||||||
var r = d[di];
|
var r = dataRows[di];
|
||||||
h += '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left font-semibold text-slate-700">' + r[0] + '</td>' +
|
var rawNet = r[1][0];
|
||||||
'<td class="py-2 text-right font-semibold ' + netCls(di, r[1][0]) + '">' + moneyInt(r[1][0]) + '</td>' +
|
var isNet = r[0] === '部门净利' || r[0] === '部门现金流';
|
||||||
'<td class="py-2 text-right font-semibold ' + netCls(di, r[1][3]) + '">' + moneyInt(r[1][3]) + '</td>' +
|
var isCF = r[0] === '项目现金流';
|
||||||
'<td class="py-2 text-right font-semibold ' + netCls(di, r[1][4]) + '">' + moneyInt(r[1][4]) + '</td>' +
|
var cls = isNet ? (rawNet >= 0 ? 'text-green-600' : 'text-red-600') : isCF ? (rawNet >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-800';
|
||||||
'<td class="py-2 text-right font-semibold ' + netCls(di, r[1][1]) + '">' + moneyInt(r[1][1]) + '</td>' +
|
h += '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left font-semibold text-slate-700">' + r[0] + '</td>' +
|
||||||
'<td class="py-2 text-right">' + qoq(r[2][0], r[2][1]) + '</td>' +
|
'<td class="py-2 text-right font-semibold ' + cls + '">' + moneyInt(r[1][0]) + '</td>' +
|
||||||
'<td class="py-2 text-right font-semibold ' + netCls(di, r[1][2]) + '">' + moneyInt(r[1][2]) + '</td>' +
|
'<td class="py-2 text-right font-semibold ' + cls + '">' + moneyInt(r[1][3]) + '</td>' +
|
||||||
'<td class="py-2 text-right">' + qoq(r[3][0], r[3][1]) + '</td></tr>';
|
'<td class="py-2 text-right font-semibold ' + cls + '">' + moneyInt(r[1][4]) + '</td>' +
|
||||||
}
|
'<td class="py-2 text-right font-semibold ' + cls + '">' + moneyInt(r[1][1]) + '</td>' +
|
||||||
return h;
|
'<td class="py-2 text-right">' + qoq(r[2][0], r[2][1]) + '</td>' +
|
||||||
|
'<td class="py-2 text-right font-semibold ' + cls + '">' + moneyInt(r[1][2]) + '</td>' +
|
||||||
|
'<td class="py-2 text-right">' + qoq(r[3][0], r[3][1]) + '</td></tr>';
|
||||||
|
}
|
||||||
|
return card('<h3 class="text-sm font-bold text-slate-700 mb-3">' + title + '</h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500">指标</th><th class="py-2 text-right font-semibold text-slate-700">年度累计</th><th class="py-2 text-right font-semibold text-slate-700">上季度累计</th><th class="py-2 text-right font-semibold text-slate-700">上月累计</th><th class="py-2 text-right font-semibold text-slate-700">本季度累计</th><th class="py-2 text-right font-semibold text-slate-400">季环比</th><th class="py-2 text-right font-semibold text-slate-700">本月累计</th><th class="py-2 text-right font-semibold text-slate-400">月环比</th></tr></thead><tbody>' + h + '</tbody></table></div>', 'p-4');
|
||||||
|
};
|
||||||
|
return buildDeptTable('部门经营情况', d.slice(0, 3)) + buildDeptTable('部门现金流', d.slice(3, 6));
|
||||||
})()}
|
})()}
|
||||||
</tbody></table></div>`, "p-4")}
|
|
||||||
${card(`<h3 class="text-sm font-bold text-slate-700">业务(项目)财务概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>${thead}</thead><tbody>${tbody}</tbody></table></div>`, "p-4")}
|
|
||||||
<div class="grid grid-cols-4 gap-5">
|
|
||||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartSign"></canvas></div>`, "p-4")}
|
|
||||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartRev"></canvas></div>`, "p-4")}
|
|
||||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度回款与已付</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartCash"></canvas></div>`, "p-4")}
|
|
||||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度项目利润</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartProfit"></canvas></div>`, "p-4")}
|
|
||||||
</div>
|
|
||||||
${card(`<h2 class="text-lg font-bold">近期动态</h2><div class="mt-4 grid gap-2">${summary.recent.map((r) => `<div class="flex items-start justify-between rounded-md bg-slate-50 px-3 py-2 text-sm group"><span class="break-words">${r.content}</span><div class="flex items-center gap-2 flex-shrink-0 ml-2"><span class="text-xs text-slate-400">${r.followed_at}</span><button class="btn btn-ghost btn-sm text-red-400 opacity-0 group-hover:opacity-100 p-0 w-5 h-5" onclick="event.preventDefault();deleteActivity(${r.id})" title="删除动态"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></div></div>`).join("")}</div>`, "p-5")}
|
${card(`<h2 class="text-lg font-bold">近期动态</h2><div class="mt-4 grid gap-2">${summary.recent.map((r) => `<div class="flex items-start justify-between rounded-md bg-slate-50 px-3 py-2 text-sm group"><span class="break-words">${r.content}</span><div class="flex items-center gap-2 flex-shrink-0 ml-2"><span class="text-xs text-slate-400">${r.followed_at}</span><button class="btn btn-ghost btn-sm text-red-400 opacity-0 group-hover:opacity-100 p-0 w-5 h-5" onclick="event.preventDefault();deleteActivity(${r.id})" title="删除动态"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></div></div>`).join("")}</div>`, "p-5")}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
renderCharts(financeMonthly);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function chartOptions(yCallback) {
|
function chartOptions(yCallback) {
|
||||||
|
|||||||
@@ -45,9 +45,9 @@
|
|||||||
<div class="w-8 h-px bg-slate-700 my-2"></div>
|
<div class="w-8 h-px bg-slate-700 my-2"></div>
|
||||||
<!-- 导航 Tab 图标 -->
|
<!-- 导航 Tab 图标 -->
|
||||||
<div class="flex flex-col items-center gap-1 w-14" id="sidebarTabs">
|
<div class="flex flex-col items-center gap-1 w-14" id="sidebarTabs">
|
||||||
<div class="sidebar-tab active" data-tab="home" onclick="switchTab('home')" title="首页">
|
<div class="sidebar-tab active" data-tab="home" onclick="switchTab('home')" title="部门">
|
||||||
<i data-lucide="home" style="width:20px;height:20px"></i>
|
<i data-lucide="trending-up" style="width:20px;height:20px"></i>
|
||||||
<span class="text-[10px] mt-1">首页</span>
|
<span class="text-[10px] mt-1">部门</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-tab" data-tab="finance" onclick="switchTab('finance')" title="业务">
|
<div class="sidebar-tab" data-tab="finance" onclick="switchTab('finance')" title="业务">
|
||||||
<i data-lucide="folder-open-dot" style="width:20px;height:20px"></i>
|
<i data-lucide="folder-open-dot" style="width:20px;height:20px"></i>
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
||||||
<div class="flex items-center gap-3 w-full">
|
<div class="flex items-center gap-3 w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.0.0.5</span></p>
|
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.0.0.14</span></p>
|
||||||
<div class="flex items-center gap-4 mt-1">
|
<div class="flex items-center gap-4 mt-1">
|
||||||
<h1 class="text-2xl font-semibold" id="workspaceTitle">科普 OPC 工作台</h1>
|
<h1 class="text-2xl font-semibold" id="workspaceTitle">科普 OPC 工作台</h1>
|
||||||
<div class="hidden sm:flex items-center gap-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-full px-4 py-1.5">
|
<div class="hidden sm:flex items-center gap-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-full px-4 py-1.5">
|
||||||
|
|||||||
Reference in New Issue
Block a user