' +
deptCard +
cfCard +
@@ -1292,7 +1293,7 @@ function renderFinanceQuarterlyOverview() {
cfTbody += rowHtml + '';
}
- const yearSelectQ = `
`;
document.querySelector("#financeQuarterlyOverview").innerHTML = yearSelectQ + '
' +
card('
部门损益情况 部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出
' + deptThead + '' + deptTbody + '
', 'p-4') +
card('
部门现金流 部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入
' + deptThead + '' + cfTbody + '
', 'p-4') +
@@ -1343,7 +1344,21 @@ window.setFinMonth = (m) => { state.finView = 'monthly'; state.finMonth = m; loc
// ---- 项目标记上下文菜单 ----
-if (!state._fmMarks) state._fmMarks = (function() { try { return JSON.parse(localStorage.getItem('opc_fm_marks')) || { red: {} }; } catch(e) { return { red: {} }; } })();
+if (!state._fmMarks) state._fmMarks = (function() { try { return JSON.parse(localStorage.getItem('opc_fm_marks')) || { color: {} }; } catch(e) { return { color: {} }; } })();
+if (!state._fmMarks.color) state._fmMarks.color = {};
+
+const FM_COLORS = [
+ { key: 'red', label: '标红', hex: '#ef4444', bg: 'bg-red-50' },
+ { key: 'yellow', label: '标黄', hex: '#eab308', bg: 'bg-yellow-50' },
+ { key: 'blue', label: '标蓝', hex: '#3b82f6', bg: 'bg-blue-50' },
+ { key: 'green', label: '标绿', hex: '#22c55e', bg: 'bg-green-50' },
+ { key: 'purple', label: '标紫', hex: '#a855f7', bg: 'bg-purple-50' },
+];
+const fmMarkBg = function(id) {
+ var c = (state._fmMarks.color||{})[id];
+ return c ? { red:'bg-red-50',yellow:'bg-yellow-50',blue:'bg-blue-50',green:'bg-green-50',purple:'bg-purple-50'}[c] || '' : '';
+};
+
window.finShowCtx = function(e, pfId) {
e.preventDefault();
var menu = document.getElementById('finCtxMenu');
@@ -1352,12 +1367,27 @@ window.finShowCtx = function(e, pfId) {
menu.id = 'finCtxMenu';
menu.style.cssText = 'display:none;position:fixed;z-index:9999;background:#fff;border-radius:8px;box-shadow:0 10px 40px rgba(0,0,0,.15);border:1px solid #e2e8f0;padding:4px 0;min-width:130px';
menu.innerHTML = '
项目标记
' +
- '
标红
';
+ FM_COLORS.map(function(c) {
+ return '
' + c.label + '
';
+ }).join('') +
+ '
' +
+ '
取消标记
';
menu.addEventListener('click', function(ev) {
+ var target = ev.target.closest('[data-act]');
+ if (!target) return;
ev.stopPropagation();
+ var act = target.getAttribute('data-act');
var id = menu.getAttribute('data-pfid');
menu.style.display = 'none';
- if (state._fmMarks.red[id]) { delete state._fmMarks.red[id]; } else { state._fmMarks.red[id] = true; }
+ if (act === 'clear') {
+ delete state._fmMarks.color[id];
+ } else {
+ if (state._fmMarks.color[id] === act) {
+ delete state._fmMarks.color[id];
+ } else {
+ state._fmMarks.color[id] = act;
+ }
+ }
localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
renderFinance();
});
@@ -1366,15 +1396,6 @@ window.finShowCtx = function(e, pfId) {
menu.setAttribute('data-pfid', pfId);
menu.style.display = 'block';
menu.style.left = e.pageX + 'px';
- menu.style.top = Math.min(e.pageY, window.innerHeight - 200) + 'px';
+ menu.style.top = Math.min(e.pageY, window.innerHeight - 280) + 'px';
};
document.addEventListener('click', function() { var m = document.getElementById('finCtxMenu'); if (m) m.style.display = 'none'; });
-
-window.finCtxMenu_act = function(e, type) {
- e.stopPropagation();
- var pfId = document.getElementById("finCtxMenu").getAttribute("data-pfid");
- if (type === "clear") { delete state._fmMarks.red[pfId]; delete state._fmMarks.pin[pfId]; }
- else { if (state._fmMarks[type][pfId]) { delete state._fmMarks[type][pfId]; } else { state._fmMarks[type][pfId] = true; } }
- localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
- renderFinance();
-};
diff --git a/static/modules/plan.js b/static/modules/plan.js
index 96b1ec0..91dc31b 100644
--- a/static/modules/plan.js
+++ b/static/modules/plan.js
@@ -103,15 +103,15 @@ function renderPlan() {
const defaultMonth2 = now2.getFullYear() + "-" + String(now2.getMonth()+1).padStart(2,"0");
if (!state.planMonth) state.planMonth = defaultMonth2;
if (state.planQuarter === undefined) state.planQuarter = Math.floor(now2.getMonth() / 3);
- if (!state.planYear) state.planYear = now2.getFullYear();
- if (!state.planView) state.planView = 'annual';
+ if (state.planYear == null) state.planYear = now2.getFullYear();
+ if (!['annual','quarterly','monthly'].includes(state.planView)) state.planView = 'annual';
const qLabels = ['Q1','Q2','Q3','Q4'];
const qMonths = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]];
const nowD = new Date();
const activeQ = state.planQuarter !== undefined ? state.planQuarter : Math.floor(nowD.getMonth() / 3);
const activeMonths = qMonths[activeQ];
- const yearOpts = [2024,2025,2026,2027];
- const finHeaderBase = `
|年份:||季度:${qLabels.map((q,i) => ``).join('')}|月度:${activeMonths.map(m => { const ms = `${state.planYear}-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`;
+ const yearOpts = [2020,2021,2022,2023,2024,2025,2026,2027];
+ const finHeaderBase = `
年份:||季度:${qLabels.map((q,i) => ``).join('')}|月度:${activeMonths.map(m => { const ms = `${state.planYear}-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`;
const finAddBtn = `
`;
const isOverviewMode = false;
@@ -369,10 +369,10 @@ function renderPlan() {
const cf = cashflow ? money(cashflow) : '
—';
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
const payable = cost - paid;
- const payableCls = payable > 0 ? 'text-rose-600' : payable < 0 ? 'text-green-600' : 'text-slate-400';
+ const payableCls = payable > 0 ? 'text-green-600' : payable < 0 ? 'text-red-600' : 'text-slate-400';
const payableVal = payable ? (payable > 0 ? '+' : '') + money(payable) : '
—';
const pending = rev - payment;
- const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-700';
+ const pendingCls = pending > 0 ? 'text-green-600' : pending < 0 ? 'text-red-600' : 'text-slate-700';
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '
—';
var budgetArr = [];
var expenseArr = [];
@@ -463,12 +463,12 @@ function renderPlan() {
const qRange = qRanges[selQ];
const sumBudget = (pf, field) => {
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.planYear) 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.planYear || state.planYear === 0)) total += parseFloat(b[field] || 0); }); } catch (e) {}
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; const y = parseInt((e.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.planYear) 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.planYear || state.planYear === 0)) total += parseFloat(e[field] || 0); }); } catch (e) {}
return total;
};
let dataItems = [];
@@ -1113,6 +1113,8 @@ function renderPlanOverview() {
for (var ei = 0; ei < planExp.length; ei++) {
var em = (planExp[ei].expense_month || '').substring(0, 7);
var mi = parseInt(em.substring(5)) - 1;
+ var emYear = parseInt(em.substring(0, 4)) || 0;
+ if (state.planYear !== 0 && emYear && emYear !== state.planYear) continue;
if (mi >= 0 && mi < 12) expMonthly[mi] += parseFloat(planExp[ei].amount || 0);
}
var expAnnual = 0; for (var ei = 0; ei < 12; ei++) expAnnual += expMonthly[ei];
@@ -1149,6 +1151,8 @@ function renderPlanOverview() {
for (var epi = 0; epi < planExp.length; epi++) {
var epm = (planExp[epi].expense_month || '').substring(0, 7);
var epmi = parseInt(epm.substring(5)) - 1;
+ var epmYear = parseInt(epm.substring(0, 4)) || 0;
+ if (state.planYear !== 0 && epmYear && epmYear !== state.planYear) continue;
if (epmi >= 0 && epmi < 12) expPaidMonthly[epmi] += parseFloat(planExp[epi].incurred_amount || 0);
}
var expPaidAnnual = 0; for (var epi = 0; epi < 12; epi++) expPaidAnnual += expPaidMonthly[epi];
@@ -1175,14 +1179,15 @@ function renderPlanOverview() {
}
var cfCard = card('
部门现金流 部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入
' + deptThead + '' + cfTbody + '
', 'p-4');
- document.querySelector("#planOverview").innerHTML = '
' +
+ const yearSelectOv = `
📅 选择年份:
`;
+ document.querySelector("#planOverview").innerHTML = yearSelectOv + '
' +
deptCard +
cfCard +
card('
项目经营详情 所有项目产生的签单,确收,毛利,回款,成本,支出,现金流
' + thead + '' + tbody + '
', 'p-4') +
'
' +
- card('
月度签约趋势
' + state.planYear + '', 'p-4') +
- card('
月度确收与毛利
' + state.planYear + '', 'p-4') +
- card('
月度回款与已付
' + state.planYear + '', 'p-4') +
+ card('
月度签约趋势
' + (state.planYear===0 ? '所有年份' : state.planYear) + '', 'p-4') +
+ card('
月度确收与毛利
' + (state.planYear===0 ? '所有年份' : state.planYear) + '', 'p-4') +
+ card('
月度回款与已付
' + (state.planYear===0 ? '所有年份' : state.planYear) + '', 'p-4') +
'
';
if (window.lucide) window.lucide.createIcons();
@@ -1312,7 +1317,8 @@ function renderPlanQuarterlyOverview() {
cfTbody += rowHtml + '';
}
- document.querySelector("#planQuarterlyOverview").innerHTML = '
' +
+ const yearSelectQ = `
📅 选择年份:
`;
+ document.querySelector("#planQuarterlyOverview").innerHTML = yearSelectQ + '
' +
card('
部门损益情况 部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出
' + deptThead + '' + deptTbody + '
', 'p-4') +
card('
部门现金流 部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入
' + deptThead + '' + cfTbody + '
', 'p-4') +
card('
项目经营详情 所有项目产生的签单,确收,毛利,回款,成本,支出,现金流
' + thead + '' + tbody + '
', 'p-4') +
diff --git a/templates/index.html b/templates/index.html
index 7c8e144..cc19cce 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -106,7 +106,7 @@
-
OPC Manager v1.2.0.41
+
OPC Manager {{ app_version }}