diff --git a/static/modules/expense.js b/static/modules/expense.js index 12080c0..5a71502 100644 --- a/static/modules/expense.js +++ b/static/modules/expense.js @@ -84,7 +84,9 @@ function renderExpense() { EXPENSE_TYPES.forEach(function(t) { modal += ''; }); modal += '
'; - document.querySelector("#expense").innerHTML = '
' + + var target = document.querySelector("#financeExpense") || document.querySelector("#financeTabExpense") || document.querySelector("#expense"); + if (!target) return; + target.innerHTML = '
' + '
' + toolbar + '
' + '
' + '
费用记录' + filtered.length + '
' + diff --git a/static/modules/finance.js b/static/modules/finance.js index a1e7968..1a429c7 100644 --- a/static/modules/finance.js +++ b/static/modules/finance.js @@ -116,11 +116,11 @@ function renderFinance() { const finHeaderBase = `视图:${toolDateSelect}`; const finAddBtn = ``; - const finFilterTabs = `
`; + const finFilterTabs = `
`; document.querySelector("#finance").innerHTML = `
${finFilterTabs} - ${card(`
${finHeaderBase}
${finAddBtn}
`, "p-4")} + ${state.finFilter !== 'expense' ? card(`
${finHeaderBase}
${finAddBtn}
`, "p-4") : ''}
${(() => { + if (state.finFilter === 'expense') { + setTimeout(() => renderExpense(), 10); + return `
`; + } const isUnsigned = state.finFilter === '待签约'; const METRIC_CARDS = [ { label: '签约金额', value: ctx => moneyWan(ctx.signTotal), color: 'text-slate-700' }, diff --git a/static/modules/performance.js b/static/modules/performance.js new file mode 100644 index 0000000..31cd249 --- /dev/null +++ b/static/modules/performance.js @@ -0,0 +1,124 @@ +// performance.js — 绩效模块 +window.renderPerformance = () => { + const data = state.data; + if (!data) return; + + if (state.perfQuarter === undefined) state.perfQuarter = Math.floor(new Date().getMonth() / 3); + const q = state.perfQuarter; + const qLabel = ['Q1 (1-3月)', 'Q2 (4-6月)', 'Q3 (7-9月)', 'Q4 (10-12月)']; + const qRanges = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]]; + const qRange = qRanges[q]; + const storageKey = 'opc-performance-Q' + (q + 1); + + const pfs = (data.projectFinances || []).filter(p => p.status === '已签约'); + + const sumQ = (field) => { + let total = 0; + pfs.forEach(p => { + try { JSON.parse(p.budget_data || '[]').forEach(b => { const m = parseInt((b.month || '').substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(b[field] || 0); }); } catch (e) {} + }); + return Math.round(total); + }; + + const signTotal = Math.round(pfs.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 revTotal = sumQ('rev'); + const grossTotal = sumQ('gross'); + const paymentTotal = sumQ('payment'); + + const rows = [ + { key: 'sign', label: '签约', complete: signTotal, icon: 'file-text', defWeight: 10 }, + { key: 'rev', label: '收入', complete: revTotal, icon: 'dollar-sign', defWeight: 20 }, + { key: 'gross', label: '毛利', complete: grossTotal, icon: 'trending-up', defWeight: 50 }, + { key: 'payment', label: '回款', complete: paymentTotal, icon: 'coins', defWeight: 20 }, + ]; + + const saved = JSON.parse(localStorage.getItem(storageKey) || '{}'); + + function val(key, field, def) { + const r = saved[key] || {}; + return r[field] !== undefined && r[field] !== '' ? Number(r[field]) : def; + } + + function moneyWan(v) { + if (v >= 10000) return (v / 10000).toFixed(1) + '万'; + return v.toLocaleString(); + } + + let totalScore = 0; + const tableRows = rows.map(r => { + const targetWan = val(r.key, 'targetWan', 0); + const target = Math.round(targetWan * 10000); + const weight = val(r.key, 'weight', r.defWeight); + const score = target > 0 ? Math.round((r.complete / target) * weight * 10) / 10 : 0; + totalScore += score; + + return ` + ${r.label} + +
+ + +
+ + ${moneyWan(r.complete)} + +
+ +
+ + ${score.toFixed(1)} + `; + }).join(''); + + const quarterTabs = `
` + qLabel.map((l, i) => { + return ``; + }).join('') + `
`; + + document.querySelector("#performance").innerHTML = ` + +
+ ${quarterTabs} +
+ + + + + + + + + ${tableRows} + + + + + + + +
指标目标(万)完成情况权重评分
合计100${totalScore.toFixed(1)}
+
+
`; + + if (window.lucide) window.lucide.createIcons(); +}; + +window.switchPerfQuarter = (q) => { + state.perfQuarter = q; + renderPerformance(); +}; + +window.updatePerfScore = (key, field, value) => { + const q = state.perfQuarter || 0; + const storageKey = 'opc-performance-Q' + (q + 1); + const saved = JSON.parse(localStorage.getItem(storageKey) || '{}'); + saved[key] = saved[key] || {}; + saved[key][field] = value; + localStorage.setItem(storageKey, JSON.stringify(saved)); + renderPerformance(); +}; diff --git a/static/modules/products.js b/static/modules/products.js index 6e2cc82..3c36906 100644 --- a/static/modules/products.js +++ b/static/modules/products.js @@ -187,8 +187,13 @@ function renderProducts() { return productSort.dir > 0 ? '' : ''; }; const sortTh = (f, label, extra='') => `${label}${sortIcon(f)}${extra}`; + const tab = state.productTab || 'version'; + const tabs = `
`; + document.querySelector("#products").innerHTML = `
+ ${tabs} + ${tab === 'version' ? `
@@ -230,6 +235,7 @@ function renderProducts() {
+ ` : `

和内部联动,真正快速做实交付很重要

  1. 门店的大屏幕,要上,一个视频拍摄出来,然后在几百家门店同步轮巡播放,还是很震撼的
  2. 拍摄的内容,按病种整理成不同的专区,然后以 H5 或者小程序的形式,在门店科普教育,患者群,企业微信推送下去
  3. 同理,在保险的理赔环节,报案环节推送下去,而不是只是挂一个 H5 的专区
  4. 和门店、医生联动,把患者群都建立起来,把群的数量建起来
  5. 用源泉把患者的档案都建立起来,真正的有档案沉淀,AI 推送沉淀
  6. 用 AI + 人工,把随访量也都弄起来
  7. 对应的运营活动,还有排行版,在有一定量以后,要逐步的做起来,包括科普的排行版,科普的明星医生等等

要形成一份专门的报告,真正的证明我们科普和患者管理的价值

`} `; if (window.lucide) window.lucide.createIcons(); } @@ -342,3 +348,8 @@ window.cyclePriority = async (id) => { renderProducts(); } catch (e) { toast("更新失败:" + e.message, "error"); } }; + +window.switchProductTab = (v) => { + state.productTab = v; + renderProducts(); +}; diff --git a/static/modules/proposals.js b/static/modules/proposals.js index 3cc03f6..1ea68d9 100644 --- a/static/modules/proposals.js +++ b/static/modules/proposals.js @@ -47,13 +47,11 @@ function renderProposals() { const isStandard = state.proposalTab === "standard"; document.querySelector("#proposals").innerHTML = `
-
-
- - -
- ${!isStandard ? `` : ''} +
+ +
+
${!isStandard ? `` : ''}
${isStandard ? `
这是每一条 OPC 线,必须要梳理清楚的 7 份资料,项目不可以删除,只可以更新附件,请大家将最新的材料上传
` : `
在这里新建,并且上传您希望与团队其他成员共享的资料
`} ${isStandard ? renderStandardTable(standardItems) : renderOtherTable(otherItems)}
diff --git a/static/modules/utils.js b/static/modules/utils.js index be67084..64bf31b 100644 --- a/static/modules/utils.js +++ b/static/modules/utils.js @@ -130,6 +130,12 @@ function switchTab(tab) { localStorage.setItem("opc-active-tab", tab); document.querySelectorAll(".sidebar-tab").forEach((btn) => btn.classList.toggle("active", btn.dataset.tab === tab)); document.querySelectorAll(".panel").forEach((panel) => panel.classList.toggle("active", panel.id === tab)); + + // 更新顶部标题 + const titles = { home: 'OPC 工作台', finance: '业务管理', projects: '重点工作台账', proposals: '业务方案', products: '版本与运营', performance: '季度绩效考核' }; + const titleEl = document.querySelector('#workspaceTitle'); + if (titleEl) titleEl.textContent = titles[tab] || state.tenant + ' OPC 工作台'; + render(); } @@ -151,9 +157,8 @@ function render() { renderProjects(); renderProposals(); renderProducts(); - renderOperations(); + renderPerformance(); renderFinance(); - renderExpense(); if (window.lucide) window.lucide.createIcons(); } @@ -164,7 +169,7 @@ function renderActive() { else if (tab === "projects") renderProjects(); else if (tab === "proposals") renderProposals(); else if (tab === "products") renderProducts(); - else if (tab === "operations") renderOperations(); + else if (tab === "performance") renderPerformance(); else if (tab === "finance") renderFinance(); if (window.lucide) window.lucide.createIcons(); } diff --git a/templates/index.html b/templates/index.html index 95d5e65..b72d2d6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -49,17 +49,13 @@ 首页
-