#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
生命科学销售部 · 销售额明细（2026-08-22 ~ 2026-09-28）· 自包含 HTML
=====================================================================
数据源（唯一）：
  ① 订单  ~/Desktop/Hermes输出-工作类/销售分析/销售订单_20260101-20260928.xlsx（ERP 导出）
  ② 组织架构 ~/Desktop/hermas输入-工作台数据库/销售组织架构/统一工作台运行及授权.xlsx 「人员清单」

口径（可勾稽，不做任何估算）：
  · 销售额 = Σ「价税合计」，条件：单据类型 ≠ 预测订单 且 价税合计 > 0
    - 赠品行「是否赠品=是」全为 0 元，自动排除（实测 17,989 行金额恒为 0）
    - 预测订单（单据编号 FO 开头）全期 61 行 / 33.14 万，按既定铁律不计销售
    - 「测试销售订单」14,517 行全为 0 元赠品，不计
  · 部门归属取「人员清单」的『部门』列；区域取『区域』列，浙江科研*/广州科研* 归并为两大区
  · 期间 = 合同日期 ∈ [2026-08-22, 2026-09-28]（ERP 数据截至 9/28）

输出：销售分析/生命科学销售部_销售额明细_20260822-20260928.html
"""
import io
import json
import os
import sys
import warnings

import pandas as pd

warnings.filterwarnings('ignore')

BASE = os.path.expanduser('~/Desktop/Hermes输出-工作类')
sys.path.insert(0, os.path.join(BASE, '脚本'))
import brand_kit as BK  # noqa: E402

ORDER_XLSX = os.path.join(BASE, '销售分析/销售订单_20260101-20260928.xlsx')
ORG_XLSX = os.path.expanduser('~/Desktop/hermas输入-工作台数据库/销售组织架构/统一工作台运行及授权.xlsx')
OUT = os.path.join(BASE, '销售分析/生命科学销售部_销售额明细_20260822-20260928.html')

A = pd.Timestamp('2026-08-22')
B = pd.Timestamp('2026-09-28')
DEPT = '生命科学销售部'


def norm_region(r):
    r = str(r)
    if '浙江' in r:
        return '浙江大区'
    if '广州' in r:
        return '广州大区'
    return r


# ───────────────────── 1. 读数据 ─────────────────────
df = pd.read_excel(ORDER_XLSX, sheet_name='Sheet1')
df['合同日期'] = pd.to_datetime(df['合同日期'], errors='coerce')
df['价税合计'] = pd.to_numeric(df['价税合计'], errors='coerce').fillna(0)
df['销售数量'] = pd.to_numeric(df['销售数量'], errors='coerce').fillna(0)

org = pd.read_excel(ORG_XLSX, sheet_name='人员清单')
dept = org[org['部门'] == DEPT][['销售员', '区域', '角色', '是否在职']].copy()

# 销售额口径
sales = df[(df['单据类型'] != '预测订单') & (df['价税合计'] > 0)].copy()

# 全公司同窗口（做占比分母）
comp = sales[(sales['合同日期'] >= A) & (sales['合同日期'] <= B)]
COMP_TOTAL = comp['价税合计'].sum()

# 本部门
win = comp[comp['销售员'].isin(dept['销售员'])].merge(dept, on='销售员', how='left')
win['大区'] = win['区域'].map(norm_region)
win['期段'] = win['合同日期'].map(lambda d: 'A' if d < pd.Timestamp('2026-09-01') else 'B')

TOTAL = win['价税合计'].sum()
NDAYS = (B - A).days + 1


def wan(v):
    return round(float(v) / 1e4, 2)


# ───────────────────── 2. 汇总 ─────────────────────
people = []
for _, r in dept.iterrows():
    sub = win[win['销售员'] == r['销售员']]
    people.append({
        '销售员': r['销售员'], '大区': norm_region(r['区域']), '区域': r['区域'],
        '角色': r['角色'], '在职': r['是否在职'],
        'a': wan(sub[sub['期段'] == 'A']['价税合计'].sum()),
        'b': wan(sub[sub['期段'] == 'B']['价税合计'].sum()),
        'amt': wan(sub['价税合计'].sum()),
        'cust': int(sub['客户'].nunique()), 'ord': int(sub['单据编号'].nunique()),
        'lines': int(len(sub)),
    })
for p in people:
    p['pct'] = round(p['amt'] / (TOTAL / 1e4) * 100, 1) if TOTAL else 0.0
    p['avg'] = int(round(p['amt'] * 1e4 / p['ord'])) if p['ord'] else 0
people.sort(key=lambda x: -x['amt'])

active = [p for p in people if p['amt'] > 0]
regions = []
for rg, g in win.groupby('大区'):
    amt = g['价税合计'].sum()
    regions.append({
        '大区': rg, '人数': int(g['销售员'].nunique()), 'ord': int(g['单据编号'].nunique()),
        'cust': int(g['客户'].nunique()), 'amt': wan(amt),
        'pct': round(amt / TOTAL * 100, 1) if TOTAL else 0.0,
        'avg': int(round(amt / g['单据编号'].nunique())) if g['单据编号'].nunique() else 0,
    })
regions.sort(key=lambda x: -x['amt'])

cust_rows = []
for c, g in win.groupby('客户'):
    top = g.groupby('销售员')['价税合计'].sum().idxmax()
    cust_rows.append({'客户': c, '大区': norm_region(g['大区'].iloc[0]), 'amt': wan(g['价税合计'].sum()),
                      'ord': int(g['单据编号'].nunique()), 'by': top})
cust_rows.sort(key=lambda x: -x['amt'])

sku_rows = []
for k, g in win.groupby('货号#'):
    nm = g['物料名称'].dropna().iloc[0] if len(g['物料名称'].dropna()) else ''
    sku_rows.append({'货号': k, '名称': str(nm)[:46], 'qty': round(float(g['销售数量'].sum()), 1),
                     'amt': wan(g['价税合计'].sum()), 'cust': int(g['客户'].nunique())})
sku_rows.sort(key=lambda x: -x['amt'])

daily = []
for d, g in win.groupby(win['合同日期'].dt.date):
    daily.append({'d': str(d), 'ord': int(g['单据编号'].nunique()), 'amt': wan(g['价税合计'].sum())})
daily.sort(key=lambda x: x['d'])
DMAX = max([x['amt'] for x in daily] or [1])

# 供「索引」用的原始记录
raw = [{
    '日期': str(r['合同日期'].date()), '单据编号': r['单据编号'], '客户': r['客户'],
    '销售员': r['销售员'], '大区': r['大区'], '区域': r['区域'],
    '货号': r['货号#'], '物料名称': r['物料名称'], '数量': float(r['销售数量'] or 0),
    '价税合计': float(r['价税合计']),
} for _, r in win.sort_values('合同日期', ascending=False).iterrows()]


# ───────────────────── 3. HTML ─────────────────────
def kpi(v, l, s=''):
    return ('<div class="kpi"><div class="v">%s</div><div class="l">%s</div>'
            '<div class="s">%s</div></div>' % (v, l, s))


REG_OPTS = ''.join('<option>%s</option>' % r['大区'] for r in regions)

HTML = r"""<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>生命科学销售部 · 销售额明细（2026-08-22 ~ 09-28）</title>
<style>
:root{--blue:#0043C1;--deep:#002D80;--tint:#F1F5FE;--line:#E4E9F2;--bg:#F5F7FA;
 --ink:#0F172A;--ink2:#475569;--grey:#94A3B8;--ok:#0d904f;--warn:#B45309;--bad:#C0271D;
 --f:'Microsoft YaHei','PingFang SC','Helvetica Neue',Arial,sans-serif;}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);font-family:var(--f);color:var(--ink);font-size:13px}
.page{max-width:1320px;margin:0 auto;padding:22px 20px 96px}
h1{font-size:22px;margin:6px 0 4px}
h2{font-size:15px;margin:26px 0 10px;padding-left:10px;border-left:4px solid var(--blue)}
.sub{color:var(--ink2);font-size:12.5px;line-height:1.7}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin:16px 0 4px}
.kpi{background:#fff;border:1px solid var(--line);border-left:4px solid var(--blue);
 border-radius:8px;padding:12px 14px}
.kpi .v{font-size:22px;font-weight:700;color:var(--deep);letter-spacing:-.5px}
.kpi .l{font-size:12px;color:var(--ink2);margin-top:3px}
.kpi .s{font-size:11px;color:var(--grey);margin-top:2px}
.tools{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:10px 0 6px}
.tools input,.tools select{padding:7px 10px;border:1px solid var(--line);border-radius:6px;
 font-size:12.5px;font-family:var(--f);background:#fff;outline:none}
.tools input{min-width:230px}
.tools input:focus,.tools select:focus{border-color:var(--blue)}
.tools button{padding:7px 12px;border:1px solid var(--line);background:#fff;border-radius:6px;
 font-size:12.5px;cursor:pointer;font-family:var(--f)}
.tools button:hover{border-color:var(--blue);color:var(--blue)}
.card{background:#fff;border:1px solid var(--line);border-radius:8px;overflow:hidden;margin-top:8px}
table{width:100%;border-collapse:collapse;font-size:12.5px}
th{background:var(--tint);color:var(--deep);font-weight:700;text-align:left;padding:9px 10px;
 white-space:nowrap;cursor:pointer;user-select:none;border-bottom:1px solid var(--line)}
th:hover{background:#E4ECFD}
th .ar{color:var(--grey);font-size:10px;margin-left:3px}
td{padding:8px 10px;border-bottom:1px solid #F0F3F8;vertical-align:top}
tr:last-child td{border-bottom:none}
tbody tr:hover{background:#FAFBFE}
.num{text-align:right;font-variant-numeric:tabular-nums}
.b{font-weight:700;color:var(--deep)}
.zj{color:var(--blue);font-weight:600}
.gz{color:var(--ok);font-weight:600}
.off{color:var(--grey)}
.tag{display:inline-block;padding:1px 7px;border-radius:10px;font-size:11px;background:var(--tint);
 color:var(--blue);border:1px solid #D6E0F7}
.tag.g{background:#E8F6EE;color:var(--ok);border-color:#C6E7D4}
.tag.r{background:#FDEDEC;color:var(--bad);border-color:#F5CFCB}
.bars{margin-top:6px}
.bar{display:flex;align-items:center;gap:8px;margin:3px 0}
.bar .d{width:78px;color:var(--ink2);font-size:11.5px}
.bar .t{flex:1;height:13px;background:#EDF1F8;border-radius:3px;overflow:hidden}
.bar .t i{display:block;height:100%;background:linear-gradient(90deg,#2F6BD8,#0043C1)}
.bar .n{width:74px;text-align:right;font-variant-numeric:tabular-nums;font-size:11.5px}
.note{background:#fff;border:1px solid var(--line);border-left:4px solid var(--warn);
 border-radius:8px;padding:13px 16px;margin-top:10px;font-size:12.5px;line-height:1.85;color:var(--ink2)}
.note b{color:var(--ink)}
.note code{background:var(--tint);padding:1px 5px;border-radius:3px;color:var(--deep);font-size:11.5px}
.empty{padding:22px;text-align:center;color:var(--grey)}
.modal{position:fixed;inset:0;background:rgba(15,23,42,.55);display:none;z-index:99;
 align-items:center;justify-content:center;padding:24px}
.modal.on{display:flex}
.modal .box{background:#fff;border-radius:10px;max-width:1000px;width:100%;max-height:82vh;
 display:flex;flex-direction:column;overflow:hidden}
.modal .hd{padding:13px 18px;border-bottom:1px solid var(--line);font-weight:700;color:var(--deep);
 display:flex;justify-content:space-between;align-items:center}
.modal .hd button{border:none;background:none;font-size:20px;cursor:pointer;color:var(--grey)}
.modal .bd{overflow:auto;padding:14px 18px}
.modal pre{font-size:11.5px;line-height:1.6;background:#F8FAFD;padding:12px;border-radius:6px;
 overflow:auto;margin:0;color:#22405f}
</style></head><body>
<div class="page">
  <h1>生命科学销售部 · 销售额明细</h1>
  <div class="sub">
    统计区间：<b>2026-08-22 ~ 2026-09-28</b>（__NDAYS__ 天，ERP 数据截至 2026-09-28）&nbsp;·&nbsp;
    部门口径：组织架构「人员清单」<code>部门 = 生命科学销售部</code>（__NPEOPLE__ 人，含 __NOFF__ 人离职）&nbsp;·&nbsp;
    销售额口径：Σ 价税合计，<b>剔除赠品与预测订单</b>
  </div>

  <div class="kpis">
    __KPIS__
  </div>

  <h2>一、按人（含区域）</h2>
  <div class="tools">
    <input id="q1" placeholder="搜索销售员 / 大区 / 角色…" oninput="flt('t1','q1','c1')">
    <select id="c1" onchange="flt('t1','q1','c1')"><option value="">全部大区</option>__REGOPTS__</select>
    <button onclick="rst('t1','q1','c1')">⟲ 重置</button>
    <button onclick="csv('t1','生命科学部_按人')">📥 导出CSV</button>
    <span id="n1" class="sub"></span>
  </div>
  <div class="card"><table id="t1">
    <thead><tr>
      <th data-k="销售员">销售员<span class="ar">↕</span></th>
      <th data-k="大区">大区<span class="ar">↕</span></th>
      <th data-k="区域">区域<span class="ar">↕</span></th>
      <th data-k="角色">角色<span class="ar">↕</span></th>
      <th data-k="在职">状态<span class="ar">↕</span></th>
      <th data-k="a" class="num">8/22–8/31（万）<span class="ar">↕</span></th>
      <th data-k="b" class="num">9/1–9/28（万）<span class="ar">↕</span></th>
      <th data-k="amt" class="num">合计（万）<span class="ar">↕</span></th>
      <th data-k="pct" class="num">占比%<span class="ar">↕</span></th>
      <th data-k="cust" class="num">客户数<span class="ar">↕</span></th>
      <th data-k="ord" class="num">订单数<span class="ar">↕</span></th>
      <th data-k="avg" class="num">单均（元）<span class="ar">↕</span></th>
    </tr></thead><tbody></tbody></table></div>

  <h2>二、按区域</h2>
  <div class="card"><table id="t2">
    <thead><tr>
      <th data-k="大区">大区<span class="ar">↕</span></th>
      <th data-k="人数" class="num">出单人数<span class="ar">↕</span></th>
      <th data-k="cust" class="num">客户数<span class="ar">↕</span></th>
      <th data-k="ord" class="num">订单数<span class="ar">↕</span></th>
      <th data-k="amt" class="num">销售额（万）<span class="ar">↕</span></th>
      <th data-k="pct" class="num">占比%<span class="ar">↕</span></th>
      <th data-k="avg" class="num">单均（元）<span class="ar">↕</span></th>
    </tr></thead><tbody></tbody></table></div>

  <h2>三、按日走势</h2>
  <div class="card" style="padding:14px 16px"><div class="bars">__BARS__</div></div>

  <h2>四、客户明细</h2>
  <div class="tools">
    <input id="q4" placeholder="搜索客户…" oninput="flt('t4','q4')">
    <button onclick="rst('t4','q4')">⟲ 重置</button>
    <button onclick="csv('t4','生命科学部_客户')">📥 导出CSV</button>
    <span id="n4" class="sub"></span>
  </div>
  <div class="card"><table id="t4">
    <thead><tr>
      <th data-k="客户">客户<span class="ar">↕</span></th>
      <th data-k="大区">大区<span class="ar">↕</span></th>
      <th data-k="amt" class="num">销售额（万）<span class="ar">↕</span></th>
      <th data-k="ord" class="num">订单数<span class="ar">↕</span></th>
      <th data-k="by">主跟销售<span class="ar">↕</span></th>
    </tr></thead><tbody></tbody></table></div>

  <h2>五、主销货号</h2>
  <div class="tools">
    <input id="q5" placeholder="搜索货号 / 品名…" oninput="flt('t5','q5')">
    <button onclick="rst('t5','q5')">⟲ 重置</button>
    <button onclick="csv('t5','生命科学部_货号')">📥 导出CSV</button>
    <span id="n5" class="sub"></span>
  </div>
  <div class="card"><table id="t5">
    <thead><tr>
      <th data-k="货号">货号<span class="ar">↕</span></th>
      <th data-k="名称">品名<span class="ar">↕</span></th>
      <th data-k="qty" class="num">数量<span class="ar">↕</span></th>
      <th data-k="amt" class="num">销售额（万）<span class="ar">↕</span></th>
      <th data-k="cust" class="num">客户数<span class="ar">↕</span></th>
    </tr></thead><tbody></tbody></table></div>

  <h2>六、口径说明与可勾稽项</h2>
  <div class="note">
    <b>① 销售额定义</b>：<code>单据类型 ≠ 预测订单</code> 且 <code>价税合计 &gt; 0</code> 的行求和。<br>
    &nbsp;&nbsp;· 赠品行（<code>是否赠品 = 是</code>）全期 __GIFTROWS__ 行，金额<b>恒为 0 元</b>，故不影响合计；<br>
    &nbsp;&nbsp;· 预测订单（单据编号 <code>FO</code> 开头）全期 __FOROWS__ 行 / __FOAMT__ 万，<b>按既定铁律不计销售</b>；<br>
    &nbsp;&nbsp;· 「测试销售订单」全期 __TESTROWS__ 行，<b>全部为 0 元赠品</b>，不计。<br>
    <b>② 勾稽</b>：本表生命科学部合计 __TOTAL__ 万 = 9月段 __SEP__ 万 + 8月段 __AUG__ 万；
    其中 9 月段与既有月报口径「生命科学 6.9 万」一致 ✔<br>
    <b>③ 部门归属</b>：取组织架构「人员清单」的『部门』列，<b>不按产品线推断</b>。名单 __NPEOPLE__ 人中，
    __NOFF__ 人已离职（本区间无销售额），故实际出单 __NACT__ 人。<br>
    <b>④ 未计入</b>：刘新元（销售总监，可下单产品线含「生命科学+诊断原料」）同期 __LXAMT__ 万——
    其部门归属为「诊断原料销售部」，<b>不计入本部门</b>；如需并入请告知。<br>
    <b>⑤ 数据源</b>：<code>销售分析/销售订单_20260101-20260928.xlsx</code>（ERP 导出）+
    <code>销售组织架构/统一工作台运行及授权.xlsx</code>。全公司同区间合计 __COMPTOTAL__ 万。
  </div>

  <h2>七、底层数据（可索引）</h2>
  <div class="tools">
    <button onclick="openRaw()">📋 查看原始记录（__NRAW__ 行）</button>
    <button onclick="csv('t1','生命科学部_按人')">📥 导出按人CSV</button>
  </div>
  <div class="note" style="border-left-color:var(--blue)">
    原始记录 = 本区间生命科学部全部有效销售行（日期 / 单号 / 客户 / 销售员 / 大区 / 货号 / 数量 / 价税合计），
    共 __NRAW__ 行，按日期倒序。
  </div>
</div>

<div class="modal" id="rawModal" onclick="if(event.target===this)closeRaw()">
  <div class="box">
    <div class="hd"><span>📋 底层原始记录（__NRAW__ 行）</span><button onclick="closeRaw()">×</button></div>
    <div class="bd"><div class="tools">
      <input id="qr" placeholder="过滤…" oninput="filRaw()">
      <button onclick="csvRaw()">📥 导出CSV</button>
      <span id="nr" class="sub"></span>
    </div><pre id="rawPre"></pre></div>
  </div>
</div>

<script>
var D = {
  people: __J_PEOPLE__,
  regions: __J_REGIONS__,
  cust: __J_CUST__,
  sku: __J_SKU__,
  raw: __J_RAW__
};
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){
  return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});}
function fmt(v,dec){ if(v===''||v==null) return ''; return (dec?Number(v).toFixed(dec):v); }

var TBL = {
  t1:{cols:[{k:'销售员'},{k:'大区',cls:function(v){return v.indexOf('浙江')>=0?'zj':'gz';}},
            {k:'区域'},{k:'角色'},{k:'在职',fmt:function(v){return v==='在职'
              ?'<span class="tag g">在职</span>':'<span class="tag r">'+esc(v)+'</span>';}},
            {k:'a',n:1},{k:'b',n:1},{k:'amt',n:1,b:1},{k:'pct',n:1},{k:'cust',n:1},{k:'ord',n:1},{k:'avg',n:1}],
       src:function(){return D.people;}},
  t2:{cols:[{k:'大区',cls:function(v){return v.indexOf('浙江')>=0?'zj':'gz';}},{k:'人数',n:1},
            {k:'cust',n:1},{k:'ord',n:1},{k:'amt',n:1,b:1},{k:'pct',n:1},{k:'avg',n:1}],
       src:function(){return D.regions;}},
  t4:{cols:[{k:'客户',b:1},{k:'大区',cls:function(v){return v.indexOf('浙江')>=0?'zj':'gz';}},
            {k:'amt',n:1,b:1},{k:'ord',n:1},{k:'by'}], src:function(){return D.cust;}},
  t5:{cols:[{k:'货号',b:1},{k:'名称'},{k:'qty',n:1},{k:'amt',n:1,b:1},{k:'cust',n:1}],
       src:function(){return D.sku;}}
};
function cell(c,v,row){
  var t = c.fmt ? c.fmt(v,row) : esc(v);
  var cl = c.n ? 'num' : '';
  if(c.b) cl += ' b';
  if(c.cls) cl += ' ' + c.cls(v,row);
  return '<td class="'+cl.trim()+'">'+(t===''||t==null?'-':t)+'</td>';
}
function draw(id, data){
  var t = TBL[id], tb = document.querySelector('#'+id+' tbody');
  tb.innerHTML = data.length ? data.map(function(r){
    return '<tr>' + t.cols.map(function(c){ return cell(c, r[c.k], r); }).join('') + '</tr>';
  }).join('') : '<tr><td class="empty" colspan="'+t.cols.length+'">无匹配数据</td></tr>';
}
function cur(id){
  var t = TBL[id], rows = t.src().slice();
  var v = (document.getElementById(id==='t1'?'q1':id==='t4'?'q4':'q5')||{}).value || '';
  v = v.trim().toLowerCase();
  if(v) rows = rows.filter(function(r){
    return t.cols.some(function(c){ return String(r[c.k]||'').toLowerCase().indexOf(v)>=0; }); });
  var sel = document.getElementById(id==='t1'?'c1':null);
  if(sel && sel.value) rows = rows.filter(function(r){ return r['大区']===sel.value; });
  if(t.sortK){
    var dir = t.sortD, k = t.sortK;
    rows.sort(function(a,b){
      var x=a[k], y=b[k];
      if(typeof x==='number'||typeof y==='number') return (x-y)*dir;
      return String(x).localeCompare(String(y),'zh')*dir; });
  }
  return rows;
}
function render(id){
  var rows = cur(id); draw(id, rows);
  var el = document.getElementById(id==='t1'?'n1':id==='t4'?'n4':id==='t5'?'n5':null);
  if(el) el.textContent = '显示 ' + rows.length + ' / ' + TBL[id].src().length + ' 行';
}
function flt(id,q,sel){ render(id); }
function rst(id,q,sel){ var a=document.getElementById(q); if(a)a.value='';
  var b=document.getElementById(sel); if(b)b.value=''; render(id); }
document.addEventListener('click', function(e){
  var th = e.target.closest('th[data-k]'); if(!th) return;
  var id = th.closest('table').id, k = th.getAttribute('data-k'), t = TBL[id];
  if(t.sortK===k) t.sortD = -t.sortD; else { t.sortK = k; t.sortD = -1; }
  th.closest('thead').querySelectorAll('th').forEach(function(x){
    var s = x.querySelector('.ar'); if(s) s.textContent='↕'; });
  var s2 = th.querySelector('.ar'); if(s2) s2.textContent = t.sortD<0 ? '↓' : '↑';
  render(id);
});
function csv(id, name){
  var t = TBL[id], rows = cur(id);
  var head = t.cols.map(function(c){return c.k;});
  var lines = [head.join(',')].concat(rows.map(function(r){
    return head.map(function(h){ var v=r[h]==null?'':String(r[h]);
      return /[",\n]/.test(v) ? '"'+v.replace(/"/g,'""')+'"' : v; }).join(','); }));
  var blob = new Blob(['\ufeff'+lines.join('\r\n')], {type:'text/csv;charset=utf-8'});
  var a = document.createElement('a'); a.href = URL.createObjectURL(blob);
  a.download = name + '.csv'; a.click();
}
function openRaw(){ document.getElementById('rawModal').classList.add('on'); filRaw(); }
function closeRaw(){ document.getElementById('rawModal').classList.remove('on'); }
function filRaw(){
  var v = (document.getElementById('qr').value||'').trim().toLowerCase();
  var rows = D.raw.filter(function(r){
    return !v || Object.keys(r).some(function(k){ return String(r[k]).toLowerCase().indexOf(v)>=0; }); });
  document.getElementById('nr').textContent = '显示 ' + rows.length + ' / ' + D.raw.length + ' 行';
  document.getElementById('rawPre').textContent = JSON.stringify(rows, null, 1);
  window.__rawShown = rows;
}
function csvRaw(){
  var rows = window.__rawShown || D.raw, head = Object.keys(D.raw[0]||{});
  var lines = [head.join(',')].concat(rows.map(function(r){
    return head.map(function(h){ var v=r[h]==null?'':String(r[h]);
      return /[",\n]/.test(v) ? '"'+v.replace(/"/g,'""')+'"' : v; }).join(','); }));
  var blob = new Blob(['\ufeff'+lines.join('\r\n')], {type:'text/csv;charset=utf-8'});
  var a = document.createElement('a'); a.href = URL.createObjectURL(blob);
  a.download = '生命科学部_原始记录.csv'; a.click();
}
['t1','t2','t4','t5'].forEach(render);
</script>
</body></html>"""

# 填充占位符
kpis = (
    kpi('%.2f<span style="font-size:13px"> 万</span>' % wan(TOTAL), '区间销售额', '占全公司 %.1f%%' % (TOTAL / COMP_TOTAL * 100)) +
    kpi('%d<span style="font-size:13px"> 人</span>' % len(active), '出单人数', '编制 %d 人 / 离职 %d 人' %
        (len(dept), sum(1 for p in people if p['在职'] != '在职'))) +
    kpi('%d' % int(win['客户'].nunique()), '成交客户数', '共 %d 个订单' % int(win['单据编号'].nunique())) +
    kpi('%.2f<span style="font-size:13px"> 万</span>' % wan(win[win['期段'] == 'B']['价税合计'].sum()), '9/1–9/28', '8/22–8/31 %.2f 万' % wan(win[win['期段'] == 'A']['价税合计'].sum())) +
    kpi('%d' % int(win['单据编号'].nunique() and round(TOTAL / win['单据编号'].nunique())), '单均（元）', '%d 天区间' % NDAYS) +
    kpi('%d' % len(daily), '有销售天数', '共 %d 天' % NDAYS)
)

bars = ''.join(
    '<div class="bar"><span class="d">%s</span><span class="t"><i style="width:%.1f%%"></i></span>'
    '<span class="n">%.2f 万</span></div>' % (x['d'], x['amt'] / DMAX * 100 if DMAX else 0, x['amt'])
    for x in daily)

lx = comp[comp['销售员'] == '刘新元']['价税合计'].sum()

HTML = (HTML
        .replace('__NDAYS__', str(NDAYS))
        .replace('__NPEOPLE__', str(len(dept)))
        .replace('__NOFF__', str(sum(1 for p in people if p['在职'] != '在职')))
        .replace('__NACT__', str(len(active)))
        .replace('__KPIS__', kpis)
        .replace('__REGOPTS__', REG_OPTS)
        .replace('__BARS__', bars)
        .replace('__TOTAL__', '%.2f' % wan(TOTAL))
        .replace('__SEP__', '%.2f' % wan(win[win['期段'] == 'B']['价税合计'].sum()))
        .replace('__AUG__', '%.2f' % wan(win[win['期段'] == 'A']['价税合计'].sum()))
        .replace('__GIFTROWS__', '{:,}'.format(int((df['是否赠品'] == '是').sum())))
        .replace('__FOROWS__', str(int((df['单据类型'] == '预测订单').sum())))
        .replace('__FOAMT__', '%.2f' % wan(df[df['单据类型'] == '预测订单']['价税合计'].sum()))
        .replace('__TESTROWS__', '{:,}'.format(int((df['单据类型'] == '测试销售订单').sum())))
        .replace('__COMPTOTAL__', '%.2f' % wan(COMP_TOTAL))
        .replace('__LXAMT__', '%.2f' % wan(lx))
        .replace('__NRAW__', str(len(raw)))
        .replace('__J_PEOPLE__', json.dumps(people, ensure_ascii=False))
        .replace('__J_REGIONS__', json.dumps(regions, ensure_ascii=False))
        .replace('__J_CUST__', json.dumps(cust_rows, ensure_ascii=False))
        .replace('__J_SKU__', json.dumps(sku_rows, ensure_ascii=False))
        .replace('__J_RAW__', json.dumps(raw, ensure_ascii=False)))

HTML = BK.inject(HTML, theme='light', tag='营销中心 · 销售管理',
                 footer_extra='数据源：ERP 销售订单导出（截至 2026-09-28）')
v = BK.verify(HTML)
assert all(v.values()), '品牌自检未过: %s' % v

io.open(OUT, 'w', encoding='utf-8').write(HTML)
print('✅ 已生成 %s' % OUT)
print('   %d 字节 / 区间合计 %.2f 万 / 出单 %d 人 / 客户 %d 个 / 订单 %d 单 / 原始记录 %d 行'
      % (len(HTML.encode()), wan(TOTAL), len(active), win['客户'].nunique(),
         win['单据编号'].nunique(), len(raw)))
print('   品牌自检: %s' % v)
