# -*- coding: utf-8 -*-
"""Build a self-contained, read-only 2026-10 customer rollup page.

Data sources (local, authorized):
  - 数据/customer_rollup_v2.json (2026-11-01 snapshot; projects + activities)
  - 数据/orders_detail_2026.json (ERP order detail, latest 2026-11-03/04)
  - 数据/opps_by_customer.json (opportunity snapshot, 2026-07-11)

Output:
  对外物料/客户拉通大表_2026年10月_内部评审_20261103_V01.html
"""
import json
import os
import re
import html
from collections import Counter, defaultdict
from datetime import datetime

BASE = os.path.expanduser('~/Desktop/Hermes输出-工作类')
OUT_DIR = os.path.join(BASE, '对外物料')
OUT = os.path.join(OUT_DIR, '客户拉通大表_2026年10月_内部评审_20261104_V01.html')

START = '2026-10-01'
END = '2026-10-31'
REGION_RE = re.compile(r'-(广东|浙江|江苏|上海|北京|山东|四川|湖北|湖南|河南|福建|安徽|江西|河北|天津|重庆|陕西|辽宁|吉林|黑龙江|云南|贵州|广西|海南|山西|内蒙古|新疆|西藏|青海|宁夏|甘肃)$')

DEPT_MAP = {
    '诊断原料销售拓展部': '销售拓展部',
    '诊断原料大客户销售部': '大客户部',
    '浙江': '生命科学部·浙江',
    '广东': '生命科学部·广东',
}


def norm(s):
    s = (s or '').strip()
    s = REGION_RE.sub('', s)
    s = re.sub(r'\s+', '', s)
    return s


def load_json(path):
    with open(path, encoding='utf-8') as f:
        return json.load(f)


def dept_name(d):
    return DEPT_MAP.get(d, d or '—')


def money(v):
    try:
        return round(float(v or 0), 2)
    except Exception:
        return 0.0


def top(counter, n=2, sep=' / '):
    items = [k for k, v in counter.most_common(n) if k]
    return sep.join(items) if items else '—'


def main():
    rollup = load_json(os.path.join(BASE, '数据', 'customer_rollup_v2.json'))
    orders = load_json(os.path.join(BASE, '数据', 'orders_detail_2026.json'))['orders']
    projects = rollup.get('projects') or []
    activities = rollup.get('activities') or []

    oct_acts = [a for a in activities if START <= (a.get('date') or '') <= END]
    oct_orders = [o for o in orders.values() if START <= (o.get('date') or '')[:10] <= END]

    # Sales -> department mapping from all historical activities (helps order-only customers).
    sales_dept = defaultdict(Counter)
    for a in activities:
        if a.get('sales') and a.get('dept'):
            sales_dept[a['sales']][a['dept']] += 1

    customers = defaultdict(lambda: {
        'acts': [], 'orders': [], 'projs': [],
        'depts': Counter(), 'sales': Counter(), 'scenes': Counter(),
        'plines': Counter(), 'opps': set(),
    })

    for a in oct_acts:
        k = norm(a.get('customer'))
        c = customers[k]
        c['acts'].append(a)
        if a.get('dept'):
            c['depts'][a['dept']] += 1
        if a.get('sales'):
            c['sales'][a['sales']] += 1
        if a.get('project_scene'):
            c['scenes'][a['project_scene']] += 1
        if a.get('product_line'):
            c['plines'][a['product_line']] += 1
        for o in (a.get('opp_names') or []):
            if o:
                c['opps'].add(o)

    for o in oct_orders:
        k = norm(o.get('customer'))
        c = customers[k]
        c['orders'].append(o)
        if o.get('sales'):
            c['sales'][o['sales']] += 1

    for p in projects:
        k = norm(p.get('cust'))
        c = customers[k]
        c['projs'].append(p)
        if p.get('dept'):
            c['depts'][p['dept']] += 1
        if p.get('sales'):
            c['sales'][p['sales']] += 1
        if p.get('scene'):
            c['scenes'][p['scene']] += 1
        if p.get('product_line'):
            c['plines'][p['product_line']] += 1
        if p.get('opp'):
            c['opps'].add(p['opp'])

    rows = []
    for cust, c in customers.items():
        if not cust:
            continue
        # Department: activity/project hint first, then sales->dept fallback.
        dept = c['depts'].most_common(1)[0][0] if c['depts'] else ''
        if not dept:
            for sp, _ in c['sales'].most_common(3):
                if sales_dept.get(sp):
                    dept = sales_dept[sp].most_common(1)[0][0]
                    break
        sales_top = c['sales'].most_common(2)
        sales = ' / '.join([x[0] for x in sales_top]) if sales_top else '—'
        amount = sum(money(o.get('total')) for o in c['orders'])
        std_amount = sum(money(o.get('total')) for o in c['orders'] if o.get('type') == '标准销售订单')
        dates = [a.get('date') for a in c['acts'] if a.get('date')]
        last_act = max(dates) if dates else ''
        order_dates = [(o.get('date') or '')[:10] for o in c['orders'] if o.get('date')]
        last_order = max(order_dates) if order_dates else ''
        opps = sorted(c['opps'])
        rows.append({
            'cust': cust,
            'dept': dept_name(dept),
            'sales': sales,
            'opp_count': len(opps),
            'opps': opps[:4],
            'act_count': len(c['acts']),
            'last_act': last_act,
            'order_count': len(c['orders']),
            'amount': round(amount, 2),
            'std_amount': round(std_amount, 2),
            'last_order': last_order,
            'scene': top(c['scenes'], 2),
            'plines': top(c['plines'], 2),
            'sales_n': len([x for x in c['sales'].values() if x > 0]),
        })

    # Stable order: amount desc, activity desc, customer.
    rows.sort(key=lambda r: (-r['amount'], -r['act_count'], r['cust']))

    # Activity detail records (only fields needed; no contact/phone/address).
    act_rows = []
    for a in oct_acts:
        cust = norm(a.get('customer'))
        if not cust:
            continue
        desc = (a.get('desc') or '').replace('\n', ' ').strip()
        nxt = (a.get('next') or '').replace('\n', ' ').strip()
        desc = re.sub(r'https?://\S+', '', desc)
        nxt = re.sub(r'https?://\S+', '', nxt)
        desc = re.sub(r'\s+', ' ', desc).strip()
        nxt = re.sub(r'\s+', ' ', nxt).strip()
        summary = desc[:220]
        if nxt:
            summary = (summary + ' ｜ 下一步：' + nxt)[:300]
        act_rows.append({
            'date': a.get('date') or '',
            'cust': cust,
            'dept': dept_name(a.get('dept')),
            'sales': a.get('sales') or a.get('submitted_by') or '—',
            'atype': a.get('activity_type') or '—',
            'ctype': a.get('content_type') or '—',
            'scene': a.get('project_scene') or '—',
            'opps': ' / '.join(a.get('opp_names') or []) or '—',
            'summary': summary,
        })
    act_rows.sort(key=lambda x: (x['date'], x['cust']), reverse=True)

    # KPI + distributions.
    active_customers = len(rows)
    with_act = sum(1 for r in rows if r['act_count'] > 0)
    with_order = sum(1 for r in rows if r['order_count'] > 0)
    with_opp = sum(1 for r in rows if r['opp_count'] > 0)
    total_amount = round(sum(r['amount'] for r in rows), 2)
    total_std = round(sum(r['std_amount'] for r in rows), 2)
    depts = Counter(r['dept'] for r in rows if r['dept'] and r['dept'] != '—')
    sales_set = set()
    for r in rows:
        for s in [x.strip() for x in r['sales'].split(' / ')]:
            if s and s != '—':
                sales_set.add(s)
    order_types = Counter(o.get('type') or '—' for o in oct_orders)
    project_active = sum(1 for p in projects if norm(p.get('cust')) in customers and (customers[norm(p.get('cust'))]['acts'] or customers[norm(p.get('cust'))]['orders']))

    payload = {
        'meta': {
            'month': '2026年10月（自然月）',
            'start': START,
            'end': END,
            'generated': datetime.now().strftime('%Y-%m-%d %H:%M'),
            'source_rollup': '数据/customer_rollup_v2.json（生成 2026-11-01 16:55）',
            'source_orders': '数据/orders_detail_2026.json（数据截至 2026-11-03）',
            'source_opps': '数据/opps_by_customer.json（商机快照 2026-07-11）',
            'customer_count': active_customers,
            'with_activity': with_act,
            'with_order': with_order,
            'with_opp': with_opp,
            'activity_count': len(act_rows),
            'order_count': len(oct_orders),
            'amount': total_amount,
            'std_amount': total_std,
            'project_count': len(projects),
            'project_active': project_active,
            'sales_count': len(sales_set),
            'dept_count': len(depts),
            'order_types': dict(order_types),
        },
        'customers': rows,
        'activities': act_rows,
    }

    # HTML uses a real embedded JSON snapshot; zero external requests.
    data_js = json.dumps(payload, ensure_ascii=False, separators=(',', ':'))

    html_doc = r'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>客户拉通大表 · 2026年10月 · 内部评审</title>
<style>
:root{--pri:#0f3d6e;--pri2:#1a73e8;--teal:#0e9f9a;--ink:#1f2937;--muted:#64748b;--line:#dbe4ef;--soft:#f6f9fd;--bg:#eef3fa;--warn:#b45309;--warnbg:#fff7ed}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font-family:"Times New Roman","Songti SC","STSong","SimSun","宋体",serif;-webkit-print-color-adjust:exact;print-color-adjust:exact}
.wrap{max-width:1500px;margin:0 auto;padding:18px 22px 34px}
.hero{background:linear-gradient(135deg,#0b2f57 0%,#0f4c81 55%,#0e7490 100%);color:#fff;border-radius:16px;padding:22px 26px 18px;position:relative;overflow:hidden}
.hero:after{content:"";position:absolute;right:-40px;top:-60px;width:260px;height:260px;border:1px solid rgba(255,255,255,.18);border-radius:50%}
.hero h1{margin:0 0 6px;font-size:27px;letter-spacing:.04em;font-weight:700}
.hero .sub{font-size:13px;color:#d8ecff;line-height:1.7}
.hero .chips{margin-top:12px;display:flex;gap:8px;flex-wrap:wrap}
.chip{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(255,255,255,.28);background:rgba(255,255,255,.09);border-radius:999px;padding:4px 10px;font-size:11.5px;color:#eaf6ff}
.warn{margin:12px 0 14px;border:1px solid #fed7aa;background:var(--warnbg);color:#7c2d12;border-radius:10px;padding:10px 13px;font-size:12.5px;line-height:1.65}
.warn b{color:#9a3412}
.kpis{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:10px;margin:14px 0}
.kpi{background:#fff;border:1px solid var(--line);border-top:3px solid var(--teal);border-radius:10px;padding:12px 13px}
.kpi b{display:block;font-family:"Times New Roman",serif;font-size:24px;line-height:1.05;color:var(--pri)}
.kpi span{display:block;margin-top:6px;font-size:11px;color:var(--muted)}
.kpi em{display:block;margin-top:5px;font-style:normal;font-size:10.5px;color:#8a99ab}
.toolbar{background:#fff;border:1px solid var(--line);border-radius:12px;padding:12px 14px;margin:12px 0;display:flex;gap:9px;flex-wrap:wrap;align-items:center}
.toolbar input,.toolbar select{border:1px solid #cfdae8;border-radius:8px;padding:7px 9px;font-size:12px;background:#fff;color:var(--ink);font-family:inherit;outline:none}
.toolbar input{min-width:240px;flex:1}
.toolbar label{font-size:12px;color:#475569;display:flex;align-items:center;gap:5px;white-space:nowrap}
.toolbar .count{font-size:12px;color:var(--muted);margin-left:auto;white-space:nowrap}
.tabs{display:flex;gap:6px;margin:14px 0 0;border-bottom:1px solid var(--line)}
.tab{border:0;background:transparent;color:#5b6b82;padding:10px 14px;font-size:13px;cursor:pointer;border-bottom:2px solid transparent;font-family:inherit}
.tab.on{color:var(--pri);border-bottom-color:var(--teal);font-weight:700}
.panel{display:none;background:#fff;border:1px solid var(--line);border-top:0;border-radius:0 0 12px 12px;padding:12px 14px 16px}
.panel.on{display:block}
.tablewrap{overflow:auto;border:1px solid var(--line);border-radius:9px;max-height:70vh}
table{width:100%;border-collapse:separate;border-spacing:0;table-layout:fixed;font-size:11.6px}
th,td{border-right:1px solid var(--line);border-bottom:1px solid var(--line);padding:8px 8px;vertical-align:top;text-align:left;line-height:1.45}
th{position:sticky;top:0;z-index:3;background:#e7effa;color:#1e3a5f;font-size:11px;font-weight:700;white-space:nowrap}
td.n,th.n{text-align:right;white-space:nowrap}
td.c,th.c{text-align:center;white-space:nowrap}
tbody tr:hover td{background:#f8fbff}
td.stick,th.stick{position:sticky;left:0;z-index:2;background:#fff}
th.stick{z-index:4;background:#e7effa}
tbody tr:hover td.stick{background:#f8fbff}
.cust{font-weight:700;color:var(--pri)}
.sub{font-size:10.5px;color:var(--muted);margin-top:3px;line-height:1.4}
.badge{display:inline-block;border-radius:999px;padding:2px 7px;font-size:10px;background:#eef6ff;color:#1d4ed8;border:1px solid #d7e9ff}
.badge.warnb{background:#fff7ed;color:#9a3412;border-color:#fed7aa}
.money{font-family:"Times New Roman",serif;font-weight:700;color:var(--pri)}
.muted{color:var(--muted)}
.pager{display:flex;align-items:center;gap:10px;justify-content:flex-end;padding:10px 2px 0;font-size:12px;color:var(--muted)}
.pager button{border:1px solid #cfdae8;background:#fff;border-radius:7px;padding:5px 10px;font-size:12px;cursor:pointer;font-family:inherit}
.pager button:disabled{opacity:.45;cursor:default}
.note{margin-top:12px;font-size:11px;color:var(--muted);line-height:1.7}
.dict{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.card{border:1px solid var(--line);border-radius:10px;background:var(--soft);padding:12px 14px}
.card h3{margin:0 0 7px;font-size:13px;color:var(--pri)}
.card p,.card li{font-size:11.8px;line-height:1.7;color:#334155}
.card ul{margin:5px 0 0 17px;padding:0}
.card code{background:#fff;border:1px solid var(--line);border-radius:4px;padding:1px 4px;font-size:10.8px;color:#0f3d6e}
@media (max-width:1100px){.kpis{grid-template-columns:repeat(3,minmax(0,1fr))}.dict{grid-template-columns:1fr}}
@media (max-width:640px){.wrap{padding:10px}.hero{padding:16px}.hero h1{font-size:21px}.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.toolbar input{min-width:100%;flex:1 1 100%}.tablewrap{max-height:65vh}}
</style>
</head>
<body>
<div class="wrap">
  <div class="hero">
    <h1>客户拉通大表</h1>
    <div class="sub">2026 年 10 月（自然月）· 客户 / 商机 / 活动 / 订单 · 内部评审版</div>
    <div class="chips">
      <span class="chip">数据截止 2026-10-31</span>
      <span class="chip">静态快照 · 非实时接口</span>
      <span class="chip">仅限公司内部商务 / IT 评审</span>
      <span class="chip" id="genChip">生成时间 —</span>
    </div>
  </div>
  <div class="warn"><b>内部资料，请勿外传。</b>本页含真实客户名称与订单金额，仅用于公司内部评审；页面为生成时点的静态快照，不随系统自动更新。客户名称已去掉活动记录中的区域后缀；“保密客户”为 ERP 匿名记录，保持原文，不做反推。</div>
  <div class="kpis" id="kpis"></div>
  <div class="toolbar">
    <input id="q" placeholder="搜索客户 / 商机 / 产品线…" autocomplete="off">
    <select id="dept"><option value="">全部部门</option></select>
    <select id="sales"><option value="">全部销售</option></select>
    <select id="scene"><option value="">全部场景</option></select>
    <select id="ctype" style="display:none"><option value="">全部内容类型</option></select>
    <label id="onlyOrdersWrap"><input type="checkbox" id="onlyOrders"> 只看有订单</label>
    <span class="count" id="count"></span>
  </div>
  <div class="tabs">
    <button class="tab on" data-tab="cust">客户视图</button>
    <button class="tab" data-tab="act">活动明细</button>
    <button class="tab" data-tab="dict">字段口径 / IT 说明</button>
  </div>
  <div class="panel on" id="panel-cust">
    <div class="tablewrap">
      <table id="custTable">
        <thead><tr>
          <th class="stick" style="width:260px">客户</th>
          <th style="width:120px">部门</th>
          <th style="width:110px">销售</th>
          <th class="c" style="width:58px">商机数</th>
          <th class="n" style="width:72px">10月活动</th>
          <th style="width:92px">最近活动</th>
          <th class="n" style="width:64px">订单数</th>
          <th class="n" style="width:98px">订单金额(万)</th>
          <th class="n" style="width:92px">标准订单(万)</th>
          <th style="width:150px">主要场景</th>
          <th style="width:170px">主要产品线</th>
        </tr></thead>
        <tbody></tbody>
      </table>
    </div>
    <div class="pager"><span id="custPageInfo"></span><button id="custPrev">上一页</button><button id="custNext">下一页</button></div>
    <div class="note">客户视图按“客户”聚合：同一客户的多条活动、多个商机、多张订单合并为一行；订单金额含测试单/预测单，标准销售订单金额单列。无客户名的陌拜活动不进入本表。</div>
  </div>
  <div class="panel" id="panel-act">
    <div class="tablewrap">
      <table id="actTable">
        <thead><tr>
          <th style="width:88px">日期</th>
          <th style="width:230px">客户</th>
          <th style="width:120px">部门</th>
          <th style="width:100px">销售</th>
          <th style="width:90px">活动类型</th>
          <th style="width:100px">内容类型</th>
          <th style="width:86px">场景</th>
          <th style="width:180px">商机</th>
          <th>摘要</th>
        </tr></thead>
        <tbody></tbody>
      </table>
    </div>
    <div class="pager"><span id="actPageInfo"></span><button id="actPrev">上一页</button><button id="actNext">下一页</button></div>
    <div class="note">活动明细来自飞书活动回填快照；摘要截取原文前 220 字，附“下一步”时最多 300 字。未展示结构化联系人、电话、地址、合同号字段；摘要保留业务原文，可能含客户人员姓名。</div>
  </div>
  <div class="panel" id="panel-dict">
    <div class="dict">
      <div class="card"><h3>口径</h3><ul>
        <li><b>月份：</b>2026-10-01 至 2026-10-31（自然月）。</li>
        <li><b>客户数：</b>当月有活动或有订单的客户；客户名去区域后缀后归并。</li>
        <li><b>商机数：</b>商机快照中该客户的商机数；快照生成于 2026-07-11。</li>
        <li><b>活动：</b>飞书活动回填快照中当月活动；无客户名的活动不入客户表。</li>
        <li><b>订单：</b>ERP 订单明细按合同日期归月；金额为价税合计，含测试单/预测单；标准销售订单金额单列。</li>
        <li><b>部门：</b>活动/商机中的部门提示；仅订单客户按销售员历史活动部门回填。</li>
      </ul></div>
      <div class="card"><h3>数据源与刷新</h3><ul>
        <li><code>数据/customer_rollup_v2.json</code>：客户/商机/活动回填，生成 2026-11-01 16:55。</li>
        <li><code>数据/orders_detail_2026.json</code>：ERP 订单明细，数据截至 2026-11-03。</li>
        <li><code>数据/opps_by_customer.json</code>：商机快照，2026-07-11。</li>
        <li>本页是<b>静态快照</b>；正式系统建议由服务端按月份生成 <code>customer_rollup_YYYYMM.json</code>，或给现有数据接口增加 <code>month=YYYY-MM</code> 参数。</li>
      </ul></div>
      <div class="card"><h3>字段字典</h3><ul>
        <li><b>客户：</b>归一化客户名（去区域后缀），保留“保密客户”原文。</li>
        <li><b>部门 / 销售：</b>按活动、商机、订单中出现频次取主值；多人时显示前两位。</li>
        <li><b>主要场景：</b>项目场景众数；<b>主要产品线：</b>活动/商机产品线众数。</li>
        <li><b>订单金额(万)：</b>价税合计 ÷ 10000，保留两位；<b>标准订单(万)：</b>仅标准销售订单。</li>
      </ul></div>
      <div class="card"><h3>IT 对接建议</h3><ul>
        <li>现有接口：<code>GET /api/customer-rollup/data?limit=500&amp;offset=0</code>，当前仅刘新元账号可读。</li>
        <li>建议新增月度参数与只读角色；前端不再内嵌全量数据，由服务端按权限下发。</li>
        <li>权限：商务/IT 只读；客户归属按 <code>sales</code> 字段过滤；无归属客户仅管理员可见。</li>
        <li>本页不含结构化联系人、电话、地址、合同号字段；活动摘要保留业务原文，可能含客户人员姓名。如要对外接入，请按最小必要字段另行评审。</li>
      </ul></div>
    </div>
    <div class="note" id="sourceNote"></div>
  </div>
</div>
<script>
const DATA = __DATA_JSON__;
const PAGE_CUST = 50, PAGE_ACT = 100;
let custPage = 1, actPage = 1, tab = 'cust';

function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
function fmtMoney(v){return (Number(v||0)/10000).toLocaleString('zh-CN',{minimumFractionDigits:2,maximumFractionDigits:2});}
function fmtInt(v){return Number(v||0).toLocaleString('zh-CN');}
function uniq(arr){return Array.from(new Set(arr.filter(Boolean))).sort(function(a,b){return a.localeCompare(b,'zh-CN');});}

function init(){
  document.getElementById('genChip').textContent='生成时间 '+DATA.meta.generated;
  var k=DATA.meta;
  var cards=[
    ['活跃客户',fmtInt(k.customer_count),'有活动 '+k.with_activity+' · 有订单 '+k.with_order],
    ['商机',fmtInt(k.project_count),'有商机客户 '+k.with_opp+' · 本月有动作商机 '+k.project_active],
    ['10月活动',fmtInt(k.activity_count),'覆盖销售 '+k.sales_count+' 人'],
    ['10月订单',fmtInt(k.order_count),'标准 '+ (k.order_types['标准销售订单']||0) +' · 测试 '+ (k.order_types['测试销售订单']||0) +' · 预测 '+ (k.order_types['预测订单']||0)],
    ['订单金额',fmtMoney(k.amount),'万元 · 含测试/预测单'],
    ['覆盖销售',fmtInt(k.sales_count),'人 · 含活动/订单归属']
  ];
  document.getElementById('kpis').innerHTML=cards.map(function(c){return '<div class="kpi"><b>'+c[1]+'</b><span>'+c[0]+'</span><em>'+c[2]+'</em></div>';}).join('');
  // filters
  var depts=uniq(DATA.customers.map(function(r){return r.dept;}));
  var sales=uniq(DATA.customers.flatMap(function(r){return String(r.sales||'').split(' / ');}));
  var scenes=uniq(DATA.customers.flatMap(function(r){return String(r.scene||'').split(' / ');}));
  var ctypes=uniq(DATA.activities.map(function(r){return r.ctype;}));
  fill('dept',depts); fill('sales',sales); fill('scene',scenes); fill('ctype',ctypes);
  document.getElementById('sourceNote').textContent='数据源：'+k.source_rollup+'；'+k.source_orders+'；'+k.source_opps+'。本页生成时间 '+k.generated+'。';
  bind();
  renderCust(); renderAct();
}
function fill(id,arr){
  var s=document.getElementById(id);
  arr.forEach(function(x){var o=document.createElement('option'); o.value=x; o.textContent=x; s.appendChild(o);});
}
function bind(){
  ['q','dept','sales','scene','ctype','onlyOrders'].forEach(function(id){
    var el=document.getElementById(id);
    el.addEventListener(id==='q'?'input':'change',function(){custPage=1;actPage=1;renderCust();renderAct();});
  });
  document.querySelectorAll('.tab').forEach(function(b){b.onclick=function(){switchTab(b.dataset.tab);};});
  document.getElementById('custPrev').onclick=function(){if(custPage>1){custPage--;renderCust();}};
  document.getElementById('custNext').onclick=function(){custPage++;renderCust();};
  document.getElementById('actPrev').onclick=function(){if(actPage>1){actPage--;renderAct();}};
  document.getElementById('actNext').onclick=function(){actPage++;renderAct();};
}
function switchTab(t){
  tab=t;
  document.querySelectorAll('.tab').forEach(function(b){b.classList.toggle('on',b.dataset.tab===t);});
  document.querySelectorAll('.panel').forEach(function(p){p.classList.toggle('on',p.id==='panel-'+t);});
  document.getElementById('ctype').style.display=(t==='act')?'':'none';
  document.getElementById('onlyOrdersWrap').style.display=(t==='cust')?'':'none';
}
function filteredCust(){
  var q=document.getElementById('q').value.trim().toLowerCase();
  var d=document.getElementById('dept').value, s=document.getElementById('sales').value, sc=document.getElementById('scene').value;
  var only=document.getElementById('onlyOrders').checked;
  return DATA.customers.filter(function(r){
    if(only && r.order_count<=0) return false;
    if(d && r.dept!==d) return false;
    if(s && String(r.sales).split(' / ').indexOf(s)<0) return false;
    if(sc && String(r.scene).split(' / ').indexOf(sc)<0) return false;
    if(q){
      var hay=(r.cust+' '+(r.opps||[]).join(' ')+' '+r.plines+' '+r.scene).toLowerCase();
      if(hay.indexOf(q)<0) return false;
    }
    return true;
  });
}
function renderCust(){
  var rows=filteredCust();
  var pages=Math.max(1,Math.ceil(rows.length/PAGE_CUST));
  if(custPage>pages) custPage=pages;
  var start=(custPage-1)*PAGE_CUST, slice=rows.slice(start,start+PAGE_CUST);
  var tb=document.querySelector('#custTable tbody');
  tb.innerHTML=slice.map(function(r){
    var opp=(r.opps&&r.opps.length)?r.opps.slice(0,2).join(' / '):'未关联商机';
    var more=(r.opp_count>2)?(' +'+(r.opp_count-2)):'';
    return '<tr>'+
      '<td class="stick"><div class="cust">'+esc(r.cust)+'</div><div class="sub">'+esc(opp)+more+'</div></td>'+
      '<td>'+esc(r.dept)+'</td>'+
      '<td>'+esc(r.sales)+'</td>'+
      '<td class="c">'+(r.opp_count?r.opp_count:'—')+'</td>'+
      '<td class="n">'+r.act_count+'</td>'+
      '<td>'+(r.last_act?esc(r.last_act):'—')+'</td>'+
      '<td class="n">'+r.order_count+'</td>'+
      '<td class="n money">'+fmtMoney(r.amount)+'</td>'+
      '<td class="n">'+fmtMoney(r.std_amount)+'</td>'+
      '<td>'+esc(r.scene)+'</td>'+
      '<td>'+esc(r.plines)+'</td>'+
    '</tr>';
  }).join('');
  document.getElementById('count').textContent='客户 '+rows.length+' 行'+(document.getElementById('onlyOrders').checked?'（仅有订单）':'');
  document.getElementById('custPageInfo').textContent='第 '+custPage+' / '+pages+' 页 · 共 '+rows.length+' 个客户';
  document.getElementById('custPrev').disabled=custPage<=1;
  document.getElementById('custNext').disabled=custPage>=pages;
}
function filteredAct(){
  var q=document.getElementById('q').value.trim().toLowerCase();
  var d=document.getElementById('dept').value, s=document.getElementById('sales').value, sc=document.getElementById('scene').value, ct=document.getElementById('ctype').value;
  return DATA.activities.filter(function(r){
    if(d && r.dept!==d) return false;
    if(s && r.sales!==s) return false;
    if(sc && r.scene!==sc) return false;
    if(ct && r.ctype!==ct) return false;
    if(q){
      var hay=(r.cust+' '+r.opps+' '+r.summary+' '+r.atype+' '+r.ctype).toLowerCase();
      if(hay.indexOf(q)<0) return false;
    }
    return true;
  });
}
function renderAct(){
  var rows=filteredAct();
  var pages=Math.max(1,Math.ceil(rows.length/PAGE_ACT));
  if(actPage>pages) actPage=pages;
  var start=(actPage-1)*PAGE_ACT, slice=rows.slice(start,start+PAGE_ACT);
  var tb=document.querySelector('#actTable tbody');
  tb.innerHTML=slice.map(function(r){
    return '<tr>'+
      '<td>'+esc(r.date)+'</td>'+
      '<td><div class="cust">'+esc(r.cust)+'</div></td>'+
      '<td>'+esc(r.dept)+'</td>'+
      '<td>'+esc(r.sales)+'</td>'+
      '<td>'+esc(r.atype)+'</td>'+
      '<td>'+esc(r.ctype)+'</td>'+
      '<td>'+esc(r.scene)+'</td>'+
      '<td>'+esc(r.opps)+'</td>'+
      '<td>'+esc(r.summary)+'</td>'+
    '</tr>';
  }).join('');
  document.getElementById('actPageInfo').textContent='第 '+actPage+' / '+pages+' 页 · 共 '+rows.length+' 条活动';
  document.getElementById('actPrev').disabled=actPage<=1;
  document.getElementById('actNext').disabled=actPage>=pages;
  if(tab==='act') document.getElementById('count').textContent='活动 '+rows.length+' 条';
}
init();
</script>
</body>
</html>'''

    html_doc = html_doc.replace('__DATA_JSON__', data_js)
    # brand header/footer
    import sys
    sys.path.insert(0, os.path.join(BASE, '脚本'))
    import brand_kit as BK
    html_doc = BK.inject(html_doc, theme='light', tag='客户拉通大表 · 内部评审',
                         footer_extra='数据截止 2026-10-31 · 静态快照')
    # 对外部/跨部门评审件：清掉品牌样式注释里的内部页面措辞
    html_doc = html_doc.replace('/* 白底顶栏变体（工作台页面） */', '/* 白底顶栏变体 */')
    bad = [k for k, ok in BK.verify(html_doc).items() if not ok]
    if bad:
        raise RuntimeError('brand_kit verify failed: %s' % bad)

    os.makedirs(OUT_DIR, exist_ok=True)
    with open(OUT, 'w', encoding='utf-8') as f:
        f.write(html_doc)
    print('WROTE', OUT, os.path.getsize(OUT))
    print('META', json.dumps(payload['meta'], ensure_ascii=False, indent=2))


if __name__ == '__main__':
    main()
