feat: 经营报表年份支持 + 项目管理年份过滤修复
- 经营月报/季报增加年份选择入口,数据按选中年份过滤 - 年份下拉框扩展至 2020-2027,增加「所有年份」选项 - 项目管理视图全年/季度/月度均按年份过滤项目列表 - 修复月度视图在所有年份模式下数据查询(substring→slice) - 修复 finView/finFilter 初始化,确保全年标签默认选中 - 修复 yearly API 缺失 tenant 参数导致跨工作台数据错误 - 费用数据(expense)遍历加年份过滤,季报指标改为从 fm 累加
This commit is contained in:
+2
-2
@@ -61,8 +61,8 @@ def attach_common(conn, resource, items):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def monthly_finance(conn, tenant="科普·无界"):
|
def monthly_finance(conn, tenant="科普·无界", year=None):
|
||||||
_year = date.today().year
|
_year = year or date.today().year
|
||||||
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
|
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
|
||||||
pfs = rows(conn,
|
pfs = rows(conn,
|
||||||
"SELECT sign_amount, sign_month, budget_data, expense_data FROM project_finances WHERE tenant=?",
|
"SELECT sign_amount, sign_month, budget_data, expense_data FROM project_finances WHERE tenant=?",
|
||||||
|
|||||||
@@ -489,6 +489,42 @@ def bootstrap():
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/finance/yearly")
|
||||||
|
@login_required
|
||||||
|
def finance_yearly():
|
||||||
|
"""按年份返回经营月报/季报所需的 12 月财务数据,支持历史年份切换。year=0 表示所有年份"""
|
||||||
|
_year = request.args.get("year", type=int)
|
||||||
|
if _year is None:
|
||||||
|
_year = date.today().year
|
||||||
|
tenant = request.args.get("tenant", session.get("tenants", ["科普·无界"])[0])
|
||||||
|
allowed = session.get("tenants", [])
|
||||||
|
allowed = [t if t != "总工作台" else "科普·无界" for t in allowed]
|
||||||
|
allowed = [t if t != "学会·无界" else "学术·无界" for t in allowed]
|
||||||
|
if tenant not in allowed:
|
||||||
|
tenant = allowed[0] if allowed else ""
|
||||||
|
conn = db()
|
||||||
|
try:
|
||||||
|
if _year == 0:
|
||||||
|
# 所有年份:查出所有有数据的年份,合并 12 个月
|
||||||
|
yr_rows = rows(conn,
|
||||||
|
"SELECT DISTINCT SUBSTRING(sign_month,1,4) AS y FROM project_finances WHERE tenant=? AND sign_month IS NOT NULL AND sign_month!='' ORDER BY y",
|
||||||
|
[tenant])
|
||||||
|
all_years = [int(r["y"]) for r in yr_rows if r["y"] and r["y"].isdigit()]
|
||||||
|
if not all_years:
|
||||||
|
all_years = [date.today().year]
|
||||||
|
merged = [{"month": f"所有-{m+1:02d}", "revenue": 0, "gross": 0, "payment": 0, "cost": 0, "paid": 0, "sign": 0}
|
||||||
|
for m in range(12)]
|
||||||
|
for y in all_years:
|
||||||
|
fm = monthly_finance(conn, tenant, y)
|
||||||
|
for i, m in enumerate(fm):
|
||||||
|
for k in ("revenue", "gross", "payment", "cost", "paid", "sign"):
|
||||||
|
merged[i][k] = round(merged[i][k] + (m[k] or 0), 2)
|
||||||
|
return jsonify({"year": 0, "financeMonthly": merged})
|
||||||
|
return jsonify({"year": _year, "financeMonthly": monthly_finance(conn, tenant, _year)})
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
# ---------- 通用 CRUD ----------
|
# ---------- 通用 CRUD ----------
|
||||||
|
|
||||||
@bp.route("/api/<resource>/list", methods=["GET"])
|
@bp.route("/api/<resource>/list", methods=["GET"])
|
||||||
|
|||||||
+115
-39
@@ -105,15 +105,21 @@ function renderFinance() {
|
|||||||
const defaultMonth2 = now2.getFullYear() + "-" + String(now2.getMonth()+1).padStart(2,"0");
|
const defaultMonth2 = now2.getFullYear() + "-" + String(now2.getMonth()+1).padStart(2,"0");
|
||||||
if (!state.finMonth) state.finMonth = defaultMonth2;
|
if (!state.finMonth) state.finMonth = defaultMonth2;
|
||||||
if (state.finQuarter === undefined) state.finQuarter = Math.floor(now2.getMonth() / 3);
|
if (state.finQuarter === undefined) state.finQuarter = Math.floor(now2.getMonth() / 3);
|
||||||
if (!state.finYear) state.finYear = now2.getFullYear();
|
if (state.finYear == null) state.finYear = now2.getFullYear();
|
||||||
if (!state.finView) state.finView = 'annual';
|
if (state.finMonthlyYear === undefined) state.finMonthlyYear = state.finYear;
|
||||||
|
if (!['annual','quarterly','monthly'].includes(state.finView)) state.finView = 'annual';
|
||||||
|
if (!['overview','quarterlyOverview','projects','expense'].includes(state.finFilter)) state.finFilter = 'projects';
|
||||||
|
if (state.finView === 'monthly' && state.finMonth) {
|
||||||
|
const monthSuffix = state.finMonth.slice(-2);
|
||||||
|
state.finMonth = state.finYear + '-' + monthSuffix;
|
||||||
|
}
|
||||||
const qLabels = ['Q1','Q2','Q3','Q4'];
|
const qLabels = ['Q1','Q2','Q3','Q4'];
|
||||||
const qMonths = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]];
|
const qMonths = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]];
|
||||||
const nowD = new Date();
|
const nowD = new Date();
|
||||||
const activeQ = state.finQuarter !== undefined ? state.finQuarter : Math.floor(nowD.getMonth() / 3);
|
const activeQ = state.finQuarter !== undefined ? state.finQuarter : Math.floor(nowD.getMonth() / 3);
|
||||||
const activeMonths = qMonths[activeQ];
|
const activeMonths = qMonths[activeQ];
|
||||||
const yearOpts = [2024,2025,2026,2027];
|
const yearOpts = [2020,2021,2022,2023,2024,2025,2026,2027];
|
||||||
const finHeaderBase = `<div class="flex items-center gap-2"><span class="text-sm text-slate-500">年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 rounded text-sm font-medium border border-slate-200 bg-white">${yearOpts.map(y => `<option value="${y}" ${state.finYear===y?'selected':''}>${y}</option>`).join('')}</select><span class="text-slate-300 mx-2">|</span><button onclick="setFinView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='annual'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全年</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">季度:</span>${qLabels.map((q,i) => `<button onclick="setFinQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='quarterly'&&activeQ===i?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${q}</button>`).join('')}<span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">月度:</span>${activeMonths.map(m => { const ms = `${state.finYear}-${String(m).padStart(2,'0')}`; return `<button onclick="setFinMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='monthly'&&state.finMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
|
const finHeaderBase = `<div class="flex items-center gap-2"><span class="text-sm text-slate-500">年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 rounded text-sm font-medium border border-slate-200 bg-white">${`<option value="0" ${state.finYear===0?'selected':''}>所有年份</option>`}${yearOpts.map(y => `<option value="${y}" ${state.finYear===y?'selected':''}>${y}年</option>`).join('')}</select><span class="text-slate-300 mx-2">|</span><button onclick="setFinView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='annual'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全年</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">季度:</span>${qLabels.map((q,i) => `<button onclick="setFinQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='quarterly'&&activeQ===i?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${q}</button>`).join('')}<span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">月度:</span>${activeMonths.map(m => { const ms = `${state.finYear}-${String(m).padStart(2,'0')}`; return `<button onclick="setFinMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='monthly'&&state.finMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
|
||||||
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 isOverviewMode = false;
|
const isOverviewMode = false;
|
||||||
@@ -242,13 +248,21 @@ 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') {
|
if (state.finFilter === 'overview' || state.finFilter === 'quarterlyOverview') {
|
||||||
setTimeout(() => renderFinanceOverview(), 10);
|
const isQ = state.finFilter === 'quarterlyOverview';
|
||||||
return `<div id="financeOverview"></div>`;
|
const renderFn = isQ ? renderFinanceQuarterlyOverview : renderFinanceOverview;
|
||||||
|
const containerId = isQ ? 'financeQuarterlyOverview' : 'financeOverview';
|
||||||
|
if (state.finMonthlyYear !== state.finYear) {
|
||||||
|
const y = state.finYear;
|
||||||
|
fetch(`/api/finance/yearly?year=${y}&tenant=${encodeURIComponent(state.tenant)}`).then(r => r.json()).then(d => {
|
||||||
|
if (d && d.financeMonthly) { state.data.financeMonthly = d.financeMonthly; state.finMonthlyYear = d.year; }
|
||||||
|
else { state.data.financeMonthly = []; state.finMonthlyYear = y; }
|
||||||
|
renderFn();
|
||||||
|
}).catch(() => { state.finMonthlyYear = y; renderFn(); });
|
||||||
|
} else {
|
||||||
|
setTimeout(renderFn, 10);
|
||||||
}
|
}
|
||||||
if (state.finFilter === 'quarterlyOverview') {
|
return `<div id="${containerId}"></div>`;
|
||||||
setTimeout(() => renderFinanceQuarterlyOverview(), 10);
|
|
||||||
return `<div id="financeQuarterlyOverview"></div>`;
|
|
||||||
}
|
}
|
||||||
if (state.finFilter === 'expense') {
|
if (state.finFilter === 'expense') {
|
||||||
setTimeout(() => renderExpense(), 10);
|
setTimeout(() => renderExpense(), 10);
|
||||||
@@ -378,17 +392,26 @@ function renderFinance() {
|
|||||||
const defaultMonth = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0");
|
const defaultMonth = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0");
|
||||||
if (!state.finMonth) state.finMonth = defaultMonth;
|
if (!state.finMonth) state.finMonth = defaultMonth;
|
||||||
const selMonth = state.finMonth;
|
const selMonth = state.finMonth;
|
||||||
|
const isAllMonth = selMonth.startsWith('0-');
|
||||||
|
const monthSuffix = selMonth.slice(-2);
|
||||||
var dataItems = [];
|
var dataItems = [];
|
||||||
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) {}
|
let ed = []; try { ed = JSON.parse(pf.expense_data || "[]"); } catch (e) {}
|
||||||
|
let rev = 0, payment = 0, gross = 0, cost = 0, paid = 0;
|
||||||
|
if (isAllMonth) {
|
||||||
|
bd.forEach(x => { if ((x.month||"").substring(5) === monthSuffix) { rev += parseFloat(x.rev||0)||0; gross += parseFloat(x.gross||0)||0; payment += parseFloat(x.payment||0)||0; } });
|
||||||
|
ed.forEach(x => { if ((x.month||"").substring(5) === monthSuffix) { cost += parseFloat(x.cost||0)||0; paid += parseFloat(x.paid||0)||0; } });
|
||||||
|
rev = Math.round(rev); payment = Math.round(payment); gross = Math.round(gross); cost = Math.round(cost); paid = Math.round(paid);
|
||||||
|
} else {
|
||||||
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 e = ed.find(x => (x.month || "") === selMonth) || {};
|
||||||
const rev = Math.round(parseFloat(b.rev || 0) || 0);
|
rev = Math.round(parseFloat(b.rev || 0) || 0);
|
||||||
const payment = Math.round(parseFloat(b.payment || 0) || 0);
|
payment = Math.round(parseFloat(b.payment || 0) || 0);
|
||||||
const gross = Math.round(parseFloat(b.gross || 0) || 0);
|
gross = Math.round(parseFloat(b.gross || 0) || 0);
|
||||||
const cost = Math.round(parseFloat(e.cost || 0) || 0);
|
cost = Math.round(parseFloat(e.cost || 0) || 0);
|
||||||
const paid = Math.round(parseFloat(e.paid || 0) || 0);
|
paid = Math.round(parseFloat(e.paid || 0) || 0);
|
||||||
|
}
|
||||||
if (!rev && !payment && !cost && !paid && !gross) return;
|
if (!rev && !payment && !cost && !paid && !gross) return;
|
||||||
dataItems.push({
|
dataItems.push({
|
||||||
pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
||||||
@@ -408,8 +431,12 @@ function renderFinance() {
|
|||||||
sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += d.gross;
|
sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += d.gross;
|
||||||
rows.push(isUnsigned ? tdRowUnsigned(d.pf, d.cost, d.gross) : tdRow(d.pf, d.rev, d.payment, d.cost, d.paid, d.gross));
|
rows.push(isUnsigned ? tdRowUnsigned(d.pf, d.cost, d.gross) : tdRow(d.pf, d.rev, d.payment, d.cost, d.paid, d.gross));
|
||||||
});
|
});
|
||||||
const signTotal = Math.round(allPfs.filter(x => (x.sign_month || "").substring(0, 7) === selMonth).reduce((a, p) => a + (p.sign_amount || 0), 0));
|
const signTotal = isAllMonth
|
||||||
const signCnt = allPfs.filter(x => (x.sign_month || "").substring(0, 7) === selMonth).length;
|
? Math.round(allPfs.filter(x => { const sm = x.sign_month || ""; return sm.length >= 7 && sm.substring(5) === monthSuffix; }).reduce((a, p) => a + (p.sign_amount || 0), 0))
|
||||||
|
: Math.round(allPfs.filter(x => (x.sign_month || "").substring(0, 7) === selMonth).reduce((a, p) => a + (p.sign_amount || 0), 0));
|
||||||
|
const signCnt = isAllMonth
|
||||||
|
? allPfs.filter(x => { const sm = x.sign_month || ""; return sm.length >= 7 && sm.substring(5) === monthSuffix; }).length
|
||||||
|
: allPfs.filter(x => (x.sign_month || "").substring(0, 7) === selMonth).length;
|
||||||
return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '该月份暂无数据');
|
return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '该月份暂无数据');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,12 +452,12 @@ function renderFinance() {
|
|||||||
const qRange = qRanges[selQ];
|
const qRange = qRanges[selQ];
|
||||||
const sumBudget = (pf, field) => {
|
const sumBudget = (pf, field) => {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; const y = parseInt((b.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.finYear) total += parseFloat(b[field] || 0); }); } catch (e) {}
|
try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; const y = parseInt((b.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && (y === state.finYear || state.finYear === 0)) total += parseFloat(b[field] || 0); }); } catch (e) {}
|
||||||
return total;
|
return total;
|
||||||
};
|
};
|
||||||
const sumExpense = (pf, field) => {
|
const sumExpense = (pf, field) => {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; const y = parseInt((e.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.finYear) total += parseFloat(e[field] || 0); }); } catch (e) {}
|
try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; const y = parseInt((e.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && (y === state.finYear || state.finYear === 0)) total += parseFloat(e[field] || 0); }); } catch (e) {}
|
||||||
return total;
|
return total;
|
||||||
};
|
};
|
||||||
var dataItems = [];
|
var dataItems = [];
|
||||||
@@ -459,25 +486,52 @@ function renderFinance() {
|
|||||||
sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += d.gross;
|
sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += d.gross;
|
||||||
rows.push(isUnsigned ? tdRowUnsigned(d.pf, d.cost, d.gross) : tdRow(d.pf, d.rev, d.payment, d.cost, d.paid, d.gross));
|
rows.push(isUnsigned ? tdRowUnsigned(d.pf, d.cost, d.gross) : tdRow(d.pf, d.rev, d.payment, d.cost, d.paid, d.gross));
|
||||||
});
|
});
|
||||||
const signTotal = Math.round(allPfs.filter(x => { const m = parseInt((x.sign_month || "0").substring(5)) || 0; return qRange.includes(m); }).reduce((a, p) => a + (p.sign_amount || 0), 0));
|
const signTotal = Math.round(allPfs.filter(x => {
|
||||||
const signCnt = allPfs.filter(x => qRange.includes(parseInt((x.sign_month || "0").substring(5)) || 0)).length;
|
const m = parseInt((x.sign_month || "0").substring(5)) || 0;
|
||||||
|
const qy = parseInt((x.sign_month || "0").substring(0, 4)) || 0;
|
||||||
|
return qRange.includes(m) && (qy === state.finYear || state.finYear === 0);
|
||||||
|
}).reduce((a, p) => a + (p.sign_amount || 0), 0));
|
||||||
|
const signCnt = allPfs.filter(x => {
|
||||||
|
const m = parseInt((x.sign_month || "0").substring(5)) || 0;
|
||||||
|
const qy = parseInt((x.sign_month || "0").substring(0, 4)) || 0;
|
||||||
|
return qRange.includes(m) && (qy === state.finYear || state.finYear === 0);
|
||||||
|
}).length;
|
||||||
return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '该季度暂无数据');
|
return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '该季度暂无数据');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 总视图
|
// 总视图
|
||||||
const calcTotals = (pf) => {
|
const calcTotals = (pf, year) => {
|
||||||
let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {}
|
let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {}
|
||||||
let expense = []; try { expense = JSON.parse(pf.expense_data || "[]"); } catch (e) {}
|
let expense = []; try { expense = JSON.parse(pf.expense_data || "[]"); } catch (e) {}
|
||||||
let rev = 0, payment = 0, gross = 0;
|
let rev = 0, payment = 0, gross = 0;
|
||||||
budget.forEach(b => { rev += parseFloat(b.rev || 0) || 0; gross += parseFloat(b.gross || 0) || 0; payment += parseFloat(b.payment || 0) || 0; });
|
budget.forEach(b => {
|
||||||
|
if (year !== 0) {
|
||||||
|
const by = parseInt((b.month || "").substring(0, 4)) || 0;
|
||||||
|
if (by && by !== year) return;
|
||||||
|
}
|
||||||
|
rev += parseFloat(b.rev || 0) || 0; gross += parseFloat(b.gross || 0) || 0; payment += parseFloat(b.payment || 0) || 0;
|
||||||
|
});
|
||||||
let cost = 0, paid = 0;
|
let cost = 0, paid = 0;
|
||||||
expense.forEach(e => { cost += parseFloat(e.cost || 0) || 0; paid += parseFloat(e.paid || 0) || 0; });
|
expense.forEach(e => {
|
||||||
|
if (year !== 0) {
|
||||||
|
const ey = parseInt((e.month || "").substring(0, 4)) || 0;
|
||||||
|
if (ey && ey !== year) return;
|
||||||
|
}
|
||||||
|
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;
|
const allPfs = pfs;
|
||||||
var dataItems = allPfs.map(pf => {
|
var dataItems = [];
|
||||||
var t = calcTotals(pf);
|
allPfs.forEach(pf => {
|
||||||
return {
|
var t = calcTotals(pf, state.finYear);
|
||||||
|
if (state.finYear !== 0) {
|
||||||
|
const sy = parseInt((pf.sign_month || "").substring(0, 4)) || 0;
|
||||||
|
const hasFinance = t.rev || t.payment || t.cost || t.paid || t.gross;
|
||||||
|
const hasSign = sy === state.finYear;
|
||||||
|
if (!hasFinance && !hasSign) return;
|
||||||
|
}
|
||||||
|
dataItems.push({
|
||||||
pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
||||||
rev: t.rev, gross: t.gross, cost: t.cost,
|
rev: t.rev, gross: t.gross, cost: t.cost,
|
||||||
payment: t.payment, paid: t.paid, cashflow: t.payment - t.paid,
|
payment: t.payment, paid: t.paid, cashflow: t.payment - t.paid,
|
||||||
@@ -485,7 +539,7 @@ function renderFinance() {
|
|||||||
gross_rate: t.rev && t.gross ? Math.round(t.gross / t.rev * 100) : 0,
|
gross_rate: t.rev && t.gross ? Math.round(t.gross / t.rev * 100) : 0,
|
||||||
pay_rate: t.rev && t.payment ? Math.round(t.payment / t.rev * 100) : 0,
|
pay_rate: t.rev && t.payment ? Math.round(t.payment / t.rev * 100) : 0,
|
||||||
paid_rate: t.cost && t.paid ? Math.round(t.paid / t.cost * 100) : 0,
|
paid_rate: t.cost && t.paid ? Math.round(t.paid / t.cost * 100) : 0,
|
||||||
};
|
});
|
||||||
});
|
});
|
||||||
var sortK = state.finSort && state.finSort.key ? state.finSort.key.split('|')[1] : null;
|
var sortK = state.finSort && state.finSort.key ? state.finSort.key.split('|')[1] : null;
|
||||||
dataItems = sortData(dataItems, sortK);
|
dataItems = sortData(dataItems, sortK);
|
||||||
@@ -495,8 +549,16 @@ function renderFinance() {
|
|||||||
sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += d.gross;
|
sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += d.gross;
|
||||||
rows.push(isUnsigned ? tdRowUnsigned(d.pf, d.cost, d.gross) : tdRow(d.pf, d.rev, d.payment, d.cost, d.paid, d.gross));
|
rows.push(isUnsigned ? tdRowUnsigned(d.pf, d.cost, d.gross) : tdRow(d.pf, d.rev, d.payment, d.cost, d.paid, d.gross));
|
||||||
});
|
});
|
||||||
const signTotal = Math.round(allPfs.reduce((a, p) => a + (p.sign_amount || 0), 0));
|
const signTotal = Math.round(allPfs.filter(p => {
|
||||||
const signCnt = allPfs.length;
|
if (state.finYear === 0) return true;
|
||||||
|
const sy = parseInt((p.sign_month || "").substring(0, 4)) || 0;
|
||||||
|
return sy === state.finYear;
|
||||||
|
}).reduce((a, p) => a + (p.sign_amount || 0), 0));
|
||||||
|
const signCnt = allPfs.filter(p => {
|
||||||
|
if (state.finYear === 0) return true;
|
||||||
|
const sy = parseInt((p.sign_month || "").substring(0, 4)) || 0;
|
||||||
|
return sy === state.finYear;
|
||||||
|
}).length;
|
||||||
return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '暂无数据');
|
return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '暂无数据');
|
||||||
})()}
|
})()}
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -992,7 +1054,9 @@ function renderFinanceOverview() {
|
|||||||
const fm = financeMonthly || [];
|
const fm = financeMonthly || [];
|
||||||
const mLbl = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
|
const mLbl = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
|
||||||
const LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
|
const LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
|
||||||
const annualVals = [m.signed_annual||m.signed_amount||0, m.revenue_annual||0, m.gross_annual||0, m.cost_annual||0, m.paid_annual||0, m.payment_annual||0, (m.payment_annual||0) - (m.paid_annual||0)];
|
const annualSums = { sign:0, revenue:0, gross:0, cost:0, paid:0, payment:0 };
|
||||||
|
fm.forEach(x => { annualSums.sign += x.sign||0; annualSums.revenue += x.revenue||0; annualSums.gross += x.gross||0; annualSums.cost += x.cost||0; annualSums.paid += x.paid||0; annualSums.payment += x.payment||0; });
|
||||||
|
const annualVals = [annualSums.sign, annualSums.revenue, annualSums.gross, annualSums.cost, annualSums.paid, annualSums.payment, annualSums.payment - annualSums.paid];
|
||||||
const fmFields = ['sign','revenue','gross','cost','paid','payment',null];
|
const fmFields = ['sign','revenue','gross','cost','paid','payment',null];
|
||||||
var thead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
var thead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||||
for (var mi = 0; mi < 12; mi++) { thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + mLbl[mi] + '</th>'; }
|
for (var mi = 0; mi < 12; mi++) { thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + mLbl[mi] + '</th>'; }
|
||||||
@@ -1022,6 +1086,8 @@ function renderFinanceOverview() {
|
|||||||
if ((finExp[ei].status || '') === '暂不计入') continue;
|
if ((finExp[ei].status || '') === '暂不计入') continue;
|
||||||
var em = (finExp[ei].expense_month || '').substring(0, 7);
|
var em = (finExp[ei].expense_month || '').substring(0, 7);
|
||||||
var mi2 = parseInt(em.substring(5)) - 1;
|
var mi2 = parseInt(em.substring(5)) - 1;
|
||||||
|
var emYear = parseInt(em.substring(0, 4)) || 0;
|
||||||
|
if (state.finYear !== 0 && emYear && emYear !== state.finYear) continue;
|
||||||
if (mi2 >= 0 && mi2 < 12) expMonthly[mi2] += parseFloat(finExp[ei].amount || 0);
|
if (mi2 >= 0 && mi2 < 12) expMonthly[mi2] += parseFloat(finExp[ei].amount || 0);
|
||||||
}
|
}
|
||||||
var expAnnual = 0; for (var ei = 0; ei < 12; ei++) expAnnual += expMonthly[ei];
|
var expAnnual = 0; for (var ei = 0; ei < 12; ei++) expAnnual += expMonthly[ei];
|
||||||
@@ -1057,6 +1123,8 @@ function renderFinanceOverview() {
|
|||||||
if ((finExp[epi].status || '') === '暂不计入') continue;
|
if ((finExp[epi].status || '') === '暂不计入') continue;
|
||||||
var epm = (finExp[epi].expense_month || '').substring(0, 7);
|
var epm = (finExp[epi].expense_month || '').substring(0, 7);
|
||||||
var epmi = parseInt(epm.substring(5)) - 1;
|
var epmi = parseInt(epm.substring(5)) - 1;
|
||||||
|
var epmYear = parseInt(epm.substring(0, 4)) || 0;
|
||||||
|
if (state.finYear !== 0 && epmYear && epmYear !== state.finYear) continue;
|
||||||
if (epmi >= 0 && epmi < 12) expPaidMonthly[epmi] += parseFloat(finExp[epi].incurred_amount || 0);
|
if (epmi >= 0 && epmi < 12) expPaidMonthly[epmi] += parseFloat(finExp[epi].incurred_amount || 0);
|
||||||
}
|
}
|
||||||
var expPaidAnnual = 0; for (var epi = 0; epi < 12; epi++) expPaidAnnual += expPaidMonthly[epi];
|
var expPaidAnnual = 0; for (var epi = 0; epi < 12; epi++) expPaidAnnual += expPaidMonthly[epi];
|
||||||
@@ -1083,14 +1151,15 @@ function renderFinanceOverview() {
|
|||||||
}
|
}
|
||||||
var cfCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
|
var cfCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
|
||||||
|
|
||||||
document.querySelector("#financeOverview").innerHTML = '<div class="grid gap-2.5">' +
|
const yearSelectOv = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.finYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.finYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
|
||||||
|
document.querySelector("#financeOverview").innerHTML = yearSelectOv + '<div class="grid gap-2.5">' +
|
||||||
deptCard +
|
deptCard +
|
||||||
cfCard +
|
cfCard +
|
||||||
card('<h3 class="text-base font-bold text-slate-700 mb-3">项目经营详情 <span class="text-sm text-slate-500 font-normal ml-3">所有项目产生的签单,确收,毛利,回款,当期成本,当期流出,现金流</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
|
card('<h3 class="text-base font-bold text-slate-700 mb-3">项目经营详情 <span class="text-sm text-slate-500 font-normal ml-3">所有项目产生的签单,确收,毛利,回款,当期成本,当期流出,现金流</span></h3><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-3 gap-2.5">' +
|
'<div class="grid grid-cols-3 gap-2.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-600">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-600">' + (state.finYear===0 ? '所有年份' : state.finYear) + '</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-600">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-600">' + (state.finYear===0 ? '所有年份' : state.finYear) + '</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-600">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-600">' + (state.finYear===0 ? '所有年份' : state.finYear) + '</span></div><div style="position:relative;height:200px"><canvas id="finChartCash"></canvas></div>', 'p-4') +
|
||||||
'</div></div>';
|
'</div></div>';
|
||||||
|
|
||||||
if (window.lucide) window.lucide.createIcons();
|
if (window.lucide) window.lucide.createIcons();
|
||||||
@@ -1111,7 +1180,9 @@ function renderFinanceQuarterlyOverview() {
|
|||||||
|
|
||||||
// 财务概览: 7 rows
|
// 财务概览: 7 rows
|
||||||
var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
|
var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
|
||||||
var annualVals = [m.signed_annual||m.signed_amount||0, m.revenue_annual||0, m.gross_annual||0, m.cost_annual||0, m.paid_annual||0, m.payment_annual||0, (m.payment_annual||0) - (m.paid_annual||0)];
|
var annualSums = { sign:0, revenue:0, gross:0, cost:0, paid:0, payment:0 };
|
||||||
|
fm.forEach(x => { annualSums.sign += x.sign||0; annualSums.revenue += x.revenue||0; annualSums.gross += x.gross||0; annualSums.cost += x.cost||0; annualSums.paid += x.paid||0; annualSums.payment += x.payment||0; });
|
||||||
|
var annualVals = [annualSums.sign, annualSums.revenue, annualSums.gross, annualSums.cost, annualSums.paid, annualSums.payment, annualSums.payment - annualSums.paid];
|
||||||
var fmFields = ['sign','revenue','gross','cost','paid','payment',null];
|
var fmFields = ['sign','revenue','gross','cost','paid','payment',null];
|
||||||
var qData = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]];
|
var qData = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]];
|
||||||
for (var qi = 0; qi < 4; qi++) {
|
for (var qi = 0; qi < 4; qi++) {
|
||||||
@@ -1156,11 +1227,13 @@ function renderFinanceQuarterlyOverview() {
|
|||||||
if ((finExp[ei].status || '') === '暂不计入') continue;
|
if ((finExp[ei].status || '') === '暂不计入') continue;
|
||||||
var em = (finExp[ei].expense_month || '').substring(0, 7);
|
var em = (finExp[ei].expense_month || '').substring(0, 7);
|
||||||
var emi = parseInt(em.substring(5)) - 1;
|
var emi = parseInt(em.substring(5)) - 1;
|
||||||
|
var emYear = parseInt(em.substring(0, 4)) || 0;
|
||||||
|
if (state.finYear !== 0 && emYear && emYear !== state.finYear) continue;
|
||||||
if (emi >= 0 && emi < 12) { var eqi = Math.floor(emi / 3); expQ[eqi] += parseFloat(finExp[ei].amount || 0); }
|
if (emi >= 0 && emi < 12) { var eqi = Math.floor(emi / 3); expQ[eqi] += parseFloat(finExp[ei].amount || 0); }
|
||||||
}
|
}
|
||||||
var expAnnual = 0; for (var ei = 0; ei < 4; ei++) expAnnual += expQ[ei];
|
var expAnnual = 0; for (var ei = 0; ei < 4; ei++) expAnnual += expQ[ei];
|
||||||
var grossQ = [qData[2][0], qData[2][1], qData[2][2], qData[2][3]];
|
var grossQ = [qData[2][0], qData[2][1], qData[2][2], qData[2][3]];
|
||||||
var grossAnnual = m.gross_annual || 0;
|
var grossAnnual = 0; for (var ei = 0; ei < fm.length; ei++) grossAnnual += (fm[ei] ? fm[ei].gross : 0);
|
||||||
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||||
for (var qi = 0; qi < 4; qi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q' + (qi+1) + '</th>'; if (qi > 0) deptThead += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">环比</th>'; }
|
for (var qi = 0; qi < 4; qi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q' + (qi+1) + '</th>'; if (qi > 0) deptThead += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">环比</th>'; }
|
||||||
deptThead += '</tr>';
|
deptThead += '</tr>';
|
||||||
@@ -1186,12 +1259,14 @@ function renderFinanceQuarterlyOverview() {
|
|||||||
|
|
||||||
// 业务线现金流: quarterly
|
// 业务线现金流: quarterly
|
||||||
var cfQ = [qData[5][0], qData[5][1], qData[5][2], qData[5][3]];
|
var cfQ = [qData[5][0], qData[5][1], qData[5][2], qData[5][3]];
|
||||||
var cfAnnual = m.payment_annual || 0;
|
var cfAnnual = 0; for (var ei = 0; ei < fm.length; ei++) cfAnnual += (fm[ei] ? fm[ei].payment : 0);
|
||||||
var expPaidQ = [0,0,0,0];
|
var expPaidQ = [0,0,0,0];
|
||||||
for (var ei = 0; ei < finExp.length; ei++) {
|
for (var ei = 0; ei < finExp.length; ei++) {
|
||||||
if ((finExp[ei].status || '') === '暂不计入') continue;
|
if ((finExp[ei].status || '') === '暂不计入') continue;
|
||||||
var em = (finExp[ei].expense_month || '').substring(0, 7);
|
var em = (finExp[ei].expense_month || '').substring(0, 7);
|
||||||
var emi = parseInt(em.substring(5)) - 1;
|
var emi = parseInt(em.substring(5)) - 1;
|
||||||
|
var emYear = parseInt(em.substring(0, 4)) || 0;
|
||||||
|
if (state.finYear !== 0 && emYear && emYear !== state.finYear) continue;
|
||||||
if (emi >= 0 && emi < 12) { var eqi = Math.floor(emi / 3); expPaidQ[eqi] += parseFloat(finExp[ei].incurred_amount || 0); }
|
if (emi >= 0 && emi < 12) { var eqi = Math.floor(emi / 3); expPaidQ[eqi] += parseFloat(finExp[ei].incurred_amount || 0); }
|
||||||
}
|
}
|
||||||
var expPaidAnnual = 0; for (var ei = 0; ei < 4; ei++) expPaidAnnual += expPaidQ[ei];
|
var expPaidAnnual = 0; for (var ei = 0; ei < 4; ei++) expPaidAnnual += expPaidQ[ei];
|
||||||
@@ -1217,7 +1292,8 @@ function renderFinanceQuarterlyOverview() {
|
|||||||
cfTbody += rowHtml + '</tr>';
|
cfTbody += rowHtml + '</tr>';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelector("#financeQuarterlyOverview").innerHTML = '<div class="grid gap-2.5">' +
|
const yearSelectQ = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.finYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.finYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
|
||||||
|
document.querySelector("#financeQuarterlyOverview").innerHTML = yearSelectQ + '<div class="grid gap-2.5">' +
|
||||||
card('<h3 class="text-base font-bold text-slate-700 mb-3">部门损益情况 <span class="text-sm text-slate-500 font-normal ml-3">部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + deptTbody + '</tbody></table></div>', 'p-4') +
|
card('<h3 class="text-base font-bold text-slate-700 mb-3">部门损益情况 <span class="text-sm text-slate-500 font-normal ml-3">部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + deptTbody + '</tbody></table></div>', 'p-4') +
|
||||||
card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4') +
|
card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4') +
|
||||||
card('<h3 class="text-base font-bold text-slate-700 mb-3">项目经营详情 <span class="text-sm text-slate-500 font-normal ml-3">所有项目产生的签单,确收,毛利,回款,当期成本,当期流出,现金流</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
|
card('<h3 class="text-base font-bold text-slate-700 mb-3">项目经营详情 <span class="text-sm text-slate-500 font-normal ml-3">所有项目产生的签单,确收,毛利,回款,当期成本,当期流出,现金流</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
|
||||||
|
|||||||
@@ -1036,10 +1036,10 @@ window.deletePlanItem = async () => {
|
|||||||
|
|
||||||
|
|
||||||
// 从 planFinances 计算月度数据
|
// 从 planFinances 计算月度数据
|
||||||
function computePlanMonthly(planFinances) {
|
function computePlanMonthly(planFinances, year) {
|
||||||
var pfs = planFinances || [];
|
var pfs = planFinances || [];
|
||||||
var months = [];
|
var months = [];
|
||||||
var year = new Date().getFullYear();
|
year = year || new Date().getFullYear();
|
||||||
for (var mi = 1; mi <= 12; mi++) { months.push(year + '-' + String(mi).padStart(2, '0')); }
|
for (var mi = 1; mi <= 12; mi++) { months.push(year + '-' + String(mi).padStart(2, '0')); }
|
||||||
var data = [];
|
var data = [];
|
||||||
for (var i = 0; i < 12; i++) {
|
for (var i = 0; i < 12; i++) {
|
||||||
@@ -1062,7 +1062,7 @@ function computePlanMonthly(planFinances) {
|
|||||||
// 总览渲染
|
// 总览渲染
|
||||||
function renderPlanOverview() {
|
function renderPlanOverview() {
|
||||||
const planFinances = state.data.planFinances || [];
|
const planFinances = state.data.planFinances || [];
|
||||||
const fm = computePlanMonthly(planFinances);
|
const fm = computePlanMonthly(planFinances, state.planYear);
|
||||||
const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
|
const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
|
||||||
const moneyWan = (v) => {
|
const moneyWan = (v) => {
|
||||||
const n = Number(v || 0);
|
const n = Number(v || 0);
|
||||||
@@ -1180,9 +1180,9 @@ function renderPlanOverview() {
|
|||||||
cfCard +
|
cfCard +
|
||||||
card('<h3 class="text-base font-bold text-slate-700 mb-3">项目经营详情 <span class="text-sm text-slate-500 font-normal ml-3">所有项目产生的签单,确收,毛利,回款,成本,支出,现金流</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
|
card('<h3 class="text-base font-bold text-slate-700 mb-3">项目经营详情 <span class="text-sm text-slate-500 font-normal ml-3">所有项目产生的签单,确收,毛利,回款,成本,支出,现金流</span></h3><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-3 gap-2.5">' +
|
'<div class="grid grid-cols-3 gap-2.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-600">2026</span></div><div style="position:relative;height:200px"><canvas id="planChartSign"></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-600">' + state.planYear + '</span></div><div style="position:relative;height:200px"><canvas id="planChartSign"></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-600">2026</span></div><div style="position:relative;height:200px"><canvas id="planChartRev"></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-600">' + state.planYear + '</span></div><div style="position:relative;height:200px"><canvas id="planChartRev"></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-600">2026</span></div><div style="position:relative;height:200px"><canvas id="planChartCash"></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-600">' + state.planYear + '</span></div><div style="position:relative;height:200px"><canvas id="planChartCash"></canvas></div>', 'p-4') +
|
||||||
'</div></div>';
|
'</div></div>';
|
||||||
|
|
||||||
if (window.lucide) window.lucide.createIcons();
|
if (window.lucide) window.lucide.createIcons();
|
||||||
@@ -1195,7 +1195,7 @@ function renderPlanOverview() {
|
|||||||
|
|
||||||
function renderPlanQuarterlyOverview() {
|
function renderPlanQuarterlyOverview() {
|
||||||
const planFinances = state.data.planFinances || [];
|
const planFinances = state.data.planFinances || [];
|
||||||
const fm = computePlanMonthly(planFinances);
|
const fm = computePlanMonthly(planFinances, state.planYear);
|
||||||
const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
|
const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
|
||||||
const card = (body, cls) => '<div class="card ' + (cls || 'p-4') + '">' + body + '</div>';
|
const card = (body, cls) => '<div class="card ' + (cls || 'p-4') + '">' + body + '</div>';
|
||||||
var annualSums2 = { sign: 0, revenue: 0, gross: 0, cost: 0, payment: 0, paid: 0 };
|
var annualSums2 = { sign: 0, revenue: 0, gross: 0, cost: 0, payment: 0, paid: 0 };
|
||||||
|
|||||||
Reference in New Issue
Block a user