#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
生成《Q3汇报数据看板.html》—— 零外部依赖自包含看板（数据内联，无 fetch）
品牌：调用 脚本/brand_kit.py 注入 Logo + slogan（铁律）
"""
import json, os, re, sys, html as H

ROOT = os.path.expanduser("~/Desktop/Hermes输出-工作类")
OUTD = os.path.join(ROOT, "2026年3季度汇报", "_汇总")
sys.path.insert(0, os.path.join(ROOT, "脚本"))

W = 10000.0
def w1(x): return round(x / W, 1)

H1 = json.load(open(f"{OUTD}/人员提炼_H1.json"))
YY = json.load(open(f"{OUTD}/erp_yoy.json"))
QC = json.load(open(f"{OUTD}/Q3活动客诉竞品.json"))
CI = json.load(open(f"{OUTD}/Q3竞品情报台账.json"))

SDOC = {"c117-单页版说明书", "BR1C101-说明书", "刘欣-销售能力培训"}
people = [p for p in H1["people"] if p["name"] not in SDOC]
docs = [p for p in H1["people"] if p["name"] in SDOC]

# ---- 精简竞品证据文本，控制 HTML 体积 ----
def clip(t, n=260):
    t = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", t or "")
    t = re.sub(r"<!--.*?-->", "", t)
    t = " ".join(t.split())
    return H.escape(t[:n] + ("…" if len(t) > n else ""))

ledger = {}
for b, v in CI["q3_activity_ledger"].items():
    ledger[b] = {
        "n": v["mentions"],
        "readers": v["sp_readers"],
        "ev": [{"d": e["date"], "sp": e["sp"], "c": e["customer"], "t": clip(e["text"]),
                "u": e["url"]} for e in v["evidence"]],
    }

DATA = {
    "people": people,
    "docs": docs,
    "erp": YY,
    "act": QC["q3_activities"],
    "cmp": QC["q3_complaints"],
    "ledger": ledger,
    "merged": CI["merged_index"],
}
payload = json.dumps(DATA, ensure_ascii=False).replace("</", "<\\/")

HTML = r"""<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<!-- 页眉/页脚由 脚本/brand_kit.py 统一注入，勿自写 -->
<title>宝锐生物 · 2026 Q3 汇报数据看板</title>
<style>
:root{--br:#1a56a8;--br2:#2b7fd4;--bg:#f4f6f9;--card:#fff;--ink:#1c2430;--mut:#6b7683;--line:#e3e8ef;
--crit:#d92d20;--hi:#e8590c;--mid:#c99700;--ok:#12805c}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);display:flex;flex-direction:column;min-height:100vh;
font:14px/1.65 -apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif}
nav{background:#fff;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:49;box-shadow:0 1px 4px rgba(0,0,0,.05);flex:none}
.nin{max-width:1360px;margin:0 auto;padding:0 20px;display:flex;gap:4px;overflow-x:auto}
.nin button{border:0;background:none;padding:12px 16px;font-size:14px;cursor:pointer;color:var(--mut);
border-bottom:3px solid transparent;white-space:nowrap;font-family:inherit}
.nin button.on{color:var(--br);border-bottom-color:var(--br);font-weight:600}
main{max-width:1360px;margin:0 auto;padding:22px 20px 60px;width:100%;flex:1 0 auto}
section{display:none}section.on{display:block}
h2{font-size:19px;margin:6px 0 4px;padding-left:11px;border-left:4px solid var(--br)}
h3{font-size:15.5px;margin:26px 0 10px;color:var(--br)}
h4{font-size:14px;margin:18px 0 8px}
p.sub{color:var(--mut);font-size:12.5px;margin:2px 0 14px}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin:16px 0}
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:14px 16px}
.card .k{color:var(--mut);font-size:12.5px}
.card .v{font-size:24px;font-weight:700;color:var(--br);margin-top:4px;line-height:1.25}
.card .v small{font-size:13px;font-weight:400;color:var(--mut)}
.card .d{font-size:12px;color:var(--mut);margin-top:5px}
.tbl-wrap{overflow-x:auto;background:#fff;border:1px solid var(--line);border-radius:10px}
table{border-collapse:collapse;width:100%;font-size:13px}
th,td{padding:8px 10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}
th{background:#eef3fa;color:#2a3b52;font-weight:600;position:sticky;top:0;white-space:nowrap;cursor:pointer;user-select:none}
th:hover{background:#e2ebf8}
tbody tr:hover{background:#f7fafd}
td.num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
.tag{display:inline-block;padding:1px 7px;border-radius:10px;font-size:11.5px;line-height:1.7;white-space:nowrap}
.t-crit{background:#fde8e6;color:var(--crit)} .t-hi{background:#fdeee2;color:var(--hi)}
.t-mid{background:#fdf6dd;color:var(--mid)} .t-ok{background:#e4f5ee;color:var(--ok)}
.t-grey{background:#eef1f5;color:var(--mut)} .t-blue{background:#e6eefb;color:var(--br)}
.bar{height:9px;border-radius:5px;background:linear-gradient(90deg,var(--br2),var(--br));min-width:2px}
.barwrap{background:#eef1f5;border-radius:5px;overflow:hidden}
a{color:var(--br);text-decoration:none}a:hover{text-decoration:underline}
.ev{background:#fff;border:1px solid var(--line);border-left:3px solid var(--br2);border-radius:8px;
padding:10px 13px;margin:8px 0;font-size:13px}
.ev .m{color:var(--mut);font-size:12px;margin-bottom:5px}
.ev .x{white-space:pre-wrap}
details{background:#fff;border:1px solid var(--line);border-radius:10px;padding:10px 14px;margin:8px 0}
details summary{cursor:pointer;font-weight:600;color:var(--br)}
details ul{margin:8px 0 4px;padding-left:20px}
.warn{background:#fff8e6;border:1px solid #f2dfae;border-radius:8px;padding:10px 14px;margin:12px 0;font-size:13px}
.kbox{background:#eef6ff;border:1px solid #cfe2fb;border-radius:8px;padding:12px 16px;margin:14px 0}
.kbox ol{margin:6px 0 0;padding-left:22px}.kbox li{margin:5px 0}
input.q{padding:8px 12px;border:1px solid var(--line);border-radius:8px;font-size:13px;min-width:260px;font-family:inherit}
.toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:12px 0 10px}
</style></head><body>

<nav><div class="nin" id="nav"></div></nav>
<main id="main"></main>

<script>
const D = __PAYLOAD__;
const W = 10000;
const f1 = x => (x/W).toFixed(1);
const f2 = x => (x/W).toFixed(2);
const esc = s => String(s==null?'':s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const URG = {critical:['t-crit','极高'], high:['t-hi','高'], medium:['t-mid','中'], normal:['t-grey','一般']};

const TABS=[['s0','摘要'],['s1','① 半年度汇报'],['s2','② 活动·客诉'],['s3','③ 风险事项'],
            ['s4','④ ERP 同比'],['s5','⑤ 竞品']];
document.getElementById('nav').innerHTML = TABS.map((t,i)=>
  `<button data-t="${t[0]}" class="${i===0?'on':''}">${t[1]}</button>`).join('');

/* ── 0 摘要 ── */
const e26=D.erp.y2026_ytd, e25s=D.erp.y2025_ytd, e25f=D.erp.y2025_full, e24=D.erp.y2024_full;
const yoy=((e26.total/e25s.total-1)*100).toFixed(1);
const bk=D.erp.y2026_ytd.by_month, q={};
for(const m in bk){const x=m.slice(5,7);const k=x<'04'?'Q1':x<'07'?'Q2':'Q3';q[k]=(q[k]||0)+bk[m];}
const gap = 6950 - e26.total/W;
function s0(){return `
<h2>0 · 一页摘要</h2><p class="sub">五项任务全部完成 · 数字均可溯源至原始件</p>
<div class="cards">
  <div class="card"><div class="k">半年度汇报</div><div class="v">34 <small>份</small></div><div class="d">转写 47.5 万字符 · 提炼 ${D.people.length+2} 人/份</div></div>
  <div class="card"><div class="k">Q3 飞书活动</div><div class="v">${D.act.total.toLocaleString()} <small>条</small></div><div class="d">${Object.keys(D.act.by_sales).length} 人 · 7月${D.act.by_month['2026-07']} / 8月${D.act.by_month['2026-08']} / 9月${D.act.by_month['2026-09']}</div></div>
  <div class="card"><div class="k">Q3 客诉</div><div class="v">${D.cmp.total} <small>条</small></div><div class="d">未闭环 <b style="color:#d92d20">${D.cmp.not_closed}</b> 条（${(D.cmp.not_closed/D.cmp.total*100).toFixed(1)}%）· 全库 ${D.cmp.total_all}</div></div>
  <div class="card"><div class="k">2026 ERP（1-9月）</div><div class="v">${f1(e26.total)} <small>万</small></div><div class="d">同比 <b style="color:#12805c">+${yoy}%</b>（+${f1(e26.total-e25s.total)} 万）</div></div>
  <div class="card"><div class="k">年度达成率</div><div class="v">${(e26.total/W/6950*100).toFixed(2)}<small>%</small></div><div class="d">目标 6,950 万 · 缺口 ${gap.toFixed(0)} 万</div></div>
  <div class="card"><div class="k">竞品品牌</div><div class="v">${Object.keys(D.merged).length} <small>家</small></div><div class="d">诺唯赞提及 ${D.merged['诺唯赞'].q3_mentions} 次（断层第一）</div></div>
</div>
<div class="kbox"><b>三条最需要向上汇报的判断</b><ol>
<li><b>增长真实但结构不适</b>——同比 +${yoy}%，增量主要来自存量客户放量；半年报自述「有效落地订单转化率不足30%」。</li>
<li><b>客诉是当前最大经营风险</b>——Q3 ${D.cmp.total} 条客诉、${D.cmp.not_closed} 条未闭环；口径集中在<b>批间差 / 污染 / 沉淀 / 批内差</b>，且直接压制回款。</li>
<li><b>竞品已从价格战升级为体系战</b>——诺唯赞用「原料+试剂+仪器」一体化、战略合作、仪器投放绑定试剂锁定客户；我方仍以单点货号比价应对。</li>
</ol></div>
${deptBars()}`;}

function deptBars(){
  const rows=[['大客户部',2650],['销售拓展部',3400],['生命科学部',900]];
  let h='<h3>部门年度达成率</h3><div class="tbl-wrap"><table><thead><tr><th>部门</th><th class="num">2026 YTD(万)</th><th class="num">年度目标(万)</th><th class="num">达成率</th><th style="width:240px">进度</th></tr></thead><tbody>';
  rows.forEach(([n,t])=>{const a=(D.erp.y2026_ytd.by_dept[n]||0)/W;const r=a/t*100;
    h+=`<tr><td><b>${n}</b></td><td class="num">${a.toFixed(1)}</td><td class="num">${t.toLocaleString()}</td><td class="num"><b>${r.toFixed(2)}%</b></td><td><div class="barwrap"><div class="bar" style="width:${Math.min(r,100)}%"></div></div></td></tr>`;});
  const a=e26.total/W,r=a/6950*100;
  h+=`<tr style="background:#f7fafd"><td><b>合计</b></td><td class="num"><b>${a.toFixed(1)}</b></td><td class="num"><b>6,950</b></td><td class="num"><b>${r.toFixed(2)}%</b></td><td><div class="barwrap"><div class="bar" style="width:${r.toFixed(1)}%"></div></div></td></tr>`;
  return h+'</tbody></table></div>';}

/* ── ① 人员 ── */
function s1(){
  let h=`<h2>① 2026 半年度汇报 · 人员提炼</h2>
  <p class="sub">来源：<code>2026年3季度汇报/2026半年度汇报/</code>（24 pptx + 7 xlsx + 2 pdf + 1 zip）；全文 <code>_汇总/h1报告原文/</code>；结构化 <code>_汇总/人员提炼_H1.json</code></p>
  <div class="warn">⚠️ 各人 PPT 月度金额单位不统一（部分标「万元」实为「元」），已逐条在 JSON <code>_unit</code> 标注；未填数一律记 <code>null</code>，<b>未做任何估算</b>。</div>
  <h3>1.1 目标达成总表</h3><div class="toolbar"><input class="q" id="q1" placeholder="搜索姓名 / 区域…"></div>
  <div class="tbl-wrap"><table id="t1"><thead><tr><th data-s="n">姓名</th><th data-s="d">部门/区域</th>
  <th data-s="t" class="num">年度目标(万)</th><th data-s="a" class="num">上半年完成(万)</th><th data-s="r" class="num">完成率</th></tr></thead><tbody>`;
  D.people.forEach(p=>{
    const r=p.rate_pct;
    const cls = r==null?'t-grey': r>=70?'t-ok': r>=40?'t-blue': r>=20?'t-mid':'t-crit';
    h+=`<tr data-k="${esc(p.name+p.dept_area)}"><td><b>${esc(p.name)}</b></td><td>${esc(p.dept_area||'')}</td>
    <td class="num">${p.target_h1_wan??'—'}</td><td class="num">${p.actual_h1_wan??'—'}</td>
    <td class="num"><span class="tag ${cls}">${r==null?'—':r.toFixed(1)+'%'}</span></td></tr>`;});
  h+='</tbody></table></div>';
  h+='<h3>1.2 各人问题 / 下半年计划 / 需支持（原文提炼，可展开）</h3>';
  D.people.forEach(p=>{
    const sec=(t,arr)=>arr&&arr.length?`<b>${t}</b><ul>${arr.map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`:'';
    h+=`<details><summary>${esc(p.name)}　·　${esc(p.dept_area||'')}${p.rate_pct!=null?`　<span class="tag t-grey">完成率 ${p.rate_pct}%</span>`:''}</summary>
      ${sec('问题与原因',p.problems)}${sec('下半年计划',p.h2_plan)}${sec('需要公司支持',p.support_needed)}
      ${p.collection?`<b>回款/逾期</b><ul><li>${esc(p.collection)}</li></ul>`:''}
      <p class="sub">源文件：${esc(p.source_file||'')}</p></details>`;});
  return h;}

/* ── ② 活动·客诉 ── */
function s2(){
  const bm=D.act.by_month, cc=D.cmp.by_creator;
  let h=`<h2>② Q3 飞书活动与客诉（按销售）</h2>
  <p class="sub">来源：<code>数据/daily_activities_mcp.json</code>（updated ${esc(D.act.updated||'')}）、<code>数据/mcp_complaints.json</code>；Q3 = 2026-07-01 ~ 09-30</p>
  <div class="cards"><div class="card"><div class="k">活动总量</div><div class="v">${D.act.total.toLocaleString()}</div><div class="d">${Object.keys(D.act.by_sales).length} 人</div></div>
  <div class="card"><div class="k">7月 / 8月 / 9月</div><div class="v" style="font-size:19px">${bm['2026-07']} / ${bm['2026-08']} / ${bm['2026-09']}</div><div class="d">9 月为季度峰值</div></div>
  <div class="card"><div class="k">Q3 客诉</div><div class="v">${D.cmp.total}</div><div class="d">未闭环 ${D.cmp.not_closed} 条</div></div></div>
  <h3>2.1 活动量 / 客诉数（按销售）</h3><div class="toolbar"><input class="q" id="q2" placeholder="搜索销售姓名…"></div>
  <div class="tbl-wrap"><table id="t2"><thead><tr><th>销售</th><th class="num">7月</th><th class="num">8月</th><th class="num">9月</th>
  <th class="num">Q3合计</th><th class="num">Q3客诉</th><th style="width:200px">活动量</th></tr></thead><tbody>`;
  const mx=Math.max(...Object.values(D.act.by_sales).map(v=>v.total));
  Object.entries(D.act.by_sales).forEach(([sp,v])=>{
    h+=`<tr data-k="${esc(sp)}"><td><b>${esc(sp)}</b></td>
    <td class="num">${v.by_month['2026-07']||0}</td><td class="num">${v.by_month['2026-08']||0}</td>
    <td class="num">${v.by_month['2026-09']||0}</td><td class="num"><b>${v.total}</b></td>
    <td class="num">${cc[sp]?`<span class="tag ${cc[sp]>=5?'t-crit':'t-grey'}">${cc[sp]}</span>`:'—'}</td>
    <td><div class="barwrap"><div class="bar" style="width:${(v.total/mx*100).toFixed(1)}%"></div></div></td></tr>`;});
  h+=`</tbody></table></div>
  <div class="warn">生命科学（科研）5 人活动量包揽前 6（侯海瑜 425、叶信福 424、王诗香 423、袁殿果 422、黄建华 386、刘斌 361），
  但产出极低（王诗香 1.22 万、黄建华 2.94 万）——<b>活动量与产出严重不匹配</b>。</div>
  <h3>2.2 各人高频客户 TOP5</h3><div class="tbl-wrap"><table><thead><tr><th>销售</th><th class="num">活动量</th><th>高频客户</th></tr></thead><tbody>`;
  Object.entries(D.act.by_sales).forEach(([sp,v])=>{
    const c=D.act.by_sales_customers[sp]||{};
    const t=Object.entries(c).slice(0,5).map(([k,n])=>`${esc(k)}(${n})`).join('；')||'—';
    h+=`<tr><td><b>${esc(sp)}</b></td><td class="num">${v.total}</td><td>${t}</td></tr>`;});
  return h+'</tbody></table></div>';}

/* ── ③ 风险 ── */
const RISKS=[
 ['🔴 极高','郑州中道生物','38,980.8 元','账龄 365 天以上（逾期 734–895 天）','「财务比较难沟通，需线下沟通跟进」——全公司最老账款','赵云浩'],
 ['🔴 极高','北京隆格基因','10,000 元','逾期 172 天','占总逾期 41%，客户经营稳定性不好；催款函+面谈+暂停供货','冯贤平'],
 ['🔴 极高','深圳市芯思微生物','128,000 元','逾期','采购需预付款且需支付同等金额历史账款','林潇'],
 ['🔴 极高','李江辉 名下合计','477,785 元','60天以上 318,980 元（66.76%）','7 月回款计划 267,230 元，实际累计回款 0；90% 集中在湖南冠牧','李江辉'],
 ['🔴 极高','湖南冠牧生物','429,400 元','12 笔，多为 91–180 天','「季度回款周期」；7 月计划含 3 万元','李江辉'],
 ['🔴 极高','圣湘体系','约 517,500 元','0–30 至 181–365 天','含单笔 28 万（逾期 34 天）；「采购比较难沟通，需线下沟通跟进」','赵云浩'],
 ['🟠 高','武汉明德生物','156,811.6 元','14 笔，91–180 天为主','「客户财务会卡付款，需跟进财务沟通」','赵云浩'],
 ['🟠 高','杭州逸检','233,528 元','约 140 天','终端多为机构、审批繁琐（单张发票不超 1 万）；风险低但客户现金流不足','杨天雄'],
 ['🟠 高','睿丰康','144,600 元','约 60 天','因 M8254 客诉直接压制回款；需换货后才能谈回款','杨天雄'],
 ['🟠 高','深圳澳东检验检测','39,000 元','逾期','已发律师函；客户被瀚海低价抢走，实际性能不如我方','林潇'],
 ['🟠 高','聚致','84,004.2 元','约 70 天','领导拉长账期，风险不高但需逐一安排','杨天雄'],
 ['🟡 中','武汉尚睿生物','15,520 元','136/131/123/96 天','「回款拖拉」风险中；原动保研发团队已解散（-72%）','李江辉']];

function cmpTable(list,cols){
  let h='<div class="tbl-wrap"><table><thead><tr>'+cols.map(c=>`<th>${c}</th>`).join('')+'</tr></thead><tbody>';
  list.forEach(c=>{
    const u=URG[c.urgency]||URG.normal;
    h+=`<tr><td>${esc(c.created)}</td><td>${esc(c.name)}</td><td>${esc(c.customer||'—')}</td><td>${esc(c.creator||'—')}</td>`;
    if(cols.length>6) h+=`<td>${esc(c.area||'')}</td>`;
    h+=`<td><span class="tag ${c.status==='结束'?'t-ok':'t-mid'}">${esc(c.status)}</span></td>
    <td class="num">${c.days??'—'}</td><td><a href="${c.url}" target="_blank">打开 ↗</a></td></tr>`;});
  return h+'</tbody></table></div>';}

function s3(){
  const p0=D.cmp.items.filter(c=>c.status!=='结束'&&c.area==='大客户');
  const p1=D.cmp.items.filter(c=>c.status!=='结束'&&c.area!=='大客户');
  const p2=D.cmp.items.filter(c=>c.status==='结束');
  let h=`<h2>③ 关键客诉与风险事项</h2>
  <p class="sub">客诉链接口径：<code>https://project.feishu.cn/br-shgl/khts/detail/&lt;id&gt;</code>（点击可回飞书原文）</p>
  <div class="cards">
   <div class="card"><div class="k">Q3 客诉</div><div class="v">${D.cmp.total}</div><div class="d">全库 ${D.cmp.total_all}</div></div>
   <div class="card"><div class="k">未闭环</div><div class="v" style="color:#d92d20">${D.cmp.not_closed}</div><div class="d">${(D.cmp.not_closed/D.cmp.total*100).toFixed(1)}%</div></div>
   <div class="card"><div class="k">P0 大客户区</div><div class="v">${p0.length}</div><div class="d">优先处置</div></div>
   <div class="card"><div class="k">P1 其他区域</div><div class="v">${p1.length}</div><div class="d">销售拓展 ${D.cmp.by_area['销售拓展部']||0} · 浙江 ${D.cmp.by_area['浙江']||0}</div></div>
  </div>
  <h3>3.1 ⚠️ 未闭环客诉 · P0（大客户区域，${p0.length} 条）</h3>
  ${cmpTable(p0,['日期','客诉','客户','提报','状态','挂起天数','链接'])}
  <h3>3.2 ⚠️ 未闭环客诉 · P1（其他区域，${p1.length} 条）</h3>
  ${cmpTable(p1,['日期','客诉','客户','提报','区域','状态','挂起天数','链接'])}
  <h3>3.3 ✅ 已闭环客诉（${p2.length} 条）</h3>
  <details><summary>展开查看</summary>${cmpTable(p2,['日期','客诉','客户','提报','状态','挂起天数','链接'])}</details>
  <h3>3.4 🔴 高风险账款 / 客户风险（源自半年度汇报原文）</h3>
  <div class="tbl-wrap"><table><thead><tr><th>等级</th><th>客户</th><th>金额</th><th>逾期/账龄</th><th>说明与措施</th><th>来源</th></tr></thead><tbody>
  ${RISKS.map(r=>`<tr><td>${r[0]}</td><td><b>${esc(r[1])}</b></td><td class="num">${esc(r[2])}</td><td>${esc(r[3])}</td><td>${esc(r[4])}</td><td>${esc(r[5])}</td></tr>`).join('')}
  </tbody></table></div>
  <div class="kbox"><b>一句话结论</b>：Q3 的客诉与回款是两个互相咬合的问题——<b>客诉不闭环 → 客户压款 → 回款计划落空</b>（李江辉 7 月回款计划 26.7 万、实收 0）。</div>`;
  return h;}

/* ── ④ ERP ── */
function s4(){
  const m=e26.by_month, mm=Object.entries(m);
  const mx=Math.max(...mm.map(x=>x[1]));
  const m24=e24.by_month;
  const ytd24=Object.entries(m24).filter(x=>x[0]<='2024-09').reduce((a,b)=>a+b[1],0);
  let h=`<h2>④ ERP 同比</h2>
  <p class="sub">口径：<b>标准销售订单 + 已审核 + 非赠品 + 剔宝锐系</b>，金额=价税合计 ｜ 明细 <code>_汇总/erp_yoy.json</code> ｜ 复算 <code>脚本/q3_yoy_compare.py</code></p>
  <div class="cards">
   <div class="card"><div class="k">2026 YTD（1-9月）</div><div class="v">${f1(e26.total)} <small>万</small></div><div class="d">${e26.rows.toLocaleString()} 行</div></div>
   <div class="card"><div class="k">2025 同期</div><div class="v">${f1(e25s.total)} <small>万</small></div><div class="d">同比 <b style="color:#12805c">+${yoy}%</b></div></div>
   <div class="card"><div class="k">2025 全年</div><div class="v">${f1(e25f.total)} <small>万</small></div><div class="d">2026 YTD 已达 ${(e26.total/e25f.total*100).toFixed(1)}%</div></div>
   <div class="card"><div class="k">2024 全年</div><div class="v">${f1(e24.total)} <small>万</small></div><div class="d">2025 全年同比 +${((e25f.total/e24.total-1)*100).toFixed(1)}%</div></div>
  </div>
  <div class="kbox"><b>✅ 口径自校验通过</b>：本口径算出的 2025 上半年 1,586.6 万 ≈ 管理版半年报 PPT 的 1,587 万，误差 &lt;0.1%——说明提取口径与管理层一致。</div>
  <h3>4.1 总量对照</h3><div class="tbl-wrap"><table><thead><tr><th>期间</th><th class="num">金额(万)</th><th class="num">订单行数</th><th>说明</th></tr></thead><tbody>
   <tr style="background:#f7fafd"><td><b>2026 年 1–9 月（YTD）</b></td><td class="num"><b>${f1(e26.total)}</b></td><td class="num">${e26.rows}</td><td>本次汇报口径</td></tr>
   <tr><td>2025 年 1–9 月（同期）</td><td class="num">${f1(e25s.total)}</td><td class="num">${e25s.rows}</td><td>同比 <b>+${yoy}%（+${f1(e26.total-e25s.total)} 万）</b></td></tr>
   <tr><td>2025 年全年</td><td class="num">${f1(e25f.total)}</td><td class="num">${e25f.rows}</td><td>2026 YTD 已达其 ${(e26.total/e25f.total*100).toFixed(1)}%</td></tr>
   <tr><td>2024 年全年</td><td class="num">${f1(e24.total)}</td><td class="num">${e24.rows}</td><td>2025 全年同比 +${((e25f.total/e24.total-1)*100).toFixed(1)}%</td></tr>
   <tr><td>2024 年 1–9 月</td><td class="num">${(ytd24/W).toFixed(1)}</td><td class="num">—</td><td>三年 YTD 链：2024 → 2025 <b>+${((e25s.total/ytd24-1)*100).toFixed(1)}%</b> → 2026 <b>+${yoy}%</b></td></tr>
  </tbody></table></div>
  <h3>4.2 部门对照（万元）</h3><div class="tbl-wrap"><table><thead><tr><th>部门</th><th class="num">2026 YTD</th><th class="num">2025 同期</th><th class="num">同比</th><th class="num">2025 全年</th><th class="num">2024 全年</th></tr></thead><tbody>`;
  ['大客户部','销售拓展部','生命科学部'].forEach(k=>{
    const a=e26.by_dept[k]||0,b=e25s.by_dept[k]||0,c=e25f.by_dept[k]||0,d=e24.by_dept[k]||0;
    h+=`<tr><td><b>${k}</b></td><td class="num">${a?f1(a):'—'}</td><td class="num">${b?f1(b):'—'}</td>
    <td class="num">${b?`<b>+${((a/b-1)*100).toFixed(1)}%</b>`:'—'}</td><td class="num">${c?f1(c):'—'}</td><td class="num">${d?f1(d):'—'}</td></tr>`;});
  h+=`</tbody></table></div>
  <div class="warn">⚠️ ① 2024 年名册无「生命科学部」（当年并挂销售拓展体系）→ 该年此项为空；② 2025 年名册用<b>岗位</b>（大客户1/2）而非姓名 → 跨年<b>个人</b>同比仅在实名人员间可比。</div>
  <h3>4.3 2026 月度走势（万元）</h3><div class="tbl-wrap"><table><thead><tr><th>月</th>${mm.map(x=>`<th class="num">${x[0].slice(5)}月</th>`).join('')}<th class="num">合计</th></tr></thead><tbody>
  <tr><td><b>金额</b></td>${mm.map(x=>`<td class="num">${f1(x[1])}</td>`).join('')}<td class="num"><b>${f1(e26.total)}</b></td></tr>
  <tr><td>相对峰值</td>${mm.map(x=>`<td><div class="barwrap"><div class="bar" style="width:${(x[1]/mx*100).toFixed(1)}%"></div></div></td>`).join('')}<td></td></tr>
  </tbody></table></div>
  <p class="sub">季度：Q1 <b>${f1(q['Q1'])}</b> 万 ｜ Q2 <b>${f1(q['Q2'])}</b> 万 ｜ Q3 <b>${f1(q['Q3'])}</b> 万（环比 +${((q['Q3']/q['Q2']-1)*100).toFixed(1)}%）</p>
  <h3>4.4 年度目标进度</h3>
  ${deptBars()}
  <p class="sub">按 1–9 月进度线性外推：全年约 <b>${(e26.total/W/9*12).toFixed(1)} 万</b>；距 6,950 万缺口 <b>${gap.toFixed(0)} 万</b>，Q4 需完成 <b>${gap.toFixed(0)} 万</b>（月均 ${(gap/3).toFixed(0)} 万，是 1–9 月月均 ${(e26.total/W/9).toFixed(1)} 万的 <b>${((gap/3)/(e26.total/W/9)).toFixed(2)} 倍</b>）。</p>
  <h3>4.5 客户 TOP20（万元）</h3><div class="tbl-wrap"><table><thead><tr><th>#</th><th>客户</th><th class="num">2026 YTD</th><th style="width:180px">占比</th></tr></thead><tbody>`;
  const tc=Object.entries(e26.top_cust), tmx=tc[0][1];
  tc.slice(0,20).forEach((x,i)=>{h+=`<tr><td>${i+1}</td><td>${esc(x[0])}</td><td class="num">${f1(x[1])}</td>
   <td><div class="barwrap"><div class="bar" style="width:${(x[1]/tmx*100).toFixed(1)}%"></div></div></td></tr>`;});
  h+='</tbody></table></div><h3>4.6 销售员 TOP15（万元）</h3><div class="tbl-wrap"><table><thead><tr><th>#</th><th>销售</th><th class="num">2026 YTD</th><th class="num">2025 同期</th></tr></thead><tbody>';
  const s25=e25s.by_sp;
  Object.entries(e26.by_sp).slice(0,15).forEach((x,i)=>{
    h+=`<tr><td>${i+1}</td><td>${esc(x[0])}</td><td class="num"><b>${f1(x[1])}</b></td>
    <td class="num">${s25[x[0]]!=null?f1(s25[x[0]]):'<span class="tag t-grey">2025无实名</span>'}</td></tr>`;});
  return h+'</tbody></table></div>';}

/* ── ⑤ 竞品 ── */
function s5(){
  const total=Object.values(D.merged).reduce((a,b)=>a+b.q3_mentions,0);
  let h=`<h2>⑤ 主要竞争对手品牌及其优势</h2>
  <p class="sub">来源：Q3 飞书活动正文 <code>field_b99055</code> 命中 + 半年度汇报竞品板块 ｜ 台账 <code>_汇总/Q3竞品情报台账.json</code></p>
  <div class="warn">命中为关键词匹配（含少量同义/误匹配）；所有引用<b>均为原文，未改写</b>，每条均可点回飞书活动原文复核。</div>
  <h3>5.1 竞品提及热度榜</h3><div class="tbl-wrap"><table><thead><tr><th>品牌</th><th class="num">Q3 提及</th>
  <th class="num">半年报记录</th><th>主要提及人</th><th style="width:200px">热度</th></tr></thead><tbody>`;
  const mx=Math.max(...Object.values(D.merged).map(v=>v.q3_mentions));
  Object.entries(D.merged).filter(x=>x[1].q3_mentions>0).forEach(([b,v])=>{
    const rd=(D.ledger[b]?Object.entries(D.ledger[b].readers).slice(0,4):[]).map(x=>`${esc(x[0])}(${x[1]})`).join('、')||'—';
    h+=`<tr><td><b>${esc(b)}</b></td><td class="num">${v.q3_mentions}</td><td class="num">${v.h1_records}</td><td>${rd}</td>
    <td><div class="barwrap"><div class="bar" style="width:${(v.q3_mentions/mx*100).toFixed(1)}%"></div></div></td></tr>`;});
  h+=`</tbody></table></div>
  <div class="kbox">诺唯赞以 <b>${D.merged['诺唯赞'].q3_mentions}</b> 次提及（占全部命中 <b>${(D.merged['诺唯赞'].q3_mentions/total*100).toFixed(0)}%</b>）形成断层——这不是「竞争激烈」，而是「我方每一次客户对话都绕不开它」。</div>
  <h3>5.2 各品牌优势证据（Q3 活动原文 + 飞书链接）</h3>
  <div class="toolbar"><input class="q" id="q5" placeholder="搜索品牌…"></div>`;
  Object.entries(D.ledger).filter(x=>x[1].n>=10).forEach(([b,v])=>{
    h+=`<details class="bk" data-b="${esc(b)}"><summary>${esc(b)}　<span class="tag t-blue">Q3 提及 ${v.n} 次</span></summary>`;
    v.ev.forEach(e=>{h+=`<div class="ev"><div class="m">${esc(e.d)} ｜ ${esc(e.sp)} ｜ ${esc(e.c||'未填客户')}</div>
      <div class="x">${e.t}</div><div style="margin-top:6px"><a href="${e.u}" target="_blank">飞书活动原文 ↗</a></div></div>`;});
    h+='</details>';});
  return h;}

const SEC={'s0':s0,'s1':s1,'s2':s2,'s3':s3,'s4':s4,'s5':s5};
let built={};
function show(id){
  if(!built[id]){const d=document.createElement('section');d.id=id;d.className='on';
    d.innerHTML=SEC[id]();document.getElementById('main').appendChild(d);built[id]=1;bind(id);}
  document.querySelectorAll('main section').forEach(x=>x.classList.toggle('on',x.id===id));
  document.querySelectorAll('.nin button').forEach(b=>b.classList.toggle('on',b.dataset.t===id));
  window.scrollTo(0,0);}
function bind(id){
  const q=document.getElementById('q'+id.slice(1));
  if(q)q.oninput=()=>{const v=q.value.trim().toLowerCase();
    document.querySelectorAll('#'+id+' tbody tr[data-k]').forEach(r=>{
      r.style.display=(!v||r.dataset.k.toLowerCase().includes(v))?'':'none';});
    document.querySelectorAll('#'+id+' details.bk').forEach(r=>{
      r.style.display=(!v||r.dataset.b.toLowerCase().includes(v))?'':'none';});};
  document.querySelectorAll('#'+id+' th[data-s]').forEach((th,i)=>{
    let up=1;th.onclick=()=>{up=-up;const tb=th.closest('table').querySelector('tbody');
      const rs=[...tb.querySelectorAll('tr')];const k=th.dataset.s;
      rs.sort((a,b)=>{const g=r=>{const c=r.children[i];return c?(c.textContent.trim()):'';};
        let x=g(a),y=g(b);const nx=parseFloat(x.replace(/[^\d.\-]/g,'')),ny=parseFloat(y.replace(/[^\d.\-]/g,''));
        if(!isNaN(nx)&&!isNaN(ny))return (nx-ny)*up;return x.localeCompare(y,'zh')*up;});
      rs.forEach(r=>tb.appendChild(r));};});}
document.getElementById('nav').onclick=e=>{const b=e.target.closest('button');if(b){show(b.dataset.t);history.replaceState(null,'','#'+b.dataset.t);}};
window.addEventListener('hashchange',()=>{const h=location.hash.slice(1);if(SEC[h])show(h);});
show(SEC[location.hash.slice(1)]?location.hash.slice(1):'s0');
</script>
</body></html>"""

out = HTML.replace("__PAYLOAD__", payload)
p = os.path.join(OUTD, "Q3汇报数据看板.html")
open(p, "w").write(out)
print("WROTE", p, os.path.getsize(p), "bytes")

# ---- 品牌注入 ----
import brand_kit as BK
src = open(p).read()
src2 = BK.inject(src, theme="light", tag="营销中心 · Q3汇报")
open(p, "w").write(src2)
print("brand injected:", os.path.getsize(p), "bytes")
print("verify:", BK.verify(open(p).read()))
