This commit is contained in:
@@ -63,10 +63,11 @@ def attach_common(conn, resource, items):
|
|||||||
def monthly_finance(conn, tenant="科普·无界"):
|
def monthly_finance(conn, tenant="科普·无界"):
|
||||||
months = [f"2026-{m:02d}" for m in range(1, 13)]
|
months = [f"2026-{m:02d}" for m in range(1, 13)]
|
||||||
pfs = rows(conn,
|
pfs = rows(conn,
|
||||||
"SELECT sign_amount, sign_month, status, budget_data FROM project_finances WHERE tenant=? AND status='已签约'",
|
"SELECT sign_amount, sign_month, status, budget_data, expense_data FROM project_finances WHERE tenant=? AND status='已签约'",
|
||||||
[tenant])
|
[tenant])
|
||||||
|
|
||||||
parsed_budgets = []
|
parsed_budgets = []
|
||||||
|
expense_maps = {}
|
||||||
for pf in pfs:
|
for pf in pfs:
|
||||||
try:
|
try:
|
||||||
budget = json.loads(pf.get("budget_data") or "[]")
|
budget = json.loads(pf.get("budget_data") or "[]")
|
||||||
@@ -79,11 +80,23 @@ def monthly_finance(conn, tenant="科普·无界"):
|
|||||||
"rev": float(b.get("rev") or 0),
|
"rev": float(b.get("rev") or 0),
|
||||||
"gross": float(b.get("gross") or 0),
|
"gross": float(b.get("gross") or 0),
|
||||||
"payment": float(b.get("payment") or 0),
|
"payment": float(b.get("payment") or 0),
|
||||||
"cost": float(b.get("cost") or 0),
|
|
||||||
"paid": float(b.get("paid") or 0),
|
|
||||||
}
|
}
|
||||||
parsed_budgets.append((pf, budget_map))
|
parsed_budgets.append((pf, budget_map))
|
||||||
|
|
||||||
|
# Parse expense_data for cost/paid
|
||||||
|
try:
|
||||||
|
ed = json.loads(pf.get("expense_data") or "[]")
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
ed = []
|
||||||
|
em = {}
|
||||||
|
for e in ed:
|
||||||
|
key = (e.get("month") or "").replace("-", "_")
|
||||||
|
em[key] = {
|
||||||
|
"cost": float(e.get("cost") or 0),
|
||||||
|
"paid": float(e.get("paid") or 0),
|
||||||
|
}
|
||||||
|
expense_maps[pf.get("id", id(pf))] = em
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
for month in months:
|
for month in months:
|
||||||
key = month.replace("-", "_")
|
key = month.replace("-", "_")
|
||||||
@@ -96,8 +109,11 @@ def monthly_finance(conn, tenant="科普·无界"):
|
|||||||
revenue += b["rev"]
|
revenue += b["rev"]
|
||||||
gross += b["gross"]
|
gross += b["gross"]
|
||||||
payment += b["payment"]
|
payment += b["payment"]
|
||||||
cost += b["cost"]
|
em = expense_maps.get(pf.get("id", id(pf)), {})
|
||||||
paid += b["paid"]
|
e = em.get(key)
|
||||||
|
if e:
|
||||||
|
cost += e["cost"]
|
||||||
|
paid += e["paid"]
|
||||||
data.append({
|
data.append({
|
||||||
"month": month,
|
"month": month,
|
||||||
"revenue": round(revenue, 2),
|
"revenue": round(revenue, 2),
|
||||||
|
|||||||
@@ -18,6 +18,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
|
||||||
|
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
|
||||||
|
|
||||||
migrate_create_tables()
|
migrate_create_tables()
|
||||||
@@ -26,5 +27,6 @@ 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_data_split()
|
||||||
migrate_seed_users()
|
migrate_seed_users()
|
||||||
migrate_seed_demo_data()
|
migrate_seed_demo_data()
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ def migrate_add_columns():
|
|||||||
_add_column_if_missing(conn, "expense_records", "incurred_amount",
|
_add_column_if_missing(conn, "expense_records", "incurred_amount",
|
||||||
"ALTER TABLE expense_records ADD COLUMN incurred_amount DOUBLE NOT NULL DEFAULT 0")
|
"ALTER TABLE expense_records ADD COLUMN incurred_amount DOUBLE NOT NULL DEFAULT 0")
|
||||||
|
|
||||||
|
# project_finances 费用明细 JSON 数据
|
||||||
|
_add_column_if_missing(conn, "project_finances", "expense_data",
|
||||||
|
"ALTER TABLE project_finances ADD COLUMN expense_data TEXT")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print("[migrate] 加列迁移完成")
|
print("[migrate] 加列迁移完成")
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
56
backend/migrations/data_split.py
Normal file
56
backend/migrations/data_split.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
"""migrations/data_split.py — 预算数据拆分迁移:budget_data → expense_data
|
||||||
|
|
||||||
|
旧结构:budget_data = [{month, rev, gross, payment, cost, paid}]
|
||||||
|
新结构:expense_data = [{month, expense_type, cost, paid, exp_note}]
|
||||||
|
|
||||||
|
部署时自动:检测未拆分的旧数据,将 cost/paid 从 budget_data 迁移到 expense_data。
|
||||||
|
幂等:只处理 expense_data 为空的记录。
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_data_split():
|
||||||
|
"""一次性数据拆分迁移"""
|
||||||
|
from db import db, _exec
|
||||||
|
import json
|
||||||
|
|
||||||
|
conn = db()
|
||||||
|
try:
|
||||||
|
# 查询未迁移的记录
|
||||||
|
cur = conn.cursor(dictionary=True)
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id, budget_data, expense_data FROM project_finances "
|
||||||
|
"WHERE (expense_data IS NULL OR expense_data = '' OR expense_data = '[]') "
|
||||||
|
"AND budget_data IS NOT NULL AND budget_data != '' AND budget_data != '[]'"
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
migrated = 0
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
budget = json.loads(row.get("budget_data") or "[]")
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
expense_rows = []
|
||||||
|
for b in budget:
|
||||||
|
expense_rows.append({
|
||||||
|
"month": b.get("month", ""),
|
||||||
|
"expense_type": "",
|
||||||
|
"cost": float(b.get("cost") or 0),
|
||||||
|
"paid": float(b.get("paid") or 0),
|
||||||
|
"exp_note": "",
|
||||||
|
})
|
||||||
|
|
||||||
|
if expense_rows:
|
||||||
|
_exec(
|
||||||
|
conn,
|
||||||
|
"UPDATE project_finances SET expense_data = ? WHERE id = ?",
|
||||||
|
(json.dumps(expense_rows, ensure_ascii=False), row["id"]),
|
||||||
|
)
|
||||||
|
migrated += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print(f"[migrate] 数据拆分迁移完成:{migrated} 条记录已从 budget_data 提取 cost/paid 到 expense_data")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
@@ -23,7 +23,7 @@ TABLES = {
|
|||||||
"products": ("product_versions", ["product_name", "version", "version_goal", "priority", "start_date", "plan_date", "dev_done_date", "test_date", "launch_date", "status", "notes", "tenant"]),
|
"products": ("product_versions", ["product_name", "version", "version_goal", "priority", "start_date", "plan_date", "dev_done_date", "test_date", "launch_date", "status", "notes", "tenant"]),
|
||||||
"finance": ("finance_records", ["month", "project_name", "record_type", "category", "amount", "occurred_date", "notes", "tenant"]),
|
"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"]),
|
"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"]),
|
"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", "tenant"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,6 +266,26 @@ def bootstrap():
|
|||||||
if b:
|
if b:
|
||||||
total += float(b.get(field) or 0)
|
total += float(b.get(field) or 0)
|
||||||
return total
|
return total
|
||||||
|
|
||||||
|
def t_parse_expense(pf):
|
||||||
|
try:
|
||||||
|
ed = json.loads(pf.get("expense_data") or "[]")
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
ed = []
|
||||||
|
return {(e.get("month") or "").replace("-", "_"): e for e in ed}
|
||||||
|
|
||||||
|
t_em = {pf["id"]: t_parse_expense(pf) for pf in t_pfs}
|
||||||
|
|
||||||
|
def t_sum_expense(field, months_range):
|
||||||
|
total = 0
|
||||||
|
for pf in t_pfs:
|
||||||
|
em = t_em.get(pf["id"], {})
|
||||||
|
for m in months_range:
|
||||||
|
e = em.get(f"2026_{m:02d}")
|
||||||
|
if e:
|
||||||
|
total += float(e.get(field) or 0)
|
||||||
|
return total
|
||||||
|
|
||||||
_now_month = date.today().month
|
_now_month = date.today().month
|
||||||
_q_start = ((_now_month - 1) // 3) * 3 + 1
|
_q_start = ((_now_month - 1) // 3) * 3 + 1
|
||||||
_q_range = range(_q_start, _q_start + 3)
|
_q_range = range(_q_start, _q_start + 3)
|
||||||
@@ -300,11 +320,11 @@ def bootstrap():
|
|||||||
"payment_month": t_sum_budget("payment", [_now_month]),
|
"payment_month": t_sum_budget("payment", [_now_month]),
|
||||||
"payment_prev_q": t_sum_budget("payment", _prev_q_range),
|
"payment_prev_q": t_sum_budget("payment", _prev_q_range),
|
||||||
"payment_prev_month": t_sum_budget("payment", [_prev_month]) if _prev_month else 0,
|
"payment_prev_month": t_sum_budget("payment", [_prev_month]) if _prev_month else 0,
|
||||||
"cost_annual": t_sum_budget("cost", range(1, 13)),
|
"cost_annual": t_sum_expense("cost", range(1, 13)),
|
||||||
"cost_q2": t_sum_budget("cost", _q_range),
|
"cost_q2": t_sum_expense("cost", _q_range),
|
||||||
"cost_month": t_sum_budget("cost", [_now_month]),
|
"cost_month": t_sum_expense("cost", [_now_month]),
|
||||||
"cost_prev_q": t_sum_budget("cost", _prev_q_range),
|
"cost_prev_q": t_sum_expense("cost", _prev_q_range),
|
||||||
"cost_prev_month": t_sum_budget("cost", [_prev_month]) if _prev_month else 0,
|
"cost_prev_month": t_sum_expense("cost", [_prev_month]) if _prev_month else 0,
|
||||||
})
|
})
|
||||||
# 费用汇总(从 expense_records 表计算)
|
# 费用汇总(从 expense_records 表计算)
|
||||||
def t_expense_sum(m_range):
|
def t_expense_sum(m_range):
|
||||||
@@ -322,11 +342,11 @@ def bootstrap():
|
|||||||
all_metrics[-1]["expense_month"] = t_expense_sum([_now_month])
|
all_metrics[-1]["expense_month"] = t_expense_sum([_now_month])
|
||||||
all_metrics[-1]["expense_prev_q"] = t_expense_sum(_prev_q_range)
|
all_metrics[-1]["expense_prev_q"] = t_expense_sum(_prev_q_range)
|
||||||
all_metrics[-1]["expense_prev_month"] = t_expense_sum([_prev_month]) if _prev_month else 0
|
all_metrics[-1]["expense_prev_month"] = t_expense_sum([_prev_month]) if _prev_month else 0
|
||||||
all_metrics[-1]["paid_annual"] = t_sum_budget("paid", range(1, 13))
|
all_metrics[-1]["paid_annual"] = t_sum_expense("paid", range(1, 13))
|
||||||
all_metrics[-1]["paid_q2"] = t_sum_budget("paid", _q_range)
|
all_metrics[-1]["paid_q2"] = t_sum_expense("paid", _q_range)
|
||||||
all_metrics[-1]["paid_month"] = t_sum_budget("paid", [_now_month])
|
all_metrics[-1]["paid_month"] = t_sum_expense("paid", [_now_month])
|
||||||
all_metrics[-1]["paid_prev_q"] = t_sum_budget("paid", _prev_q_range)
|
all_metrics[-1]["paid_prev_q"] = t_sum_expense("paid", _prev_q_range)
|
||||||
all_metrics[-1]["paid_prev_month"] = t_sum_budget("paid", [_prev_month]) if _prev_month else 0
|
all_metrics[-1]["paid_prev_month"] = t_sum_expense("paid", [_prev_month]) if _prev_month else 0
|
||||||
all_metrics[-1]["proj_expense_annual"] = t_sum_budget("expense", range(1, 13))
|
all_metrics[-1]["proj_expense_annual"] = t_sum_budget("expense", range(1, 13))
|
||||||
all_metrics[-1]["proj_expense_q2"] = t_sum_budget("expense", _q_range)
|
all_metrics[-1]["proj_expense_q2"] = t_sum_budget("expense", _q_range)
|
||||||
all_metrics[-1]["proj_expense_month"] = t_sum_budget("expense", [_now_month])
|
all_metrics[-1]["proj_expense_month"] = t_sum_budget("expense", [_now_month])
|
||||||
@@ -382,6 +402,15 @@ def bootstrap():
|
|||||||
|
|
||||||
budget_maps = [(pf, parse_budget(pf)) for pf in signed_pfs]
|
budget_maps = [(pf, parse_budget(pf)) for pf in signed_pfs]
|
||||||
|
|
||||||
|
def parse_expense(pf):
|
||||||
|
try:
|
||||||
|
ed = json.loads(pf.get("expense_data") or "[]")
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
ed = []
|
||||||
|
return {(e.get("month") or "").replace("-", "_"): e for e in ed}
|
||||||
|
|
||||||
|
expense_maps = [(pf, parse_expense(pf)) for pf in signed_pfs]
|
||||||
|
|
||||||
def sum_budget(field, months_range):
|
def sum_budget(field, months_range):
|
||||||
total = 0
|
total = 0
|
||||||
for pf, bm in budget_maps:
|
for pf, bm in budget_maps:
|
||||||
@@ -391,6 +420,15 @@ def bootstrap():
|
|||||||
total += float(b.get(field) or 0)
|
total += float(b.get(field) or 0)
|
||||||
return total
|
return total
|
||||||
|
|
||||||
|
def sum_expense(field, months_range):
|
||||||
|
total = 0
|
||||||
|
for pf, em in expense_maps:
|
||||||
|
for m in months_range:
|
||||||
|
e = em.get(f"2026_{m:02d}")
|
||||||
|
if e:
|
||||||
|
total += float(e.get(field) or 0)
|
||||||
|
return total
|
||||||
|
|
||||||
_now_month = date.today().month
|
_now_month = date.today().month
|
||||||
_q_start = ((_now_month - 1) // 3) * 3 + 1
|
_q_start = ((_now_month - 1) // 3) * 3 + 1
|
||||||
_q_range = range(_q_start, _q_start + 3)
|
_q_range = range(_q_start, _q_start + 3)
|
||||||
@@ -408,15 +446,15 @@ def bootstrap():
|
|||||||
rev_prev_month = sum_budget("rev", [_prev_month]) if _prev_month else 0
|
rev_prev_month = sum_budget("rev", [_prev_month]) if _prev_month else 0
|
||||||
gross_prev_month = sum_budget("gross", [_prev_month]) if _prev_month else 0
|
gross_prev_month = sum_budget("gross", [_prev_month]) if _prev_month else 0
|
||||||
payment_annual = sum_budget("payment", range(1, 13))
|
payment_annual = sum_budget("payment", range(1, 13))
|
||||||
cost_annual = sum_budget("cost", range(1, 13))
|
cost_annual = sum_expense("cost", range(1, 13))
|
||||||
payment_q2 = sum_budget("payment", _q_range)
|
payment_q2 = sum_budget("payment", _q_range)
|
||||||
cost_q2 = sum_budget("cost", _q_range)
|
cost_q2 = sum_expense("cost", _q_range)
|
||||||
payment_month = sum_budget("payment", [_now_month])
|
payment_month = sum_budget("payment", [_now_month])
|
||||||
cost_month = sum_budget("cost", [_now_month])
|
cost_month = sum_expense("cost", [_now_month])
|
||||||
payment_prev_q = sum_budget("payment", _prev_q_range)
|
payment_prev_q = sum_budget("payment", _prev_q_range)
|
||||||
cost_prev_q = sum_budget("cost", _prev_q_range)
|
cost_prev_q = sum_expense("cost", _prev_q_range)
|
||||||
payment_prev_month = sum_budget("payment", [_prev_month]) if _prev_month else 0
|
payment_prev_month = sum_budget("payment", [_prev_month]) if _prev_month else 0
|
||||||
cost_prev_month = sum_budget("cost", [_prev_month]) if _prev_month else 0
|
cost_prev_month = sum_expense("cost", [_prev_month]) if _prev_month else 0
|
||||||
def expense_sum(month_range):
|
def expense_sum(month_range):
|
||||||
total = 0
|
total = 0
|
||||||
for r in expense:
|
for r in expense:
|
||||||
@@ -432,11 +470,11 @@ 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
|
||||||
paid_annual = sum_budget("paid", range(1, 13))
|
paid_annual = sum_expense("paid", range(1, 13))
|
||||||
paid_q2 = sum_budget("paid", _q_range)
|
paid_q2 = sum_expense("paid", _q_range)
|
||||||
paid_month = sum_budget("paid", [_now_month])
|
paid_month = sum_expense("paid", [_now_month])
|
||||||
paid_prev_q = sum_budget("paid", _prev_q_range)
|
paid_prev_q = sum_expense("paid", _prev_q_range)
|
||||||
paid_prev_month = sum_budget("paid", [_prev_month]) if _prev_month else 0
|
paid_prev_month = sum_expense("paid", [_prev_month]) if _prev_month else 0
|
||||||
proj_expense_annual = sum_budget("expense", range(1, 13))
|
proj_expense_annual = sum_budget("expense", range(1, 13))
|
||||||
proj_expense_q2 = sum_budget("expense", _q_range)
|
proj_expense_q2 = sum_budget("expense", _q_range)
|
||||||
proj_expense_month = sum_budget("expense", [_now_month])
|
proj_expense_month = sum_budget("expense", [_now_month])
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ function renderFinance() {
|
|||||||
<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>
|
||||||
<button class="finance-tab" data-tab="budget" onclick="switchFinanceTab('budget')"><i data-lucide="calendar" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>项目流水</button>
|
<button class="finance-tab" data-tab="revpay" onclick="switchFinanceTab('revpay')"><i data-lucide="dollar-sign" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>确收与回款</button>
|
||||||
|
<button class="finance-tab" data-tab="cost" onclick="switchFinanceTab('cost')"><i data-lucide="receipt" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>费用明细</button>
|
||||||
<button class="finance-tab" data-tab="exec" onclick="switchFinanceTab('exec')"><i data-lucide="play-circle" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>执行信息</button>
|
<button class="finance-tab" data-tab="exec" onclick="switchFinanceTab('exec')"><i data-lucide="play-circle" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>执行信息</button>
|
||||||
<button class="finance-tab" data-tab="tasks" onclick="switchFinanceTab('tasks')"><i data-lucide="list-checks" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>任务管理</button>
|
<button class="finance-tab" data-tab="tasks" onclick="switchFinanceTab('tasks')"><i data-lucide="list-checks" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>任务管理</button>
|
||||||
<button class="finance-tab" data-tab="activity" onclick="switchFinanceTab('activity')"><i data-lucide="message-square" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>活动与跟进</button>
|
<button class="finance-tab" data-tab="activity" onclick="switchFinanceTab('activity')"><i data-lucide="message-square" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>活动与跟进</button>
|
||||||
@@ -163,34 +164,43 @@ function renderFinance() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="financeTabBudget" class="hidden">
|
<div id="financeTabRevpay" class="hidden">
|
||||||
<div class="grid grid-cols-6 gap-3 mb-4" id="budgetSummary">
|
<div class="grid grid-cols-3 gap-3 mb-4" id="revpaySummary">
|
||||||
<div class="bg-blue-50 rounded-lg p-3 text-center border border-blue-100">
|
<div class="bg-blue-50 rounded-lg p-3 text-center border border-blue-100">
|
||||||
<p class="text-xs text-blue-600 font-medium">总确收</p>
|
<p class="text-xs text-blue-600 font-medium">总确收</p>
|
||||||
<p class="text-lg font-bold text-blue-700" id="budgetTotalRev">¥0</p>
|
<p class="text-lg font-bold text-blue-700" id="revpayTotalRev">¥0</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-green-50 rounded-lg p-3 text-center border border-green-100">
|
<div class="bg-green-50 rounded-lg p-3 text-center border border-green-100">
|
||||||
<p class="text-xs text-green-600 font-medium">总毛利</p>
|
<p class="text-xs text-green-600 font-medium">总毛利</p>
|
||||||
<p class="text-lg font-bold text-green-700" id="budgetTotalGross">¥0</p>
|
<p class="text-lg font-bold text-green-700" id="revpayTotalGross">¥0</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-amber-50 rounded-lg p-3 text-center border border-amber-100" id="budgetTotalPaymentCard">
|
<div class="bg-amber-50 rounded-lg p-3 text-center border border-amber-100">
|
||||||
<p class="text-xs text-amber-600 font-medium">总回款</p>
|
<p class="text-xs text-amber-600 font-medium">总回款</p>
|
||||||
<p class="text-lg font-bold text-amber-700" id="budgetTotalPayment">¥0</p>
|
<p class="text-lg font-bold text-amber-700" id="revpayTotalPayment">¥0</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-rose-50 rounded-lg p-3 text-center border border-rose-100" id="budgetTotalCostCard">
|
</div>
|
||||||
|
<table class="w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id="revpayTable">
|
||||||
|
<thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2.5 text-left font-medium text-slate-500" style="min-width:140px">月份</th><th class="p-2.5 text-right font-medium text-slate-500">确收</th><th class="p-2.5 text-right font-medium text-slate-500">毛利</th><th class="p-2.5 text-right font-medium text-slate-500">回款</th><th class="p-2.5 text-left font-medium text-slate-500">备注</th><th class="p-2.5 w-8"></th></tr></thead>
|
||||||
|
<tbody id="revpayTbody"></tbody>
|
||||||
|
</table>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm mt-3" onclick="addRevpayRow()"><i data-lucide="plus"></i>添加月份</button>
|
||||||
|
</div>
|
||||||
|
<div id="financeTabCost" class="hidden">
|
||||||
|
<div class="grid grid-cols-2 gap-3 mb-4" id="costSummary">
|
||||||
|
<div class="bg-rose-50 rounded-lg p-3 text-center border border-rose-100">
|
||||||
<p class="text-xs text-rose-600 font-medium">总应付</p>
|
<p class="text-xs text-rose-600 font-medium">总应付</p>
|
||||||
<p class="text-lg font-bold text-rose-700" id="budgetTotalCost">¥0</p>
|
<p class="text-lg font-bold text-rose-700" id="costTotalCost">¥0</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-purple-50 rounded-lg p-3 text-center border border-purple-100">
|
<div class="bg-purple-50 rounded-lg p-3 text-center border border-purple-100">
|
||||||
<p class="text-xs text-purple-600 font-medium">总已付</p>
|
<p class="text-xs text-purple-600 font-medium">总已付</p>
|
||||||
<p class="text-lg font-bold text-purple-700" id="budgetTotalPaid">¥0</p>
|
<p class="text-lg font-bold text-purple-700" id="costTotalPaid">¥0</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id="budgetTable">
|
<table class="w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id="costTable">
|
||||||
<thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2.5 text-left font-medium text-slate-500" style="min-width:140px">月份</th><th class="p-2.5 text-right font-medium text-slate-500">确收</th><th class="p-2.5 text-right font-medium text-slate-500">毛利</th><th class="p-2.5 text-right font-medium text-slate-500">回款</th><th class="p-2.5 text-right font-medium text-slate-500">应付</th><th class="p-2.5 text-right font-medium text-slate-500">已付</th><th class="p-2.5 w-8"></th></tr></thead>
|
<thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2.5 text-left font-medium text-slate-500" style="min-width:140px">月份</th><th class="p-2.5 text-left font-medium text-slate-500" style="min-width:120px">费用类型</th><th class="p-2.5 text-right font-medium text-slate-500">应付</th><th class="p-2.5 text-right font-medium text-slate-500">已付</th><th class="p-2.5 text-left font-medium text-slate-500">备注</th><th class="p-2.5 w-8"></th></tr></thead>
|
||||||
<tbody id="budgetTbody"></tbody>
|
<tbody id="costTbody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
<button type="button" class="btn btn-ghost btn-sm mt-3" onclick="addBudgetRow()"><i data-lucide="plus"></i>添加月份</button>
|
<button type="button" class="btn btn-ghost btn-sm mt-3" onclick="addCostRow()"><i data-lucide="plus"></i>添加月份</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="financeTabTasks" class="hidden">
|
<div id="financeTabTasks" class="hidden">
|
||||||
<table class="w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id="taskTable">
|
<table class="w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id="taskTable">
|
||||||
@@ -293,12 +303,14 @@ function renderFinance() {
|
|||||||
let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0;
|
let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0;
|
||||||
allPfs.forEach(pf => {
|
allPfs.forEach(pf => {
|
||||||
let bd = []; try { bd = JSON.parse(pf.budget_data || "[]"); } catch (e) {}
|
let bd = []; try { bd = JSON.parse(pf.budget_data || "[]"); } catch (e) {}
|
||||||
|
let ed = []; try { ed = JSON.parse(pf.expense_data || "[]"); } catch (e) {}
|
||||||
const b = bd.find(x => (x.month || "") === selMonth) || {};
|
const b = bd.find(x => (x.month || "") === selMonth) || {};
|
||||||
|
const e = ed.find(x => (x.month || "") === selMonth) || {};
|
||||||
const rev = Math.round(parseFloat(b.rev || 0) || 0);
|
const rev = Math.round(parseFloat(b.rev || 0) || 0);
|
||||||
const payment = Math.round(parseFloat(b.payment || 0) || 0);
|
const payment = Math.round(parseFloat(b.payment || 0) || 0);
|
||||||
const cost = Math.round(parseFloat(b.cost || 0) || 0);
|
|
||||||
const paid = Math.round(parseFloat(b.paid || 0) || 0);
|
|
||||||
const gross = Math.round(parseFloat(b.gross || 0) || 0);
|
const gross = Math.round(parseFloat(b.gross || 0) || 0);
|
||||||
|
const cost = Math.round(parseFloat(e.cost || 0) || 0);
|
||||||
|
const paid = Math.round(parseFloat(e.paid || 0) || 0);
|
||||||
if (!rev && !payment && !cost && !paid && !gross) return;
|
if (!rev && !payment && !cost && !paid && !gross) return;
|
||||||
sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
|
sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
|
||||||
rows.push(tdRow(pf, rev, payment, cost, paid, gross));
|
rows.push(tdRow(pf, rev, payment, cost, paid, gross));
|
||||||
@@ -323,14 +335,19 @@ function renderFinance() {
|
|||||||
try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(b[field] || 0); }); } catch (e) {}
|
try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(b[field] || 0); }); } catch (e) {}
|
||||||
return total;
|
return total;
|
||||||
};
|
};
|
||||||
|
const sumExpense = (pf, field) => {
|
||||||
|
let total = 0;
|
||||||
|
try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(e[field] || 0); }); } catch (e) {}
|
||||||
|
return total;
|
||||||
|
};
|
||||||
let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0;
|
let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0;
|
||||||
const rows = [];
|
const rows = [];
|
||||||
allPfs.forEach(pf => {
|
allPfs.forEach(pf => {
|
||||||
const rev = sumBudget(pf, "rev");
|
const rev = sumBudget(pf, "rev");
|
||||||
const payment = sumBudget(pf, "payment");
|
const payment = sumBudget(pf, "payment");
|
||||||
const cost = sumBudget(pf, "cost");
|
|
||||||
const paid = sumBudget(pf, "paid");
|
|
||||||
const gross = sumBudget(pf, "gross");
|
const gross = sumBudget(pf, "gross");
|
||||||
|
const cost = sumExpense(pf, "cost");
|
||||||
|
const paid = sumExpense(pf, "paid");
|
||||||
sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
|
sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
|
||||||
rows.push(tdRow(pf, rev, payment, cost, paid, gross));
|
rows.push(tdRow(pf, rev, payment, cost, paid, gross));
|
||||||
});
|
});
|
||||||
@@ -342,9 +359,11 @@ function renderFinance() {
|
|||||||
// 总视图
|
// 总视图
|
||||||
const calcTotals = (pf) => {
|
const calcTotals = (pf) => {
|
||||||
let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {}
|
let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {}
|
||||||
let rev = 0, payment = 0, cost = 0, gross = 0;
|
let expense = []; try { expense = JSON.parse(pf.expense_data || "[]"); } catch (e) {}
|
||||||
budget.forEach(b => { rev += parseFloat(b.rev || 0) || 0; gross += parseFloat(b.gross || 0) || 0; payment += parseFloat(b.payment || 0) || 0; cost += parseFloat(b.cost || 0) || 0; });
|
let rev = 0, payment = 0, gross = 0;
|
||||||
const paid = parseFloat(pf.total_paid) || 0;
|
budget.forEach(b => { rev += parseFloat(b.rev || 0) || 0; gross += parseFloat(b.gross || 0) || 0; payment += parseFloat(b.payment || 0) || 0; });
|
||||||
|
let cost = 0, paid = 0;
|
||||||
|
expense.forEach(e => { cost += parseFloat(e.cost || 0) || 0; paid += parseFloat(e.paid || 0) || 0; });
|
||||||
return { rev: Math.round(rev), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid), gross: Math.round(gross) };
|
return { rev: Math.round(rev), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid), gross: Math.round(gross) };
|
||||||
};
|
};
|
||||||
const allPfs = pfs.filter(x => x.status === state.finFilter);
|
const allPfs = pfs.filter(x => x.status === state.finFilter);
|
||||||
@@ -371,60 +390,88 @@ window.openFinanceModal = () => {
|
|||||||
if (dept) dept.value = state.tenant;
|
if (dept) dept.value = state.tenant;
|
||||||
const pfIdInput = form.querySelector('[name="pf_id"]');
|
const pfIdInput = form.querySelector('[name="pf_id"]');
|
||||||
if (!pfIdInput || !pfIdInput.value) {
|
if (!pfIdInput || !pfIdInput.value) {
|
||||||
initBudgetTable(null);
|
initRevpayTable(null);
|
||||||
|
initCostTable(null);
|
||||||
initTaskTable(null);
|
initTaskTable(null);
|
||||||
document.querySelector("#financeDeleteBtn").classList.add("hidden");
|
document.querySelector("#financeDeleteBtn").classList.add("hidden");
|
||||||
}
|
}
|
||||||
modal.classList.remove("hidden");
|
modal.classList.remove("hidden");
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addBudgetRow = (month = '', rev = '', gross = '', payment = '', cost = '', paid = '') => {
|
window.addRevpayRow = (month = '', rev = '', gross = '', payment = '', rev_note = '') => {
|
||||||
const tbody = document.querySelector("#budgetTbody");
|
const tbody = document.querySelector("#revpayTbody");
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
const row = document.createElement("tr");
|
const row = document.createElement("tr");
|
||||||
row.innerHTML = `<td><select name="budget_month[]" class="form-ctrl form-ctrl-sm w-full" style="min-width:140px" onchange="updateBudgetSummary()">${monthOptions(month)}</select></td>
|
row.innerHTML = `<td><select name="budget_month[]" class="form-ctrl form-ctrl-sm w-full" style="min-width:140px" onchange="updateRevpaySummary()">${monthOptions(month)}</select></td>
|
||||||
<td><input name="budget_rev[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${rev}" oninput="updateBudgetSummary()"></td>
|
<td><input name="budget_rev[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${rev}" oninput="updateRevpaySummary()"></td>
|
||||||
<td><input name="budget_gross[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${gross}" oninput="updateBudgetSummary()"></td>
|
<td><input name="budget_gross[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${gross}" oninput="updateRevpaySummary()"></td>
|
||||||
<td><input name="budget_payment[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${payment}" oninput="updateBudgetSummary()"></td>
|
<td><input name="budget_payment[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${payment}" oninput="updateRevpaySummary()"></td>
|
||||||
<td><input name="budget_cost[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${cost}" oninput="updateBudgetSummary()"></td>
|
<td><input name="budget_rev_note[]" class="form-ctrl form-ctrl-sm w-full" placeholder="备注" value="${rev_note}"></td>
|
||||||
<td><input name="budget_paid[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${paid}" oninput="updateBudgetSummary()"></td>
|
<td><button type="button" class="btn btn-ghost btn-sm text-red-500 p-0 w-6 h-6" onclick="this.closest('tr').remove();updateRevpaySummary()"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></td>`;
|
||||||
<td><button type="button" class="btn btn-ghost btn-sm text-red-500 p-0 w-6 h-6" onclick="this.closest('tr').remove();updateBudgetSummary()"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></td>`;
|
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
if (window.lucide) window.lucide.createIcons();
|
if (window.lucide) window.lucide.createIcons();
|
||||||
};
|
};
|
||||||
|
|
||||||
window.updateBudgetSummary = () => {
|
window.addCostRow = (month = '', expense_type = '', cost = '', paid = '', exp_note = '') => {
|
||||||
const revEl = document.querySelector("#budgetTotalRev");
|
const tbody = document.querySelector("#costTbody");
|
||||||
const grossEl = document.querySelector("#budgetTotalGross");
|
if (!tbody) return;
|
||||||
const paymentEl = document.querySelector("#budgetTotalPayment");
|
const row = document.createElement("tr");
|
||||||
const costEl = document.querySelector("#budgetTotalCost");
|
row.innerHTML = `<td><select name="expense_month[]" class="form-ctrl form-ctrl-sm w-full" style="min-width:140px" onchange="updateCostSummary()">${monthOptions(month)}</select></td>
|
||||||
const paidEl = document.querySelector("#budgetTotalPaid");
|
<td><input name="expense_type[]" class="form-ctrl form-ctrl-sm w-full" placeholder="费用类型" value="${expense_type}"></td>
|
||||||
if (!revEl || !grossEl) return;
|
<td><input name="expense_cost[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${cost}" oninput="updateCostSummary()"></td>
|
||||||
|
<td><input name="expense_paid[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${paid}" oninput="updateCostSummary()"></td>
|
||||||
|
<td><input name="expense_note[]" class="form-ctrl form-ctrl-sm w-full" placeholder="备注" value="${exp_note}"></td>
|
||||||
|
<td><button type="button" class="btn btn-ghost btn-sm text-red-500 p-0 w-6 h-6" onclick="this.closest('tr').remove();updateCostSummary()"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></td>`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
if (window.lucide) window.lucide.createIcons();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateRevpaySummary = () => {
|
||||||
|
const revEl = document.querySelector("#revpayTotalRev");
|
||||||
|
const grossEl = document.querySelector("#revpayTotalGross");
|
||||||
|
const paymentEl = document.querySelector("#revpayTotalPayment");
|
||||||
|
if (!revEl || !paymentEl) return;
|
||||||
const revInputs = document.querySelectorAll('[name="budget_rev[]"]');
|
const revInputs = document.querySelectorAll('[name="budget_rev[]"]');
|
||||||
const grossInputs = document.querySelectorAll('[name="budget_gross[]"]');
|
const grossInputs = document.querySelectorAll('[name="budget_gross[]"]');
|
||||||
const paymentInputs = document.querySelectorAll('[name="budget_payment[]"]');
|
const paymentInputs = document.querySelectorAll('[name="budget_payment[]"]');
|
||||||
const costInputs = document.querySelectorAll('[name="budget_cost[]"]');
|
let totalRev = 0, totalGross = 0, totalPayment = 0;
|
||||||
const paidInputs = document.querySelectorAll('[name="budget_paid[]"]');
|
|
||||||
let totalRev = 0, totalGross = 0, totalPayment = 0, totalCost = 0, totalPaid = 0;
|
|
||||||
revInputs.forEach(el => { totalRev += parseFloat(el.value) || 0; });
|
revInputs.forEach(el => { totalRev += parseFloat(el.value) || 0; });
|
||||||
grossInputs.forEach(el => { totalGross += parseFloat(el.value) || 0; });
|
grossInputs.forEach(el => { totalGross += parseFloat(el.value) || 0; });
|
||||||
paymentInputs.forEach(el => { totalPayment += parseFloat(el.value) || 0; });
|
paymentInputs.forEach(el => { totalPayment += parseFloat(el.value) || 0; });
|
||||||
costInputs.forEach(el => { totalCost += parseFloat(el.value) || 0; });
|
|
||||||
paidInputs.forEach(el => { totalPaid += parseFloat(el.value) || 0; });
|
|
||||||
revEl.textContent = money(totalRev);
|
revEl.textContent = money(totalRev);
|
||||||
grossEl.textContent = money(totalGross);
|
if (grossEl) grossEl.textContent = money(totalGross);
|
||||||
if (paymentEl) paymentEl.textContent = money(totalPayment);
|
paymentEl.textContent = money(totalPayment);
|
||||||
if (costEl) costEl.textContent = money(totalCost);
|
|
||||||
if (paidEl) paidEl.textContent = money(totalPaid);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.initBudgetTable = (budgetData) => {
|
window.updateCostSummary = () => {
|
||||||
const tbody = document.querySelector("#budgetTbody");
|
const costEl = document.querySelector("#costTotalCost");
|
||||||
|
const paidEl = document.querySelector("#costTotalPaid");
|
||||||
|
if (!costEl || !paidEl) return;
|
||||||
|
const costInputs = document.querySelectorAll('[name="expense_cost[]"]');
|
||||||
|
const paidInputs = document.querySelectorAll('[name="expense_paid[]"]');
|
||||||
|
let totalCost = 0, totalPaid = 0;
|
||||||
|
costInputs.forEach(el => { totalCost += parseFloat(el.value) || 0; });
|
||||||
|
paidInputs.forEach(el => { totalPaid += parseFloat(el.value) || 0; });
|
||||||
|
costEl.textContent = money(totalCost);
|
||||||
|
paidEl.textContent = money(totalPaid);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.initRevpayTable = (budgetData) => {
|
||||||
|
const tbody = document.querySelector("#revpayTbody");
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
const rows = budgetData || [];
|
const rows = budgetData || [];
|
||||||
rows.forEach(r => addBudgetRow(r.month || '', r.rev || '', r.gross || '', r.payment || '', r.cost || '', r.paid || ''));
|
rows.forEach(r => addRevpayRow(r.month || '', r.rev || '', r.gross || '', r.payment || '', r.rev_note || ''));
|
||||||
setTimeout(() => updateBudgetSummary(), 50);
|
setTimeout(() => updateRevpaySummary(), 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.initCostTable = (expenseData) => {
|
||||||
|
const tbody = document.querySelector("#costTbody");
|
||||||
|
if (!tbody) return;
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
const rows = expenseData || [];
|
||||||
|
rows.forEach(r => addCostRow(r.month || '', r.expense_type || '', r.cost || '', r.paid || '', r.exp_note || ''));
|
||||||
|
setTimeout(() => updateCostSummary(), 50);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- 任务管理 tab ----------
|
// ---------- 任务管理 tab ----------
|
||||||
@@ -557,7 +604,8 @@ window.editPfSignMonth = (event, pfId) => {
|
|||||||
window.switchFinanceTab = (tab) => {
|
window.switchFinanceTab = (tab) => {
|
||||||
document.querySelectorAll(".finance-tab").forEach(b => b.classList.toggle("active", b.dataset.tab === tab));
|
document.querySelectorAll(".finance-tab").forEach(b => b.classList.toggle("active", b.dataset.tab === tab));
|
||||||
document.querySelector("#financeTabInfo").classList.toggle("hidden", tab !== "info");
|
document.querySelector("#financeTabInfo").classList.toggle("hidden", tab !== "info");
|
||||||
document.querySelector("#financeTabBudget").classList.toggle("hidden", tab !== "budget");
|
document.querySelector("#financeTabRevpay").classList.toggle("hidden", tab !== "revpay");
|
||||||
|
document.querySelector("#financeTabCost").classList.toggle("hidden", tab !== "cost");
|
||||||
document.querySelector("#financeTabExec").classList.toggle("hidden", tab !== "exec");
|
document.querySelector("#financeTabExec").classList.toggle("hidden", tab !== "exec");
|
||||||
document.querySelector("#financeTabTasks").classList.toggle("hidden", tab !== "tasks");
|
document.querySelector("#financeTabTasks").classList.toggle("hidden", tab !== "tasks");
|
||||||
document.querySelector("#financeTabActivity").classList.toggle("hidden", tab !== "activity");
|
document.querySelector("#financeTabActivity").classList.toggle("hidden", tab !== "activity");
|
||||||
@@ -662,7 +710,10 @@ window.openPfEditModal = (pfId) => {
|
|||||||
setVal("other_info", pf.other_info);
|
setVal("other_info", pf.other_info);
|
||||||
let budgetData = [];
|
let budgetData = [];
|
||||||
try { budgetData = JSON.parse(pf.budget_data || "[]"); } catch (e) { budgetData = []; }
|
try { budgetData = JSON.parse(pf.budget_data || "[]"); } catch (e) { budgetData = []; }
|
||||||
initBudgetTable(budgetData.length ? budgetData : null);
|
initRevpayTable(budgetData.length ? budgetData : null);
|
||||||
|
let expenseData = [];
|
||||||
|
try { expenseData = JSON.parse(pf.expense_data || "[]"); } catch (e) { expenseData = []; }
|
||||||
|
initCostTable(expenseData.length ? expenseData : null);
|
||||||
let taskData = [];
|
let taskData = [];
|
||||||
try { taskData = JSON.parse(pf.task_data || "[]"); } catch (e) { taskData = []; }
|
try { taskData = JSON.parse(pf.task_data || "[]"); } catch (e) { taskData = []; }
|
||||||
initTaskTable(taskData.length ? taskData : null);
|
initTaskTable(taskData.length ? taskData : null);
|
||||||
@@ -690,31 +741,50 @@ window.createFinance = async (event) => {
|
|||||||
const revs = form.querySelectorAll('[name="budget_rev[]"]');
|
const revs = form.querySelectorAll('[name="budget_rev[]"]');
|
||||||
const grosses = form.querySelectorAll('[name="budget_gross[]"]');
|
const grosses = form.querySelectorAll('[name="budget_gross[]"]');
|
||||||
const payments = form.querySelectorAll('[name="budget_payment[]"]');
|
const payments = form.querySelectorAll('[name="budget_payment[]"]');
|
||||||
const costs = form.querySelectorAll('[name="budget_cost[]"]');
|
const revNotes = form.querySelectorAll('[name="budget_rev_note[]"]');
|
||||||
const paids = form.querySelectorAll('[name="budget_paid[]"]');
|
|
||||||
const budgetRows = [];
|
const budgetRows = [];
|
||||||
let totalRev = 0, totalGross = 0, totalPaidFromBudget = 0;
|
let totalRev = 0, totalGross = 0, totalPayment = 0;
|
||||||
for (let i = 0; i < months.length; i++) {
|
for (let i = 0; i < months.length; i++) {
|
||||||
const m = months[i].value.trim();
|
const m = months[i].value.trim();
|
||||||
if (!m) continue;
|
if (!m) continue;
|
||||||
const rev = parseFloat(revs[i].value) || 0;
|
const rev = parseFloat(revs[i].value) || 0;
|
||||||
const gross = parseFloat(grosses[i].value) || 0;
|
const gross = parseFloat(grosses[i].value) || 0;
|
||||||
const payment = parseFloat(payments[i].value) || 0;
|
const payment = parseFloat(payments[i].value) || 0;
|
||||||
const cost = parseFloat(costs[i].value) || 0;
|
const rev_note = (revNotes[i] ? revNotes[i].value : '') || '';
|
||||||
const paid = parseFloat(paids[i].value) || 0;
|
budgetRows.push({ month: m, rev, gross, payment, rev_note });
|
||||||
budgetRows.push({ month: m, rev, gross, payment, cost, paid });
|
|
||||||
totalRev += rev;
|
totalRev += rev;
|
||||||
totalGross += gross;
|
totalGross += gross;
|
||||||
totalPaidFromBudget += paid;
|
totalPayment += payment;
|
||||||
}
|
}
|
||||||
data.budget_data = JSON.stringify(budgetRows);
|
data.budget_data = JSON.stringify(budgetRows);
|
||||||
data.total_rev = totalRev;
|
data.total_rev = totalRev;
|
||||||
data.total_gross = totalGross;
|
data.total_gross = totalGross;
|
||||||
data.total_paid = totalPaidFromBudget;
|
|
||||||
let totalPayment = 0, totalCost = 0;
|
|
||||||
for (const r of budgetRows) { totalPayment += r.payment; totalCost += r.cost; }
|
|
||||||
data.total_payment = totalPayment;
|
data.total_payment = totalPayment;
|
||||||
|
|
||||||
|
// 费用明细数据
|
||||||
|
const expMonths = form.querySelectorAll('[name="expense_month[]"]');
|
||||||
|
const expTypes = form.querySelectorAll('[name="expense_type[]"]');
|
||||||
|
const expCosts = form.querySelectorAll('[name="expense_cost[]"]');
|
||||||
|
const expPaids = form.querySelectorAll('[name="expense_paid[]"]');
|
||||||
|
const expNotes = form.querySelectorAll('[name="expense_note[]"]');
|
||||||
|
const expenseRows = [];
|
||||||
|
let totalCost = 0, totalPaid = 0;
|
||||||
|
for (let i = 0; i < expMonths.length; i++) {
|
||||||
|
const m = expMonths[i].value.trim();
|
||||||
|
if (!m) continue;
|
||||||
|
const expense_type = (expTypes[i] ? expTypes[i].value : '') || '';
|
||||||
|
const cost = parseFloat(expCosts[i].value) || 0;
|
||||||
|
const paid = parseFloat(expPaids[i].value) || 0;
|
||||||
|
const exp_note = (expNotes[i] ? expNotes[i].value : '') || '';
|
||||||
|
expenseRows.push({ month: m, expense_type, cost, paid, exp_note });
|
||||||
|
totalCost += cost;
|
||||||
|
totalPaid += paid;
|
||||||
|
}
|
||||||
|
data.expense_data = JSON.stringify(expenseRows);
|
||||||
|
data.total_paid = totalPaid;
|
||||||
data.total_cost = totalCost;
|
data.total_cost = totalCost;
|
||||||
|
for (const r of budgetRows) { totalPayment += r.payment; }
|
||||||
|
data.total_payment = totalPayment;
|
||||||
// 收集任务管理数据
|
// 收集任务管理数据
|
||||||
const taskTypeInputs = form.querySelectorAll('[name="task_type[]"]');
|
const taskTypeInputs = form.querySelectorAll('[name="task_type[]"]');
|
||||||
const taskCountInputs = form.querySelectorAll('[name="task_count[]"]');
|
const taskCountInputs = form.querySelectorAll('[name="task_count[]"]');
|
||||||
|
|||||||
Reference in New Issue
Block a user