#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""PATCH-2：看板 UI —— 新增「官方通报·真实数据」视图 + 计分口径说明更新 + 插值开关"""
import os, shutil
BASE = "/Users/liuxinyuan/Desktop/Hermes输出-工作类"
P = os.path.join(BASE, "数据/_kpi_dashboard_template.html")
h = open(P).read()
LOG = []
def rep(old, new, label):
    global h
    assert old in h, "MISS: " + label
    h = h.replace(old, new, 1)
    LOG.append(label)

# ---------- A. CSS ----------
rep("</style>", """
/* ---- 官方通报真实数据视图 ---- */
.vtabs{display:flex;gap:6px;align-items:center}
.vtabs .tab{font-weight:700}
.rhead{display:flex;gap:10px;align-items:center;margin:2px 0 8px}
.rhead .nm{font-size:16px;font-weight:700;color:var(--ink)}
.rhead .rk{font-size:11px;color:#fff;background:var(--blue);border-radius:20px;padding:2px 8px}
.rhead .rk.g{background:var(--t3)} .rhead .rk.r{background:var(--zero)}
.rbig{margin-left:auto;text-align:right;line-height:1.25}
.rbig b{font-size:22px;color:var(--blue)}
.rbig small{font-size:11px;color:var(--ink3)}
.chk{display:inline-flex;gap:5px;align-items:center;font-size:11px;border-radius:20px;padding:2px 9px;border:1px solid var(--line2)}
.chk.ok{color:#137A4B;background:#E8F8F0;border-color:#B7E6CE}
.chk.diff{color:#B4622E;background:#FDF3E7;border-color:#F0D9B5}
.chk.na{color:var(--ink3);background:#F7FAFC;border-color:var(--line)}
table.real{width:100%;border-collapse:collapse;font-size:12px}
table.real th{background:#F4F8FC;color:var(--ink2);font-weight:600;text-align:left;padding:5px 6px;border-bottom:1px solid var(--line2);white-space:nowrap}
table.real td{padding:5px 6px;border-bottom:1px dashed var(--line);vertical-align:top}
table.real td.c,table.real th.c{text-align:center}
table.real tr.z td{color:var(--zero)}
table.real tr.b td{color:var(--t3);font-weight:600}
.rsum{display:flex;gap:14px;flex-wrap:wrap;margin-top:8px;padding-top:8px;border-top:1px solid var(--line2);font-size:12px;color:var(--ink2)}
.rsum b{color:var(--blue);font-size:14px}
.tt td{text-align:center}
.tt .cell{border-radius:5px;padding:3px 0;font-variant-numeric:tabular-nums}
.tt .cA{background:#E8F8F0;color:#137A4B;font-weight:700}
.tt .cB{background:#EAF2FE;color:#1B5E8C;font-weight:700}
.tt .cC{background:#FDF3E7;color:#B4622E;font-weight:700}
.tt .cD{background:#FDECEC;color:#B23B3B;font-weight:700}
.tt .cN{color:var(--ink3)}
</style>""", "CSS")

# ---------- B. 顶栏视图切换 ----------
rep("""  <span class="chip warn" id="chipMode">示例数据演示</span>""",
    """  <span class="chip warn" id="chipMode">示例数据演示</span>
  <div class="vtabs">
    <button class="tab on" id="btnViewActual">官方通报 · 真实数据</button>
    <button class="tab" id="btnViewSim">积分卡算分</button>
  </div>""", "view switcher")

# ---------- C. 视图容器 ----------
rep("""<div class="wrap">
  <div class="banner" id="banner"></div>""",
    """<div class="wrap">

  <div id="viewActual">
    <div class="sumgrid" id="sumgridReal"></div>
    <div class="panel">
      <h3>官方已算分 · 逐人明细 <span>数据源：各部门《考核分数通报（2026年1–8月）》原件；「复核」= 用本页规则引擎按通报的实际达成值重算关键任务得分，与官方值比对</span></h3>
      <div class="grid" id="gridReal"></div>
    </div>
    <div class="panel">
      <h3>考核得分月度趋势 <span>官方通报各月「总得分」，用于看长期走势与异常月</span></h3>
      <table class="real tt" id="tblTrend"></table>
    </div>
  </div>

  <div id="viewSim">
  <div class="banner" id="banner"></div>""", "view containers")

rep("""  <div class="panel">
    <h3>数据接入位 <span>每一项 KPI 的真实来源与当前状态 —— 下一步按此表逐项打通</span></h3>""",
    """  </div><!-- /viewSim -->

  <div class="panel">
    <h3>数据接入位 <span>每一项 KPI 的真实来源与当前状态 —— 下一步按此表逐项打通</span></h3>""", "close viewSim")

# ---------- D. 计分设置：插值开关 ----------
rep("""      <label class="fld">未达 T1 记分<input class="ipt" id="setB0" value="0"></label>""",
    """      <label class="fld">未达 T1 记分<input class="ipt" id="setB0" value="0"></label>
      <label class="fld" style="flex-direction:row;align-items:center;gap:6px">档位间线性插值
        <input type="checkbox" id="setIP" style="width:16px;height:16px"> <span class="sublabel">默认关（官方通报实证=档位离散）</span></label>""",
    "interp switch")

# ---------- E. 口径说明更新 ----------
rep("""      <li><b>关键任务得分</b> = Σ (该项权重 / 权重合计) × 70 × 档位分。档位分刻度：<b>T1 达标 60 分、T2 达标 100 分、T3 达标 120 分</b>，档位间线性插值，未达 T1 记 0 分，达 T3 封顶 120 分。</li>""",
    """      <li><b>档位分（官方通报实证口径）</b>：<b>未达 T1 记 0 分；达 T1 记 60 分；达 T2 记 100 分；达 T3 记 120 分</b> —— <b>档位离散，档位之间不插值</b>（例：T2=34、T3=38 时，达成 35 与 37 同为 100 分）。需插值可在「计分设置」勾选。</li>
      <li><b>折算得分</b> = 该项得分 × 该项权重（与官方通报「折算得分」列同口径，如 120 分 × 30% = 36 分）。</li>
      <li><b>关键任务得分</b> = Σ 各项折算得分（100 分制）。大客户部/拓展部权重合计 100% → 满分 100，再 <b>×0.7</b> 计入总分；生命科学部的考核项权重合计本身即为 70%，<b>不再打折</b>，达 T3 上限 84 分。</li>""",
    "口径说明1")

rep("""      <li><b>考核得分</b> = 关键任务得分 + 上级评分合计（协议：大客户部/拓展部 15+15=30，生命科学区域经理 15+15=30，生命科学销售工程师 18+18=36 → 常规满分 106 分，按「算出来多少就是多少」执行）。</li>
      <li><b>得分率</b> = 考核得分 ÷ 常规满分，用于评级；全项达 T3 时关键任务得 84 分（70×1.2），即挑战上限。</li>""",
    """      <li><b>考核得分（总得分）</b> = 关键任务得分（折算70%） + 上级评分合计。上级评分满分：大客户部/拓展部 15+15=30（部分月份为部门经理评分 30 分单列），生命科学区域经理 15+15=30，生命科学销售工程师 区域经理18+销售总监18=36。<b>常规满分</b>：大客户部/拓展部 100 分，生命科学销售工程师 120 分（关键任务 84 + 上级 36，与官方通报一致）。</li>
      <li><b>得分率</b> = 考核得分 ÷ 常规满分，用于评级；全项达 T3 时即挑战上限（大客户部/拓展部 114 分，生命科学销售工程师 120 分）。</li>""",
    "口径说明2")

# ---------- F. 真实数值回填（abc / x2 也取官方通报） ----------
rep("""      const kk = key(p.name,'item',it.no);
      let v = state.vals[kk];
      if(v===undefined||v===''){ v = state.real? '' : Math.round(((t2!==undefined?t2:0.5)*(0.95+0.6*((p.name.length*7)%10)/10))*100)/100; }""",
    """      const kk = key(p.name,'item',it.no);
      let v = state.vals[kk];
      if((v===undefined||v==='') && state.real){ const ov = officialVal(p,it); if(ov!==null) v = ov; }
      if(v===undefined||v===''){ v = state.real? '' : Math.round(((t2!==undefined?t2:0.5)*(0.95+0.6*((p.name.length*7)%10)/10))*100)/100; }""",
    "abc official backfill")

rep("""      const kk = key(p.name,'item',it.no);
      let v = state.vals[kk];
      if(v===undefined||v===''){ v = state.real? '' : Math.round(it.t2*(0.8+((p.name.length*3)%40)/100)); }""",
    """      const kk = key(p.name,'item',it.no);
      let v = state.vals[kk];
      if((v===undefined||v==='') && state.real){ const ov = officialVal(p,it); if(ov!==null) v = ov; }
      if(v===undefined||v===''){ v = state.real? '' : Math.round(it.t2*(0.8+((p.name.length*3)%40)/100)); }""",
    "x2 official backfill")

# 输入框显示：真实模式下把官方值显示出来
rep("""      const v = state.vals[kk]!==undefined? state.vals[kk] : (state.real?'':(row.raw===null?'':row.raw));
      h += scaleBar(row.raw, it, row.r?row.r.cls:'') + `<div class="line"><span class="sublabel">促销期业绩（元）</span>""",
    """      const v = state.vals[kk]!==undefined? state.vals[kk] : ((state.real&&row.raw!==null)? row.raw : (state.real?'':(row.raw===null?'':row.raw)));
      h += scaleBar(row.raw, it, row.r?row.r.cls:'') + `<div class="line"><span class="sublabel">促销期业绩（元）</span>""",
    "x2 input value")

rep("""      const v = state.vals[kk]!==undefined? state.vals[kk] : (autoValue(p,it)!==null? autoValue(p,it) : (!state.real&&row.raw!==null? row.raw : ''));""",
    """      const v = state.vals[kk]!==undefined? state.vals[kk] : (autoValue(p,it)!==null? autoValue(p,it) : ((state.real&&row.raw!==null)? row.raw : (!state.real&&row.raw!==null? row.raw : '')));""",
    "num input value")

rep("""      const chosen = state.vals[key(p.name,'abc',it.no)]||'A';
      const kk = key(p.name,'item',it.no);
      const v = state.vals[kk]!==undefined? state.vals[kk] : (state.real?'':(row.raw===null?'':row.raw));""",
    """      const chosen = state.vals[key(p.name,'abc',it.no)]||'A';
      const kk = key(p.name,'item',it.no);
      const v = state.vals[kk]!==undefined? state.vals[kk] : ((state.real&&row.raw!==null)? row.raw : (state.real?'':(row.raw===null?'':row.raw)));""",
    "abc input value")

# 逐项折算行 → 官方口径 + 官方得分对照
rep("""    h += `<div class="line" style="justify-content:space-between;margin-top:7px">
        <span class="sublabel">档位分 ${r? (r.s*100).toFixed(0):'—'}（T1=60 / T2=100 / T3=120） · 折算 ${r? (it.weight/ (cfg.weight_sum||1) * state.full * r.s).toFixed(1):'—'} 分</span>
        <span class="sublabel">来源：${esc(it.source||'—')} · 统计：${esc(it.owner||'—')}</span></div>""",
    """    const offTag = row.off? `官方 ${row.off.s===null?'—':Number(row.off.s).toFixed(0)} 分 · 折算 ${row.off.d===null?'—':Number(row.off.d).toFixed(1)} 分` : '官方：—';
    h += `<div class="line" style="justify-content:space-between;margin-top:7px">
        <span class="sublabel">档位分 ${r? (r.s*100).toFixed(0):'—'}（T1=60 / T2=100 / T3=120） · 折算 ${row.cut!==null? row.cut.toFixed(1):'—'} 分${row.off&&row.off.d!==null&&row.cut!==null&&Math.abs(row.off.d-row.cut)>0.06? ' <b style="color:#B4622E">（官方 '+Number(row.off.d).toFixed(1)+'）</b>':''}</span>
        <span class="sublabel">${offTag} · 来源：${esc(it.source||'—')} · 统计：${esc(it.owner||'—')}</span></div>""",
    "item footer official")

# 卡片合计区：关键任务/折算/总分 + 官方复核
rep("""  h += `<div class="suprow"><span class="g"><b>上级评分合计</b></span><b>${res.supSum.toFixed(1)} / ${cfg.superiors.reduce((a,b)=>a+b.full,0)}</b></div>
    <div class="line" style="justify-content:space-between"><span>关键任务得分</span><b>${res.task.toFixed(1)} / ${state.full}</b></div>
    <div class="note">${res.populated}/${res.n} 项已填，其中 ${res.crit} 项低于 T1 门槛（记0分）。</div></div></div></div>`;""",
    """  h += `<div class="suprow"><span class="g"><b>上级评分合计</b></span><b>${res.supSum.toFixed(1)} / ${res.supFull}</b></div>
    <div class="line" style="justify-content:space-between"><span>关键任务得分（100分制）</span><b>${res.task.toFixed(1)}${res.f70<1? ` ×0.7 = ${res.task70.toFixed(1)}`:''}</b></div>
    <div class="line" style="justify-content:space-between"><span><b>考核得分</b></span><b style="font-size:14px;color:var(--blue)">${res.total.toFixed(1)} / ${res.cap.toFixed(0)}</b></div>
    ${res.official? `<div class="rsum"><span>官方通报：总分 <b>${Number(res.official.total).toFixed(1)}</b> · 关键任务 <b>${Number(res.official.task).toFixed(1)}</b>${res.official.rank? ' · 排名 '+res.official.rank:''}${res.official.coef!==null&&res.official.coef!==undefined? ' · 系数 '+res.official.coef:''}</span><span class="chk ${res.check}">${res.check==='ok'?'✓ 复核一致':(res.check==='diff'?'△ 与官方有差异':'— 无官方值')}</span></div>`:''}
    <div class="note">${res.populated}/${res.n} 项已填，其中 ${res.crit} 项低于 T1 门槛（记0分）。</div></div></div></div>`;""",
    "card totals official")

# ---------- G. render 拆分 + renderActual + 趋势 ----------
rep("""function render(){
  renderTabs();
  const ps = peopleOf(state.roleKey);""",
    """function renderChrome(){
  $('viewActual').className = state.view==='actual'? '' : 'hide';
  $('viewSim').className    = state.view==='actual'? 'hide' : '';
  $('btnViewActual').className = 'tab'+(state.view==='actual'?' on':'');
  $('btnViewSim').className    = 'tab'+(state.view==='sim'?' on':'');
  $('chipMode').textContent = state.view==='actual'? '官方通报真实数据' : (state.real? '真实数据 + 手填' : '示例数据演示');
  $('chipMode').className = 'chip' + (state.view==='actual'? '' : (state.real?'':' warn'));
  $('chipMonth').textContent = state.view==='actual'? (MONTH_LABEL[state.actMonth]||state.actMonth||'—') : MONTH_LABEL[state.month];
  $('chipSrc').textContent = state.view==='actual'? '数据源：各部门《考核分数通报》2026年1–8月' : '数据源：飞书活动 / ERP / 回款计划 / 人工';
}

/* 官方通报视图 */
function reviewPerson(name){
  const p = (PEOPLE[state.roleKey]||[]).find(x=>x.name===name) || {name:name, region:''};
  const sm = state.month; state.month = state.actMonth;
  let r=null; try{ r = computePerson(p); }catch(e){ r=null; }
  state.month = sm;
  return r;
}
function renderActual(){
  const c = actCfg();
  if(!c){
    $('sumgridReal').innerHTML = '<div class="sumbox"><div class="k">官方通报</div><div class="v">—</div><div class="sub">该岗位当月无通报数据，请切换月份</div></div>';
    $('gridReal').innerHTML = ''; $('tblTrend').innerHTML = ''; return;
  }
  const ppl = (c.people||[]).slice().sort((a,b)=>{
    const ra=(c.rank&&c.rank[a])||999, rb=(c.rank&&c.rank[b])||999; return ra-rb; });
  const num = n=>Number(c.total[n])||0;
  const tots = ppl.map(num), kts = ppl.map(n=>Number(c.kt[n])||0);
  const avg = tots.length? tots.reduce((a,b)=>a+b,0)/tots.length : 0;
  const avgKt = kts.length? kts.reduce((a,b)=>a+b,0)/kts.length : 0;
  const supFull = (c.sup||[]).reduce((a,s)=>a+(s.full||0),0);
  const f70 = (c.wsum>=0.999)? 0.7 : 1.0;
  const cap = 100*f70 + supFull;
  const top = ppl[0], low = ppl[ppl.length-1];
  $('sumgridReal').innerHTML = `
    <div class="sumbox"><div class="k">通报月份</div><div class="v">${esc((MONTH_LABEL[state.actMonth]||state.actMonth||'').replace('2026年','').replace('月',''))}<small>月</small></div><div class="sub">${esc(ROLES[state.roleKey].name)} · ${esc(ROLES[state.roleKey].level)} · 官方原件</div></div>
    <div class="sumbox"><div class="k">参评人数 / 考核项</div><div class="v">${ppl.length}<small>人</small></div><div class="sub">${c.items.length} 项 · 权重合计 ${((c.wsum||0)*100).toFixed(0)}%</div></div>
    <div class="sumbox"><div class="k">平均考核得分</div><div class="v">${avg.toFixed(1)}<small>/${cap.toFixed(0)}</small></div><div class="sub">关键任务平均 ${avgKt.toFixed(1)} · 上级评分满分 ${supFull}</div></div>
    <div class="sumbox"><div class="k">最高 / 最低</div><div class="v">${num(top).toFixed(1)}<small> / ${ppl.length>1?num(low).toFixed(1):'—'}</small></div><div class="sub">${esc(top)}${ppl.length>1? ' · '+esc(low):''}</div></div>`;

  $('gridReal').innerHTML = ppl.map(n=>{
    const kt = Number(c.kt[n]), total = num(n);
    const kt70 = (c.kt70 && c.kt70[n]!==undefined && c.kt70[n]!==null)? Number(c.kt70[n]) : kt*f70;
    const coef = (c.coef && c.coef[n]!==undefined)? c.coef[n] : null;
    const sups = (c.sup||[]).map(s=>({n:s.n, full:s.full, v:(s.s[n]===undefined?null:s.s[n])})).filter(x=>x.v!==null);
    const supSum = sups.reduce((a,b)=>a+(Number(b.v)||0),0);
    const rk = (c.rank&&c.rank[n])||null;
    const rkCls = (rk===1)? 'g' : ((rk===ppl.length&&ppl.length>1)? 'r' : '');
    const rows = (c.items||[]).map((it,i)=>{
      const v = it.v[n], s = it.s[n], d = it.d[n];
      const T = it.T||{};
      const zero = (s!==null && s!==undefined && Number(s)<=0);
      const t3 = (s!==null && s!==undefined && Number(s)>=120);
      const cls = zero? 'z' : (t3? 'b':'');
      const tv = t=> (T[t]===null||T[t]===undefined)? '—' : T[t];
      return `<tr class="${cls}"><td>${i+1}. ${esc(it.n)}</td><td class="c">${((it.w||0)*100).toFixed(0)}%</td>
        <td class="c sublabel">${tv('T1')} / ${tv('T2')} / ${tv('T3')}</td>
        <td class="c">${v===null||v===undefined?'—':(Math.abs(Number(v))<1.5? (Number(v)*100).toFixed(0)+'%' : (Number(v)%1===0? Number(v).toFixed(0) : Number(v).toFixed(2)))}</td>
        <td class="c"><b>${s===null||s===undefined?'—':Number(s).toFixed(0)}</b></td>
        <td class="c">${d===null||d===undefined?'—':Number(d).toFixed(1)}</td></tr>`;
    }).join('');
    const rev = (ROLES[state.roleKey].months && ROLES[state.roleKey].months[state.actMonth])? reviewPerson(n) : null;
    const chk = rev? (Math.abs(rev.task-kt)<=0.06? 'ok':'diff') : 'na';
    const chkTag = rev? (chk==='ok'? '✓ 复核一致（本页引擎重算 = 官方）' : `△ 差异：本页重算 ${rev.task.toFixed(1)} vs 官方 ${kt.toFixed(1)}`) : '— 当月规则未定义，未做复核';
    return `<div class="card">
      <div class="rhead">
        <div><span class="nm">${esc(n)}</span> ${rk? `<span class="rk ${rkCls}">第 ${rk} 名 / ${ppl.length}</span>`:''}
          <div class="sublabel">${esc(ROLES[state.roleKey].name)} · ${esc(ROLES[state.roleKey].level)}${coef!==null&&coef!==undefined? ' · 绩效系数 '+coef:''}</div></div>
        <div class="rbig"><b>${total.toFixed(1)}</b><small> / ${cap.toFixed(0)} 分</small><div class="sublabel">官方总得分</div></div>
      </div>
      <table class="real">
        <tr><th>考核项目</th><th class="c" style="width:44px">权重</th><th class="c" style="width:96px">T1/T2/T3</th><th class="c" style="width:64px">实际达成</th><th class="c" style="width:44px">得分</th><th class="c" style="width:48px">折算</th></tr>
        ${rows}
        <tr><td colspan="4"><b>关键任务得分（100分制）</b>${f70<1? ' · 折算70%':''}</td><td class="c"><b>${kt.toFixed(1)}</b></td><td class="c"><b>${kt70.toFixed(1)}</b></td></tr>
        ${sups.map(s=>`<tr><td colspan="4">${esc(s.n)}（满分 ${s.full}）</td><td class="c">${Number(s.v).toFixed(1)}</td><td class="c">—</td></tr>`).join('')}
        <tr class="b"><td colspan="4"><b>上级评分合计</b></td><td class="c"><b>${supSum.toFixed(1)}</b></td><td class="c">—</td></tr>
        <tr class="b"><td colspan="4"><b>考核得分（总得分）</b></td><td class="c" colspan="2"><b>${total.toFixed(1)} / ${cap.toFixed(0)}</b></td></tr>
      </table>
      <div class="rsum"><span class="chk ${chk}">${esc(chkTag)}</span></div>
    </div>`;
  }).join('');

  // 趋势：所有月份
  const ms = actMonths();
  const allP = [];
  ms.forEach(m=>{ (ACT.roles[state.roleKey][m].people||[]).forEach(n=>{ if(allP.indexOf(n)<0) allP.push(n); }); });
  const cellCls = v=> v===null? 'cN' : (v>=100? 'cA' : (v>=80? 'cB' : (v>=60? 'cC':'cD')));
  let th = '<tr><th style="width:110px">姓名</th>' + ms.map(m=>`<th class="c">${esc((MONTH_LABEL[m]||m).replace('2026年','').replace('月',''))}月</th>`).join('') + '<th class="c">平均</th><th class="c">最高</th></tr>';
  const trs = allP.map(n=>{
    const vals = ms.map(m=>{ const mm = ACT.roles[state.roleKey][m]; return (mm.total && mm.total[n]!==undefined)? Number(mm.total[n]) : null; });
    const has = vals.filter(v=>v!==null);
    const av = has.length? has.reduce((a,b)=>a+b,0)/has.length : null;
    return `<tr><td><b>${esc(n)}</b></td>` + vals.map(v=>`<td><div class="cell ${cellCls(v)}">${v===null?'—':v.toFixed(1)}</div></td>`).join('')
      + `<td class="sublabel">${av===null?'—':av.toFixed(1)}</td><td class="sublabel">${has.length? Math.max(...has).toFixed(1):'—'}</td></tr>`;
  }).join('');
  $('tblTrend').innerHTML = th + trs;
}

function renderSim(){
  const ps = peopleOf(state.roleKey);""", "renderActual")

rep("""  $('swReal').className = 'sw' + (state.real?' on':'');
}""",
    """  $('swReal').className = 'sw' + (state.real?' on':'');
  $('banner').innerHTML = $('banner').innerHTML;
}

function render(){
  renderTabs();
  if(state.view==='actual') renderActual(); else renderSim();
  renderChrome();
}""", "render dispatch")

open(P, "w").write(h)
print("PATCH-2 OK:", len(LOG), "edits")
for x in LOG: print("  -", x)
