diff --git a/static/modules/finance.js b/static/modules/finance.js index 00e9d0f..0d44f26 100644 --- a/static/modules/finance.js +++ b/static/modules/finance.js @@ -98,7 +98,7 @@ function renderFinance() { })(); const sm = pf.sign_month || ""; const signMonthCell = `${sm || '—'}`; - return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${signMonthCell}${money(pf.sign_amount)}${mCols}${totalCol}`; + return `${esc(pf.customer_name)}${signMonthCell}${money(pf.sign_amount)}${mCols}${totalCol}`; }; const now2 = new Date(); @@ -285,14 +285,14 @@ function renderFinance() { }; const SIGNED_COLS = [ { key: '_expand', label: '' }, - { key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' }, + { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' }, { key: 'rev', label: '已确收' }, { key: 'gross', label: '毛利' }, { key: 'gross_rate', label: '毛利率' }, { key: 'payment', label: '回款' }, { key: 'pending', label: '应收' }, { key: 'pay_rate', label: '回款率' }, { key: 'cost', label: '成本' }, { key: 'paid', label: '支出' }, { key: 'payable', label: '应付' }, { key: 'paid_rate', label: '付款率' }, { key: 'cashflow', label: '现金流' }, ]; const UNSIGNED_COLS = [ - { key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' }, + { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' }, { key: 'sign_month', label: '签约月份' }, { key: 'gross', label: '毛利' }, { key: 'cost', label: '成本' }, , ]; @@ -317,7 +317,7 @@ function renderFinance() { const cardHtml = (r, i) => '
' + r[0] + '
' + r[1] + '
'; const fs = '
' + cardHtml(rows_data[0],0) + cardHtml(rows_data[1],1) + cardHtml(rows_data[2],2) + '' + cardHtml(rows_data[3],3) + cardHtml(rows_data[4],4) + cardHtml(rows_data[5],5) + '' + cardHtml(rows_data[6],6) + cardHtml(rows_data[7],7) + cardHtml(rows_data[8],8) + '
'; const thead = isUnsigned ? buildThead(UNSIGNED_COLS, 'uns') : buildThead(SIGNED_COLS, 'sig'); - const theadCols = isUnsigned ? 6 : 15; + const theadCols = isUnsigned ? 5 : 14; const tbody = rows.length ? `${rows.join("")}` : `${emptyText}`; const subtitle = isUnsigned ? '合同 → 毛利 → 成本 → 项目利润' @@ -366,11 +366,11 @@ function renderFinance() { var bCf = bPay - ePaid; return '' + m + '' + (bRev ? money(bRev) : '—') + '' + (bGross ? money(bGross) : '—') + '' + (bPay ? money(bPay) : '—') + '' + (eCost ? money(eCost) : '—') + '' + (ePaid ? money(ePaid) : '—') + '' + (bCf ? money(bCf) : '—') + '' + (bRev - bPay ? money(bRev - bPay) : '—') + ''; }).join(''); - return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${money(pf.sign_amount)}${fmtNoUnit(rev)}${fmtNoUnit(gross)}${grossR}${fmtNoUnit(payment)}${pendingVal}${payR}${fmtNoUnit(cost)}${fmtNoUnit(paid)}${payableVal}${paidR}${cf}${detailRows}
月份确收毛利回款成本支出现金流应收
`; + return `${esc(pf.customer_name)}${money(pf.sign_amount)}${fmtNoUnit(rev)}${fmtNoUnit(gross)}${grossR}${fmtNoUnit(payment)}${pendingVal}${payR}${fmtNoUnit(cost)}${fmtNoUnit(paid)}${payableVal}${paidR}${cf}${detailRows}
月份确收毛利回款成本支出现金流应收
`; }; // 待签约简版行 const tdRowUnsigned = (pf, cost, gross) => { - return `${esc(pf.customer_name)}${money(pf.sign_amount)}${pf.sign_month || '—'}${fmtNoUnit(gross)}${fmtNoUnit(cost)}`; + return `${esc(pf.customer_name)}${money(pf.sign_amount)}${pf.sign_month || '—'}${fmtNoUnit(gross)}${fmtNoUnit(cost)}`; }; if (state.finView === 'monthly') { @@ -958,7 +958,8 @@ window.createFinance = async (event) => { document.querySelector("#financeModalTitle").textContent = "新增项目财务"; closeFinanceModal(); state.data.projectFinances = await api("/api/projectFinances/list?tenant=" + encodeURIComponent(state.tenant)); - renderFinance(); + localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks)); + renderFinance(); } catch (error) { toast("保存失败:" + error.message, "error"); } @@ -974,7 +975,8 @@ window.deleteFinanceItem = async () => { await api(`/api/projectFinances/${pfId}`, { method: "DELETE" }); closeFinanceModal(); state.data.projectFinances = await api("/api/projectFinances/list?tenant=" + encodeURIComponent(state.tenant)); - renderFinance(); + localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks)); + renderFinance(); toast("已删除", "success"); } catch (error) { toast("删除失败:" + error.message, "error"); @@ -1254,8 +1256,51 @@ function renderOverviewCharts(data) { }; // ====== 筛选器控制函数 ====== -window.switchFinFilter = (f) => { state.finFilter = f; renderFinance(); }; -window.setFinView = (v) => { state.finView = v; renderFinance(); }; -window.setFinYear = (y) => { state.finYear = parseInt(y); renderFinance(); }; -window.setFinQuarter = (q) => { state.finView = 'quarterly'; state.finQuarter = q; renderFinance(); }; -window.setFinMonth = (m) => { state.finView = 'monthly'; state.finMonth = m; renderFinance(); }; +window.switchFinFilter = (f) => { state.finFilter = f; localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks)); + renderFinance(); }; +window.setFinView = (v) => { state.finView = v; localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks)); + renderFinance(); }; +window.setFinYear = (y) => { state.finYear = parseInt(y); localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks)); + renderFinance(); }; +window.setFinQuarter = (q) => { state.finView = 'quarterly'; state.finQuarter = q; localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks)); + renderFinance(); }; +window.setFinMonth = (m) => { state.finView = 'monthly'; state.finMonth = m; localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks)); + renderFinance(); }; + + +// ---- 项目标记上下文菜单 ---- +if (!state._fmMarks) state._fmMarks = (function() { try { return JSON.parse(localStorage.getItem('opc_fm_marks')) || { red: {} }; } catch(e) { return { red: {} }; } })(); +window.finShowCtx = function(e, pfId) { + e.preventDefault(); + var menu = document.getElementById('finCtxMenu'); + if (!menu) { + menu = document.createElement('div'); + 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 = '
项目标记
' + + '
标红
'; + menu.addEventListener('click', function(ev) { + ev.stopPropagation(); + 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; } + localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks)); + renderFinance(); + }); + document.body.appendChild(menu); + } + 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'; +}; +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 c270d69..88bb88b 100644 --- a/static/modules/plan.js +++ b/static/modules/plan.js @@ -96,7 +96,7 @@ function renderPlan() { })(); const sm = pf.sign_month || ""; const signMonthCell = `${sm || '—'}`; - return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${esc(pf.weight || "20%")}${esc(pf.project_type || "待签约")}${signMonthCell}${money(pf.sign_amount)}${mCols}${totalCol}`; + return `${esc(pf.customer_name)}${esc(pf.weight || "20%")}${esc(pf.project_type || "待签约")}${signMonthCell}${money(pf.sign_amount)}${mCols}${totalCol}`; }; const now2 = new Date(); @@ -336,7 +336,7 @@ function renderPlan() { const cardHtml = (r, i) => '
' + r[0] + '
' + r[1] + '
'; const fs = '
' + cardHtml(rows_data[0],0) + cardHtml(rows_data[1],1) + cardHtml(rows_data[2],2) + '' + cardHtml(rows_data[3],3) + cardHtml(rows_data[4],4) + cardHtml(rows_data[5],5) + '' + cardHtml(rows_data[6],6) + cardHtml(rows_data[7],7) + cardHtml(rows_data[8],8) + '
'; const thead = isUnsigned ? buildThead(UNSIGNED_COLS, 'uns') : buildThead(SIGNED_COLS, 'sig'); - const theadCols = isUnsigned ? 9 : 15; + const theadCols = isUnsigned ? 8 : 14; const tbody = rows.length ? `${rows.join("")}` : `${emptyText}`; const subtitle = isUnsigned ? '合同 → 毛利 → 成本 → 项目利润' @@ -398,14 +398,14 @@ function renderPlan() { var bCf = bPay - ePaid; return '' + m + '' + (bRev ? money(bRev) : '—') + '' + (bGross ? money(bGross) : '—') + '' + (bPay ? money(bPay) : '—') + '' + (eCost ? money(eCost) : '—') + '' + (ePaid ? money(ePaid) : '—') + '' + (bCf ? money(bCf) : '—') + '' + (bRev - bPay ? money(bRev - bPay) : '—') + ''; }).join(''); - return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}${money(pf.sign_amount)}${fmtNoUnit(rev)}${fmtNoUnit(gross)}${grossR}${fmtNoUnit(payment)}${pendingVal}${payR}${fmtNoUnit(cost)}${fmtNoUnit(paid)}${payableVal}${cf}${detailRows}
月份确收毛利回款当期成本当期流出现金流应收
`; + return `${esc(pf.customer_name)}${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}${money(pf.sign_amount)}${fmtNoUnit(rev)}${fmtNoUnit(gross)}${grossR}${fmtNoUnit(payment)}${pendingVal}${payR}${fmtNoUnit(cost)}${fmtNoUnit(paid)}${payableVal}${cf}${detailRows}
月份确收毛利回款当期成本当期流出现金流应收
`; }; // 待签约简版行 const tdRowUnsigned = (pf, cost, gross) => { const profit = gross; const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600'; const pfVal = profit ? money(profit) : ''; - return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}${money(pf.sign_amount)}${pf.sign_month || '—'}${fmtNoUnit(gross)}${fmtNoUnit(cost)}${pfVal}`; + return `${esc(pf.customer_name)}${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}${money(pf.sign_amount)}${pf.sign_month || '—'}${fmtNoUnit(gross)}${fmtNoUnit(cost)}${pfVal}`; }; if (state.planView === 'monthly') { @@ -1012,7 +1012,8 @@ window.createPlanFinance = async (event) => { document.querySelector("#planModalTitle").textContent = "新增项目财务"; closePlanModal(); state.data.planFinances = await api("/api/planFinances/list?tenant=" + encodeURIComponent(state.tenant)); - renderPlan(); + localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks)); + renderPlan(); } catch (error) { toast("刷新失败:" + error.message, "error"); } @@ -1028,7 +1029,8 @@ window.deletePlanItem = async () => { await api(`/api/planFinances/${pfId}`, { method: "DELETE" }); closePlanModal(); state.data.planFinances = await api("/api/planFinances/list?tenant=" + encodeURIComponent(state.tenant)); - renderPlan(); + localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks)); + renderPlan(); toast("已删除", "success"); } catch (error) { toast("删除失败:" + error.message, "error"); @@ -1356,7 +1358,8 @@ window.planInlineUpdate = async (pfId, field, value) => { try { await api("/api/planFinances/" + pfId, { method: "PUT", body: JSON.stringify({ data: { [field]: value } }) }); state.data.planFinances = await api("/api/planFinances/list?tenant=" + encodeURIComponent(state.tenant)); - renderPlan(); + localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks)); + renderPlan(); toast("已更新", "success"); } catch (error) { toast("更新失败:" + error.message, "error"); @@ -1364,9 +1367,53 @@ window.planInlineUpdate = async (pfId, field, value) => { }; // ====== 筛选器控制函数 ====== -window.switchPlanFilter = (f) => { state.planFilter = f; renderPlan(); }; -window.setPlanView = (v) => { state.planView = v; renderPlan(); }; -window.setPlanYear = (y) => { state.planYear = parseInt(y); renderPlan(); }; -window.setPlanQuarter = (q) => { state.planView = 'quarterly'; state.planQuarter = q; renderPlan(); }; -window.setPlanMonth = (m) => { state.planView = 'monthly'; state.planMonth = m; renderPlan(); }; -window.setPlanStatusFilter = (f) => { state.planStatusFilter = f; renderPlan(); }; +window.switchPlanFilter = (f) => { state.planFilter = f; localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks)); + renderPlan(); }; +window.setPlanView = (v) => { state.planView = v; localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks)); + renderPlan(); }; +window.setPlanYear = (y) => { state.planYear = parseInt(y); localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks)); + renderPlan(); }; +window.setPlanQuarter = (q) => { state.planView = 'quarterly'; state.planQuarter = q; localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks)); + renderPlan(); }; +window.setPlanMonth = (m) => { state.planView = 'monthly'; state.planMonth = m; localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks)); + renderPlan(); }; +window.setPlanStatusFilter = (f) => { state.planStatusFilter = f; localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks)); + renderPlan(); }; + + +// ---- 项目标记上下文菜单 ---- +if (!state._pmMarks) state._pmMarks = (function() { try { return JSON.parse(localStorage.getItem('opc_pm_marks')) || { red: {} }; } catch(e) { return { red: {} }; } })(); +window.planShowCtx = function(e, pfId) { + e.preventDefault(); + var menu = document.getElementById('planCtxMenu'); + if (!menu) { + menu = document.createElement('div'); + menu.id = 'planCtxMenu'; + 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 = '
项目标记
' + + '
标红
'; + menu.addEventListener('click', function(ev) { + ev.stopPropagation(); + var id = menu.getAttribute('data-pfid'); + menu.style.display = 'none'; + if (state._pmMarks.red[id]) { delete state._pmMarks.red[id]; } else { state._pmMarks.red[id] = true; } + localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks)); + renderPlan(); + }); + document.body.appendChild(menu); + } + 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'; +}; +document.addEventListener('click', function() { var m = document.getElementById('planCtxMenu'); if (m) m.style.display = 'none'; }); + +window.planCtxMenu_act = function(e, type) { + e.stopPropagation(); + var pfId = document.getElementById("planCtxMenu").getAttribute("data-pfid"); + if (type === "clear") { delete state._pmMarks.red[pfId]; delete state._pmMarks.pin[pfId]; } + else { if (state._pmMarks[type][pfId]) { delete state._pmMarks[type][pfId]; } else { state._pmMarks[type][pfId] = true; } } + localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks)); + renderPlan(); +}; diff --git a/templates/index.html b/templates/index.html index 79d687c..6badee7 100644 --- a/templates/index.html +++ b/templates/index.html @@ -84,7 +84,7 @@
-

OPC Manager v1.2.0.38

+

OPC Manager v1.2.0.39

科普 OPC 工作台