#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""把官方「考核分数通报」真实数据接进积分卡看板模板（一次性 PATCH 脚本）
改动：
 1) 计分口径改为官方实证口径：档位离散（≥T1=60 / ≥T2=100 / ≥T3=120），可选插值
 2) 折算 = 得分 × 权重；关键任务得分 = Σ折算（100分制）；大客户/拓展 ×0.7 + 上级评分；科研权重合计已含 0.7，直接加上级评分
 3) 新增「官方通报·真实数据」视图：1–8 月逐人总分/关键任务/上级评分/各项明细 + 趋势
 4) 真实数据模式下，积分卡达成值自动回填官方通报数值，并标注官方得分与复核结果
"""
import os, shutil, datetime

BASE = "/Users/liuxinyuan/Desktop/Hermes输出-工作类"
P = os.path.join(BASE, "数据/_kpi_dashboard_template.html")
h = open(P).read()
shutil.copy(P, P + ".bak")
LOG = []


def rep(old, new, label):
    global h
    assert old in h, "MISS: " + label
    h = h.replace(old, new, 1)
    LOG.append(label)


# ---------- 1) 注入官方通报数据 ----------
rep("const SCHEME = __SCHEME_JSON__;",
    "const SCHEME = __SCHEME_JSON__;\nconst ACT = __ACT_JSON__;        /* 官方「考核分数通报」真实数据 1–8月 */\nconst ACTMAP = __ACTMAP_JSON__;  /* 协议书考核项 -> 官方通报项 映射 */",
    "inject ACT")

# ---------- 2) state：新增 view / actMonth / interp ----------
rep("""let state = {
  month: SCHEME.meta.months[SCHEME.meta.months.length-1],
  roleKey: 'dk',
  real: false,                     // false = 示例数据演示""",
    """let state = {
  view: 'actual',                  // actual = 官方通报真实数据 | sim = 积分卡算分
  month: SCHEME.meta.months[SCHEME.meta.months.length-1],
  actMonth: null,                  // 官方通报视图月份（初始化见下）
  roleKey: 'dk',
  real: true,                      // 真实数据模式（默认开）
  interp: false,                   // 档位间线性插值（官方实证=离散，默认关）""",
    "state fields")

rep("""try{ const s = JSON.parse(localStorage.getItem(LS_KEY)||'null'); if(s&&s.vals) state.vals = s.vals; }catch(e){}""",
    """try{ const s = JSON.parse(localStorage.getItem(LS_KEY)||'null'); if(s&&s.vals) state.vals = s.vals; }catch(e){}
(function(){ const r = ACT.roles[state.roleKey]||{}; const ms = Object.keys(r).sort(); state.actMonth = ms.length? ms[ms.length-1] : null; })();""",
    "actMonth init")

# ---------- 3) tierScore：官方口径=档位离散 ----------
old_tier = """  if(t2===null||t2===undefined||t2<=t1) return {s:a.T1, tier:'达T1', cls:'tA'};
  if(v < t2){ const r=(v-t1)/(t2-t1); return {s:a.T1+(a.T2-a.T1)*r, tier:'T1→T2', cls:'tA'}; }
  if(t3===null||t3===undefined||t3<=t2) return {s:a.T2, tier:'达T2', cls:'tB'};
  if(v < t3){ const r=(v-t2)/(t3-t2); return {s:a.T2+(a.T3-a.T2)*r, tier:'T2→T3', cls:'tB'}; }
  return {s:a.T3, tier:'达T3 挑战', cls:'tC'};"""
new_tier = """  if(t2===null||t2===undefined||t2<=t1) return {s:a.T1, tier:'达T1', cls:'tA'};
  if(v < t2) return state.interp
      ? {s:a.T1+(a.T2-a.T1)*(v-t1)/(t2-t1), tier:'T1→T2（插值）', cls:'tA'}
      : {s:a.T1, tier:'达T1（未达T2）', cls:'tA'};
  if(t3===null||t3===undefined||t3<=t2) return {s:a.T2, tier:'达T2', cls:'tB'};
  if(v < t3) return state.interp
      ? {s:a.T2+(a.T3-a.T2)*(v-t2)/(t3-t2), tier:'T2→T3（插值）', cls:'tB'}
      : {s:a.T2, tier:'达T2（未达T3）', cls:'tB'};
  return {s:a.T3, tier:'达T3 挑战', cls:'tC'};"""
rep(old_tier, new_tier, "tierScore discrete")

# ---------- 4) 官方通报读取助手 ----------
rep("""function key(...a){ return [state.month, state.roleKey, ...a].join('|'); }""",
    """function key(...a){ return [state.month, state.roleKey, ...a].join('|'); }

/* ---------- 官方通报真实数据（考核分数通报 1–8 月） ---------- */
function actCfg(){ const r = ACT.roles[state.roleKey]; return r? (r[state.actMonth]||null) : null; }
function actMap(){ const m = ACTMAP[state.roleKey]; return m? (m[state.actMonth]||{}) : {}; }
function actIdxOf(no){ const v = actMap()[String(no)]; return (v&&v.length)? v : null; }
function actItemAt(i){ const c = actCfg(); return c? c.items[i] : null; }
function actMonths(){ const r = ACT.roles[state.roleKey]||{}; return Object.keys(r).sort(); }
function officialVal(p, item){          // 单指数值型：官方通报的「实际达成」
  const idx = actIdxOf(item.no); if(!idx || idx.length!==1) return null;
  const it = actItemAt(idx[0]); if(!it) return null;
  const v = it.v[p.name];
  return (v===null||v===undefined||v==='')? null : v;
}
function officialOne(p, item){          // 单指：达成/官方得分/折算
  const idx = actIdxOf(item.no); if(!idx || idx.length!==1) return null;
  const it = actItemAt(idx[0]); if(!it) return null;
  const g = x=>(x===null||x===undefined||x==='')? null : Number(x);
  return {name:it.n, w:it.w, T:it.T, v:g(it.v[p.name]), s:g(it.s[p.name]), d:g(it.d[p.name])};
}
function officialAssign(p, item){       // 指派任务（官方多条，按官方得分加权）
  const idx = actIdxOf(item.no); if(!idx) return null;
  let sw=0, acc=0; const items=[];
  idx.forEach(i=>{ const it=actItemAt(i); if(!it) return;
    const s=Number(it.s[p.name]); if(isNaN(s)||it.s[p.name]===null) return;
    sw += (it.w||0); acc += (it.w||0)*s;
    items.push({name:it.n.slice(0,60), w:it.w, s:s, d:Number(it.d[p.name])||0, v:it.v[p.name]});
  });
  if(!sw) return null;
  return {rate: acc/sw/100, items:items, kt:acc/100};
}
function officialSup(p){                // 官方上级评分合计
  const c = actCfg(); if(!c || !c.sup) return null;
  let s=0, has=false;
  c.sup.forEach(x=>{ const v=x.s[p.name]; if(v!==null&&v!==undefined&&v!==''){ s+=Number(v); has=true; } });
  return has? s : null;
}""", "ACT helpers")

# ---------- 5) autoValue：优先官方通报数值 ----------
rep("""function autoValue(p, item){
  if(!state.real) return null;
  const per = (SCHEME.real_values[state.month]||{})['拜访类活动条数'] || {};
  const v = per[p.name];
  if(v===undefined||v===null) return null;
  const WL = ['有效客户拜访次数','协助拜访课题组数量'];   // 仅通用拜访类项可用活动条数近似
  if(WL.indexOf(item.item)>=0) return v;                 // 参考口径，未做打卡×活动匹配校准
  return null;
}""",
    """function autoValue(p, item){
  if(!state.real) return null;
  const ov = officialVal(p, item);                         // ① 官方考核通报（已算分的实际达成）
  if(ov!==null) return ov;
  const per = (SCHEME.real_values[state.month]||{})['拜访类活动条数'] || {};
  const v = per[p.name];
  if(v===undefined||v===null) return null;
  const WL = ['有效客户拜访次数','协助拜访课题组数量'];
  if(WL.indexOf(item.item)>=0) return v;
  return null;
}""", "autoValue official")

# ---------- 6) statusDot：新增官方通报来源 ----------
rep("""function statusDot(from, item){
  if(from==='input') return '<span class="dot d-man"></span>手填';""",
    """function statusDot(from, item){
  if(from==='official') return '<span class="dot d-ok"></span>官方通报实数';
  if(from==='input') return '<span class="dot d-man"></span>手填';""",
    "statusDot official")

# ---------- 7) assignScore：真实模式用官方得分 ----------
rep("""function assignScore(p){
  const cfg = cfgOf(state.roleKey);
  if(!cfg.assign) return null;""",
    """function assignScore(p){
  const cfg = cfgOf(state.roleKey);
  if(!cfg.assign) return null;
  const oa = state.real? officialAssign(p, {no: MANUAL_NO}) : null;
  if(oa) return {s: Math.min(1.2, oa.rate), used: oa.items.length, official: true};""",
    "assignScore official")

rep("""const ROLES = SCHEME.roles;
const PEOPLE = SCHEME.people;""",
    """const ROLES = SCHEME.roles;
const PEOPLE = SCHEME.people;
const MANUAL_NO = (function(){ let n=null; Object.keys(ROLES).forEach(rk=>{ ['2026-08','2026-09'].forEach(m=>{ const cf=ROLES[rk].months[m]; if(!cf) return; cf.items.forEach(it=>{ if(it.type==='manual') n = n||it.no; }); }); }); return n; })();""",
    "MANUAL_NO")

# ---------- 8) superiorScores：真实模式用官方上级评分 ----------
rep("""function superiorScores(p){
  const cfg = cfgOf(state.roleKey);
  return cfg.superiors.map((s,i)=>{
    const k = key(p.name,'sup',i);
    let v = state.vals[k];
    if(v===undefined||v===''){ v = state.real ? '' : Math.round(s.full*(0.72+((p.name.charCodeAt(i%p.name.length)+i*7)%22)/100)); }
    return {def:s, v:(v===''?null:Number(v))};
  });
}""",
    """function superiorScores(p){
  const cfg = cfgOf(state.roleKey);
  const pool = state.real? officialSup(p) : null;
  let left = pool;
  return cfg.superiors.map((s,i)=>{
    const k = key(p.name,'sup',i);
    let v = state.vals[k];
    if((v===undefined||v==='') && pool!==null && pool!==undefined){
      const take = Math.max(0, Math.min(s.full, left));
      left -= take;
      v = (i===cfg.superiors.length-1 && left>0)? take+left : take;
    }
    if(v===undefined||v===''){ v = state.real ? '' : Math.round(s.full*(0.72+((p.name.charCodeAt(i%p.name.length)+i*7)%22)/100)); }
    return {def:s, v:(v===''?null:Number(v)), official:(pool!==null)};
  });
}""", "superiorScores official")

# ---------- 9) computePerson：官方折算口径 + 复核 ----------
rep("""  const rows = items.map(it=>{
    let r=null, from='empty', raw=null;
    if(it.type==='manual'){
      const a = assignScore(p);
      if(a){ r={s:a.s, tier:'指派任务 '+a.used+'项', cls:a.s>=0.8?'tC':(a.s>=0.6?'tB':'t0')}; from='assign'; }
    } else if(it.type==='abc'){""",
    """  const rows = items.map(it=>{
    let r=null, from='empty', raw=null, off=null;
    if(it.type==='manual'){
      const a = assignScore(p);
      off = state.real? officialAssign(p, it) : null;
      if(a){ r={s:a.s, tier:(a.official? '官方通报 '+a.used+' 项':'指派任务 '+a.used+'项'), cls:a.s>=0.8?'tC':(a.s>=0.6?'tB':'t0')}; from = a.official?'official':'assign'; }
    } else if(it.type==='abc'){""",
    "computePerson manual official")

rep("""      if(v!==''){ raw=Number(v); r = tierScore(raw, t1, t2, t3); }
      else from='empty';
      if(v!=='') from = state.vals[kk]!==undefined?'input':'demo';""",
    """      if(v!==''){ raw=Number(v); r = tierScore(raw, t1, t2, t3); }
      else from='empty';
      if(v!=='') from = state.vals[kk]!==undefined?'input':(state.real?'official':'demo');""",
    "abc from official")

rep("""      const g = itemValue(p, it);
      raw = g.v; from = g.from;
      if(g.v!==null) r = tierScore(g.v, it.t1, it.t2, it.t3);
    }
    if(r && r.s!==null){ populated++; acc += (it.weight||0)/wsum * state.full * r.s; if(r.s<=0) crit++; }
    return {it:it, r:r, raw:raw, from:from};
  });""",
    """      const g = itemValue(p, it);
      raw = g.v; from = g.from;
      if(g.v!==null) r = tierScore(g.v, it.t1, it.t2, it.t3);
    }
    if(!off) off = officialOne(p, it);
    const cut = (r && r.s!==null)? (it.weight||0) * (r.s*100) : null;   // 折算 = 得分 × 权重
    if(r && r.s!==null){ populated++; acc += cut; if(r.s<=0) crit++; }
    return {it:it, r:r, raw:raw, from:from, off:off, cut:cut};
  });""", "computePerson cut")

rep("""  const sups = superiorScores(p);
  const supSum = sups.reduce((a,b)=>a+(b.v||0),0);
  const total = acc + supSum;
  const cap = state.full + cfg.superiors.reduce((a,b)=>a+b.full,0);
  const challenge = cap + state.full*(state.anchors.T3-1);
  const rate = cap>0 ? total/cap*100 : 0;
  const grade = rate>=state.rating.A?'A':(rate>=state.rating.B?'B':(rate>=state.rating.C?'C':'D'));
  return {rows:rows, task:acc, sups:sups, supSum:supSum, total:total, cap:cap, challenge:challenge,
          rate:rate, grade:grade, crit:crit, populated:populated, n:items.length};""",
    """  const sups = superiorScores(p);
  const supSum = sups.reduce((a,b)=>a+(b.v||0),0);
  const supFull = cfg.superiors.reduce((a,b)=>a+b.full,0);
  const f70 = (wsum>=0.999)? 0.7 : 1.0;          // 权重合计=100% → 关键任务×0.7；科研权重合计已含0.7
  const task = acc;                              // 关键任务得分（100 分制）
  const task70 = task*f70;
  const total = task70 + supSum;
  const cap = 100*f70 + supFull;
  const challenge = 120*f70 + supFull;
  const rate = cap>0 ? total/cap*100 : 0;
  const grade = rate>=state.rating.A?'A':(rate>=state.rating.B?'B':(rate>=state.rating.C?'C':'D'));
  const oc = state.real? actCfg() : null;
  const official = (oc && oc.kt && oc.kt[p.name]!==undefined)? {
      task: Number(oc.kt[p.name]), total: Number(oc.total[p.name]), rank: oc.rank? oc.rank[p.name]:null,
      coef: (oc.coef&&oc.coef[p.name]!==undefined)? oc.coef[p.name]:null,
      sup: officialSup(p)
  } : null;
  const check = (official && official.task!==null)? (Math.abs(official.task - task)<=0.06? 'ok':'diff') : null;
  return {rows:rows, task:task, task70:task70, f70:f70, sups:sups, supSum:supSum, supFull:supFull,
          total:total, cap:cap, challenge:challenge, rate:rate, grade:grade, crit:crit,
          populated:populated, n:items.length, official:official, check:check};""",
    "computePerson official math")

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