#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""大客户部「货号下降原因判定台」生成器

输入：数据/大客户部_货号下降_待判定.json
输出：工具/大客户部_货号下降判定台.html（自包含、零外链、localStorage 存草稿、可导出 JSON/CSV）
口径：下降原因人工三选一 —— ① 丢单（需求未变，流向其他供应商）② 需求减少 ③ 换用我方其他货号
"""
import io
import json
import os

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
SRC = W + '/数据/大客户部_货号下降_待判定.json'
OUT = W + '/工具/大客户部_货号下降判定台.html'

D = json.load(io.open(SRC, encoding='utf-8'))
DATA_JS = json.dumps(D, ensure_ascii=False, separators=(',', ':'))
print('数据：下降 %d ｜ 新增 %d ｜ 上量 %d ｜ %.0f KB'
      % (len(D['declines']), len(D['news']), len(D['grows']), len(DATA_JS) / 1024))

CSS = """
*{box-sizing:border-box}
body{margin:0;background:#f4f6fa;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;color:#1b2430;font-size:13px}
.wrap{max-width:1560px;margin:0 auto;padding:14px 16px 40px}
h1{font-size:20px;margin:10px 0 4px}
.sub{color:#66707d;font-size:12px;line-height:1.8;margin-bottom:12px}
.scope{margin:8px 0 12px;border:1px solid #e3e8f1;border-radius:9px;background:#fbfcfe}
.scope summary{cursor:pointer;padding:9px 12px;font-size:12.5px;font-weight:700;color:#8a5a12}
.scope-in{padding:0 13px 12px;font-size:12px;line-height:1.75;color:#3c4654}
.scope-in p{margin:5px 0}
.scope-in code{background:#eef2f8;padding:0 4px;border-radius:4px;font-size:11px}
table.rm{margin-top:6px;font-size:11.5px}
table.rm th{background:#8a5a12}
.tabs{display:flex;gap:6px;margin:10px 0 12px;border-bottom:2px solid #dfe5ee}
.tab{padding:8px 16px;cursor:pointer;border-radius:8px 8px 0 0;font-weight:700;color:#5b6675;background:#eaeef5}
.tab.on{background:#1F4E79;color:#fff}
.card{background:#fff;border:1px solid #e3e8f1;border-radius:10px;padding:12px 14px;margin-bottom:12px}
.kpis{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:12px}
.kpi{background:#fff;border:1px solid #e3e8f1;border-top:3px solid #1F4E79;border-radius:9px;padding:9px 11px}
.kpi .l{font-size:11px;color:#66707d;font-weight:700}
.kpi .v{font-size:20px;font-weight:900;line-height:1.25}
.kpi .s{font-size:11px;color:#9aa3b2}
.bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px}
input[type=text],select{padding:6px 9px;border:1px solid #cfd8e4;border-radius:7px;font-size:12.5px;background:#fff}
.btn{padding:6px 12px;border:1px solid #1F4E79;background:#1F4E79;color:#fff;border-radius:7px;font-size:12.5px;font-weight:700;cursor:pointer}
.btn.ghost{background:#fff;color:#1F4E79}
.btn.warn{background:#b3261e;border-color:#b3261e}
table{width:100%;border-collapse:collapse;font-size:12px}
th{background:#1F4E79;color:#fff;padding:7px 8px;text-align:left;white-space:nowrap;position:sticky;top:0;z-index:2}
td{padding:7px 8px;border-bottom:1px solid #eef1f6;vertical-align:top}
tbody tr:hover{background:#f7fafd}
.mono{font-family:ui-monospace,Menlo,monospace}
.num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
.drop{color:#b3261e;font-weight:800}
.hint{font-size:11px;color:#5b6675;line-height:1.55}
.chip{display:inline-block;font-size:10.5px;padding:1px 6px;border-radius:9px;background:#eef2f8;color:#24405e;margin:1px 2px;cursor:pointer;border:1px solid #dbe3ee}
.chip.same{background:#fff4e0;border-color:#e8c987;color:#8a5a12}
.sug{font-size:11px;font-weight:700;padding:2px 7px;border-radius:9px;white-space:nowrap}
.sug.s1{background:#fde8e6;color:#b3261e}.sug.s2{background:#e8f0fb;color:#1F4E79}.sug.s3{background:#e8f6ee;color:#1c7c54}
.pick{display:flex;gap:4px;flex-wrap:wrap}
.pick label{display:inline-flex;align-items:center;gap:3px;font-size:11.5px;padding:3px 7px;border:1px solid #cfd8e4;border-radius:7px;cursor:pointer;background:#fff;white-space:nowrap}
.pick input{margin:0}
.pick label:has(input:checked){border-color:#1F4E79;background:#eaf1f9;font-weight:800}
.pick label.r1:has(input:checked){border-color:#b3261e;background:#fdeceb}
.pick label.r3:has(input:checked){border-color:#1c7c54;background:#e9f7ef}
.note{width:100%;margin-top:4px}
.done{background:#f6fbf7}
.warnrow{background:#fffdf6}
.stat{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.big{font-size:15px;font-weight:900}
.tiny{font-size:11px;color:#66707d}
.hidden{display:none}
textarea{padding:6px 8px;border:1px solid #cfd8e4;border-radius:7px;font-size:12px;font-family:inherit;resize:vertical}
"""

JS = r"""
var D = __DATA__;
var KEY = 'dk_sku_classify_v1';
var ST = load();
function load(){ try{ var s=JSON.parse(localStorage.getItem(KEY)||'{}'); return {dec:s.dec||{},conf:s.conf||{}} }catch(e){ return {dec:{},conf:{}} } }
function save(){ try{ localStorage.setItem(KEY, JSON.stringify(ST)) }catch(e){} }
function wan(x){ return (x/1e4).toFixed(1) }
function esc(s){ return (s==null?'':String(s)).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]}) }
function idAt(i){ return D.declines[i].id }
function decAt(i){ var k=idAt(i); return ST.dec[k]||(ST.dec[k]={}) }
function confAt(kind,i){ var k=kind+'|'+i; return ST.conf[k]||(ST.conf[k]={}) }

/* —— 判定：三选一（事件委托，不拼 DOM id）—— */
function pick(el){
  var i = +el.closest('tr').dataset.i, d = decAt(i);
  d.reason = el.value; d.status='已判定';
  if (el.value==='1') d.note_sku='';
  if (el.value==='2'){ d.note_who=''; d.note_sku=''; }
  if (el.value==='3'){ d.note_who=''; }
  save(); renderReason(i); renderKPI();
}
function setNote(el){
  var i = +el.closest('tr').dataset.i, d = decAt(i);
  d[el.dataset.f] = el.value; save(); renderKPI();
}
function setSku(el){
  var i = +el.closest('tr').dataset.i, d = decAt(i);
  d.note_sku = el.dataset.sku; save(); renderReason(i); renderKPI();
}
function setConf(el){
  var tr = el.closest('tr'), i = +tr.dataset.i, kind = tr.dataset.kind, c = confAt(kind,i);
  c[el.dataset.f] = (el.type==='checkbox') ? el.checked : el.value;
  save(); renderKPI();
}

/* —— 单行「判定 + 备注」—— */
function renderReason(i){
  var x = D.declines[i], d = ST.dec[x.id]||{}, r = d.reason||'';
  var box = document.querySelector('tr[data-i="'+i+'"] .rz'); if(!box) return;
  var h = '<div class="pick">'
    + '<label class="r1"><input type="radio" name="p'+i+'" value="1" '+(r==='1'?'checked':'')+' onchange="pick(this)">① 丢单</label>'
    + '<label><input type="radio" name="p'+i+'" value="2" '+(r==='2'?'checked':'')+' onchange="pick(this)">② 需求减少</label>'
    + '<label class="r3"><input type="radio" name="p'+i+'" value="3" '+(r==='3'?'checked':'')+' onchange="pick(this)">③ 换我方货号</label></div>';
  if (r==='1') h += '<input class="note" type="text" data-f="note_who" placeholder="丢给了谁？（竞品 / 客户换供应商）" value="'+esc(d.note_who)+'" oninput="setNote(this)">';
  if (r==='2') h += '<input class="note" type="text" data-f="note_who" placeholder="需求为什么减少？（项目终止/预算/自产/季节）" value="'+esc(d.note_who)+'" oninput="setNote(this)">';
  if (r==='3'){
    h += '<input class="note" type="text" data-f="note_sku" placeholder="换成我方的什么货号？" value="'+esc(d.note_sku)+'" oninput="setNote(this)">';
    if (x.hint_ups && x.hint_ups.length){
      h += '<div class="hint" style="margin-top:3px">同客户上升货号（点击填入）：';
      x.hint_ups.forEach(function(u){
        h += '<span class="chip'+(u.same_mother?' same':'')+'" data-sku="'+esc(u.sku)+'" onclick="setSku(this)">'+esc(u.sku)+(u.same_mother?'·同母体':'')+' +'+wan(u.delta)+'万</span>';
      });
      h += '</div>';
    }
  }
  box.innerHTML = h;
}

function rowDecline(x, i){
  var d = ST.dec[x.id]||{}, cls = d.reason?'done':((x.a25-x.a26)>200000?'warnrow':'');
  var line = (x.hint_ups||[]).map(function(u){ return '<span class="chip'+(u.same_mother?' same':'')+'">'+esc(u.sku)+' +'+wan(u.delta)+'万</span>' }).join('') || '<span class="tiny">—</span>';
  var sc = x.suggest.indexOf('①')>=0?'s1':(x.suggest.indexOf('②')>=0?'s2':'s3');
  return '<tr data-i="'+i+'" class="'+cls+'">'
    + '<td>'+esc(x.cust)+'<div class="tiny">'+esc(x.sp)+'</div></td>'
    + '<td class="mono">'+esc(x.sku)+'<div class="tiny">'+esc(x.name)+'</div></td>'
    + '<td class="num">'+wan(x.a25)+'</td><td class="num">'+wan(x.a26)+'</td>'
    + '<td class="num drop">'+x.pct.toFixed(0)+'%</td><td class="num tiny">'+esc(x.last)+'</td>'
    + '<td>'+line+'<div class="tiny">同母体 '+x.fam_pct.toFixed(0)+'% · 客户 '+x.cust_pct.toFixed(0)+'%</div></td>'
    + '<td><span class="sug '+sc+'">'+esc(x.suggest)+'</span><div class="tiny">'+esc(x.suggest_why)+'</div></td>'
    + '<td class="rz"></td></tr>';
}

function renderTable(){
  var q=(document.getElementById('q').value||'').toLowerCase(), f=document.getElementById('f').value;
  var idx=[], html=[];
  D.declines.forEach(function(x,i){
    if (q && (x.cust+x.sku+x.name+x.sp).toLowerCase().indexOf(q)<0) return;
    var r=(ST.dec[x.id]||{}).reason||'';
    if (f==='todo'&&r) return; if (f==='done'&&!r) return;
    if (['1','2','3'].indexOf(f)>=0 && r!==f) return;
    idx.push(i); html.push(rowDecline(x,i));
  });
  document.getElementById('tb').innerHTML = html.join('');
  idx.forEach(function(i){ renderReason(i) });
  document.getElementById('cnt').textContent = '显示 '+idx.length+' / '+D.declines.length+' 条';
}

/* —— 新增 / 上量 —— */
function rowNew(x, kind, i){
  var c = confAt(kind,i);
  return '<tr data-i="'+i+'" data-kind="'+kind+'">'
    + '<td>'+esc(x.cust)+'<div class="tiny">'+esc(x.sp)+'</div></td>'
    + '<td class="mono">'+esc(x.sku)+'<div class="tiny">'+esc(x.name)+'</div></td>'
    + '<td class="num">'+(x.a25?wan(x.a25):'0.0')+'</td><td class="num">'+wan(x.a26)+'</td>'
    + '<td class="num">'+(x.pct!=null?('+'+x.pct.toFixed(0)+'%'):'新增')+'</td>'
    + '<td><label class="tiny"><input type="checkbox" data-f="reg" '+(c.reg?'checked':'')+' onchange="setConf(this)"> 已确认走注册申报</label><br>'
    + '<label class="tiny"><input type="checkbox" data-f="mass" '+(c.mass?'checked':'')+' onchange="setConf(this)"> 已确认量产/上量</label>'
    + '<input class="note" type="text" data-f="note" placeholder="备注：项目 / 注册阶段 / 预计放量" value="'+esc(c.note)+'" oninput="setConf(this)"></td></tr>';
}
function renderNew(){
  var h=[];
  D.news.forEach(function(x,i){ h.push(rowNew(x,'new',i)) });
  D.grows.forEach(function(x,i){ h.push(rowNew(x,'grow',i)) });
  document.getElementById('tbN').innerHTML = h.join('');
  renderKPI();
}

/* —— KPI / 汇总 —— */
function renderKPI(){
  var dec=D.declines, tot=0, by={'1':{n:0,a:0},'2':{n:0,a:0},'3':{n:0,a:0}}, done=0;
  dec.forEach(function(x){
    var v=x.a25-x.a26; tot+=v;
    var r=(ST.dec[x.id]||{}).reason;
    if(r){ done++; by[r].n++; by[r].a+=v }
  });
  var pct = dec.length?Math.round(done/dec.length*100):0;
  document.getElementById('k1').textContent=dec.length;
  document.getElementById('k2').textContent=wan(tot)+' 万';
  document.getElementById('k3').textContent=done+' / '+dec.length+'（'+pct+'%）';
  document.getElementById('k4').textContent=wan(by['1'].a)+' 万'; document.getElementById('k4s').textContent=by['1'].n+' 条';
  document.getElementById('k5').textContent=wan(by['2'].a)+' 万'; document.getElementById('k5s').textContent=by['2'].n+' 条';
  document.getElementById('k6').textContent=wan(by['3'].a)+' 万'; document.getElementById('k6s').textContent=by['3'].n+' 条';

  var cust={};
  dec.forEach(function(x){
    var c=cust[x.cust]||(cust[x.cust]={tot:0,a1:0,a2:0,a3:0,todo:0});
    var v=x.a25-x.a26, r=(ST.dec[x.id]||{}).reason; c.tot+=v;
    if(r) c['a'+r]+=v; else c.todo+=v;
  });
  var arr=Object.keys(cust).map(function(k){return [k,cust[k]]}).sort(function(a,b){return b[1].tot-a[1].tot});
  document.getElementById('tbC').innerHTML=arr.map(function(kv){
    var k=kv[0],c=kv[1];
    return '<tr><td>'+esc(k)+'</td><td class="num">'+wan(c.tot)+'</td><td class="num">'+wan(c.a1)+'</td><td class="num">'+wan(c.a2)+'</td><td class="num">'+wan(c.a3)+'</td><td class="num">'+(c.todo?('<b style="color:#b45309">'+wan(c.todo)+'</b>'):'0.0')+'</td></tr>';
  }).join('');

  var det=[];
  dec.forEach(function(x){
    var d=ST.dec[x.id]||{}; if(!d.reason) return;
    var tag=({1:'① 丢单',2:'② 需求减少',3:'③ 换号'})[d.reason];
    var note=d.reason==='1'?('丢给：'+(d.note_who||'—')):(d.reason==='2'?('原因：'+(d.note_who||'—')):('换成：'+(d.note_sku||'—')));
    det.push('<tr><td>'+esc(x.cust)+'</td><td class="mono">'+esc(x.sku)+'</td><td class="num">'+wan(x.a25)+'</td><td class="num">'+wan(x.a26)+'</td><td>'+tag+'</td><td>'+esc(note)+'</td></tr>');
  });
  document.getElementById('tbD').innerHTML=det.join('')||'<tr><td colspan="6" class="tiny">还没有已判定记录</td></tr>';

  var nReg=0,nMass=0;
  D.news.forEach(function(x,i){var c=confAt('new',i); if(c.reg)nReg++; if(c.mass)nMass++});
  D.grows.forEach(function(x,i){var c=confAt('grow',i); if(c.reg)nReg++; if(c.mass)nMass++});
  document.getElementById('n1').textContent=D.news.length;
  document.getElementById('n2').textContent=D.grows.length;
  document.getElementById('n3').textContent=nReg;
  document.getElementById('n4').textContent=nMass;
}

/* —— 导出 —— */
function dl(name,text,type){
  var b=new Blob([text],{type:type||'application/json;charset=utf-8'});
  var a=document.createElement('a'); a.href=URL.createObjectURL(b); a.download=name;
  document.body.appendChild(a); a.click(); setTimeout(function(){URL.revokeObjectURL(a.href);a.remove()},300);
}
function exportJSON(){
  var dec=D.declines.map(function(x){
    var d=ST.dec[x.id]||{};
    return {cust:x.cust,sp:x.sp,sku:x.sku,name:x.name,a25:x.a25,a26:x.a26,pct:x.pct,
      reason:d.reason||'', reason_text:({1:'丢单',2:'需求减少',3:'换号'})[d.reason]||'',
      note_who:d.note_who||'', note_sku:d.note_sku||'', status:d.reason?'已判定':'待判定'};
  });
  var opp=[];
  D.news.forEach(function(x,i){var c=confAt('new',i); opp.push({type:'新增',cust:x.cust,sku:x.sku,name:x.name,a26:x.a26,pct:null,reg_confirmed:!!c.reg,mass_confirmed:!!c.mass,note:c.note||''})});
  D.grows.forEach(function(x,i){var c=confAt('grow',i); opp.push({type:'上量',cust:x.cust,sku:x.sku,name:x.name,a26:x.a26,pct:x.pct,reg_confirmed:!!c.reg,mass_confirmed:!!c.mass,note:c.note||''})});
  dl('大客户部_货号判定_'+new Date().toISOString().slice(0,10)+'.json', JSON.stringify({meta:D.meta,declines:dec,opportunities:opp},null,1));
}
function exportCSV(){
  var head=['客户','销售员','货号','品名','2025(元)','2026(元)','降幅%','原因','备注1(丢给谁/需求原因)','备注2(换成货号)'];
  var lines=[head.join(',')];
  D.declines.forEach(function(x){
    var d=ST.dec[x.id]||{};
    var rt=({1:'丢单',2:'需求减少',3:'换号'})[d.reason]||'待判定';
    var row=[x.cust,x.sp,x.sku,x.name,x.a25,x.a26,x.pct,rt,d.note_who||'',d.note_sku||''];
    lines.push(row.map(function(v){var s=String(v==null?'':v);return /[",\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s}).join(','));
  });
  dl('大客户部_货号判定_明细.csv','\ufeff'+lines.join('\n'),'text/csv;charset=utf-8');
}
function resetAll(){
  if(!confirm('清空本机所有判定（不可恢复）？建议先导出 JSON。')) return;
  ST={dec:{},conf:{}}; save(); renderTable(); renderNew();
}
function switchTab(i){
  ['A','B','C'].forEach(function(t,k){
    document.getElementById('tab'+t).classList.toggle('on',k===i);
    document.getElementById('pane'+t).classList.toggle('hidden',k!==i);
  });
  if(i===2) renderKPI();
}
window.addEventListener('DOMContentLoaded',function(){ renderTable(); renderNew(); });

"""

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="wrap">
<h1>大客户部 · 货号下降原因判定台（2025 vs 2026 同期）</h1>
<div class="sub">
口径：标准销售订单 + 非赠品 + 已审核，<b>2025 年 1–9 月 vs 2026 年 1–9 月</b>｜大客户部 <b>__NCUST__ 家客户</b>｜下降纳入条件：2025 同期 ≥1 万元且降幅 ≥30%<br>
判定规则（人工三选一，必须备注）：<b>① 丢单</b>＝客户需求量未变、流向其他供应商 → 备注「丢给谁」；<b>② 需求减少</b>＝没丢单、客户需求量下降 → 备注原因；<b>③ 换我方货号</b>＝没丢单、改用我方其他货号 → 备注「换成什么货号」
　·　建议标签只是<b>算法线索</b>（同母体/客户整体变化），最终以人工判定为准。草稿自动存在本机浏览器。
</div>

<details class="scope">
<summary>口径修正记录（2026-10-17 第二版）：客户范围改以《诊断原料&amp;生命科学客户清单20260930.xls》为准 ▾</summary>
<div class="scope-in">
 <p><b>权威源：</b>官方客户清单「大客户销售部」sheet = <b>__N93__ 家</b>（17 个集团 + 独立客户），其中 2025/2026 有 ERP 流水 <b>__N47__ 家</b>；部门与销售员均以该表为准，<b>不再用 ERP 开单人推导、也不按金额反推</b>。</p>
 <p><b>旧版四处错：</b>① 用 ERP「销售员」圈客户（开单人≠客户归属，管理者刘新元、外部门同事都会挂单）；② 名单混入刘新元（管理）、任鑫/杜建（销售拓展部）；③ ERP 2025 是「大客户1/大客户2」岗位名、2026 是人名，跨年比对错位；④ 一条订单命中就整户纳入。</p>
 <p><b>数字变化：</b>下降 <b>74 条 / 577.9 万 / 28 家</b> → <b>__ND__ 条 / __DAMT__ 万 / __NDC__ 家</b>；新增 23→__NN__；上量 18→__NG__。官方清单还把 5 家不在名录的客户剔出（迈克生物、潮州凯普、四川安可瑞、四川迪亚、西安安瑞科），并补入 3 家有流水的（河南正锐、洛阳易研、郑州大学）。</p>
 <table class="rm"><thead><tr><th>被剔除客户（原下降清单内）</th><th>官方销售员</th><th>官方部门</th><th class="num">2025(万)</th><th class="num">2026(万)</th></tr></thead><tbody>
__REMOVED__
 </tbody></table>
 <p class="tiny">另：官方清单对个别客户归属与 10-06 的人工覆盖不同（如优联瑞康官方=徐卓凡、成都普利泰=陈溪子、上海快灵=张文蝶），已一律以官方清单为准。</p>
</div>
</details>

<div class="kpis">
  <div class="kpi"><div class="l">下降条目</div><div class="v" id="k1">—</div><div class="s">客户 × 货号</div></div>
  <div class="kpi"><div class="l">下降金额合计</div><div class="v" id="k2">—</div><div class="s">2025 同期 − 2026 同期</div></div>
  <div class="kpi"><div class="l">判定进度</div><div class="v" id="k3">—</div><div class="s">已判定 / 全部</div></div>
  <div class="kpi" style="border-top-color:#b3261e"><div class="l">① 丢单</div><div class="v" id="k4">—</div><div class="s" id="k4s">—</div></div>
  <div class="kpi" style="border-top-color:#1F4E79"><div class="l">② 需求减少</div><div class="v" id="k5">—</div><div class="s" id="k5s">—</div></div>
  <div class="kpi" style="border-top-color:#1c7c54"><div class="l">③ 换我方货号</div><div class="v" id="k6">—</div><div class="s" id="k6s">—</div></div>
</div>

<div class="tabs">
  <div class="tab on" id="tabA" onclick="switchTab(0)">A · 下降待判定（__ND__）</div>
  <div class="tab" id="tabB" onclick="switchTab(1)">B · 新机会 / 上量（__NN__ + __NG__）</div>
  <div class="tab" id="tabC" onclick="switchTab(2)">C · 汇总与导出</div>
</div>

<div id="paneA">
  <div class="bar">
    <input type="text" id="q" placeholder="搜索 客户 / 货号 / 品名 / 销售员" style="width:280px" oninput="renderTable()">
    <select id="f" onchange="renderTable()">
      <option value="">全部</option>
      <option value="todo">只看未判定</option>
      <option value="done">只看已判定</option>
      <option value="1">只看 ① 丢单</option>
      <option value="2">只看 ② 需求减少</option>
      <option value="3">只看 ③ 换号</option>
    </select>
    <span class="tiny" id="cnt"></span>
    <span style="margin-left:auto"></span>
    <button class="btn ghost" onclick="exportCSV()">导出 CSV</button>
    <button class="btn" onclick="exportJSON()">导出 JSON（含判定）</button>
  </div>
  <div class="card" style="padding:0;overflow:auto;max-height:74vh">
    <table><thead><tr>
      <th>客户 / 销售员</th><th>货号 / 品名</th><th>2025(万)</th><th>2026(万)</th><th>降幅</th><th>最后成交</th>
      <th>线索：同客户上升货号 / 同母体·客户变化</th><th>建议（算法线索）</th><th style="min-width:300px">人工判定 + 备注</th>
    </tr></thead><tbody id="tb"></tbody></table>
  </div>
</div>

<div id="paneB" class="hidden">
  <div class="card">
    <b>B · 新拿到机会</b>：<b>新增</b> __NN__ 条（2026 首次成交 ≥3 万）、<b>上量</b> __NG__ 条（≥2× 于 2025 同期）。
    请勾选是否<b>已确认走注册申报</b> / <b>已确认量产上量</b>，并写清项目阶段 —— 这两项决定它算不算「可形成的后续采购转化」。
    <div class="bar" style="margin-top:8px"><span class="tiny">已确认注册申报 <b id="n3">0</b> 条 ｜ 已确认量产 <b id="n4">0</b> 条 ｜ 新增 <b id="n1">0</b> ｜ 上量 <b id="n2">0</b></span>
      <span style="margin-left:auto"></span><button class="btn" onclick="exportJSON()">导出 JSON</button></div>
  </div>
  <div class="card" style="padding:0;overflow:auto;max-height:74vh">
    <table><thead><tr><th>客户 / 销售员</th><th>货号 / 品名</th><th>2025(万)</th><th>2026(万)</th><th>变化</th><th style="min-width:320px">转化确认</th></tr></thead><tbody id="tbN"></tbody></table>
  </div>
</div>

<div id="paneC" class="hidden">
  <div class="stat">
    <div class="card"><div class="big">按客户 · 下降金额与原因分布（万元）</div>
      <table style="margin-top:8px"><thead><tr><th>客户</th><th class="num">下降合计</th><th class="num">① 丢单</th><th class="num">② 需求减少</th><th class="num">③ 换号</th><th class="num">未判定</th></tr></thead><tbody id="tbC"></tbody></table>
    </div>
    <div class="card"><div class="big">已判定明细</div>
      <div style="max-height:52vh;overflow:auto"><table><thead><tr><th>客户</th><th>货号</th><th class="num">2025</th><th class="num">2026</th><th>原因</th><th>备注</th></tr></thead><tbody id="tbD"></tbody></table></div>
    </div>
  </div>
  <div class="card">
    <div class="bar"><button class="btn" onclick="exportJSON()">导出 JSON（判定 + 机会确认）</button>
      <button class="btn ghost" onclick="exportCSV()">导出 CSV（判定明细）</button>
      <button class="btn warn" onclick="resetAll()">清空本机判定</button>
      <span class="tiny" style="margin-left:8px">JSON 可直接回传给我，我会把判定写回数据层并重算丢单/换号/需求减少的最终结论。</span></div>
  </div>
</div>
</div>
<script>__JS__</script></body></html>"""

REMOVED = [
    ('上海市重大传染病和生物安全研究院', '徐卓凡', '销售拓展部', 14.1, 0.2),
    ('成都普利泰生物科技有限公司', '陈溪子', '销售拓展部', 21.2, 11.5),
    ('优联瑞康(上海)基因科技有限公司', '徐卓凡', '销售拓展部', 12.1, 1.3),
    ('深圳市雅为世纪科技有限公司', '陈溪子', '销售拓展部', 37.2, 9.8),
    ('上海快灵生物科技有限公司', '张文蝶', '销售拓展部', 11.2, 1.7),
    ('重庆京因生物科技有限责任公司', '陈溪子', '销售拓展部', 12.6, 7.8),
    ('苏州旭达生物科技有限公司', '张文蝶', '销售拓展部', 1.1, 0.2),
    ('复旦大学', '吴云', '生命科学部', 5.2, 0.5),
    ('成都大学', '陈溪子', '销售拓展部', 1.1, 0.5),
    ('江苏和福生物科技有限公司', '张文蝶', '销售拓展部', 5.0, 0.0),
    ('浙江塔歌诺思生物技术有限公司', '杨天雄', '销售拓展部', 2.0, 0.0),
]
_rm = '\n'.join('<tr><td>%s</td><td>%s</td><td>%s</td><td class="num">%.1f</td><td class="num">%.1f</td></tr>' % r for r in REMOVED)
_damt = sum(x['a25'] - x['a26'] for x in D['declines']) / 1e4
_ndc = len({x['cust'] for x in D['declines']})
page = (HTML.replace('__CSS__', CSS).replace('__JS__', JS.replace('__DATA__', DATA_JS))
        .replace('__ND__', str(len(D['declines']))).replace('__NN__', str(len(D['news'])))
        .replace('__NG__', str(len(D['grows']))).replace('__NCUST__', str(D['meta']['dk_cust']))
        .replace('__REMOVED__', _rm).replace('__DAMT__', '%.1f' % _damt).replace('__NDC__', str(_ndc))
        .replace('__N93__', '93').replace('__N47__', '47'))
assert '__CSS__' not in page and '__JS__' not in page and '__DATA__' not in page
io.open(OUT, 'w', encoding='utf-8').write(page)
print('✅ %s（%.0f KB）' % (OUT, os.path.getsize(OUT) / 1024))
