#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成《科研促销期订单 · 课题组待补挂清单》交互式 HTML。

用途：促销期 78 张科研单，销售/区域经理逐单选定「课题组」（下拉取自飞书商机台账 1744 个），
      导出 JSON 后可回灌重算「按课题组的促销档位与礼品发放表」。
"""
import io
import json
import os
import re
import sys
import tempfile
import subprocess

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

OUTDIR = os.path.join(ROOT, '营销活动', '2026-10实验加速季')
os.makedirs(OUTDIR, exist_ok=True)
OUT = os.path.join(OUTDIR, '科研促销期订单_课题组待补挂清单.html')

D = json.load(io.open(ROOT + '/销售分析/科研销售_按人与区域_20260820-20260930.json', encoding='utf-8'))
KT = json.load(io.open(ROOT + '/数据/ketizu_list.json', encoding='utf-8'))['ketizu']
OM = json.load(io.open(ROOT + '/数据/order_ketizu_map.json', encoding='utf-8'))['map']

# 按单据编号聚合（一单多行 → 金额求和）
orders = {}
for r in D['raw']:
    no = str(r['单据编号']).strip()
    o = orders.setdefault(no, {'no': no, 'date': r['合同日期'], 'customer': r['客户'],
                               'sales': r['销售员'], 'amount': 0.0, 'lines': 0})
    o['amount'] += float(r['价税合计'])
    o['lines'] += 1
for no, o in orders.items():
    o['amount'] = round(o['amount'], 2)
    o['known'] = OM.get(no, '')
rows = sorted(orders.values(), key=lambda x: -x['amount'])
total = round(sum(o['amount'] for o in rows), 2)
known_n = sum(1 for o in rows if o['known'])

kt_names = sorted(KT.keys())

CSS = """
*{box-sizing:border-box}
body{margin:0;background:#f5f7fa;color:#1f2328;font:14px/1.65 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
.wrap{max-width:1400px;margin:0 auto;padding:18px}
h1{font-size:23px;font-weight:900;color:#0F3A8A;margin:8px 0 4px}
.sub{color:#5f6368;font-size:13px;margin-bottom:14px}
.card{background:#fff;border-radius:12px;padding:15px 17px;margin-bottom:14px;box-shadow:0 1px 3px rgba(16,24,40,.08)}
.kpi{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:14px}
.k{background:#fff;border-radius:10px;padding:11px 15px;min-width:132px;border-left:4px solid #1852B6;box-shadow:0 1px 3px rgba(16,24,40,.08)}
.k .t{font-size:12px;color:#5f6368}.k .v{font-size:21px;font-weight:900;color:#1852B6}
.bar{display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-bottom:10px}
input[type=text]{border:1px solid #D0D5DD;border-radius:8px;padding:6px 10px;font-size:13px;min-width:200px}
.btn{border:1px solid #D0D5DD;background:#fff;border-radius:8px;padding:6px 12px;font-size:13px;cursor:pointer;color:#344054}
.btn:hover{background:#F2F6FD;border-color:#1852B6;color:#1852B6}
.btn.p{background:#1852B6;border-color:#1852B6;color:#fff}.btn.p:hover{background:#0F3A8A}
.tw{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{padding:7px 9px;border-bottom:1px solid #EEF1F5;vertical-align:middle}
th{background:#F0F4FB;color:#0F3A8A;font-weight:700;position:sticky;top:0;z-index:2;text-align:left}
th.r,td.r{text-align:right}
tbody tr:hover{background:#F7FAFF}
select{width:100%;max-width:260px;border:1px solid #D0D5DD;border-radius:7px;padding:5px 7px;font-size:12.5px;background:#fff}
select.filled{background:#EAF6EE;border-color:#7DC49A;font-weight:700;color:#0E7A45}
select.empty{background:#FFF9E9;border-color:#E9A800}
tbody tr.done{background:#F5FBF7}
.warn{background:#FFF8E6;border-left:4px solid #E9A800;border-radius:9px;padding:11px 14px;font-size:13px;margin-bottom:13px}
.warn b{color:#8A6100}
.foot{background:#fff;border-radius:12px;padding:15px 17px;font-size:12.5px;color:#475467;line-height:1.9}
.foot h3{font-size:15px;color:#0F3A8A;margin:0 0 7px}
pre{background:#F7F9FC;border:1px solid #EEF1F5;border-radius:8px;padding:10px;font-size:11.5px;max-height:300px;overflow:auto}
.tip{font-size:12px;color:#98A2B3}
"""

html = f"""<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>科研促销期订单 · 课题组待补挂清单</title>
<style>{CSS}</style></head><body><div class="wrap">

<h1>科研促销期订单 · 课题组待补挂清单</h1>
<div class="sub">区间 <b>2026-08-20 ~ 09-30</b> ｜ 生命科学销售部 ｜ 共 <b>{len(rows)}</b> 张订单
 ｜ 课题组下拉取自飞书「销售管理 · 商机」台账（<b>{len(kt_names)}</b> 个）｜ 内部资料</div>

<div class="warn"><b>为什么需要这张表：</b>飞书「销售订单」有 <code>关联一级课题组</code> 字段，但促销期 1037 张单据里只有
<b>16 张</b>填了（科研口径 {len(rows)} 张里只有 {known_n} 张）—— 课题组归属<b>录得不全</b>，因此「按课题组发礼品」暂时算不出来。
请逐单选好课题组后点「导出映射 JSON」发回，我即可重算<b>按课题组的促销档位与礼品发放表</b>。</div>

<div class="kpi">
  <div class="k"><div class="t">订单数</div><div class="v">{len(rows)}</div></div>
  <div class="k"><div class="t">订单金额合计</div><div class="v">{total:,.0f}</div><div class="tip">元（含税）</div></div>
  <div class="k"><div class="t">已选出课题组</div><div class="v" id="k_done">{known_n}</div></div>
  <div class="k"><div class="t">待补</div><div class="v" id="k_left">{len(rows) - known_n}</div></div>
</div>

<div class="card">
  <div class="bar">
    <input type="text" id="q" placeholder="筛选：单号 / 客户 / 销售员 / 课题组…">
    <button class="btn" onclick="csv()">导出 CSV</button>
    <button class="btn p" onclick="exp()">导出映射 JSON</button>
    <button class="btn" onclick="onlyEmpty()">只看未填</button>
    <button class="btn" onclick="reset()">重置本机记录</button>
    <span class="tip" id="msg"></span>
  </div>
  <div class="tw"><table id="t">
    <thead><tr>
      <th style="width:34px">#</th><th style="width:132px">单据编号</th><th style="width:92px">合同日期</th>
      <th>客户（开票主体）</th><th style="width:78px">销售员</th>
      <th class="r" style="width:92px">金额(元)</th><th style="width:280px">课题组 <span style="color:#C0392B">*</span></th>
    </tr></thead><tbody id="tb"></tbody></table></div>
  <div class="tip" style="margin-top:8px">改动自动保存在本机浏览器；点「导出映射 JSON」把结果发回即可。下拉里没有的课题组可直接输入新名称。</div>
</div>

<div class="card"><h3 style="margin:0 0 8px;font-size:15px;color:#0F3A8A">导出的内容（可手动复制发回）</h3>
  <pre id="out">{'（点「导出映射 JSON」后这里会回显）'}</pre></div>

<div class="foot"><h3>口径说明</h3>
① 金额与订单行来自 ERP 导出件 <code>{D['src']}</code>，口径与《科研销售_按人与区域》一致
（标准销售订单 & 已审核 & 非赠品 & 客户不含宝锐/宝泰/横琴）。<br>
② 课题组候选来自飞书 <code>销售管理.商机</code> 中名称含「课题组」的记录，共 {len(kt_names)} 个
（其中已标注模式的：一类 806 / 二类 101 / 三类 93）。<br>
③ 已预填 {known_n} 张原本就挂了课题组的单据，可覆盖修正。<br>
④ 本文件含真实客户与金额，<b>仅内部使用，不得对外发送</b>；不回传任何服务器，纯本机操作。
</div>
</div>

<script>
var ORD = {json.dumps(rows, ensure_ascii=False)};
var KT = {json.dumps(kt_names, ensure_ascii=False)};
var KEY = 'bior_kt_fill_v1';
var SAVED = {{}};
try {{ SAVED = JSON.parse(localStorage.getItem(KEY) || '{{}}'); }} catch (e) {{ SAVED = {{}}; }}

function val(o) {{ return SAVED[o.no] !== undefined ? SAVED[o.no] : (o.known || ''); }}

function render() {{
  var q = (document.getElementById('q').value || '').trim().toLowerCase();
  var only = window.__onlyEmpty === true;
  var tb = document.getElementById('tb');
  var h = '', done = 0, shown = 0;
  ORD.forEach(function (o, i) {{
    var v = val(o);
    if (v) done++;
    var hay = (o.no + o.customer + o.sales + v).toLowerCase();
    if ((q && hay.indexOf(q) < 0) || (only && v)) return;
    shown++;
    var opts = '<option value="">— 请选择课题组 —</option>';
    KT.forEach(function (k) {{
      opts += '<option value="' + k.replace(/"/g, '&quot;') + '"' + (k === v ? ' selected' : '') + '>' + k + '</option>';
    }});
    h += '<tr class="' + (v ? 'done' : '') + '"><td class="r">' + (i + 1) + '</td><td>' + o.no + '</td><td>' + o.date +
      '</td><td>' + o.customer + '</td><td>' + o.sales + '</td><td class="r">' + o.amount.toLocaleString() +
      '</td><td><select data-no="' + o.no + '" class="' + (v ? 'filled' : 'empty') + '">' + opts + '</select></td></tr>';
  }});
  tb.innerHTML = h || '<tr><td colspan="7" style="text-align:center;color:#98A2B3;padding:22px">没有匹配的订单</td></tr>';
  document.getElementById('k_done').textContent = done;
  document.getElementById('k_left').textContent = ORD.length - done;
  document.getElementById('msg').textContent = '当前显示 ' + shown + ' / ' + ORD.length + ' 条';
  Array.prototype.forEach.call(tb.querySelectorAll('select'), function (s) {{
    s.addEventListener('change', function () {{
      var no = s.getAttribute('data-no');
      if (s.value) SAVED[no] = s.value; else delete SAVED[no];
      localStorage.setItem(KEY, JSON.stringify(SAVED));
      s.className = s.value ? 'filled' : 'empty';
      render();
    }});
  }});
}}
var composing = false;
document.getElementById('q').addEventListener('compositionstart', function () {{ composing = true; }});
document.getElementById('q').addEventListener('compositionend', function () {{ composing = false; render(); }});
document.getElementById('q').addEventListener('input', function () {{ if (!composing) render(); }});
function onlyEmpty() {{ window.__onlyEmpty = !window.__onlyEmpty; render(); }}
function reset() {{
  if (!confirm('清空本机已填的课题组（不影响导出的文件）？')) return;
  SAVED = {{}}; localStorage.removeItem(KEY); render();
}}
function dl(text, name, mime) {{
  var b = new Blob(['\\ufeff' + text], {{ type: mime + ';charset=utf-8' }});
  var a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = name; a.click();
  setTimeout(function () {{ URL.revokeObjectURL(a.href); }}, 2000);
}}
function csv() {{
  var lines = ['单据编号,合同日期,客户,销售员,金额元,课题组'];
  ORD.forEach(function (o) {{
    var v = val(o);
    lines.push([o.no, o.date, o.customer, o.sales, o.amount, v].map(function (x) {{
      return '"' + String(x).replace(/"/g, '""') + '"';
    }}).join(','));
  }});
  dl(lines.join('\\n'), '科研促销期_订单课题组映射.csv', 'text/csv');
}}
function exp() {{
  var m = {{}}, miss = [];
  ORD.forEach(function (o) {{ var v = val(o); if (v) m[o.no] = v; else miss.push(o.no); }});
  var payload = {{ range: '2026-08-20~2026-09-30', dept: '生命科学销售部',
                   filled: Object.keys(m).length, total: ORD.length, missing: miss, map: m }};
  var s = JSON.stringify(payload, null, 1);
  document.getElementById('out').textContent = s;
  dl(s, '科研促销期_订单课题组映射.json', 'application/json');
  document.getElementById('msg').textContent = '已导出：填了 ' + Object.keys(m).length + ' / ' + ORD.length + '，未填 ' + miss.length;
}}
render();
</script>
</body></html>
"""

html = BK.inject(html, theme='light', tag='营销中心 · 销售管理',
                 footer_extra='科研促销期订单 · 课题组待补挂清单 ｜ 2026-08-20~09-30')
io.open(OUT, 'w', encoding='utf-8').write(html)

print('=== 质检 ===')
vr = BK.verify(html)
bad = [k for k, v in vr.items() if not v]
print(('✅' if not bad else '❌'), '品牌套件', '全 True' if not bad else bad)
ext = re.findall(r'(?:src|href)\s*=\s*["\'](?:https?:)?//', html)
print(('✅' if not ext else '❌'), '外链数 %d' % len(ext))
blocks = re.findall(r'<script[^>]*>(.*?)</script>', html, re.S)
fail = 0
for b in blocks:
    fd, p = tempfile.mkstemp(suffix='.js'); os.write(fd, b.encode()); os.close(fd)
    r = subprocess.run(['node', '--check', p], capture_output=True, text=True)
    if r.returncode: fail += 1; print('   ❌', r.stderr.strip().splitlines()[-1][:110])
    os.unlink(p)
print(('✅' if not fail else '❌'), 'node --check：%d 块 / %d 报错' % (len(blocks), fail))
print('  订单 %d 张 ｜ 金额 %.0f 元 ｜ 预填 %d ｜ 课题组候选 %d' % (len(rows), total, known_n, len(kt_names)))
print('  输出 %s（%.0f KB）' % (OUT, os.path.getsize(OUT) / 1024.0))
