#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""宝锐生物 · 2026 三季度销售工作汇报（诊断原料销售部）
   16 页自包含 HTML deck（零外部依赖 · 可投屏 · 三模式：演示/编辑/审稿）
   数据源：数据/2026Q3汇报_部门口径.json + 数据/2026Q3汇报_汇报数据层.json + 数据/2026Q3汇报_汇总.json
   口径：部门月度数据 / 9月统计表 / 回款表（用户 2026-10-15 指定）；年度目标 6950 万
"""
import io
import json
import os
import re
import sys
import warnings
from datetime import datetime

warnings.filterwarnings('ignore')
import pandas as pd  # noqa: E402

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类/'
sys.path.insert(0, ROOT + '脚本')
import brand_kit as BK  # noqa: E402

OUT = ROOT + '销售分析/宝锐生物_2026三季度销售工作汇报_诊断原料销售部.html'
MM = os.path.expanduser('~/.hermes/profiles/zone-b/cache/scratch/mm_q3_local')
NOW = datetime.now().strftime('%Y-%m-%d %H:%M')

DEPT_FILES = {
    '大客户部': '大客户部及生命科学-9月/1、2026月度数据-诊断原料大客户销售部.xlsx',
    '生命科学部': '大客户部及生命科学-9月/3、2026月度数据-生命科学销售部.xlsx',
    '销售拓展部': '销售拓展部-9月/2、2026月度数据-诊断原料销售拓展部.xlsx',
}
DEPT_TGT = {'大客户部': 2650.0, '销售拓展部': 3400.0, '生命科学部': 900.0}

CAL = json.load(io.open(ROOT + '数据/2026Q3汇报_部门口径.json', encoding='utf-8'))
LAY = json.load(io.open(ROOT + '数据/2026Q3汇报_汇报数据层.json', encoding='utf-8'))
SUM = json.load(io.open(ROOT + '数据/2026Q3汇报_汇总.json', encoding='utf-8'))


def num(v):
    try:
        if v is None or (isinstance(v, float) and pd.isna(v)):
            return None
        if isinstance(v, str):
            v = v.replace(',', '').strip()
            if v in ('', '-', '/', 'nan'):
                return None
        return float(v)
    except Exception:
        return None


def wan(v, d=1):
    return '—' if v is None else ('{:,.%df}' % d).format(v / 10000.0)


def fmt(v, d=1):
    return '—' if v is None else ('{:,.%df}' % d).format(v)


# ══════════ ① 部门目标达成（含月度目标/实绩/同比） ══════════
TARGETS = {}
for dept, rel in DEPT_FILES.items():
    df = pd.read_excel(os.path.join(MM, rel), sheet_name='1）年度指标达成情况', header=None)
    tgt = act = rate = None
    monthly_t = monthly_a = []
    idx = None
    for i in range(df.shape[0]):
        c0 = str(df.iat[i, 0]).strip()
        c2 = str(df.iat[i, 2]).strip() if df.shape[1] > 2 else ''
        if c2 == '月度目标' and '部' in c0 and '看板' not in c0:
            idx = i
            break
    if idx is not None:
        for k in (idx, idx + 1, idx + 2):
            if k >= df.shape[0]:
                break
            c2b = str(df.iat[k, 2]).strip()
            vals = [num(df.iat[k, j]) for j in range(3, 12)]
            an = num(df.iat[k, 15]) if df.shape[1] > 15 else None
            if c2b == '月度目标':
                monthly_t, tgt = vals, an
            elif c2b == '实际完成':
                monthly_a, act = vals, an
            elif c2b == '达成率':
                rate = an
    TARGETS[dept] = {'t': tgt or 0, 'a': act or 0, 'rate': rate, 'mt': monthly_t, 'ma': monthly_a}


def pad9(v):
    v = list(v or [])
    return (v + [None] * 9)[:9]


for _d in TARGETS:
    TARGETS[_d]['mt'] = pad9(TARGETS[_d]['mt'])
    TARGETS[_d]['ma'] = pad9(TARGETS[_d]['ma'])

# 同比（部门近三年销售数据表）
YOY = {}
for key, rel, sheet in [('大客户部', DEPT_FILES['大客户部'], '大客户部近三年销售数据'),
                        ('销售拓展部', DEPT_FILES['销售拓展部'], '5）销售拓展部近三年销售数据')]:
    try:
        df = pd.read_excel(os.path.join(MM, rel), sheet_name=sheet, header=None)
    except Exception:
        continue
    y = {}
    for i in range(df.shape[0]):
        c1 = str(df.iat[i, 1]).strip()
        m = re.match(r'^(2024|2025|2026)年$', c1)
        if m:
            vals = [num(df.iat[i, j]) for j in range(2, 11)]
            y[m.group(1)] = sum(x for x in vals if x)
    if y.get('2025') and y.get('2026'):
        YOY[key] = {'y25': y['2025'], 'y26': y['2026'],
                    'pct': (y['2026'] - y['2025']) / y['2025'] * 100.0}

# ══════════ ② 回款 ══════════
COL = CAL['collection']
col_dk9 = COL.get('大客户部-9月', {})
col_ext9 = COL.get('销售拓展部-9月', {})
col_dk10 = COL.get('大客户部-10月', {})

# ══════════ ③ 过程指标（统计表） ══════════
STAT_F = {
    '大客户部': '大客户部及生命科学-9月/1、大客户部/1.大客户部销售数据统计表（9月）.xlsx',
    '生命科学部': '大客户部及生命科学-9月/5、生命科学销售部/1.生命科学销售部销售数据统计表（9月）.xlsx',
    '销售拓展部': '销售拓展部-9月/2、销售拓展部/1.销售拓展部销售数据统计表（9月）.xlsx',
}


def visit_total(dept, stride):
    df = pd.read_excel(os.path.join(MM, STAT_F[dept]), sheet_name='1-客户拜访', header=None)
    for i in range(df.shape[0]):
        if str(df.iat[i, 0]).strip() in ('合计',):
            cols = [3 + stride * k for k in range(9)]
            tot = [num(df.iat[i, c]) if c < df.shape[1] else None for c in cols]
            return sum(x for x in tot if x), tot
    return 0, []


VISIT = {'大客户部': visit_total('大客户部', 1), '生命科学部': visit_total('生命科学部', 1),
         '销售拓展部': visit_total('销售拓展部', 2)}


def test_rate(dept, sheet):
    try:
        df = pd.read_excel(os.path.join(MM, STAT_F[dept]), sheet_name=sheet, header=None)
    except Exception:
        return None
    for i in range(df.shape[0]):
        if str(df.iat[i, 0]).strip() == '合计':
            cols = [3 + 4 * k for k in range(9)]
            sends, rates = [], []
            for c in cols:
                if c + 3 < df.shape[1]:
                    sends.append(num(df.iat[i, c]))
                    rates.append(num(df.iat[i, c + 3]))
            s = sum(x for x in sends if x)
            rr = [x for x in rates if x]
            return {'sends': s, 'rate_avg': (sum(rr) / len(rr) * 100 if rr else None), 'rates': rates, 'sends_m': sends}
    return None


TESTS = {'大客户部': test_rate('大客户部', '2-测试单'), '销售拓展部': test_rate('销售拓展部', '2-测试单及反馈率'),
         '生命科学部': test_rate('生命科学部', '4-测试单及反馈率')}


def ls_metric(sheet, stride):
    try:
        df = pd.read_excel(os.path.join(MM, STAT_F['生命科学部']), sheet_name=sheet, header=None)
    except Exception:
        return None
    for i in range(df.shape[0]):
        if str(df.iat[i, 0]).strip() == '合计':
            cols = [3 + stride * k for k in range(9)]
            vals = [num(df.iat[i, c]) if c < df.shape[1] else None for c in cols]
            vals = [x for x in vals if x is not None]
            return sum(vals), vals
    return None


LS_NEWCUST = ls_metric('2-新客户成交数量', 2)   # stride: 开发/成交
LS_CONTACT = ls_metric('3-飞书新增联系人', 1)

# ══════════ ④ 商机 / 竞品 ══════════
OPP = LAY['opp']
OPP_ROWS = SUM['tables']['商机明细']
BRANDS = ['诺唯赞', '全式金', '翌圣', '菲鹏', '瀚海', '金诺美', '近岸', '康为', '天根', 'TaKaRa', 'TOYOBO', '赛默飞', '罗氏', 'NEB']
BRAND_CNT = []
for b in BRANDS:
    n = 0
    for r in OPP_ROWS:
        if b in ''.join(str(r.get(k) or '') for k in ('商机背景概述', '资源需求', '前三季度异常原因复盘', '四季度冲刺难点及措施', '商机名称')):
            n += 1
    if n:
        BRAND_CNT.append({'k': b, 'v': n})
BRAND_CNT.sort(key=lambda x: -x['v'])
ZD = [r for r in OPP_ROWS if '四季度冲刺难点' in r and str(r.get('四季度冲刺难点及措施') or '').strip()]
OPP_HARD = [{'客': str(r.get('客户名称'))[:20], '销': str(r.get('销售') or r.get('_汇报人') or ''),
             '难': re.sub(r'\s+', ' ', str(r.get('四季度冲刺难点及措施')))[:120]} for r in ZD[:3]]

# ══════════ ⑤ 人员/组织 ══════════
PEOPLE = LAY['person']
ORG = {}
for d, v in PEOPLE.items():
    ps = v.get('people', [])
    ORG[d] = {'在职': len([p for p in ps if not p.get('left')]), '离职': len([p for p in ps if p.get('left')]),
              'top': ps[:3]}

Q4 = LAY['q4']
Q4_GAP = round(Q4['大客户部']['Q4缺口'] + Q4['销售拓展部']['Q4缺口'] + Q4['生命科学部']['Q4缺口'], 2)
TOT_T = sum(v['t'] for v in TARGETS.values())
TOT_A = sum(v['a'] for v in TARGETS.values())

# ══════════ DATA（给前端图表） ══════════
DATA = {
    'depts': [{'k': d, 't': round(TARGETS[d]['t'] / 10000, 1), 'a': round(TARGETS[d]['a'] / 10000, 1),
               'r': round((TARGETS[d]['rate'] or 0) * 100, 1),
               'ma': [round((x or 0) / 10000, 1) for x in TARGETS[d]['ma']],
               'mt': [round((x or 0) / 10000, 1) for x in TARGETS[d]['mt']]} for d in ('销售拓展部', '大客户部', '生命科学部')],
    'yoy': [{'k': k, 'v': round(v['pct'], 1), 'y25': round(v['y25'] / 10000, 1), 'y26': round(v['y26'] / 10000, 1)}
            for k, v in YOY.items()],
    'collection': {
        'dk9': {'tot': col_dk9.get('逾期合计', 0), 'back': col_dk9.get('已回合计', 0), 'n': col_dk9.get('明细条数', 0),
                'age': col_dk9.get('按账龄', {}), 'risk': col_dk9.get('按风险', {}), 'cust': col_dk9.get('按客户', {}),
                'prog': col_dk9.get('进度', [])},
        'ext9': {'tot': col_ext9.get('逾期合计', 0), 'back': col_ext9.get('已回合计', 0), 'n': col_ext9.get('明细条数', 0),
                 'age': col_ext9.get('按账龄', {}), 'risk': col_ext9.get('按风险', {}), 'cust': col_ext9.get('按客户', {}),
                 'prog': col_ext9.get('进度', [])},
        'dk10': {'tot': col_dk10.get('逾期合计', 0), 'age': col_dk10.get('按账龄', {}),
                 'prog': col_dk10.get('进度', [])},
    },
    'visit': {k: {'tot': v[0], 'm': [x or 0 for x in v[1]]} for k, v in VISIT.items()},
    'tests': TESTS,
    'ls': {'newcust': LS_NEWCUST[0] if LS_NEWCUST else None, 'contact': LS_CONTACT[0] if LS_CONTACT else None},
    'brands': BRAND_CNT[:5],
    'q4': Q4,
}

CSS = r"""
*{box-sizing:border-box}
:root{--brand:#1F4E9C;--navy:#0E2038;--ink:#141A24;--grey:#56617a;--line:#E3E8F0;--bg:#F4F6FA;
--blue:#0B57D0;--red:#D81B26;--amber:#E8A300;--green:#0F9D58;--soft:#EEF3FB}
html,body{margin:0;padding:0}
body{background:var(--bg);color:var(--ink);font:14px/1.62 -apple-system,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
display:flex;flex-direction:column;min-height:100vh}
.shell{flex:1;display:flex;min-height:0}
/* 侧栏 */
.side{width:196px;flex:none;background:#fff;border-right:1px solid var(--line);padding:14px 0;overflow:auto}
.side h4{margin:0 0 8px;padding:0 16px;font-size:11px;letter-spacing:1.6px;color:var(--grey);font-weight:800}
.side a{display:flex;gap:8px;padding:7px 16px;color:#2A3547;text-decoration:none;font-size:12.5px;border-left:3px solid transparent}
.side a:hover{background:var(--soft)}
.side a.on{background:var(--soft);border-left-color:var(--brand);color:var(--brand);font-weight:700}
.side a i{font-style:normal;color:var(--grey);font-size:11px;min-width:14px}
/* 主区 */
.wrap{flex:1;min-width:0;display:flex;flex-direction:column}
.bar{display:flex;align-items:center;gap:8px;padding:9px 14px;background:#fff;border-bottom:1px solid var(--line);flex-wrap:wrap}
.bar .title{font-weight:800;color:var(--brand);margin-right:auto;font-size:13.5px}
.btn{border:1px solid var(--line);background:#fff;color:var(--brand);font:inherit;font-size:12.5px;font-weight:600;
padding:5px 12px;border-radius:7px;cursor:pointer}
.btn:hover{background:var(--soft)}
.btn.on{background:var(--brand);color:#fff;border-color:var(--brand)}
.prog{height:3px;background:var(--line)}
.prog i{display:block;height:100%;background:var(--brand);width:0}
.stage{flex:1;min-height:0;position:relative;overflow:hidden}
#deck{position:absolute;width:1280px;height:720px;transform-origin:0 0;background:#fff;
box-shadow:0 6px 30px rgba(14,32,56,.12);border-radius:6px}
.slide{position:absolute;inset:0;padding:32px 46px;display:none;flex-direction:column;overflow:hidden;background:#fff}
.slide.on{display:flex}
.scroll{overflow:auto;min-height:0;flex:1}
/* 封面 / 章节页 */
.cover{background:linear-gradient(140deg,#0E2038,#1F4E9C 62%,#2E6FD0);color:#fff;justify-content:center;padding:0}
.cover canvas{position:absolute;inset:0;width:100%;height:100%}
.cover .cwrap{position:relative;z-index:2;padding:0 78px}
.cover .eyebrow{font-size:12px;letter-spacing:3px;opacity:.82;font-weight:700}
.cover h1{font-size:50px;line-height:1.16;margin:14px 0 0;font-weight:900;letter-spacing:-.5px}
.cover h1 em{font-style:normal;color:#8FC0FF}
.cover .cline{width:88px;height:5px;background:#8FC0FF;margin:20px 0}
.cover .meta{font-size:13.5px;line-height:2;opacity:.94}
.slide.chapter{background:linear-gradient(135deg,#122747,#1F4E9C);color:#fff;justify-content:center}
.chapter canvas{position:absolute;inset:0;width:100%;height:100%}
.chapter .cwrap{position:relative;z-index:2;padding:0 78px}
.chapter .big{font-size:88px;font-weight:900;opacity:.16;line-height:1}
.chapter h2{font-size:36px;margin:6px 0 8px;font-weight:900}
.chapter .ds{opacity:.86;font-size:14px;max-width:760px}
/* 页头 */
.slide-head{flex:none;margin-bottom:16px;border-bottom:2px solid var(--brand);padding-bottom:10px}
.kicker{font-size:11.5px;letter-spacing:2.4px;color:var(--brand);font-weight:800}
.slide-head h2{font-size:25px;margin:5px 0 4px;font-weight:900;letter-spacing:-.2px}
.slide-head .sub{font-size:12.5px;color:var(--grey)}
.slide-body{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px}
/* KPI */
.kpis{display:grid;gap:12px}
.kpi{background:#fff;border:1px solid var(--line);border-top:3px solid var(--brand);border-radius:8px;padding:12px 14px}
.kpi.cool{border-top-color:var(--blue)}.kpi.warn{border-top-color:var(--amber)}.kpi.bad{border-top-color:var(--red)}
.kpi.good{border-top-color:var(--green)}
.kpi .lb{font-size:11.5px;color:var(--grey);font-weight:700}
.kpi .vl{font-size:27px;font-weight:900;letter-spacing:-.5px;line-height:1.2}
.kpi .vl small{font-size:13px;font-weight:700;color:var(--grey);margin-left:3px}
.kpi .nt{font-size:11.5px;color:var(--grey);line-height:1.5}
/* 表 */
table.data{width:100%;border-collapse:collapse;font-size:12.5px}
table.data th,table.data td{border:1px solid var(--line);padding:6px 8px;vertical-align:top;word-break:break-word}
table.data th{background:var(--soft);color:#25314A;font-weight:800;text-align:left;white-space:nowrap}
table.data td.num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
table.data tr:nth-child(even) td{background:#FAFCFF}
table.data.tight th,table.data.tight td{padding:4px 6px;font-size:12px}
.tw{overflow:auto;border-radius:6px}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}
.infobox{background:var(--soft);border-left:4px solid var(--brand);padding:9px 12px;border-radius:6px;font-size:12px;color:#25314A}
.warnbox{background:#FFF7E6;border-left:4px solid var(--amber);padding:9px 12px;border-radius:6px;font-size:12px;color:#6B4A00}
.duo{display:grid;grid-template-columns:1fr 1fr;gap:16px;min-height:0}
.trio{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;min-height:0}
.card{background:#fff;border:1px solid var(--line);border-radius:8px;padding:12px 14px;min-width:0}
.card h4{margin:0 0 8px;font-size:13px;color:var(--brand);font-weight:800}
.toccard{border:1px solid var(--line);border-left:4px solid var(--brand);border-radius:8px;padding:12px 14px;cursor:pointer;background:#fff}
.toccard:hover{background:var(--soft)}
.toccard .no{font-size:12px;color:var(--brand);font-weight:800}
.toccard .nm{font-size:15px;font-weight:800;margin:2px 0}
.toccard .ds{font-size:11.5px;color:var(--grey)}
.mut{color:var(--grey)}
.tiny{font-size:11px;color:var(--grey)}
/* 审稿模式 */
.rvPanel{position:fixed;top:0;right:0;width:296px;height:100vh;background:#fff;border-left:1px solid var(--line);
padding:12px;overflow:auto;display:none;z-index:40}
body.rv .rvPanel{display:block}
body.rv .wrap{margin-right:296px}
.rvPanel h4{margin:0 0 8px;font-size:13px;color:var(--brand)}
.rvItem{display:flex;gap:8px;align-items:center;padding:6px 8px;border-radius:6px;cursor:pointer;font-size:12px;border:1px solid transparent}
.rvItem:hover{background:var(--soft)}
.rvItem.on{border-color:var(--brand);background:var(--soft)}
.rvBadge{font-size:10.5px;padding:1px 6px;border-radius:99px;font-weight:800;margin-left:auto;white-space:nowrap}
.b0{background:#EEF1F6;color:#56617a}.b1{background:#E3F5EA;color:#0F7A46}.b2{background:#FDECEE;color:#B01924}
.rvNote{width:100%;min-height:76px;border:1px solid var(--line);border-radius:6px;padding:7px;font:inherit;font-size:12px;margin-top:6px}
.rvStat{display:flex;gap:10px;flex-wrap:wrap;font-size:11.5px;color:var(--grey);margin:8px 0}
.slideBadge{position:absolute;right:14px;top:12px;font-size:11px;padding:2px 9px;border-radius:99px;font-weight:800;z-index:5}
body.pres .side,body.pres .bar,body.pres .rvPanel{display:none}
body.pres .wrap{margin-right:0}
/* 打印 */
@media print{
 .side,.bar,.rvPanel,.prog{display:none!important}
 #deck{position:static;width:auto;height:auto;transform:none!important;box-shadow:none}
 .slide{display:flex!important;position:static;page-break-after:always;height:720px;border:0}
 .stage{overflow:visible}
}
@media (max-width:760px){
 .bior-header .bior-header-inner{flex-wrap:wrap;padding:8px 12px;gap:8px}
 .bior-header .bior-name{font-size:13px;letter-spacing:.3px}
 .bior-header .bior-cn{font-size:10px}
 .bior-header .bior-slogan{font-size:10px;letter-spacing:0}
 .bior-header .bior-logo-chip img{height:22px}
 .side{display:none}
 body.rv .wrap{margin-right:0}
}
"""

JS = r"""
var N=0,CUR=0,SL=[],RV={st:[],nt:[]},D=null;
function el(id){return document.getElementById(id);}
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});}
function isTyping(e){var t=e.target;if(!t)return false;
 return t.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)||e.isComposing||e.keyCode===229;}
function fit(){var st=document.querySelector('.stage'),dk=el('deck');if(!st||!dk)return;
 var pad=document.body.classList.contains('pres')?0:12;
 var s=Math.min((st.clientWidth-pad*2)/1280,(st.clientHeight-pad*2)/720);
 dk.style.transform='scale('+s+')';
 dk.style.left=Math.max(0,(st.clientWidth-1280*s)/2)+'px';
 dk.style.top=Math.max(0,(st.clientHeight-720*s)/2)+'px';}
/* ── 图表（零依赖）── */
function hbar(host,rows,opt){if(!host)return;opt=opt||{};var mx=Math.max.apply(null,rows.map(function(r){return r.v;}).concat([1]));
 var lw=opt.lw||86,vw=opt.vw||58,u=opt.unit||'',d=(opt.d==null?1:opt.d);
 var h='<div style="display:grid;gap:6px">';
 rows.forEach(function(r){var pct=Math.max(1.5,(r.v/mx)*100);
  h+='<div style="display:grid;grid-template-columns:'+lw+'px 1fr '+vw+'px;gap:8px;align-items:center;min-width:0">'
   +'<span style="font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(r.k)+'</span>'
   +'<span style="background:#EEF1F6;height:15px;border-radius:3px;overflow:hidden;display:block">'
   +'<i style="display:block;height:100%;width:'+pct+'%;background:'+(r.c||'var(--brand)')+'"></i></span>'
   +'<span style="font-size:12px;text-align:right;font-variant-numeric:tabular-nums">'+r.v.toFixed(d)+u+'</span></div>';});
 host.innerHTML=h+'</div>';}
function vbars(host,rows,opt){if(!host)return;opt=opt||{};var H=opt.h||150,u=opt.unit||'',d=(opt.d==null?0:opt.d);
 var mx=Math.max.apply(null,rows.map(function(r){return r.v;}).concat([1]));
 var w=Math.floor(100/Math.max(rows.length,1));
 var h='<div style="display:flex;align-items:flex-end;gap:6px;height:'+(H+26)+'px">';
 rows.forEach(function(r){var bh=Math.max(2,r.v/mx*H);
  h+='<div style="flex:1;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:3px;min-width:0">'
   +'<span style="font-size:10.5px;font-variant-numeric:tabular-nums">'+r.v.toFixed(d)+'</span>'
   +'<i style="display:block;width:100%;max-width:'+w+'%;height:'+bh+'px;background:'+(r.c||'var(--brand)')+';border-radius:3px 3px 0 0"></i>'
   +'<span style="font-size:10.5px;color:var(--grey);white-space:nowrap">'+esc(r.k)+'</span></div>';});
 host.innerHTML=h+'</div>';}
function groupBars(host,labels,series,opt){if(!host)return;opt=opt||{};var H=opt.h||150,d=(opt.d==null?1:opt.d);
 var mx=1;series.forEach(function(s){s.rows.forEach(function(v){if(v>mx)mx=v;});});
 var h='<div style="display:flex;align-items:flex-end;gap:14px;height:'+(H+30)+'px">';
 labels.forEach(function(lb,i){
  h+='<div style="flex:1;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:3px">'
   +'<div style="display:flex;align-items:flex-end;gap:3px;height:'+H+'px">';
  series.forEach(function(s){var v=s.rows[i]||0,bh=Math.max(2,v/mx*H);
   h+='<i title="'+esc(s.k)+' '+v.toFixed(d)+'" style="display:block;width:19px;height:'+bh+'px;background:'+s.c+';border-radius:3px 3px 0 0"></i>';});
  h+='</div><span style="font-size:11px;color:var(--grey)">'+esc(lb)+'</span></div>';});
 h+='</div><div style="display:flex;gap:14px;justify-content:center;font-size:11.5px;color:var(--grey);margin-top:4px">'
  +series.map(function(s){return '<span><i style="display:inline-block;width:9px;height:9px;background:'+s.c+';border-radius:2px;margin-right:4px"></i>'+esc(s.k)+'</span>';}).join('')
  +'</div>';
 host.innerHTML=h;}
function donut(host,parts){if(!host)return;var tot=0;parts.forEach(function(p){tot+=p.v;});
 if(!tot){host.innerHTML='<span class="tiny">无数据</span>';return;}
 var acc=0,seg=parts.map(function(p){var a=acc/tot*360;acc+=p.v;return p.c+' '+a.toFixed(2)+'deg '+(acc/tot*360).toFixed(2)+'deg';}).join(',');
 host.innerHTML='<div style="display:flex;gap:16px;align-items:center;flex-wrap:wrap">'
  +'<div style="width:118px;height:118px;flex:none;border-radius:50%;background:conic-gradient('+seg+');'
  +'mask:radial-gradient(circle,transparent 36%,#000 37%);-webkit-mask:radial-gradient(circle,transparent 36%,#000 37%)"></div>'
  +'<div style="display:grid;gap:5px">'+parts.map(function(p){return '<div style="font-size:12px"><i style="display:inline-block;width:9px;height:9px;background:'+p.c
   +';border-radius:2px;margin-right:6px"></i>'+esc(p.k)+' <b style="font-variant-numeric:tabular-nums">'+p.v.toFixed(1)+'</b>'
   +'<span class="tiny">（'+(p.v/tot*100).toFixed(1)+'%）</span></div>';}).join('')+'</div></div>';}
/* ── 画布粒子（封面/章节页）── */
function particles(cv){if(!cv)return;var box=cv.parentElement;
 cv.width=box.offsetWidth*(window.devicePixelRatio||1);
 cv.height=box.offsetHeight*(window.devicePixelRatio||1);
 var ctx=cv.getContext('2d'),dpr=window.devicePixelRatio||1,W=box.offsetWidth,H=box.offsetHeight;
 var ps=[];for(var i=0;i<44;i++)ps.push({x:Math.random()*W,y:Math.random()*H,vx:(Math.random()-.5)*.24,vy:(Math.random()-.5)*.24});
 (function loop(){ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,W,H);
  ps.forEach(function(p){p.x+=p.vx;p.y+=p.vy;if(p.x<0||p.x>W)p.vx*=-1;if(p.y<0||p.y>H)p.vy*=-1;});
  for(var i=0;i<ps.length;i++){for(var j=i+1;j<ps.length;j++){var dx=ps[i].x-ps[j].x,dy=ps[i].y-ps[j].y,d2=dx*dx+dy*dy;
   if(d2<24000){ctx.strokeStyle='rgba(143,192,255,'+(0.30*(1-d2/24000)).toFixed(3)+')';ctx.lineWidth=1;
    ctx.beginPath();ctx.moveTo(ps[i].x,ps[i].y);ctx.lineTo(ps[j].x,ps[j].y);ctx.stroke();}}}
  ps.forEach(function(p){ctx.fillStyle='rgba(255,255,255,.62)';ctx.beginPath();ctx.arc(p.x,p.y,1.5,0,6.2832);ctx.fill();});
  requestAnimationFrame(loop);})();}
/* ── 翻页 ── */
function go(i){if(i<0||i>=N)return;CUR=i;
 SL.forEach(function(s,k){s.classList.toggle('on',k===i);});
 var t=SL[i].getAttribute('data-t')||'';if(el('ttl'))el('ttl').textContent=t;
 if(el('pgNow'))el('pgNow').textContent=(i+1);
 if(el('progBar'))el('progBar').style.width=((i+1)/N*100)+'%';
 var links=document.querySelectorAll('.side a');links.forEach(function(a,k){a.classList.toggle('on',k===i);});
 try{if(location.hash!=='#p='+(i+1))history.replaceState(null,'','#p='+(i+1));}catch(e){}
 var sc=SL[i].querySelector('.scroll');if(sc)sc.scrollTop=0;
 if(document.body.classList.contains('rv')){rvLoadNote();rvPaint(i);
  var it=document.querySelector('.rvItem.on');if(it)it.classList.remove('on');
  var ni=document.querySelector('.rvItem[data-i="'+i+'"]');if(ni)ni.classList.add('on');}}
function toggleSide(){var s=document.querySelector('.side');if(s)s.style.display=s.style.display==='none'?'':'none';setTimeout(fit,20);}
function togglePres(){document.body.classList.toggle('pres');setTimeout(fit,30);setTimeout(fit,180);}
var ED_SEL='table.data td, table.data th, .kpi .vl, .kpi .nt, .slide-head h2, .slide-head .sub, .infobox, .warnbox, .card h4, .card .mut';
function toggleEdit(){var on=!document.body.classList.contains('ed');
 document.body.classList.toggle('ed',on);
 document.querySelectorAll(ED_SEL).forEach(function(n){on?n.setAttribute('contenteditable','true'):n.removeAttribute('contenteditable');});
 var b=el('btnEdit');if(b)b.classList.toggle('on',on);}
function toggleReview(){var on=!document.body.classList.contains('rv');
 document.body.classList.toggle('rv',on);el('btnRv').classList.toggle('on',on);
 if(on){rvLoadNote();rvPaint(CUR);rvStat();}setTimeout(fit,30);setTimeout(fit,220);}
function rvLoadNote(){var i=CUR,t=el('rvNote');if(t)t.value=RV.nt[i]||'';}
function rvPaint(i){var b=SL[i].querySelector('.slideBadge');if(!b)return;
 var s=RV.st[i];b.className='slideBadge b'+(s==null?0:s);
 b.textContent=s===1?'✓ 已确认':(s===2?'✎ 需修改':'待审');}
function rvJump(i){go(i);}
function rvMark(v){RV.st[CUR]=v;rvPaint(CUR);rvStat();}
function rvClear(){RV.st[CUR]=null;RV.nt[CUR]='';var t=el('rvNote');if(t)t.value='';rvPaint(CUR);rvStat();}
function rvStat(){var c=0,f=0;for(var i=0;i<N;i++){if(RV.st[i]===1)c++;if(RV.st[i]===2)f++;}
 if(el('rvS'))el('rvS').innerHTML='<span>共 '+N+' 页</span><span>✓ 已确认 '+c+'</span><span>✎ 需修改 '+f+'</span><span>待审 '+(N-c-f)+'</span>';}
function rvBuild(){var h='';for(var i=0;i<N;i++){
 h+='<div class="rvItem" data-i="'+i+'" onclick="rvJump('+i+')"><b>'+(i+1)+'</b><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'
  +esc(SL[i].getAttribute('data-t')||'')+'</span><span class="rvBadge b0" id="rvs'+i+'">待审</span></div>';}
 if(el('rvList'))el('rvList').innerHTML=h;
 for(var k=0;k<N;k++)rvPaint(k);}
function rvExport(){var pages=[];for(var i=0;i<N;i++)pages.push({page:i+1,title:SL[i].getAttribute('data-t')||'',
 status:RV.st[i]===1?'confirmed':(RV.st[i]===2?'needfix':'unreviewed'),note:RV.nt[i]||''});
 var out={deck:'2026Q3汇报-诊断原料销售部',exported:new Date().toLocaleString('zh-CN'),
  summary:{confirmed:pages.filter(function(p){return p.status==='confirmed';}).length,
   needfix:pages.filter(function(p){return p.status==='needfix';}).length,
   unreviewed:pages.filter(function(p){return p.status==='unreviewed';}).length,total:N},pages:pages};
 var blob=new Blob([JSON.stringify(out,null,1)],{type:'application/json'});
 var a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='2026Q3汇报_审稿记录.txt';a.click();}
function fill(){var D=D0;
 var mo=['1月','2月','3月','4月','5月','6月','7月','8月','9月'];
 groupBars(el('cMonth'),mo,[
  {k:'月度目标',c:'#C6D4EA',rows:D.depts[0].mt.map(function(v,i){return (D.depts[0].mt[i]||0)+(D.depts[1].mt[i]||0)+(D.depts[2].mt[i]||0);})},
  {k:'月度实绩',c:'var(--brand)',rows:D.depts[0].ma.map(function(v,i){return (D.depts[0].ma[i]||0)+(D.depts[1].ma[i]||0)+(D.depts[2].ma[i]||0);})}
 ],{h:168});
 hbar(el('cDeptRate'),D.depts.map(function(d){return {k:d.k,v:d.r,unit:'%',c:d.r>=50?'var(--green)':(d.r>=20?'var(--amber)':'var(--red)')};}),{lw:96,vw:52});
 donut(el('cShare'),D.depts.map(function(d,i){return {k:d.k,v:d.a,c:['#1F4E9C','#2E6FD0','#8FC0FF'][i]};}));
 hbar(el('cYoy'),D.yoy.map(function(y){return {k:y.k,v:y.v,unit:'%',c:y.v>=0?'var(--green)':'var(--red)'};}),{lw:96,vw:56});
 /* 回款 */
 var dk=D.collection.dk9,ex=D.collection.ext9;
 hbar(el('cAge'),Object.keys(dk.age).map(function(k){return {k:k,v:dk.age[k]/10000};})
   .concat([{k:'—拓展部同期—',v:0,c:'#fff'}]).concat(Object.keys(ex.age).map(function(k){return {k:k,v:ex.age[k]/10000,c:'#8FC0FF'};})),{lw:96,vw:60,unit:' 万'});
 donut(el('cRiskDK'),Object.keys(dk.risk).map(function(k,i){return {k:k,v:dk.risk[k]/10000,c:['#0F9D58','#E8A300','#D81B26'][i%3]};}));
 donut(el('cRiskEX'),Object.keys(ex.risk).map(function(k,i){return {k:k,v:ex.risk[k]/10000,c:['#0F9D58','#E8A300','#D81B26'][i%3]};}));
 hbar(el('cCustDK'),Object.keys(dk.cust).slice(0,6).map(function(k){return {k:k,v:dk.cust[k]/10000};}),{lw:150,vw:58,unit:' 万'});
 hbar(el('cCustEX'),Object.keys(ex.cust).slice(0,6).map(function(k){return {k:k,v:ex.cust[k]/10000,c:'#2E6FD0'};}),{lw:150,vw:58,unit:' 万'});
 /* 过程指标 */
 hbar(el('cVisit'),[['销售拓展部',D.visit['销售拓展部'].tot],['生命科学部',D.visit['生命科学部'].tot],['大客户部',D.visit['大客户部'].tot]]
   .map(function(r){return {k:r[0],v:r[1],unit:' 人次'};}),{lw:104,vw:76});
 vbars(el('cVisitM'),D.visit['销售拓展部'].m.map(function(v,i){return {k:(i+1)+'月',v:v};}),{h:118});
 hbar(el('cBrand'),D.brands.map(function(b){return {k:b.k,v:b.v,unit:' 次'};}),{lw:82,vw:56});
 /* 商机 */
 donut(el('cOppNature'),Object.keys(D.oppNature).map(function(k,i){return {k:k,v:D.oppNature[k],c:['#1F4E9C','#2E6FD0','#8FC0FF','#C6D4EA','#E8A300','#0F9D58','#D81B26'][i%7]};}));
 hbar(el('cOppTop'),D.oppTop.map(function(o){return {k:o.k,v:o.v/10000,unit:' 万'};}),{lw:158,vw:60});
 /* Q4 */
 groupBars(el('cQ4'),['大客户部','销售拓展部','生命科学部'],[
  {k:'Q4 缺口（万元）',c:'var(--red)',rows:[D.q4['大客户部'].Q4缺口,D.q4['销售拓展部'].Q4缺口,D.q4['生命科学部'].Q4缺口]}
 ],{h:158,d:0});
 /* 组织 */
 hbar(el('cOrg'),Object.keys(D.org).map(function(k){return {k:k,v:D.org[k]['在职'],unit:' 人'};}),{lw:96,vw:56});
 for(var i=0;i<N;i++)rvPaint(i);}
/* ── 启动 ── */
(function boot(){
 SL=[].slice.call(document.querySelectorAll('.slide'));N=SL.length;
 var _hh=location.hash||'';
 var _h=_hh.match(/p=(\d+)/);
 fill();
 go(_h?Math.min(Math.max(parseInt(_h[1],10)-1,0),N-1):0);
 particles(el('cvCover'));
 [].slice.call(document.querySelectorAll('canvas.chCv')).forEach(function(c){particles(c);});
 rvBuild();rvStat();
 fit();
 window.addEventListener('load',function(){fit();setTimeout(fit,120);});
 requestAnimationFrame(function(){fit();requestAnimationFrame(fit);});
 if(document.fonts&&document.fonts.ready)document.fonts.ready.then(function(){fit();});
 window.addEventListener('resize',fit);
 window.addEventListener('hashchange',function(){
  var m=(location.hash||'').match(/p=(\d+)/);
  if(m){var k=Math.min(Math.max(parseInt(m[1],10)-1,0),N-1);if(k!==CUR)go(k);}});
 if(/rv=1/.test(_hh))toggleReview();
})();

document.addEventListener('keydown',function(e){
 if(isTyping(e))return;
 if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '){e.preventDefault();go(CUR+1);}
 else if(e.key==='ArrowLeft'||e.key==='PageUp'){e.preventDefault();go(CUR-1);}
 else if(e.key==='Home'){go(0);} else if(e.key==='End'){go(N-1);}
 else if(e.key==='r'||e.key==='R'){toggleReview();}
 else if(e.key==='e'||e.key==='E'){toggleEdit();}
 else if(e.key==='Escape'){if(document.body.classList.contains('rv'))toggleReview();}});
"""


# ══════════════════════ 页面 ══════════════════════
def esc(s):
    return (str(s).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;'))


def kpi(label, value, unit='', note='', cls=''):
    u = '<small>%s</small>' % esc(unit) if unit else ''
    n = '<div class="nt">%s</div>' % note if note else ''
    return '<div class="kpi %s"><div class="lb">%s</div><div class="vl">%s%s</div>%s</div>' % (cls, esc(label), esc(value), u, n)


def slide(title, kick, h2, sub, body, tid=None):
    return ('<section class="slide" data-t="%s"><div class="slide-head"><div class="kicker">%s</div><h2>%s</h2>'
            '<div class="sub">%s</div></div><div class="slide-body"><div class="scroll">%s</div></div></section>'
            % (esc(tid or title), esc(kick), esc(h2), sub, body))


PAGES = []
TOC = []


def add(title, kick, h2, sub, body):
    pg = len(PAGES) + 1
    PAGES.append(slide(title, kick, h2, sub, body))
    return pg


# ── P1 封面 ──
PAGES.append("""
<section class="slide cover" data-t="封面">
 <canvas id="cvCover"></canvas>
 <div class="cwrap">
  <div class="eyebrow">BIORI BIOTECH ｜ 营销中心 · 诊断原料销售部</div>
  <h1>2026 <em>三季度</em><br>销售工作汇报</h1>
  <div class="cline"></div>
  <div class="meta">
   汇报人：刘新元　｜　汇报日期：2026.10.16<br>
   覆盖　① 三季度目标/月度目标完成情况（含回款）　② 三季度工作总结（计划点检 · 问题与改善）<br>
   　　　③ 四季度目标及行动措施　④ 竞争状态与市场机会分析　⑤ 组织能力建设
  </div>
 </div>
</section>""")

# ── P2 目录 ──
TOC_ITEMS = [
    ('01', '三季度目标 / 月度目标完成情况', '1.1 销售目标达成 · 1.2 回款情况分析（重点逾期）', 3),
    ('02', '三季度工作总结', '2.1 工作计划点检 · 2.2 存在的问题分析与改善措施', 8),
    ('03', '四季度目标及行动措施', 'Q4 缺口测算 · 三部门措施 · 攻坚客户表', 10),
    ('04', '竞争状态与市场机会分析', 'SWOT · 竞争与价格情报 · 促销与价格策略', 13),
    ('05', '组织能力建设', '团队现状 · 培养体系与梯队', 17),
    ('06', '专项分析（10 月 · 附）', '6.1 应收账款账龄与逾期结构 · 6.2 逾期/营收主力客户 · 6.3 客户 2 年同比（集团口径）', 21),
]
PAGES.append("""
<section class="slide" data-t="目录">
 <div class="slide-head"><div class="kicker">CONTENTS</div><h2>汇报内容</h2>
 <div class="sub">口径：部门月度数据 / 9月统计表 / 回款表（销售＋财务口径）｜年度目标 <span class="mono">6,950 万</span>（大客户 2,650 · 拓展 3,400 · 生命科学 900）</div></div>
 <div class="slide-body"><div class="scroll"><div class="trio">%s</div>
 <div class="infobox" style="margin-top:14px">本 deck 只陈述<b>可溯源事实与数字</b>；金额单位未标注处均为<b>万元</b>。原表未填项一律留空，不做估算。</div>
 </div></div>
</section>""" % ''.join(
    '<div class="toccard" onclick="go(%d)"><div class="no">%s</div><div class="nm">%s</div><div class="ds">%s</div></div>'
    % (pg - 1, no, esc(nm), esc(ds)) for no, nm, ds, pg in TOC_ITEMS))

# ── P3 1.1 目标达成表 ──
rows = ''
mo = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月']
for d in ('销售拓展部', '大客户部', '生命科学部'):
    t = TARGETS[d]
    rows += ('<tr><td><b>%s</b></td><td class="num">%s</td><td class="num">%s</td><td class="num">%s</td>%s</tr>'
             % (d, fmt(t['t'] / 10000, 1), fmt(t['a'] / 10000, 1),
                fmt((t['rate'] or 0) * 100, 1) + '%',
                ''.join('<td class="num">%s</td>' % fmt((t['ma'][i] or 0) / 10000, 1) for i in range(9))))
body = ('<div class="kpis" style="grid-template-columns:repeat(4,1fr)">%s</div>'
        '<div class="tw" style="margin-top:12px"><table class="data tight"><thead><tr><th>部门</th><th>年度目标</th><th>1–9月实绩</th><th>年度达成率</th>%s</tr></thead><tbody>%s</tbody></table></div>'
        '<div class="infobox" style="margin-top:10px">全年目标 6,950 万；1–9 月已完成 <b>%s 万</b>（<b>%s%%</b>），Q4 需完成 <b>%s 万</b>。'
        '口径：各部门《2026月度数据 — 目标达成数据看板（09月）》；生命科学部 900 万中含北京区域 300 万（区域未搭建，已落空）。</div>'
        % ('<div class="kpis" style="display:contents">' +
           kpi('年度目标合计', fmt(TOT_T / 10000, 0), '万元', '大客户 2,650 ｜ 拓展 3,400 ｜ 生命科学 900') +
           kpi('1–9月实绩合计', fmt(TOT_A / 10000, 1), '万元', '三大部门合计') +
           kpi('年度达成率', fmt(TOT_A / TOT_T * 100, 1), '%', '按全年目标口径') +
           kpi('Q4 待完成', fmt(Q4_GAP, 0), '万元', '缺口＝全年目标 − 1–9月实绩', 'warn') + '</div>',
           ''.join('<th>%s</th>' % m for m in mo), rows, fmt(TOT_A / 10000, 1), fmt(TOT_A / TOT_T * 100, 1), fmt(Q4_GAP, 0)))
p3 = add('目标达成', 'CH.01 · 1.1 销售目标达成', '三季度销售目标达成情况', '按表样对照上年度，以三季度周期看同比变化', body)

# ── P4 目标达成图表 ──
yoy_txt = '；'.join('%s 1–9月 %s 万 vs 2025 同期 %s 万，<b>%+.1f%%</b>' % (y['k'], fmt(y['y26'] / 10000, 1), fmt(y['y25'] / 10000, 1), y['pct']) for y in DATA['yoy'])
body = ('<div class="duo"><div class="card"><h4>月度目标 vs 月度实绩（三部门合计，万元）</h4><div id="cMonth"></div>'
        '<div class="tiny" style="margin-top:6px">1–9 月合计实绩 %s 万；3 月、6 月、9 月为季度末冲量高峰。</div></div>'
        '<div class="card"><h4>三部门年度达成率（%%）</h4><div id="cDeptRate"></div>'
        '<div class="tiny" style="margin-top:6px">销售拓展部 64.2%% 最高；生命科学部 4.1%%，主因区域未搭建与人员流动。</div></div></div>'
        '<div class="duo" style="margin-top:12px"><div class="card"><h4>实绩结构（万元）</h4><div id="cShare"></div></div>'
        '<div class="card"><h4>1–9 月同比（%%）</h4><div id="cYoy"></div><div class="tiny" style="margin-top:6px">%s。</div></div></div>'
        % (fmt(TOT_A / 10000, 1), yoy_txt))
p4 = add('达成图表', 'CH.01 · 1.1 销售目标达成', '目标达成的月度节奏与同比', '同比口径：部门《近三年销售数据》表，2026 年数据截止 9 月 30 日', body)

# ── P5 1.2 回款总览 ──
p5 = add('回款总览', 'CH.01 · 1.2 回款情况分析', '逾期应收与回款进度', '口径：各部门《逾期应收回款计划 — 总表（回款进度）》；金额单位万元',
         '<div class="kpis" style="grid-template-columns:repeat(4,1fr)">%s</div>'
         '<div class="duo" style="margin-top:12px"><div class="card"><h4>逾期金额账龄结构（万元）</h4><div id="cAge"></div></div>'
         '<div class="card"><h4>风险分级（万元）</h4><div id="cRiskDK"></div><div class="tiny" style="margin-top:8px">上：大客户部　下：销售拓展部</div>'
         '<div id="cRiskEX" style="margin-top:8px"></div></div></div>'
         '<div class="warnbox" style="margin-top:10px">10 月最新（截止 9/30）：大客户部剩余逾期 <b>%s 万</b>（刘子研 %s · 赵云浩 %s），10 月计划 <b>%s 万</b>、已回 0 → <b>执行率 0%%</b>，Q4 首要止血项。生命科学部回款表未在报送范围内。</div>'
         % (kpi('大客户部 9月逾期', fmt(col_dk9.get('逾期合计', 0) / 10000, 1), '万元', '%d 条明细 · 已回 %s 万' % (col_dk9.get('明细条数', 0), fmt(col_dk9.get('已回合计', 0) / 10000, 1))) +
            kpi('销售拓展部 9月逾期', fmt(col_ext9.get('逾期合计', 0) / 10000, 1), '万元', '%d 条明细 · 已回 %s 万' % (col_ext9.get('明细条数', 0), fmt(col_ext9.get('已回合计', 0) / 10000, 1)), 'cool') +
            kpi('9 月已回合计', fmt((col_dk9.get('已回合计', 0) + col_ext9.get('已回合计', 0)) / 10000, 1), '万元', '两部门合计', 'good') +
            kpi('10 月剩余逾期', fmt(col_dk10.get('逾期合计', 0) / 10000, 1), '万元', '截止 2026-09-30', 'bad'),
            fmt(col_dk10.get('逾期合计', 0) / 10000, 1),
            fmt((col_dk10.get('prog', [{}])[0] or {}).get('总逾期应收', 0) / 10000, 1),
            fmt((col_dk10.get('prog', [{}, {}])[1] if len(col_dk10.get('prog', [])) > 1 else {}).get('总逾期应收', 0) / 10000, 1),
            fmt(sum((r.get('计划') or 0) for r in col_dk10.get('prog', [])) / 10000, 1)))

# ── P6 重点逾期客户与行动 ──
def prog_rows(p, dept):
    h = ''
    for r in p:
        if not r.get('总逾期应收'):
            continue
        h += ('<tr><td>%s</td><td class="num">%s</td><td class="num">%s</td><td class="num">%s</td><td class="num">%s</td><td class="num">%s</td></tr>'
              % (esc(r['人']), fmt((r['总逾期应收'] or 0) / 10000, 1), fmt((r['60天以上'] or 0) / 10000, 1),
                 fmt((r['计划'] or 0) / 10000, 1), fmt((r['累计回款'] or 0) / 10000, 1),
                 ('%.1f%%' % ((r['累计进度'] or 0) * 100))))
    return h


p6 = add('重点逾期', 'CH.01 · 1.2 回款情况分析', '重点逾期客户与分类处置', '大客户部与大客户型逾期以流程性为主；拓展部高风险集中在少数客户',
         '<div class="trio"><div class="card"><h4>大客户部 · 逐人逾期与回款</h4>'
         '<table class="data tight"><thead><tr><th>销售</th><th>总逾期</th><th>60天+</th><th>计划</th><th>已回</th><th>进度</th></tr></thead><tbody>%s</tbody></table></div>'
         '<div class="card"><h4>销售拓展部 · 逐人逾期与回款</h4>'
         '<table class="data tight"><thead><tr><th>销售</th><th>总逾期</th><th>60天+</th><th>计划</th><th>已回</th><th>进度</th></tr></thead><tbody>%s</tbody></table></div>'
         '<div class="card"><h4>逾期金额 Top 客户（万元）</h4><div id="cCustDK"></div>'
         '<div class="tiny" style="margin-top:6px">上：大客户部　下：销售拓展部</div><div id="cCustEX" style="margin-top:8px"></div></div></div>'
         '<div class="infobox" style="margin-top:12px">处置口径：① 低风险·流程性逾期 → 销售推动 + 财务对账，纳入月度计划执行率考核；'
         '② 中高风险·长账龄 → 逐单定预计回款时间与责任人，按周跟踪；③ 高风险无响应 → 升级至部门负责人与财务联合催收。</div>'
         % (prog_rows(col_dk9.get('prog', []), '大客户部'), prog_rows(col_ext9.get('prog', []), '销售拓展部')))

# ── P7 2.1 计划点检（过程指标）──
t_dk, t_ex = TESTS.get('大客户部'), TESTS.get('销售拓展部')
p7 = add('计划点检', 'CH.02 · 2.1 三季度工作计划点检', '过程指标完成情况', '口径：各部门《销售数据统计表（9月）》；不含关联公司之间的数据',
         '<div class="trio">'
         '<div class="card"><h4>客户拜访量（1–9月合计，人次）</h4><div id="cVisit"></div>'
         '<div class="tiny" style="margin-top:6px">拓展部含 NGS 类拜访 %s 人次。生命科学部人均拜访量显著高于其他部门（人少、单客户价低、需高频触达）。</div></div>'
         '<div class="card"><h4>测试单发放与反馈率</h4>'
         '<table class="data tight"><thead><tr><th>部门</th><th>1–9月发放</th><th>平均反馈率</th></tr></thead><tbody>'
         '<tr><td>大客户部</td><td class="num">%s</td><td class="num">%s</td></tr>'
         '<tr><td>销售拓展部</td><td class="num">%s</td><td class="num">%s</td></tr></tbody></table>'
         '<div class="tiny" style="margin-top:6px">目标档位 T1/12/70%%，反馈率直接决定测试转订单效率。</div></div>'
         '<div class="card"><h4>生命科学部 · 开拓类指标</h4>'
         '<table class="data tight"><tbody>'
         '<tr><th>新客户成交（单）</th><td class="num">%s</td></tr>'
         '<tr><th>飞书新增联系人（人）</th><td class="num">%s</td></tr></tbody></table>'
         '<div class="tiny" style="margin-top:6px">开拓型业务以过程指标评价，不与大客户部／拓展部横比单次拜访产出。</div></div></div>'
         '<div class="card" style="margin-top:12px"><h4>销售拓展部 · 月度客户拜访量（人次）</h4><div id="cVisitM"></div></div>'
         % (fmt(387, 0),
            fmt((t_dk or {}).get('sends', 0), 0), ('%.1f%%' % (t_dk or {}).get('rate_avg', 0)) if t_dk else '—',
            fmt((t_ex or {}).get('sends', 0), 0), ('%.1f%%' % (t_ex or {}).get('rate_avg', 0)) if t_ex else '—',
            fmt((LS_NEWCUST or [0])[0], 0), fmt((LS_CONTACT or [0])[0], 0)))

# ── P8 商机进度与重点事项 ──
opp_rows = ''
for t in OPP['top'][:8]:
    opp_rows += ('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td class="num">%s</td></tr>'
                 % (esc(str(t['客户'])[:22]), esc(str(t['商机'])[:30]), esc(str(t['销售'] or '')[:10]), esc(str(t['性质'])[:8]),
                    fmt((t['额_元'] or 0) / 10000, 1)))
hard = ''.join('<tr><td>%s</td><td>%s</td><td style="font-size:11.5px">%s</td></tr>' % (esc(h['客']), esc(h['销']), esc(h['难'])) for h in OPP_HARD)
p8 = add('商机进度', 'CH.02 · 2.1 三季度工作计划点检', '客户商机进度及重点事项', '口径：21 份三季度汇报内嵌《商机明细》表（原表直读，未换算）',
         '<div class="duo"><div class="card"><h4>商机总览（共 %s 条）</h4><div id="cOppNature"></div>'
         '<div class="tiny" style="margin-top:8px">性质分布＝各销售自报口径（存量／增量／丢失／新增），原表未填项单列。</div></div>'
         '<div class="card"><h4>金额 Top 商机（2026 年销售额，万元）</h4><div id="cOppTop"></div></div></div>'
         '<div class="card" style="margin-top:10px"><h4>四季度冲刺难点（销售原文摘录）</h4>'
         '<table class="data tight"><thead><tr><th>客户</th><th>销售</th><th>难点与措施（原文）</th></tr></thead><tbody>%s</tbody></table></div>'
         % (fmt(OPP['条数'], 0), hard))

# ── P9 2.2 问题与改善（空表）──
blank = ''.join('<tr><td class="num">%d</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>' % i for i in range(1, 7))
p9 = add('问题与改善', 'CH.02 · 2.2 存在的问题分析与改善措施', '三季度存在的问题与改善措施', '按原表样列出，内容待各部门填报',
         '<div class="tw"><table class="data"><thead><tr><th style="width:44px">序号</th><th>未完成的工作 / 存在的问题</th>'
         '<th>原因分析</th><th>改善措施</th><th style="width:120px">责任人</th><th style="width:110px">完成时间</th></tr></thead>'
         '<tbody>%s</tbody></table></div>'
         '<div class="infobox" style="margin-top:12px">本页沿用原表表样，<b>内容留空待填</b>：各部门《销售数据统计表（9月）》中「销售数据分析结论」工作表为空白模板（仅有序号 1–10），'
         '未提供已填内容，故不代为填写。填报口径建议：① 每条须落到<b>唯一责任人</b>与<b>可验证的完成时间</b>；② 原因分析须区分人员／产品／流程／客户四类；'
         '③ 改善措施须可量化（如「10 月回款计划执行率 ≥70%%」）。</div>' % blank)

# ── P10 03 四季度目标及行动措施 ──
q4_rows = ''
for d in ('大客户部', '销售拓展部', '生命科学部'):
    q = Q4[d]
    q4_rows += ('<tr><td><b>%s</b></td><td class="num">%s</td><td class="num">%s</td><td class="num">%s</td><td class="num">%s</td>'
                '<td class="num">%s</td></tr>'
                % (d, fmt(q['目标'], 1), fmt(q['1-9月'], 1), fmt(q['Q4缺口'], 2), fmt(q['月均需完成'], 2),
                   ('%.1f%%' % (q['1-9月'] / q['目标'] * 100))))
p10 = add('四季度目标', 'CH.03 四季度目标及行动措施', 'Q4 缺口测算与目标分解', '缺口＝年度目标 − 1–9 月实绩；生命科学部按 900 万口径单列（含北京区域 300 万）',
          '<div class="duo"><div class="card"><h4>Q4 缺口（万元）</h4><div id="cQ4"></div>'
          '<div class="tiny" style="margin-top:6px">三部门 Q4 合计缺口 <b>%s 万</b>，相当于 Q3 单季实绩（%s 万）的 <b>%.1f 倍</b>。</div></div>'
          '<div class="card"><h4>分段测算</h4><table class="data tight"><thead><tr><th>部门</th><th>年度目标</th><th>1–9月</th><th>Q4 缺口</th><th>月均需完成</th><th>达成率</th></tr></thead>'
          '<tbody>%s</tbody></table></div></div>' % (fmt(Q4_GAP, 0), fmt(TOT_A / 10000, 1), Q4_GAP / (TOT_A / 10000) * 3 if TOT_A else 0, q4_rows))

# ── P11 Q4 行动措施 ──
ACT = [
    ('大客户部', '圣湘关系修复 + 技术对接（脓毒症中试转订单）；万孚等成长商机稳定上量；新商机持续攻坚（tth 迭代、郑州纳克智造深度开发）；10 月重点推进逾期回款（计划执行率 0%% → ≥70%%）', '赵云浩 / 刘新元'),
    ('销售拓展部', '延续 Q3 增长节奏（Q3 已 850.3 万）；KA 客户深耕与份额提升；新客开发与测试转订单；回款高风险客户分类处置', '部门全体 · 区域负责人'),
    ('生命科学部', '科研专用产品包 + 学术营销；重点突破浙大／中山大学标杆客户；开发大客户外围口碑票仓性质小客户群；按过程指标（拜访／测试／新客户／联系人）考核', '吴云 / 韩远怀 / 黄建华'),
]
act_rows = ''.join('<tr><td><b>%s</b></td><td style="font-size:12px">%s</td><td>%s</td></tr>' % (esc(a), esc(b), esc(c)) for a, b, c in ACT)
p11 = add('四季度措施', 'CH.03 四季度目标及行动措施', '四季度行动措施与责任分工', '按部门分列；措施与 Q3 暴露的问题一一对应',
          '<div class="tw"><table class="data"><thead><tr><th style="width:104px">部门</th><th>行动措施</th><th style="width:150px">责任人</th></tr></thead><tbody>%s</tbody></table></div>'
          '<div class="card" style="margin-top:12px"><h4>部门负责人（刘新元）Q4 推进事项</h4>'
          '<table class="data tight"><tbody>'
          '<tr><th style="width:150px">① 回款止血</th><td>10 月回款计划执行率 ≥70%%；高风险客户逐单定责任人与时限，周会点检</td></tr>'
          '<tr><th>② 缺口闭环</th><td>Q4 %s 万缺口按人按月拆解，月度目标与绩效挂钩</td></tr>'
          '<tr><th>③ 攻坚客户</th><td>之江、迈克、卡尤迪、京东方知微、硕世等按攻坚表推进（见下页）</td></tr>'
          '<tr><th>④ 过程点检</th><td>拜访／测试单反馈率／新客户成交／联系人四项月度点检，落到销售员</td></tr>'
          '<tr><th>⑤ 口径统一</th><td>全年目标口径 6,950 万（生命科学 900 万）与系统月度目标同步</td></tr>'
          '<tr><th>⑥ 交接风险</th><td>离职交接月绩效与指派任务单挂钩、双方签字确认</td></tr>'
          '</tbody></table></div>' % (act_rows, fmt(Q4_GAP, 0)))

# ── P12 攻坚客户表 ──
KA = [
    ('之江生物', '刘新元', '2026年8月', '研发 / 采购总监', 'RT 单管及冻干方案', '高层开发 → 引荐后当地同事推进中层、基层关系'),
    ('迈克生物', '刘新元', '2026年8月', '中层 / 基层 / 研发', '待考察', '已有高层关系，当地销售推动中层基层关系升级'),
    ('卡尤迪', '刘新元', 'Q4', '研发 / 采购', '按项目需求定方案', '高层资源对接 + 现场测试转订单'),
    ('京东方知微', '刘新元', 'Q4', '研发 / 采购', '按项目需求定方案', '高层资源对接 + 现场测试转订单'),
    ('硕世生物', '杨心意', 'Q4', '研发 / 采购', '按项目需求定方案', '高层资源对接 + 现场测试转订单'),
]
ka_rows = ''.join('<tr><td><b>%s</b></td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td style="font-size:11.5px">%s</td></tr>' % tuple(esc(x) for x in k) for k in KA)
p12 = add('攻坚客户', 'CH.03 四季度目标及行动措施', 'Q4 攻坚客户与推进路径', '攻坚带队人／目标拜访时间／目标开发人物／主推方案／路径',
          '<div class="tw"><table class="data tight"><thead><tr><th style="width:104px">客户名称</th><th style="width:78px">攻坚带队人</th><th style="width:88px">目标拜访时间</th>'
          '<th style="width:126px">目标开发人物</th><th style="width:150px">主推方案</th><th>路径</th></tr></thead><tbody>%s</tbody></table></div>'
          '<div class="infobox" style="margin-top:12px">攻坚客户来源：三季度汇报《下半年目标及行动措施 — 攻坚客户表》原表；Q4 不做全面铺开，集中资源打重点客户。</div>' % ka_rows)

# ── P13 SWOT ──
SW = [
    ('S 优势', ['酶性能稳定，批次间一致性好', '酶与冻干服务仍在同行前列', '定制化服务灵活、响应快', '大客户部 + 拓展部 KA 客户基础牢固（拓展部 127 家客户为底盘）', '性价比竞争力']),
    ('W 劣势', ['品牌知名度低于诺唯赞、菲鹏等', '科研渠道薄弱 —— 生命科学部 9 个月累计离职 9 人', '新人产出低，人才梯队仍在成长', '华东（上海）／华西／福建／北京区域竞争力不足', '对中高端科研客户的方案能力待补']),
    ('O 机会', ['NGS 市场快速增长（三代、单细胞、二代细分场景）', '国产替代加速 —— 近年迭代老证产品逐渐拿证上量', '诊断试剂集采挤出国外原料水分，给国产高端原料机会（肿瘤、慢病）', '科研客户资产可累积（联系人／课题组）']),
    ('T 威胁', ['华中地区诺唯赞、菲鹏竞争性低价', '瀚海超低价策略蚕食部分低端市场', '行业价格战加剧', '性能迭代内卷（单管／全预混普适性、冻干预混、熔曲峰高与杂峰）']),
]
sw = ''.join('<div class="card"><h4>%s</h4><ul style="margin:0;padding-left:18px;font-size:12px;line-height:1.7">%s</ul></div>'
             % (esc(t), ''.join('<li>%s</li>' % esc(x) for x in xs)) for t, xs in SW)
p13 = add('SWOT', 'CH.04 竞争状态与市场机会分析', 'SWOT 分析（Q3 数据刷新）', '竞争分析的目的是服务 Q4 打法，不是独立的成绩展示',
          '<div class="duo" style="grid-template-columns:1fr 1fr">%s</div>' % sw)

# ── P14 竞争与价格情报 ──
br_rows = ''.join('<tr><td>%s</td><td class="num">%d</td><td class="num">%.1f%%</td></tr>' % (esc(b['k']), b['v'], b['v'] / max(OPP['条数'], 1) * 100) for b in BRAND_CNT[:6])
p14 = add('竞争与价格', 'CH.04 竞争状态与市场机会分析', '竞争格局与价格情报', '口径：413 条商机原文提及统计 + 客户课题组竞对信息原表',
          '<div class="duo"><div class="card"><h4>商机原文中的竞品提及（条）</h4><div id="cBrand"></div>'
          '<div class="tiny" style="margin-top:6px">样本＝21 份三季度汇报的 413 条商机文本；提及次数反映一线遭遇的竞争强度，不代表市场份额。</div></div>'
          '<div class="card"><h4>客户在用竞品（课题组原表摘录）</h4>'
          '<table class="data tight"><thead><tr><th>客户／课题组</th><th>在用竞品原文</th></tr></thead><tbody>'
          '<tr><td>浙江区课题组</td><td style="font-size:11.5px">RTQ：诺唯赞 R313／Q101；高保真酶：诺唯赞 p50</td></tr>'
          '<tr><td>浙江区课题组</td><td style="font-size:11.5px">逆转录诺唯赞 323；定量翌圣；高保真酶 p525</td></tr>'
          '</tbody></table>'
          '<div class="tiny" style="margin-top:6px">竞品成交价情报由一线持续补充；本页不填未经验证的价格数字。</div></div></div>'
          '<div class="duo" style="margin-top:10px"><div class="card"><h4>价格与竞争态势</h4><ul style="margin:0;padding-left:18px;font-size:12px;line-height:1.75">'
          '<li>诺唯赞为最主要竞品（提及 %d 次），定位一线对标品牌，性能认可度高、价格偏高。</li>'
          '<li>金诺美、翌圣、菲鹏在中低端价格带形成压力；瀚海以超低价蚕食低端市场。</li>'
          '<li>应对原则：<b>不追低价</b>；性能达标优先（酶活稳定性、批间差、冻干适配），保留首单折扣空间。</li>'
          '</ul></div><div class="card"><h4>竞品提及 Top6（条）</h4>'
          '<table class="data tight"><thead><tr><th>竞品</th><th>提及条数</th><th>占商机比</th></tr></thead><tbody>%s</tbody></table>'
          '<div class="tiny" style="margin-top:6px">样本 413 条商机；提及频次反映一线竞争强度，非市场份额。</div></div></div>'
          % (BRAND_CNT[0]['v'] if BRAND_CNT else 0, br_rows))

# ── P15 促销与价格策略 ──
PROMO = [
    ('公众号推文', '植物直扩、限制性内切酶、mRNA 解决方案等高影响力选题', '市场部 · 持续'),
    ('开学季促销', '大原则：不追低价，可保留首单大折扣；季度性档位与礼品配套', '市场部 · 8–9月'),
    ('货号进菜单', '整合仪器、小仪器货号进入菜单，完善主产品清单', '产品部 · 7月'),
    ('workshop 资源', '整合资源以满额赠送方式投放，绑定课题组与现场活动', '销售部 · 持续'),
    ('知识库与案例库', 'IMA 知识库信息增强（正／负反馈关联，做准确的产品与价格梯度推荐）；Agent 辅助搭建案例库', '销售支持 · 8月'),
]
pro = ''.join('<tr><td><b>%s</b></td><td>%s</td><td>%s</td></tr>' % tuple(esc(x) for x in p) for p in PROMO)
p15 = add('促销策略', 'CH.04 竞争状态与市场机会分析', 'Q4 促销与价格策略', '沿用三季度既定策略并做点检优化',
          '<div class="tw"><table class="data"><thead><tr><th style="width:130px">策略项</th><th>内容</th><th style="width:150px">责任／时间</th></tr></thead><tbody>%s</tbody></table></div>'
          '<div class="infobox" style="margin-top:12px">价格纪律：① 不追低价竞争，以性能与稳定性换溢价；② 首单大折扣仅用于新客户导入；'
          '③ 改价与账期授权按审批流程执行，不越权承诺。</div>' % pro)

# ── P16 组织能力建设 ──
org_rows = ''
for d in ('销售拓展部', '大客户部', '生命科学部'):
    o = ORG.get(d, {})
    top = '、'.join('%s %s 万' % (p['name'], fmt(p['act_wan'], 1)) for p in o.get('top', [])[:3])
    org_rows += '<tr><td><b>%s</b></td><td class="num">%d</td><td class="num">%d</td><td style="font-size:11.5px">%s</td></tr>' % (d, o.get('在职', 0), o.get('离职', 0), esc(top))
p16 = add('组织能力', 'CH.05 组织能力建设', '团队现状与培养体系', '口径：各部门《2026月度数据 — 个人年度目标达成》人员名单（含离职标注）',
          '<div class="duo"><div class="card"><h4>人员现状（人）</h4><div id="cOrg"></div>'
          '<div class="tiny" style="margin-top:6px">生命科学部 9 个月内累计离职 9 人，留存率不足一半，是业绩薄弱的主要成因。</div></div>'
          '<div class="card"><h4>人员结构与骨干产出</h4><table class="data tight"><thead><tr><th>部门</th><th>在职</th><th>离职</th><th>骨干产出（1–9月）</th></tr></thead><tbody>%s</tbody></table></div></div>'
          '<div class="card" style="margin-top:12px"><h4>培养体系与梯队（Q4 起执行）</h4>'
          '<table class="data tight"><tbody>'
          '<tr><th style="width:150px">方法与能力</th><td>每月持续销售六步法 + 解决方案能力培训</td></tr>'
          '<tr><th>综合销售能力</th><td>财务知识、合同与订单法律常识、客户采购行为与动机分析（波特五力）、关键沟通（破冰／追回款／协调内部资源）、研发项目管理与节点进度</td></tr>'
          '<tr><th>开拓型业务评价</th><td>生命科学部按过程指标（拜访／测试单／新客户／联系人）独立评价，不与成熟部门横比</td></tr>'
          '<tr><th>交接风险控制</th><td>交接月绩效与指派任务单挂钩，当事人与承接人双方签字</td></tr>'
          '</tbody></table></div>' % org_rows)

# ══════════════════ CH.06 专项分析（10 月 · 附）══════════════════
import io as _io

_W = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_AR1 = json.load(_io.open(os.path.join(_W, '数据/2026年10月应收账龄_大客户部.json'), encoding='utf-8'))
_AR2 = json.load(_io.open(os.path.join(_W, '数据/2026年10月应收账龄_销售拓展部.json'), encoding='utf-8'))
_LED = json.load(_io.open(os.path.join(_W, '数据/2026年10月应收客户账龄_主力客户.json'), encoding='utf-8'))
_YY = json.load(_io.open(os.path.join(_W, '数据/客户2年同比_集团口径.json'), encoding='utf-8'))
_BK = ['账期内', '逾期1-30天', '逾期31-60天', '逾期61-365天', '逾期365天以上（一年以上）']
_CB = {'账期内': '#3b82f6', '逾期1-30天': '#22c55e', '逾期31-60天': '#f59e0b',
       '逾期61-365天': '#ef4444', '逾期365天以上（一年以上）': '#7f1d1d'}
_wan = lambda v: v / 10000.0
_m = lambda v, d=1: format(v, ',.%df' % d)
_ALL = _AR1['detail'] + _AR2['detail']
_TOT = round(_AR1['total'] + _AR2['total'], 2)
_OVD = round(_AR1['overdue_total'] + _AR2['overdue_total'], 2)
_NDY = round(_TOT - _OVD, 2)
_AGG = {}
for _k in _BK:
    _AGG[_k] = {'n': _AR1['overall'][_k]['单数'] + _AR2['overall'][_k]['单数'],
                'v': round(_AR1['overall'][_k]['金额'] + _AR2['overall'][_k]['金额'], 2)}
_O60 = round(sum(_AGG[_k]['v'] for _k in _BK[3:]), 2)
_O60N = sum(_AGG[_k]['n'] for _k in _BK[3:])
_Y1 = _AGG[_BK[4]]
_OD = [r for r in _ALL if r['overdue']]
_MX = max(_OD, key=lambda r: r['od_days'] or 0)

# ── 章封面 CH.06 ──
PAGES.append("""
<section class="slide chapter" data-t="CH.06 专项分析">
 <canvas class="chCv"></canvas>
 <div class="cwrap"><div class="big">CH.06</div><h2>专项分析（10 月 · 附）</h2>
 <div class="ds">① 10 月应收账款账龄与逾期结构（两大部门）　② 逾期与营收主力客户（分部门 · 分销售员）　③ 客户 2 年同比（集团口径 · 大客户部）<br>
 <span style="opacity:.72;font-size:12.5px">数据源：2026 年 10 月逾期应收回款计划-总表（大客户 / 销售拓展）· ERP 销售订单（2026 主档 · 2025 全年订单）</span></div></div>
</section>""")

# ── P18 6.1 账龄与逾期结构 ──
_segs, _x = [], 0.0
for _k in _BK:
    _w = _AGG[_k]['v'] / _TOT * 1000
    _segs.append('<rect x="%.1f" y="16" width="%.1f" height="30" fill="%s"></rect>' % (_x, _w, _CB[_k]))
    if _w > 62:
        _segs.append('<text x="%.1f" y="36" font-size="12" fill="#fff" text-anchor="middle" font-weight="700">%.1f%%</text>'
                     % (_x + _w / 2, _AGG[_k]['v'] / _TOT * 100))
        _segs.append('<text x="%.1f" y="60" font-size="10.5" fill="#5f6368" text-anchor="middle">%s万</text>'
                     % (_x + _w / 2, _m(_wan(_AGG[_k]['v']), 0)))
    _x += _w
_cbar = ''.join('<div style="width:%.2f%%;background:%s;display:flex;align-items:center;justify-content:center;'
                'color:#fff;font-size:11px;font-weight:700" title="%s">%s</div>'
                % (_AGG[_k]['v'] / _TOT * 100, _CB[_k], esc(_k),
                   ('%.0f%%' % (_AGG[_k]['v'] / _TOT * 100)) if _AGG[_k]['v'] / _TOT * 100 >= 8 else '')
                for _k in _BK)
_leg = ''.join('<span style="display:inline-flex;align-items:center;gap:5px;font-size:11px;margin-right:12px">'
               '<span style="width:10px;height:10px;border-radius:2px;background:%s;display:inline-block"></span>%s %s万</span>'
               % (_CB[_k], esc(_k), _m(_wan(_AGG[_k]['v']), 1)) for _k in _BK)
_ar_rows = ''
for _lab, _d in (('大客户部', _AR1), ('销售拓展部', _AR2)):
    _tt = _d['total']
    _ar_rows += '<tr><td><b>%s</b></td>%s<td class="num">%s</td><td class="num">%s</td><td class="num">%s</td></tr>' % (
        _lab, ''.join('<td class="num">%s</td>' % _m(_wan(_d['overall'][_k]['金额']), 1) for _k in _BK),
        _m(_wan(_tt), 1), _m(_wan(_d['over60_subtotal']['金额']), 1), _m(_wan(_d['overdue_total']), 1))
_ar_rows += '<tr class="tot"><td><b>两部门合计</b></td>%s<td class="num"><b>%s</b></td><td class="num"><b>%s</b></td><td class="num"><b>%s</b></td></tr>' % (
    ''.join('<td class="num"><b>%s</b></td>' % _m(_wan(_AGG[_k]['v']), 1) for _k in _BK),
    _m(_wan(_TOT), 1), _m(_wan(_O60), 1), _m(_wan(_OVD), 1))
p18 = add('账龄结构', 'CH.06 专项分析', '10 月应收账款账龄与逾期结构',
          '截止 2026-09-30 ｜ 范围：诊断原料大客户销售部 ＋ 诊断原料销售拓展部（各一份 10 月报送文件）',
          '<div class="kpis" style="grid-template-columns:repeat(4,1fr)">%s%s%s%s</div>'
          '<div style="display:flex;height:20px;border-radius:4px;overflow:hidden;margin-top:8px">%s</div>'
          '<div class="tiny" style="margin-top:4px">%s</div>'
          '<table class="data tight" style="margin-top:8px;font-size:11.5px"><thead><tr><th>部门</th>%s<th class="num">应收合计</th><th class="num">逾期60天以上</th><th class="num">逾期合计</th></tr></thead><tbody>%s</tbody></table>'
          '<div class="tiny" style="margin-top:5px">逾期判据＝逾期金额 &gt; 0 ｜ 「逾期60天以上」＝61-365 天＋一年以上 ｜ 13 人逐人勾稽差 0.00 ｜ 最长 %d 天（%s）</div>'
          % (kpi('应收总额', _m(_wan(_TOT), 2), '万元', '%d 单' % len(_ALL)),
             kpi('逾期合计', _m(_wan(_OVD), 2), '万元', '占应收 %.1f%%' % (_OVD / _TOT * 100), 'warn'),
             kpi('账期内', _m(_wan(_NDY), 2), '万元', '%.1f%%' % (_NDY / _TOT * 100)),
             kpi('逾期60天以上', _m(_wan(_O60), 2), '万元', '%d 单 ｜ 占应收 %.1f%%' % (_O60N, _O60 / _TOT * 100), 'warn'),
             _cbar, '账期内 %s 万 ｜ 逾期 %s 万（%.1f%%）｜ 逾期60天以上 %s 万（%d 单）｜ 逾期一年以上 %s 万（%d 单）' % (_m(_wan(_NDY), 1), _m(_wan(_OVD), 1), _OVD / _TOT * 100, _m(_wan(_O60), 1), _O60N, _m(_wan(_Y1['v']), 2), _Y1['n']),
             ''.join('<th class="num">%s</th>' % esc(_k) for _k in _BK), _ar_rows,
             _MX['od_days'], esc(_MX['cust'])))

# ── P19 6.2 逾期与营收主力客户 ──
_CU = _LED['customers']
_od_top = sorted(_CU, key=lambda o: -o['overdue'])[:5]
_rev_top = sorted(_CU, key=lambda o: -o['rev2026'])[:7]
_bysp = sorted(_LED['by_sp'].items(), key=lambda kv: -kv[1]['overdue'])[:3]
_odr = ''.join('<tr><td>%s</td><td>%s</td><td>%s</td><td class="num">%s</td><td class="num">%d</td><td class="num">%s</td></tr>'
               % (esc(o['cust'][:20]), esc(o['dept']), esc(o['sp']), _m(_wan(o['overdue']), 1), o['max_days'],
                  ('%.1f%%' % o['exposure']) if o['exposure'] is not None else '零营收') for o in _od_top)
_revr = ''.join('<tr><td>%s</td><td>%s</td><td class="num">%s</td><td class="num">%s</td><td class="num">%s</td><td class="num">%s</td></tr>'
                % (esc(o['cust'][:20]), esc(o['sp']), _m(_wan(o['rev2026']), 1), _m(_wan(o['ar']), 1),
                   _m(_wan(o['overdue']), 1) if o['overdue'] else '—',
                   ('%.1f%%' % o['exposure']) if o['exposure'] is not None else '—') for o in _rev_top)
_zero = [o for o in _CU if not o['matched'] and o['overdue'] > 0]
_hi = sorted([o for o in _CU if o['exposure'] is not None and o['exposure'] >= 30], key=lambda o: -o['exposure'])[:2]
_risk = ''.join('<li>%s（%s）逾期 %s 万 ｜ 最长 %d 天 ｜ <b>本年营收 0</b></li>'
                % (esc(o['cust'][:22]), esc(o['sp']), _m(_wan(o['overdue']), 1), o['max_days']) for o in _zero)
_risk += ''.join('<li>%s（%s）逾期 %s 万 ｜ <b>逾期/营收 %.1f%%</b></li>'
                 % (esc(o['cust'][:22]), esc(o['sp']), _m(_wan(o['overdue']), 1), o['exposure']) for o in _hi)
_sp_line = ' ｜ '.join('%s %s' % (esc(sp), ('%.1f%%' % v['exposure']) if v['exposure'] is not None else '—')
                      for sp, v in sorted(_LED['by_sp'].items(), key=lambda kv: (kv[1]['exposure'] is None, -(kv[1]['exposure'] or 0)))[:6])
p19 = add('主力客户', 'CH.06 专项分析', '逾期与营收主力客户（按部门 / 按销售员）',
          '应收账龄来自 10 月报送文件；营收来自 ERP 2026 年 1-10 月销售订单（剔除赠品/测试），按客户名前配对',
          '<div class="duo">'
          '<div class="card"><h4>逾期主力客户 Top6（两部门合计）</h4><table class="data tight"><thead><tr><th>客户</th><th>部门</th><th>销售员</th><th class="num">逾期(万)</th><th class="num">最长天</th><th class="num">逾期/营收</th></tr></thead><tbody>%s</tbody></table>'
          '<div class="tiny" style="margin-top:4px">按销售员 Top3：%s</div></div>'
          '<div class="card"><h4>营收主力客户 Top8（ERP 2026，剔除赠品）</h4><table class="data tight"><thead><tr><th>客户</th><th>销售员</th><th class="num">2026营收(万)</th><th class="num">应收(万)</th><th class="num">逾期(万)</th><th class="num">逾期/营收</th></tr></thead><tbody>%s</tbody></table>'
          '<div class="tiny" style="margin-top:4px">丽珠试剂、保密客户营收为零逾期；生科原逾期占比 40.0%%。</div></div></div>'
          '<div class="warnbox" style="margin-top:10px"><b>风险客户</b><ul style="margin:4px 0 0;padding-left:18px;font-size:12px;line-height:1.7">%s</ul></div>'
          % (_odr, _sp_line, _revr, _risk))

# ── P20 6.3 客户 2 年同比（集团口径 · 大客户部）──
_yy = [r for r in _YY['rows'] if r['部门'] == '大客户部']
_t25 = sum(r['2025_1-9月'] for r in _yy)
_t26 = sum(r['2026_1-9月'] for r in _yy)
_yoy = (_t26 - _t25) / _t25 * 100 if _t25 else 0
_up = sorted([r for r in _yy if r['同比'] is not None and r['同比'] >= 30 and r['2025_1-9月'] > 0], key=lambda r: -r['增减额'])
_dn = sorted([r for r in _yy if r['同比'] is not None and r['同比'] <= -20 and r['2026_1-9月'] > 0], key=lambda r: r['增减额'])
_nw = sorted([r for r in _yy if r['2025_1-9月'] < 1 and r['2026_1-9月'] > 0], key=lambda r: -r['2026_1-9月'])
_ls = sorted([r for r in _yy if r['2026_1-9月'] < 1 and r['2025_1-9月'] > 0], key=lambda r: -r['2025_1-9月'])
_yy_rows = ''.join('<tr><td><b>%s</b></td><td class="num">%s</td><td class="num">%s</td><td class="num" style="color:%s">%s</td><td class="num">%+s</td></tr>'
                   % (esc(r['集团'][:18]), _m(_wan(r['2025_1-9月']), 1), _m(_wan(r['2026_1-9月']), 1),
                      '#b91c1c' if (r['同比'] or 0) < 0 else '#15803d',
                      ('%+.1f%%' % r['同比']) if r['同比'] is not None else '新增',
                      _m(_wan(r['增减额']), 1)) for r in _yy[:6])
_li_up = ''.join('<li>%s +%s万（%+.1f%%）</li>' % (esc(r['集团'][:16]), _m(_wan(r['增减额']), 1), r['同比']) for r in _up[:4])
_li_dn = ''.join('<li>%s %s万（%+.1f%%）</li>' % (esc(r['集团'][:16]), _m(_wan(r['增减额']), 1), r['同比']) for r in _dn[:4])
_li_nw = ''.join('<li>%s +%s万</li>' % (esc(r['集团'][:16]), _m(_wan(r['2026_1-9月']), 1)) for r in _nw[:3])
_li_nw += ''.join('<li>流失：%s −%s万</li>' % (esc(r['集团'][:16]), _m(_wan(r['2025_1-9月']), 1)) for r in _ls[:2])
p20 = add('两年同比', 'CH.06 专项分析', '客户 2 年同比（集团口径 · 大客户部）',
          '2026 年 1-9 月 vs 2025 年 1-9 月（同期）｜ 归集：有集团按集团、无集团按客户本体（集团取 CRM 权威映射）',
          '<div class="kpis" style="grid-template-columns:repeat(4,1fr)">%s%s%s%s</div>'
          '<table class="data tight" style="margin-top:8px;font-size:11.5px"><thead><tr><th>集团 / 客户</th><th class="num">2025 1-9月</th><th class="num">2026 1-9月</th><th class="num">同比</th><th class="num">增减额</th></tr></thead><tbody>%s</tbody></table>'
          '<div class="tiny" style="margin-top:6px">两超换位：丽珠 237.4→239.7（稳，第一）｜ 圣湘 293.3→150.5（−142.7，最大拖累）｜ 口径：标准销售订单＋已审核＋非赠品＋剔除宝锐系 ｜ 2025 自算与源文件透视表逐月差 0.00</div>'
          % (kpi('大客户部 2026 1-9月', _m(_wan(_t26), 1), '万元', '%d 个集团/客户' % len(_yy)),
             kpi('2025 同期', _m(_wan(_t25), 1), '万元', '同比 %+.1f%%' % _yoy, 'warn' if _yoy < 0 else ''),
             kpi('增长集团', str(len(_up)), '个', '+%s 万' % _m(_wan(sum(r['增减额'] for r in _up)), 1)),
             kpi('下滑集团', str(len(_dn)), '个', '%s 万' % _m(_wan(sum(r['增减额'] for r in _dn)), 1), 'warn'),
             _yy_rows))
p21 = add('同比结构', 'CH.06 专项分析', '同比结构变化（增长 / 下滑 / 新增 / 流失）',
          '大客户部 ｜ 2026 1-9 月 vs 2025 同期 ｜ 集团口径',
          '<div class="trio">'
          '<div class="card"><h4>增长（≥+30%%）· %d 个 · +%s 万</h4><ul style="margin:0;padding-left:16px;font-size:12px;line-height:1.7">%s</ul></div>'
          '<div class="card"><h4>下滑（≤−20%%）· %d 个 · %s 万</h4><ul style="margin:0;padding-left:16px;font-size:12px;line-height:1.7">%s</ul></div>'
          '<div class="card"><h4>新增 · %d 个 · +%s 万</h4><ul style="margin:0;padding-left:16px;font-size:12px;line-height:1.7">%s</ul></div></div>'
          '<div class="card" style="margin-top:10px;border:1.5px dashed #8fa3bf;background:#FBFCFE"><h4>分析（留白 · 待填写）</h4>'
          '<div style="min-height:120px;font-size:12px;color:#9aa3b2;line-height:1.9">▍本期同比结论与归因（由部门负责人填写）<br>▍下一步动作 / 责任人 / 时间节点</div></div>'
          % (len(_up), _m(_wan(sum(r['增减额'] for r in _up)), 1),
             ''.join('<li>%s +%s万（%+.1f%%）</li>' % (esc(r['集团'][:14]), _m(_wan(r['增减额']), 1), r['同比']) for r in _up[:5]),
             len(_dn), _m(_wan(sum(r['增减额'] for r in _dn)), 1),
             ''.join('<li>%s %s万（%+.1f%%）</li>' % (esc(r['集团'][:14]), _m(_wan(r['增减额']), 1), r['同比']) for r in _dn[:5]),
             len(_nw), _m(_wan(sum(r['2026_1-9月'] for r in _nw)), 1),
             ''.join('<li>%s +%s万</li>' % (esc(r['集团'][:14]), _m(_wan(r['2026_1-9月']), 1)) for r in _nw[:4])
             + ''.join('<li>流失：%s −%s万</li>' % (esc(r['集团'][:14]), _m(_wan(r['2025_1-9月']), 1)) for r in _ls[:2])))

# ── P17 封底 ──
PAGES.append("""
<section class="slide chapter" data-t="结束页">
 <canvas class="chCv"></canvas>
 <div class="cwrap"><div class="big">THANKS</div><h2>追求卓越品质 · 提供一流服务</h2>
 <div class="ds">宝锐生物 ｜ 诊断原料销售部 ｜ 汇报人：刘新元<br>
 电话 0756-8699969　｜　www.biori.com　｜　marketing@biori.com<br>
 <span style="opacity:.72;font-size:12.5px">数据源：各部门 2026 月度数据（09月）· 9月销售数据统计表 · 逾期应收回款计划总表 · 21 份三季度汇报内嵌商机明细　｜　生成于 %s</span></div></div>
</section>""" % NOW)

# TOC 页码按实际位置校正（目录在 P2）
TOC_ITEMS = [(no, nm, ds, pg) for no, nm, ds, pg in TOC_ITEMS]
TOC_ITEMS[0] = ('01', TOC_ITEMS[0][1], TOC_ITEMS[0][2], 3)
TOC_ITEMS[1] = ('02', TOC_ITEMS[1][1], TOC_ITEMS[1][2], 7)
TOC_ITEMS[2] = ('03', TOC_ITEMS[2][1], TOC_ITEMS[2][2], 10)
TOC_ITEMS[3] = ('04', TOC_ITEMS[3][1], TOC_ITEMS[3][2], 13)
TOC_ITEMS[4] = ('05', TOC_ITEMS[4][1], TOC_ITEMS[4][2], 16)
TOC_ITEMS[5] = ('06', TOC_ITEMS[5][1], TOC_ITEMS[5][2], 17)

DATA['oppNature'] = (lambda d: (lambda big, rest: dict(list(big.items()) + ([('其他', sum(rest.values()))] if rest else [])))(
    {k: v for k, v in d.items() if v >= 9}, {k: v for k, v in d.items() if v < 9}))(OPP['按性质'])
DATA['oppTop'] = [{'k': str(t['客户'])[:18], 'v': t['额_元'] or 0} for t in OPP['top'][:8]]
DATA['org'] = {k: {'在职': v['在职'], '离职': v['离职']} for k, v in ORG.items()}

SIDE = ''.join('<a href="javascript:void(0)" onclick="go(%d)"><i>%d</i><span>%s</span></a>'
               % (i, i + 1, esc(PAGES[i].split('data-t="')[1].split('"')[0])) for i in range(len(PAGES)))

HTML_T = """<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>宝锐生物 · 2026 三季度销售工作汇报（诊断原料销售部）</title>
<style>%s</style>
</head>
<body>
<div class="shell">
<aside class="side"><h4>目录</h4>%s</aside>
<div class="wrap">
 <div class="bar">
  <span class="title" id="ttl">封面</span>
  <button class="btn" onclick="go(CUR-1)">‹ 上一页</button>
  <button class="btn" onclick="go(CUR+1)">下一页 ›</button>
  <button class="btn" onclick="togglePres()">▣ 演示</button>
  <button class="btn" id="btnEdit" onclick="toggleEdit()">✎ 编辑</button>
  <button class="btn" id="btnRv" onclick="toggleReview()">☑ 审稿</button>
  <button class="btn" onclick="toggleSide()">≡ 目录</button>
  <span class="tiny"><b id="pgNow">1</b> / %d</span>
 </div>
 <div class="prog"><i id="progBar"></i></div>
 <div class="stage"><div id="deck">%s</div></div>
</div>
</div>
<div class="rvPanel">
 <h4>☑ 逐页确认</h4>
 <div class="rvStat" id="rvS"></div>
 <div style="display:flex;gap:6px;margin-bottom:8px">
  <button class="btn" onclick="rvMark(1)">✓ 已确认</button>
  <button class="btn" onclick="rvMark(2)">✎ 需修改</button>
  <button class="btn" onclick="rvClear()">↺ 清除</button>
  <button class="btn" onclick="rvExport()">↓ 导出</button>
 </div>
 <textarea class="rvNote" id="rvNote" placeholder="本页批注…"></textarea>
 <div id="rvList" style="margin-top:10px"></div>
</div>
<script>
var D0=%s;
%s
</script>
</body>
</html>"""

body = ''.join(PAGES)
html = HTML_T % (CSS, SIDE, len(PAGES), body, json.dumps(DATA, ensure_ascii=False), JS)

# 品牌注入（必须在最后）
html = BK.inject(html, theme='dark', tag='营销中心 · 诊断原料销售部',
                 footer_extra='宝锐生物 2026 三季度销售工作汇报 ｜ 数据源：部门月度数据(09月) · 9月统计表 · 回款计划总表 · 三季度汇报商机明细')

assert html.count('</body>') == 1, 'body 标签数异常'
assert html.count('<script') == html.count('</script>'), 'script 配平异常'

os.makedirs(os.path.dirname(OUT), exist_ok=True)
io.open(OUT, 'w', encoding='utf-8').write(html)
print('✅ 已生成 %s' % OUT)
print('   页数 %d ｜ %.0f KB' % (len(PAGES), os.path.getsize(OUT) / 1024))
v = BK.verify(html)
bad = [k for k, ok in v.items() if not ok]
print('   品牌自检 %s %s' % ('✅' if not bad else '❌', '' if not bad else bad))
print('   页眉 %d ｜ 页脚 %d ｜ 页脚 slogan %d' % (html.count('class="bior-header"'), html.count('class="bior-footer"'), html.count('守护生命健康')))
