#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成「测试台账 · 关键货号 × 重点客户」自包含 HTML
输入: 数据/test_ledger.json   输出: 工具/测试台账_7货号5客户.html
"""
import os, json, base64, datetime

ROOT = os.path.expanduser('~/Desktop/Hermes输出-工作类')
SRC = os.path.join(ROOT, '数据/test_ledger.json')
OUT = os.path.join(ROOT, '工具/测试台账_7货号5客户.html')
LOGO = os.path.expanduser('~/work/公司资料/logo_transparent.png')

d = json.load(open(SRC, encoding='utf-8'))
logo_b64 = base64.b64encode(open(LOGO, 'rb').read()).decode() if os.path.exists(LOGO) else ''
today = datetime.date.today().isoformat()

# ── 待补清单（人工核定：均有订单证据支撑）─────────────────
GAPS = [
    {"cust": "天隆", "sku": "MD2104-003006-DB", "done": "2026-04-22 起测试 6 批（100T→1000T）→ 2026-07-21 采购 20,000T+10,000T",
     "miss": "书面测试结论（转采购依据），目前反馈库 0 条", "owner": "刘新元（原张立娅，已离职）", "pri": "高"},
    {"cust": "天隆", "sku": "MD2104-003001 / -1", "done": "2026-06-03 抵货 4,800 元、2026-06-10 抵货 6,000 元（dNTP 售后事件）",
     "miss": "售后闭环结论（根因 + 改善批次确认），仅散落在订单备注", "owner": "刘新元（原张立娅，已离职）", "pri": "高"},
    {"cust": "天隆", "sku": "MD2104-092401-DB / M2274-073702 / M2274-088901-DB",
     "done": "2026-05-25~06-18 各测试 200–400T", "miss": "测试结论（是否通过、是否进入采购）", "owner": "刘新元", "pri": "中"},
    {"cust": "海尔施", "sku": "M2274-090803", "done": "4 批共 1,800T 测试（2025-06-27 起至 2026-03-12），用量最大",
     "miss": "测试结论（投入最大却无书面反馈）", "owner": "刘子研 / 刘新元", "pri": "高"},
    {"cust": "海尔施", "sku": "M2274-073702、MD2104-003001", "done": "2025-08-25 / 2025-09-04、11-20 测试",
     "miss": "测试结论与后续动作", "owner": "刘子研（原张立娅）", "pri": "中"},
    {"cust": "新产业", "sku": "M2331", "done": "2026-03-19 / 04-07 / 04-14 三批各 5ml 测试（dPCR 方向）",
     "miss": "测试结论（三批测试未见结论）", "owner": "刘新元（原张立娅）", "pri": "中"},
    {"cust": "新产业", "sku": "M2274-073702", "done": "2025-08-25 测试 200T", "miss": "测试结论（距今超一年）",
     "owner": "刘新元（原张立娅）", "pri": "低"},
    {"cust": "丽珠", "sku": "MD2104-003001", "done": "2026-01-29 测试 500T（用途：尼帕病毒检测）",
     "miss": "测试结论（丽珠主体在 HIV 项目，此条为旁支）", "owner": "刘新元（原张立娅）", "pri": "中"},
    {"cust": "佰奥莱博", "sku": "M2274-073702", "done": "2025-08-06 测试 100T / 2025-12-26 测试 12.5ml",
     "miss": "测试结论与后续动作", "owner": "刘新元（原张立娅）", "pri": "中"},
    {"cust": "佰奥长生（深圳）", "sku": "M2331", "done": "2026-04-15 测试 2.5ml；2026-05-29 有 1 条书面反馈「M2331 不如 MD2091」",
     "miss": "① 确认是否属佰奥莱博集团 ② 负向反馈的迭代闭环", "owner": "林潇", "pri": "中"},
]

CSS = """
*{box-sizing:border-box}
body{margin:0;background:#f4f7fb;color:#1f2d3d;font:14px/1.6 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
.topbar{position:sticky;top:0;z-index:50;display:flex;align-items:center;gap:12px;padding:9px 20px;background:#fff;border-bottom:3px solid #1a73c9;box-shadow:0 2px 10px rgba(16,42,80,.08)}
.topbar img{height:34px}
.brand{font-weight:800;letter-spacing:.4px;color:#12325b;font-size:15px}
.brand small{display:block;font-weight:500;font-size:11px;color:#7286a0;letter-spacing:1.6px}
.ttl{margin-left:auto;font-weight:700;color:#12325b;font-size:15px}
.ttl span{font-weight:400;color:#8a9bb0;font-size:12px;margin-left:8px}
.wrap{max-width:1560px;margin:0 auto;padding:16px 20px 60px}
.info-bar{background:#eef4fb;border:1px solid #cfe0f2;border-left:4px solid #1a73c9;border-radius:8px;padding:10px 14px;font-size:13px;color:#33506f;margin-bottom:14px}
.info-bar b{color:#12325b}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(158px,1fr));gap:11px;margin-bottom:16px}
.kpi{background:#fff;border-radius:10px;padding:12px 14px;border-left:4px solid #1a73c9;box-shadow:0 1px 6px rgba(16,42,80,.07)}
.kpi .v{font-size:23px;font-weight:800;color:#12325b;line-height:1.2}
.kpi .l{font-size:12px;color:#7286a0;margin-top:3px}
.kpi.g{border-left-color:#12a15c}.kpi.o{border-left-color:#e8912a}.kpi.r{border-left-color:#d94b4b}.kpi.p{border-left-color:#7a5cd6}
.card{background:#fff;border-radius:11px;padding:15px 17px;margin-bottom:16px;box-shadow:0 1px 7px rgba(16,42,80,.07)}
.card>h2{margin:0 0 4px;font-size:15.5px;color:#12325b;display:flex;align-items:center;gap:7px}
.card>h2 em{font-style:normal;font-size:11.5px;font-weight:500;color:#8a9bb0;margin-left:auto}
.card .hint{font-size:12.5px;color:#7286a0;margin:2px 0 12px}
.badge{display:inline-block;padding:1.5px 7px;border-radius:11px;font-size:11.5px;line-height:1.5;white-space:nowrap}
.b-test{background:#e3eefb;color:#1a5fa8}.b-buy{background:#e2f6ec;color:#0d7a47}.b-gift{background:#fdf0dc;color:#a8641a}
.b-after{background:#fdeaea;color:#b52c2c}.b-tgt{background:#12325b;color:#fff}.b-x{background:#eef1f5;color:#6b7c91}
.b-ok{background:#e2f6ec;color:#0d7a47}.b-no{background:#fdeaea;color:#b52c2c}
.finding{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:11px}
.fc{background:#fafcff;border:1px solid #e3ecf6;border-radius:9px;padding:11px 13px}
.fc h4{margin:0 0 5px;font-size:13px;color:#12325b}
.fc p{margin:0;font-size:12.5px;color:#4a6178}
/* 时间轴 */
.tl{position:relative;margin:6px 0 0 8px;padding-left:20px;border-left:2px solid #dbe6f3}
.tl-item{position:relative;margin-bottom:11px}
.tl-item:before{content:"";position:absolute;left:-27px;top:5px;width:11px;height:11px;border-radius:50%;background:#fff;border:2.5px solid #1a73c9}
.tl-item.buy:before{border-color:#12a15c}.tl-item.gift:before{border-color:#e8912a}.tl-item.after:before{border-color:#d94b4b}
.tl-d{font-size:12px;color:#7286a0;font-weight:700}
.tl-c{font-size:13px;color:#1f2d3d}
.tl-c b{color:#12325b}
.tl-n{font-size:11.8px;color:#8a9bb0;margin-top:1px}
/* 表格 */
.tb-tools{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:9px}
.tb-q{flex:1;min-width:190px;padding:7px 11px;border:1px solid #d7e2ee;border-radius:7px;font-size:13px;outline:none}
.tb-q:focus{border-color:#1a73c9;box-shadow:0 0 0 3px rgba(26,115,201,.1)}
.tb-btn{padding:6.5px 11px;border:1px solid #d7e2ee;background:#fff;color:#33506f;border-radius:7px;font-size:12.5px;cursor:pointer}
.tb-btn:hover{border-color:#1a73c9;color:#1a73c9;background:#f5f9ff}
.tb-cnt{margin-left:auto;font-size:12.5px;color:#7286a0}
.tb-scroll{overflow:auto;max-height:620px;border:1px solid #e7eef6;border-radius:8px}
table.tb{width:100%;border-collapse:collapse;font-size:12.5px}
table.tb thead th{position:sticky;top:0;z-index:2;background:#f2f7fd;color:#12325b;text-align:left;padding:7px 9px;border-bottom:1px solid #dbe6f3;white-space:nowrap;cursor:pointer;user-select:none}
table.tb thead th.no-sort{cursor:default}
table.tb thead th .ar{color:#1a73c9;font-size:10px}
table.tb thead tr.flt th{top:33px;padding:4px 6px;background:#fbfdff;cursor:default}
table.tb thead tr.flt input{width:100%;padding:3px 6px;border:1px solid #e3ecf6;border-radius:5px;font-size:11.5px;outline:none}
table.tb tbody td{padding:6px 9px;border-bottom:1px solid #f0f5fa;vertical-align:top}
table.tb tbody tr:hover{background:#f8fbff}
table.tb tbody tr.hl{background:#fffdf3}
table.tb td.num{text-align:right;font-variant-numeric:tabular-nums}
.tb-pager{display:flex;align-items:center;gap:7px;justify-content:center;margin-top:10px;font-size:12.5px;color:#7286a0}
.tb-pager button{padding:4px 10px;border:1px solid #d7e2ee;background:#fff;border-radius:6px;cursor:pointer;font-size:12.5px}
.tb-pager button:disabled{opacity:.45;cursor:default}
.tb-empty{padding:26px;text-align:center;color:#8a9bb0;font-size:13px}
.modal{position:fixed;inset:0;background:rgba(12,28,48,.55);display:none;z-index:99;align-items:center;justify-content:center;padding:28px}
.modal.on{display:flex}
.modal-in{background:#fff;border-radius:10px;max-width:1080px;width:100%;max-height:84vh;display:flex;flex-direction:column}
.modal-in h3{margin:0;padding:12px 16px;border-bottom:1px solid #e7eef6;font-size:14px;color:#12325b}
.modal-in pre{margin:0;padding:14px 16px;overflow:auto;font-size:11.5px;line-height:1.65;background:#fbfdff;color:#33506f;white-space:pre-wrap;word-break:break-all}
.modal-in .mf{padding:10px 16px;border-top:1px solid #e7eef6;text-align:right}
.warn{background:#fff8e6;border:1px solid #f0d080;color:#7a5200;border-radius:8px;padding:10px 13px;font-size:12.5px;line-height:1.85}
.warn b{color:#8a5a00}
ul.ck{margin:6px 0 0;padding-left:20px;font-size:12.8px;color:#3c5570}
ul.ck li{margin-bottom:4px}
@media(max-width:820px){.topbar .ttl{display:none}}
"""

JS = """
function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
function mkTable(cfg){
 var host=document.querySelector(cfg.sel); if(!host) return;
 var rows=cfg.data||[], cols=cfg.cols||[], ps=cfg.pageSize||15;
 var st={q:'',f:{},sortK:cfg.sortKey||'',sortD:0,page:1};
 host.innerHTML='<div class="tb-tools"><input class="tb-q" placeholder="'+esc(cfg.search||'搜索全部字段…')+'">'+
  '<button class="tb-btn" data-a="csv">📥 导出CSV</button><button class="tb-btn" data-a="raw">📋 原始数据</button>'+
  '<button class="tb-btn" data-a="reset">⟲ 重置</button><span class="tb-cnt"></span></div>'+
  '<div class="tb-scroll"><table class="tb"><thead></thead><tbody></tbody></table></div><div class="tb-pager"></div>';
 var q=host.querySelector('.tb-q'), thead=host.querySelector('thead'), tbody=host.querySelector('tbody'),
     pager=host.querySelector('.tb-pager'), cnt=host.querySelector('.tb-cnt');
 function val(r,c){var v=r[c.key];return v==null?'':v;}
 function filtered(){
  var out=rows;
  if(st.q){var qq=st.q.toLowerCase();
   out=out.filter(function(r){return cols.some(function(c){return String(val(r,c)).toLowerCase().indexOf(qq)>-1;});});}
  Object.keys(st.f).forEach(function(k){
   var fv=(st.f[k]||'').toLowerCase(); if(!fv)return;
   out=out.filter(function(r){return String(r[k]==null?'':r[k]).toLowerCase().indexOf(fv)>-1;});});
  if(st.sortK){
   var c=cols.filter(function(x){return x.key===st.sortK;})[0]||{};
   out=out.slice().sort(function(a,b){
    var x=val(a,c),y=val(b,c);
    if(c.type==='number'){x=parseFloat(String(x).replace(/[^\\d.-]/g,''))||0;y=parseFloat(String(y).replace(/[^\\d.-]/g,''))||0;return st.sortD?y-x:x-y;}
    x=String(x).toLowerCase();y=String(y).toLowerCase();
    return st.sortD?(x<y?1:x>y?-1:0):(x<y?-1:x>y?1:0);});}
  return out;
 }
 function head(){
  var h1='<tr>'+cols.map(function(c){
   var ar=st.sortK===c.key?(st.sortD?' ▼':' ▲'):'';
   return '<th data-k="'+c.key+'" class="'+(c.sortable===false?'no-sort':'')+'">'+esc(c.label)+'<span class="ar">'+ar+'</span></th>';}).join('')+'</tr>';
  var h2='<tr class="flt">'+cols.map(function(c){
   return '<th>'+(c.filter===false?'':'<input data-f="'+c.key+'" placeholder="筛" value="'+esc(st.f[c.key]||'')+'">')+'</th>';}).join('')+'</tr>';
  thead.innerHTML=h1+h2;
  thead.querySelectorAll('th[data-k]').forEach(function(th){
   th.onclick=function(){var k=th.getAttribute('data-k');
    if(st.sortK===k){st.sortD=st.sortD?0:1;if(!st.sortD)st.sortK='';}else{st.sortK=k;st.sortD=0;}
    st.page=1;draw();};});
  thead.querySelectorAll('input[data-f]').forEach(function(inp){
   inp.oninput=function(){st.f[inp.getAttribute('data-f')]=inp.value;st.page=1;drawBody(filtered());};});
 }
 function rowHtml(r){
  return '<tr'+(r._hl?' class="hl"':'')+'>'+cols.map(function(c){
   var raw=val(r,c), out;
   if(c.format) out=c.format(raw,r); else if(c.html) out=String(raw); else out=esc(raw);
   return '<td class="'+(c.type==='number'?'num':'')+'">'+out+'</td>';}).join('')+'</tr>';
 }
 function drawBody(list){
  var tp=Math.max(1,Math.ceil(list.length/ps)); if(st.page>tp)st.page=tp;
  var p=list.slice((st.page-1)*ps,st.page*ps);
  tbody.innerHTML=p.length?p.map(rowHtml).join(''):'<tr><td colspan="'+cols.length+'" class="tb-empty">无匹配数据</td></tr>';
  cnt.textContent='共 '+list.length+' 行'+(list.length!==rows.length?'（总 '+rows.length+'）':'');
  var btns='<button '+(st.page<=1?'disabled':'')+' data-p="1">首页</button>'+
   '<button '+(st.page<=1?'disabled':'')+' data-p="'+(st.page-1)+'">上一页</button>'+
   '<span>第 '+st.page+' / '+tp+' 页</span>'+
   '<button '+(st.page>=tp?'disabled':'')+' data-p="'+(st.page+1)+'">下一页</button>'+
   '<button '+(st.page>=tp?'disabled':'')+' data-p="'+tp+'">末页</button>';
  pager.innerHTML=btns;
  pager.querySelectorAll('button[data-p]').forEach(function(b){
   b.onclick=function(){st.page=parseInt(b.getAttribute('data-p'),10);drawBody(filtered());};});
 }
 function draw(){drawBody(filtered());}
 function csv(){
  var list=filtered();
  var head=cols.map(function(c){return '"'+String(c.label).replace(/"/g,'""')+'"';}).join(',');
  var body=list.map(function(r){return cols.map(function(c){
    var v=c.csv?c.csv(val(r,c),r):val(r,c);
    return '"'+String(v==null?'':v).replace(/<[^>]*>/g,'').replace(/"/g,'""')+'"';}).join(',');}).join('\\n');
  var blob=new Blob(['\\ufeff'+head+'\\n'+body],{type:'text/csv;charset=utf-8'});
  var a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=(cfg.csvName||'export')+'.csv';a.click();
 }
 host.querySelector('.tb-q').oninput=function(e){st.q=e.target.value;st.page=1;draw();};
 host.addEventListener('click',function(e){
  var a=e.target.getAttribute&&e.target.getAttribute('data-a'); if(!a)return;
  if(a==='csv')csv();
  else if(a==='reset'){st={q:'',f:{},sortK:cfg.sortKey||'',sortD:0,page:1};draw();head();host.querySelector('.tb-q').value='';}
  else if(a==='raw'){var m=document.getElementById('rawModal');
   m.querySelector('pre').textContent=JSON.stringify(cfg.rawData||rows,null,1);m.classList.add('on');}});
 head(); draw();
}
document.addEventListener('click',function(e){
 if(e.target.id==='rawClose'||e.target.classList.contains('modal'))document.getElementById('rawModal').classList.remove('on');});
"""

# ── 表格数据构造 ───────────────────────────────────────────
TGT = ['天隆', '新产业', '丽珠', '海尔施', '佰奥莱博', '佰奥长生']
def tb(r):
    c = r.get('type', '')
    if '测试' in c: return '<span class="badge b-test">测试</span>'
    if '采购' in c: return '<span class="badge b-buy">采购</span>'
    if '赠送' in c: return '<span class="badge b-gift">赠送</span>'
    if '售后' in c: return '<span class="badge b-after">售后</span>'
    return '<span class="badge b-x">' + esc_h(c or '—') + '</span>'
def cs(r):
    k = r.get('cshort', '')
    full = esc_h(str(r.get('customer', ''))[:40])
    if not k: return esc_h(r.get('customer', '')[:28])
    cls = 'b-tgt' if k in TGT else 'b-x'
    return '<span class="badge %s" title="%s">%s</span>' % (cls, full, esc_h(k))
def esc_h(s):
    return (str(s).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;'))

ORD = []
for o in d['orders']:
    ORD.append({
        'date': o['date'] or '—', 'sku': o['sku'], 'name': o['name'], 'cust': cs(o),
        'cshort': o['cshort'], 'type': tb(o), 'qty': o['qty'], 'unit': o['unit'],
        'gift': o['gift'], 'batch': o['batch'], 'amt': o['amt'] or '', 'note': o['note'],
        '_hl': 1 if o['cshort'] else 0,
        'custtxt': o['customer'], 'typetxt': o['type'], 'nametxt': o['name'],
        'datetxt': o['date'] or '', 'sku_t': o['sku'],
    })
ACT = []
for a in d['activities']:
    if not a['cshort'] and not a['skus']:
        continue
    ACT.append({'date': a['date'] or '—', 'cust': cs(a), 'cshort': a['cshort'],
                'owner': a['owner'], 'skus': '、'.join(a['skus']) if a['skus'] else '—',
                'desc': a['desc'], 'custtxt': a['customer'], 'datetxt': a['date']})
ACT.sort(key=lambda x: x['datetxt'] or '', reverse=True)
FB = []
for f in d['feedback']:
    FB.append({'date': f['date'] or '—', 'sku': f['sku'], 'cust': cs(f), 'cshort': f['cshort'],
               'sales': f['sales'], 'text': f['text'], 'custtxt': f['customer'], 'datetxt': f['date']})
FB.sort(key=lambda x: x['datetxt'] or '', reverse=True)

CU = []
for k, v in d['cust_summary'].items():
    if k == '佰奥长生':
        continue
    owners = {}
    for a in d['activities']:
        if a['cshort'] == k and a['owner']:
            owners[a['owner']] = owners.get(a['owner'], 0) + 1
    olist = ' / '.join('%s(%d)' % (x, y) for x, y in sorted(owners.items(), key=lambda z: -z[1]))
    CU.append({'cust': '<span class="badge b-tgt">%s</span>' % esc_h(k), 'full': esc_h(v['full'][:30]),
               'first': v['first'] or '—', 'last': v['last'] or '—',
               'skus': esc_h('、'.join(v['skus'])),
               'test': v['test'], 'buy': v['buy'], 'orders': v['orders'],
               'acts': v['acts'], 'fb': ('<span class="badge b-no">0</span>' if v['fb'] == 0 else '<span class="badge b-ok">%d</span>' % v['fb']),
               'owner': esc_h(olist or '—')})
CU.append({'cust': '<span class="badge b-x">佰奥长生（深圳）</span>', 'full': '佰奥长生（深圳）科技有限公司',
           'first': '2026-04-15', 'last': '2026-04-15', 'skus': 'M2331', 'test': 1, 'buy': 0, 'orders': 1,
           'acts': 8, 'fb': '<span class="badge b-ok">1</span>', 'owner': '林潇(8)'})
for c in CU:
    c['_hl'] = 1

# 时间轴：目标客户订单
TL = sorted([o for o in d['orders'] if o['cshort']], key=lambda x: x['date'] or '')
tl_html = []
for o in TL:
    cls = 'buy' if o['is_buy'] else ('gift' if o['gift'] == '是' and '赠送' in o['type'] else ('after' if '售后' in o['type'] else ''))
    tl_html.append(
        '<div class="tl-item %s"><div class="tl-d">%s ｜ %s</div><div class="tl-c"><b>%s</b> · %s · %s%s</div>'
        '<div class="tl-n">%s%s</div></div>' % (
            cls, o['date'] or '—', esc_h(o['cshort']), esc_h(o['sku']), esc_h(o['type']),
            ('%.4g' % o['qty']) + esc_h(o['unit']), '（赠品）' if o['gift'] == '是' else '',
            esc_h(o['name'][:46]), ('　｜　' + esc_h(o['note'][:130])) if o['note'] else ''))

# ── 组装 ──────────────────────────────────────────────────
HTML = """<!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="topbar">__LOGO__
<div class="brand">BIORI BIOTECH<small>宝锐生物</small></div>
<div class="ttl">测试台账 · 关键货号 × 重点客户<span>数据截至 __TODAY__</span></div></div>
<div class="wrap">
<div class="info-bar"><b>口径</b>：订单/测试/赠送/采购 = ERP 销售订单导出-销售管理（<b>__SPAN0__ ~ __SPAN1__</b>，__ORDR__ 行）；活动 = 飞书活动模块；书面反馈 = 飞书活动反馈（<b>最早 2026-05-27</b>，2025 年无书面反馈记录）。目标客户 = 天隆 / 新产业 / 丽珠 / 海尔施 / 佰奥莱博（另含相似主体「佰奥长生（深圳）」，待确认归属）。表中带 <span class="badge b-tgt">客户</span> 徽标的行 = 目标客户，★ 高亮行同义。</div>
<div class="kpis">
<div class="kpi"><div class="v">7</div><div class="l">关键货号（含变体 10 个）</div></div>
<div class="kpi p"><div class="v">5+1</div><div class="l">重点客户 / 相似主体</div></div>
<div class="kpi"><div class="v">__TORDER__</div><div class="l">发往重点客户的订单行</div></div>
<div class="kpi o"><div class="v">__TTEST__</div><div class="l">其中测试单（无需COA）</div></div>
<div class="kpi g"><div class="v">__TQTY__</div><div class="l">测试单样本量合计（T）</div></div>
<div class="kpi r"><div class="v">1</div><div class="l">书面反馈条数（仅佰奥长生）</div></div>
</div>
<div class="card"><h2>① 核心结论 <em>三句话看完</em></h2>
<div class="finding">
<div class="fc"><h4>🟢 有测试、无书面结论</h4><p>7 个货号 2025 至今共 <b>__TORDER__ 行</b>发往这 5 家（其中测试单 <b>__TTEST__ 个</b>、赠品 5 行、采购 6 行），但书面反馈只有 <b>1 条</b>，且来自相似主体「佰奥长生（深圳）」。测试结论仅散落在<b>活动标题与订单备注</b>里。</p></div>
<div class="fc"><h4>🔵 天隆是唯一走通「测试→采购」的客户</h4><p><b>MD2104-003006-DB</b> 2026-04 起测试 6 批 → <b>2026-07-21 采购 20,000T + 10,000T</b>（0.038 元/T 抵扣）；<b>MD2104-003001</b> 也于 2026-04-07 转采购 1,000T。</p></div>
<div class="fc"><h4>🔴 天隆存在 dNTP 售后事件（金额已在备注）</h4><p>MD2104-003006-DB 抵扣 <b>6,000 元</b>、MD2104-003001-1 抵扣 <b>4,800 元</b>（2026-06），均因 dNTP 问题走抵货，<b>售后闭环结论缺失</b>。</p></div>
<div class="fc"><h4>⚠️ 投入与留痕不成正比</h4><p>海尔施 <b>M2274-090803 测试 4 批共 1,800T</b>（2025-06 起）、新产业 M2331 三批测试 —— 均无书面结论；而活动记录多登记在<b>已离职的张立娅</b>名下（天隆 13 条、丽珠 14 条、佰奥莱博 8 条）。</p></div>
</div></div>
<div class="card"><h2>② 客户档案 <em>测试起止 / 涉及货号 / 跟进人</em></h2>
<div class="hint">「测试」= 订单类型为「测试(无需COA)」；「采购」=「采购(配COA)」；「活动」= 飞书活动条数；「书面反馈」= 反馈库中该客户对本批货号的文字反馈条数。</div>
<div id="custTable"></div></div>
<div class="card"><h2>③ 时间轴 · 发往重点客户的 __TORDER__ 行 <em>按日期正序</em></h2>
<div class="hint">🔵 测试/其他　🟢 采购（配COA）　🟠 赠送　🔴 售后（换货）。备注列含真实业务信息（加急、抵货、收货老师等）。</div>
<div class="tl">__TL__</div></div>
<div class="card"><h2>④ 订单明细台账 <em>__ORDR__ 行 · 可搜索/排序/筛选/导出</em></h2>
<div class="hint">含全部客户（不限于重点客户）——便于对比「谁在测、谁在买」。重点客户行已高亮。</div>
<div id="ordTable"></div></div>
<div class="card"><h2>⑤ 活动记录 <em>__ACTR__ 条 · 涉及本批货号或重点客户</em></h2>
<div class="hint">「命中货号」= 活动描述中出现的本批货号（含短码，如「003006」）；空为「—」。按日期倒序。</div>
<div id="actTable"></div></div>
<div class="card"><h2>⑥ 书面测试反馈 <em>__FBR__ 条 · 涉 __FBCUST__ 家客户</em></h2>
<div class="hint">来源：飞书活动反馈库（最早 2026-05-27）。重点客户仅「佰奥长生（深圳）」1 条。</div>
<div id="fbTable"></div></div>
<div class="card"><h2>⑦ 待补清单 <em>__GAPN__ 条 · 均有订单证据</em></h2>
<div class="hint">缺口 = 有测试/采购事实，但缺书面结论或闭环。</div>
<div id="gapTable"></div></div>
<div class="card"><h2>⑧ 口径、风险与待确认</h2>
<div class="warn"><b>数据口径</b>：① 订单/测试/赠品/采购 = ERP「销售订单导出-销售管理」23 列 × 27,291 行；② 活动 = 飞书活动模块 5,121 条；③ 书面反馈 = 飞书活动反馈库（<b>最早记录 2026-05-27，2025 年 0 条</b>）——2025 年的测试状态只能以 ERP 测试单+订单备注为准，<b>不代表客户当时未测</b>。</div>
<ul class="ck">
<li><b>主体确认</b>：「佰奥长生（深圳）」与「佰奥莱博（西安）」是否为同一集团？本表 M2331 唯一书面反馈记在深圳主体名下。</li>
<li><b>货号 M2274-073702-1</b>：全库（ERP 563 行 / 活动 / 反馈）<b>无任何记录</b>，需确认是否已停用或并入 M2274-073702。</li>
<li><b>MD2104-003006-DB-2</b>：仅 1 行（2026-06-22 赠 5ml，批次 20260114），无测试单类型，需确认是否走测试流程。</li>
<li><b>日期异常</b>：天隆 1 行单据编号解析出「2020-26-04」，原始单据需人工核对（本表按编号解析，未做修正）。</li>
<li><b>跟进人风险</b>：本批客户的活动多登记在<b>张立娅（已离职）</b>名下（天隆 13 / 丽珠 14 / 佰奥莱博 8 / 新产业 3 / 海尔施 3），建议核对交接人并补齐近 3 个月跟进记录。</li>
<li><b>未做估算</b>：本表所有数字均来自上述数据源直接统计，未做任何插补或推测；「测试样本量合计」按订单数量字段原值累加（T 与 ml 单位混算，仅作量级参考）。</li>
</ul></div>
</div>
<div class="modal" id="rawModal"><div class="modal-in"><h3>📋 底层原始数据（JSON）</h3><pre></pre><div class="mf"><button class="tb-btn" id="rawClose">关闭</button></div></div></div>
<script>__JS__
var GAP=__GAPJSON__;
var CU=__CU__, ORD=__ORD__, ACT=__ACT__, FB=__FB__;
mkTable({sel:'#custTable',data:CU,pageSize:10,csvName:'客户档案',sortKey:'test',search:'搜索客户…',
 cols:[{key:'cust',label:'客户',filter:false,html:true},{key:'full',label:'客户全称'},{key:'first',label:'首次接触'},{key:'last',label:'最近动作'},
  {key:'skus',label:'涉及货号',filter:false},{key:'test',label:'测试',type:'number'},{key:'buy',label:'采购',type:'number'},
  {key:'orders',label:'订单行',type:'number'},{key:'acts',label:'活动',type:'number'},{key:'fb',label:'书面反馈',type:'number',html:true},
  {key:'owner',label:'跟进人（活动数）',filter:false}]});
mkTable({sel:'#ordTable',data:ORD,pageSize:20,csvName:'订单明细台账',sortKey:'datetxt',
 search:'搜索日期/货号/客户/备注…',
 cols:[{key:'date',label:'日期'},{key:'sku',label:'货号'},{key:'name',label:'物料名称'},{key:'cust',label:'客户（重点）',filter:false,html:true},
  {key:'type',label:'订单类型',filter:false,html:true},{key:'qty',label:'数量',type:'number'},{key:'unit',label:'单位'},
  {key:'gift',label:'赠品'},{key:'batch',label:'出货批号'},{key:'amt',label:'价税合计',type:'number'},{key:'note',label:'备注',filter:false}],
 csv:{'cust':function(v,r){return r.custtxt;},'type':function(v,r){return r.typetxt;}}});
mkTable({sel:'#actTable',data:ACT,pageSize:15,csvName:'活动记录',sortKey:'datetxt',
 search:'搜索活动内容/客户/销售…',
 cols:[{key:'date',label:'日期'},{key:'cust',label:'客户',filter:false,html:true},{key:'owner',label:'登记人'},
  {key:'skus',label:'命中货号',filter:false},{key:'desc',label:'活动内容',filter:false}],
 csv:{'cust':function(v,r){return r.custtxt;}}});
mkTable({sel:'#fbTable',data:FB,pageSize:15,csvName:'书面测试反馈',sortKey:'datetxt',
 search:'搜索反馈内容/客户/货号…',
 cols:[{key:'date',label:'日期'},{key:'sku',label:'货号'},{key:'cust',label:'客户',filter:false,html:true},
  {key:'sales',label:'销售'},{key:'text',label:'反馈内容',filter:false}],
 csv:{'cust':function(v,r){return r.custtxt;}}});
mkTable({sel:'#gapTable',data:GAP,pageSize:15,csvName:'待补清单',search:'搜索客户/货号…',
 cols:[{key:'cust',label:'客户'},{key:'sku',label:'货号'},{key:'done',label:'已发生的事实',filter:false},
  {key:'miss',label:'缺什么',filter:false},{key:'owner',label:'建议跟进人',filter:false},{key:'pri',label:'优先级'}],
 csv:{}});
</script></body></html>"""

TQTY = sum(o['qty'] for o in d['orders'] if o['cshort'] and 'T' in (o['unit'] or ''))
html = (HTML
        .replace('__CSS__', CSS).replace('__JS__', JS)
        .replace('__LOGO__', ('<img src="data:image/png;base64,%s" alt="BIORI">' % logo_b64) if logo_b64 else '')
        .replace('__TODAY__', today)
        .replace('__SPAN0__', d['meta']['span'][0]).replace('__SPAN1__', d['meta']['span'][1])
        .replace('__ORDR__', str(d['meta']['order_rows']))
        .replace('__ACTR__', str(len(ACT))).replace('__FBR__', str(len(FB)))
        .replace('__FBCUST__', str(len(set(f['cshort'] or f['customer'] for f in d['feedback']))))
        .replace('__GAPN__', str(len(GAPS)))
        .replace('__TORDER__', str(sum(1 for o in d['orders'] if o['cshort'])))
        .replace('__TTEST__', str(sum(1 for o in d['orders'] if o['cshort'] and o['is_test'])))
        .replace('__TQTY__', ('%.0f' % TQTY))
        .replace('__TL__', ''.join(tl_html))
        .replace('__GAPJSON__', json.dumps(GAPS, ensure_ascii=False).replace('</', '<\\/'))
        .replace('__CU__', json.dumps(CU, ensure_ascii=False).replace('</', '<\\/'))
        .replace('__ORD__', json.dumps(ORD, ensure_ascii=False).replace('</', '<\\/'))
        .replace('__ACT__', json.dumps(ACT, ensure_ascii=False).replace('</', '<\\/'))
        .replace('__FB__', json.dumps(FB, ensure_ascii=False).replace('</', '<\\/')))
open(OUT, 'w', encoding='utf-8').write(html)
print('✅ %s  %.1f KB' % (OUT, os.path.getsize(OUT) / 1024))
print('   订单行 %d ｜ 活动 %d ｜ 反馈 %d ｜ 缺口 %d ｜ 时间轴 %d'
      % (len(ORD), len(ACT), len(FB), len(GAPS), len(tl_html)))
print('   宏未替换残留:', [x for x in ['__CSS__', '__JS__', '__JSON__', '__TL__', '__TODAY__', '__TORDER__', '__TTEST__', '__TQTY__', '__ORDR__', '__ACTR__', '__FBR__', '__FBCUST__', '__GAPN__', '__SPAN0__', '__SPAN1__', '__LOGO__'] if x in html])
print('   外链数:', html.count('src="http'), '｜ GAP 已定义:', 'var GAP=' in html)
