#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""构建「客户拉通大表 v11」：V2 活动录入 / 商机审批同步版。"""
import copy
import json
import pathlib
import re

ROOT = pathlib.Path('/Users/liuxinyuan/Desktop/Hermes输出-工作类')
SRC = ROOT / '工具/客户拉通大表_demo_v10_活动任务闭环_20261031_V01.html'
OUT = ROOT / '工具/客户拉通大表_demo_v11_活动录入V2同步_20261101_V01.html'
s = SRC.read_text(encoding='utf-8')

# ---------- 1. 解析并更新 DATA.activities ----------
st = s.index('const DATA = ') + len('const DATA = ')
en = s.index(';\nconst ICONS', st)
d = json.loads(s[st:en])
acts = d.get('activities') or []
base = copy.deepcopy(acts[0]) if acts else {}

# 项目场景：商机分类第一层（用户 2026-11-01 定版）
SCENE_BY_CUST = {
    '圣湘生物科技股份有限公司': '三类证注册',
    '深圳市新产业生物医学工程股份有限公司': '三类证注册',
    '深圳迈瑞生物医疗电子股份有限公司': '三类证注册',
    '广州达安基因股份有限公司': '三类证注册',
    '上海纳全生物医学科技有限公司': '三类证注册',
    '东莞华大智造科技股份有限公司': '三类证注册',
    '广州金域医学检验集团股份有限公司': '三类证注册',
    '浙江大学生命科学研究院（课题组·示例）': '科研客户',
}
for _dept in (d.get('layer1', {}).get('depts') or []):
    for _r in (_dept.get('rows') or []):
        _r['scene'] = _r.get('scene') or SCENE_BY_CUST.get(_r.get('cust'), '其他')

def add_fields(rec, **kw):
    rec.update(kw)
    return rec

# 老两条：补 V2 字段
if len(acts) > 0:
    add_fields(acts[0], activityType='线下拜访', contentType='测试结果跟进', projectScene='三类证注册',
              approvalStatus='已通过', approval='已通过 · 刘欣', approver='刘欣', rejectReason='',
              test='TS-20260812-07｜A/B 通过，C 组 Ct 漂移 1.4',
              next='研发重做 C 组配方并复测', activityV2=True)
if len(acts) > 1:
    add_fields(acts[1], activityType='线上跟进', contentType='订单跟进', projectScene='三类证注册',
              approvalStatus='不涉及', approval='不涉及', approver='', rejectReason='',
              test='—', next='报价确认 → 合同 → 首单', activityV2=True)

# 新增 3 条 V2 场景示例
def v2rec(act_id, cust, opp, dept, sales, activity_type, content_type, project_scene, approval_status,
          approval, approver, test, next_action, desc, date='2026-10-31'):
    r = copy.deepcopy(base)
    r.update({
        'actId': act_id, 'dept': dept, 'cid': '', 'cust': cust, 'opp': opp,
        'sales': sales, 'tech': '', 'scale': '¥300,000', 'date': date, 'contact': '王工',
        'order': '', 'type': {'项目需求挖掘': '项目跟进', '测试结果跟进': '项目跟进',
                              '客户档案完善': '客户档案完善', '订单跟进': '采购跟进', '其他': '项目跟进'}.get(content_type, '项目跟进'),
        'activityType': activity_type, 'contentType': content_type, 'projectScene': project_scene,
        'approvalStatus': approval_status, 'approval': approval, 'approver': approver,
        'rejectReason': '', 'test': test, 'next': next_action,
        'desc': desc, 'todos': [{'item': next_action, 'owner': sales, 'due': '2026-11-15'}],
        'supportTypes': [], 'activityV2': True,
    })
    return r

new = [
    v2rec('ACT-20261031-V201', '圣湘生物科技股份有限公司', '呼吸道四重原料替换',
          '大客户部', '赵云浩', '客户来访', '项目需求挖掘', '三类证注册', '待主管审核',
          '待主管审核 · 刘欣', '刘欣', '—',
          '主管审核通过后寄 E43 样品，2 周内回收四重 qPCR 数据',
          '客户来访：四重呼吸道项目处于原料替换阶段；现用 A 品牌，客户反馈批间差；本次确认测试需求与预算窗口。'),
    v2rec('ACT-20261031-V202', '广州达安基因股份有限公司', '数字PCR扩增酶体系开发',
          '大客户部', '赵云浩', '协同拜访', '客户档案完善', '三类证注册', '不涉及',
          '不涉及', '', '—', '补齐采购决策链与 TOP3 供应商份额',
          '协同拜访：补齐决策链（研发/采购/质量），客户现有供应商 3 家，我司份额约 10%。'),
    v2rec('ACT-20261031-V203', '浙江大学生命科学研究院（课题组·示例）', 'mRNA 原料试用',
          '销售拓展部', '张三', '陌拜', '其他', '科研客户', '不涉及',
          '不涉及', '', '—', '整理陌拜纪要，转商机挖掘',
          '陌拜：课题组 mRNA 原料试用需求，初步了解转染与体外转录体系；待转正式商机。'),
]
d['activities'] = acts + new
d.setdefault('meta', {})
d['meta']['version'] = 'v11-20261101'
d['meta']['desc'] = '客户拉通大表 v11 · V2 活动录入/商机审批同步'
s = s[:st] + json.dumps(d, ensure_ascii=False, separators=(',', ':')) + s[en:]

# ---------- 2. 标题/说明 ----------
s = s.replace('客户拉通大表 · 活动任务闭环 DEMO', '客户拉通大表 · V2 活动同步 DEMO')
s = s.replace('客户拉通大表 · DEMO（两层设计 · 示例数据）', '客户拉通大表 · V2 活动同步 DEMO')
s = s.replace('① 第一层｜项目推移表 ｜ ② 第二层｜进入活动与待办 ｜ ③ 任务闭环｜活动派单 ｜ ④ 活动质量｜总监日报',
              '① 第一层｜项目推移表 ｜ ② 第二层｜活动录入 V2 ｜ ③ 任务闭环｜活动派单 ｜ ④ 活动质量｜总监日报 ｜ V2 活动/商机审批已并入第二层')
s = s.replace('② 第二层｜进入活动与待办', '② 第二层｜活动录入 V2')
s = s.replace('DEMO 示例数据 · 客户名为真实公司名，商机/日期/负责人/金额/活动内容均为<b>示例值</b>，未上线。',
              'V2 同步 DEMO · 前 2 条为旧结构补齐，后 3 条为 V2 场景示例；可点「从工作台同步 V2」读取真实活动与审批。')

# 项目场景作为关联对象第一层
s = s.replace('<label>客户名称（选择）<select id="f_cust"></select></label>',
              '<label>项目场景（第一层分类 · 只读继承）<input id="f_scene_ro" class="ro" readonly></label>\n        <label>客户名称（选择）<select id="f_cust"></select></label>', 1)

# ---------- 3. L2 同步条 ----------
sync_bar = '''
    <div class="v2syncbar" id="v2SyncBar" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 12px;padding:9px 12px;border:1px solid #f0d9a8;background:#fff8e6;border-radius:9px;font-size:12px;color:#7a4c08">
      <b>V2 同步</b><span id="v2SyncState">当前显示示例数据；登录工作台后可拉取真实活动与商机审批。</span>
      <span style="flex:1"></span>
      <button type="button" class="btn" onclick="loadV2FromWorkbench()">从工作台同步 V2</button>
      <a href="活动录入.html" target="_blank" style="border:1px solid #e2c98f;background:#fff;border-radius:8px;padding:6px 10px;color:#7a4c08;text-decoration:none;font-weight:700">打开 V2 活动录入</a>
    </div>
'''
needle = '  <div class="panel" id="panel-l2" style="display:none">\n'
assert needle in s
s = s.replace(needle, needle + sync_bar, 1)

# ---------- 4. renderL2 列与单元格 ----------
s = s.replace("""    {key:'type',  title:'活动类型', w:104, group:'base'},
    {key:'order', title:'关联订单/测试单', w:132, group:'base'},
    {key:'desc',  title:'活动描述（模板化）', w:430, group:'desc'},""",
"""    {key:'activityType', title:'活动类型（渠道）', w:96, group:'base'},
    {key:'contentType', title:'活动内容（场景）', w:116, group:'base'},
    {key:'approval', title:'商机审批', w:128, group:'v2'},
    {key:'test', title:'测试单/结果', w:190, group:'v2'},
    {key:'next', title:'下一步动作', w:190, group:'v2'},
    {key:'order', title:'关联订单/测试单', w:126, group:'base'},
    {key:'desc',  title:'活动描述（模板化）', w:390, group:'desc'},""", 1)
s = s.replace("""  const b=cols.filter(c=>c.group==='base'), d=cols.filter(c=>c.group==='desc');
  const td=cols.filter(c=>c.group==='todo'), wr=cols.filter(c=>c.group==='wr');""",
"""  const b=cols.filter(c=>c.group==='base'), d=cols.filter(c=>c.group==='desc');
  const td=cols.filter(c=>c.group==='todo'), wr=cols.filter(c=>c.group==='wr'), v2=cols.filter(c=>c.group==='v2');""", 1)
s = s.replace("""    + '<th class="band band-base stick" colspan="'+b.length+'" style="left:0">活动信息（抓取第一层）</th>'
    + '<th class="band band-act" colspan="'+d.length+'">活动描述（按类型模板）</th>'""",
"""    + '<th class="band band-base stick" colspan="'+b.length+'" style="left:0">活动信息（抓取第一层）</th>'
    + '<th class="band band-v2" colspan="'+v2.length+'">V2 商机/测试/下一步</th>'
    + '<th class="band band-act" colspan="'+d.length+'">活动描述（按类型模板）</th>'""", 1)
s = s.replace("第二层｜活动录入表 · 一行一活动（提交后三轨落盘：本地记录 / CloudBase / 飞书描述）",
              "第二层｜活动录入 V2 · 一行一活动（活动类型/内容两层；新商机审批；测试/订单/下一步同步）", 1)
# 行渲染新字段
s = s.replace("""      if(c.key==='desc'){
        body+='<td title="'+esc(m.desc||'')+'">'+esc(short10((m.desc||'').replace(/\\n/g,' '),90))+'</td>'; return;
      }""",
"""      if(c.key==='activityType'){
        body+='<td><span style="display:inline-block;padding:2px 7px;border-radius:999px;background:#e8f0ff;color:#1852B6;font-weight:700;font-size:11px">'+esc(m.activityType||'—')+'</span></td>'; return;
      }
      if(c.key==='contentType'){
        body+='<td><span style="display:inline-block;padding:2px 7px;border-radius:999px;background:#e6f6ef;color:#0f7a3d;font-weight:700;font-size:11px">'+esc(m.contentType||'—')+'</span></td>'; return;
      }
      if(c.key==='approval'){
        const ast=m.approvalStatus||'不涉及';
        const c=ast==='已通过'?'#0f7a3d':(ast==='待主管审核'?'#9a5b09':(ast==='需补充'?'#C0392B':(ast==='总监否决'?'#C0392B':'#64748b')));
        const bg=ast==='已通过'?'#e6f6ef':(ast==='待主管审核'?'#fff8e6':(ast==='需补充'||ast==='总监否决'?'#fdecec':'#f1f5f9'));
        body+='<td><span style="display:inline-block;padding:2px 7px;border-radius:999px;background:'+bg+';color:'+c+';font-weight:700;font-size:11px">'+esc(m.approval||ast)+'</span></td>'; return;
      }
      if(c.key==='test'){
        body+='<td title="'+esc(m.test||'')+'">'+esc(short10(m.test||'—',48))+'</td>'; return;
      }
      if(c.key==='next'){
        body+='<td title="'+esc(m.next||'')+'">'+esc(short10(m.next||'—',52))+'</td>'; return;
      }
      if(c.key==='desc'){
        body+='<td title="'+esc(m.desc||'')+'">'+esc(short10((m.desc||'').replace(/\\n/g,' '),78))+'</td>'; return;
      }""", 1)

# ---------- 5. 同步函数 ----------
sync_js = r'''
/* ================= V2 同步 ================= */
function v2ApiBase(){
  return (location.hostname==='127.0.0.1'||location.hostname==='localhost'||location.protocol==='file:')
    ? '' : 'https://api.biorisales.site:8443';
}
function v2Token(){
  try{return localStorage.getItem('bior_auth_token')||'';}catch(e){return '';}
}
function v2ChannelLabel(ch){
  return ({'online':'线上跟进','offline':'线下拜访','客户来访':'客户来访'})[ch]||ch||'—';
}
function v2MapRecord(r,appr){
  const b=(r.blocks||[])[0]||{};
  const a=appr||{};
  const items=((r.test_order||{}).items)||[];
  const test=r.test_order&&r.test_order.no
    ? (r.test_order.no+'｜'+items.map(x=>(x.sku||'')+' '+(x.result||'')).join('；'))
    : (r.feedback||'—');
  return {
    actId:r.id||'', dept:r.dept||'', cid:'', cust:r.customer||'', opp:(r.opp_names||[])[0]||b.opp||'',
    sales:r.sales||r.submitted_by||'', tech:'', scale:'', date:r.date||'', contact:r.contact||'',
    order:r.order_no||'', type:r.type||'', desc:r.feishu_text||b.need||b.visit_content||'',
    activityType:r.activity_type||v2ChannelLabel(r.channel),
    contentType:r.content_type||r.subtype||'',
    approvalStatus:a.status||'不涉及',
    approval:a.status?(({pending:'待主管审核',approved:'已通过',supplement:'需补充',vetoed:'总监否决'})[a.status]||a.status)+' · '+(a.approver||''):'不涉及',
    approver:a.approver||'', rejectReason:a.reject_reason||'',
    test:test||'—', next:(r.plan&&r.plan[0]&&r.plan[0].item)||'—',
    todos:(r.plan||[]).map(x=>({item:x.item||'',owner:x.owner||'',due:x.due||''})),
    supportTypes:[], activityV2:true
  };
}
function loadV2FromWorkbench(){
  const token=v2Token();
  const st=document.getElementById('v2SyncState');
  if(!token){ if(st)st.textContent='未检测到工作台登录态；请从工作台打开本页，或保留示例数据。'; return; }
  if(st)st.textContent='正在拉取 V2 活动与商机审批…';
  const base=v2ApiBase();
  const headers={'Authorization':'Bearer '+token};
  Promise.all([
    fetch(base+'/api/activity/list',{headers:headers}).then(r=>r.json()),
    fetch(base+'/api/opportunity/list',{headers:headers}).then(r=>r.json()).catch(()=>({ok:false,items:[]}))
  ]).then(function(res){
    const lst=res[0]||{}, appr=res[1]||{};
    if(!lst.ok){throw new Error(lst.error||'活动列表读取失败');}
    const rows=(lst.activities||[]).slice(0,80);
    return Promise.all(rows.map(function(x){
      return fetch(base+'/api/activity/detail?id='+encodeURIComponent(x.id),{headers:headers})
        .then(r=>r.json()).then(j=>j.ok?j.activity:null).catch(()=>null);
    })).then(function(ds){
      const good=ds.filter(Boolean).filter(x=>x.v2);
      const amap={};
      (appr.items||[]).forEach(a=>{amap[a.activity_id]=a;});
      if(!good.length){ if(st)st.textContent='已连接，但没有 V2 活动记录；当前保留示例数据。'; return; }
      activities = good.map(x=>v2MapRecord(x,amap[x.id]));
      renderL2();
      if(st)st.textContent='已同步 '+activities.length+' 条 V2 活动；审批状态按 /api/opportunity/list 匹配。';
      toast('V2 已同步：'+activities.length+' 条');
    });
  }).catch(function(e){
    if(st)st.textContent='同步失败：'+(e&&e.message?e.message:e)+'；当前保留示例数据。';
    toast('V2 同步失败');
  });
}
'''
# 插在 renderL2 之前
needle2 = '/* ================= 第二层活动列表 ================= */'
assert needle2 in s
s = s.replace(needle2, sync_js + '\n' + needle2, 1)

# CSS band
s = s.replace('.band-wr{background:#e0f2fe;color:#0369a1}',
              '.band-wr{background:#e0f2fe;color:#0369a1}\n.band-v2{background:#fff4dc;color:#8a5a05}', 1)

# 工作台内嵌时隐藏同步条；有登录态则自动同步
embed_js = r'''
<script>
(function(){
  var embedded = new URLSearchParams(location.search).get('embed')==='1' || (window.parent && window.parent!==window);
  if(!embedded) return;
  var bar=document.getElementById('v2SyncBar');
  if(bar) bar.style.display='none';
  try{
    if(typeof v2Token==='function' && v2Token() && typeof loadV2FromWorkbench==='function'){
      setTimeout(function(){ loadV2FromWorkbench(); }, 300);
    }
  }catch(e){}
})();
</script>
'''
s = s.replace('</body>', embed_js + '\n</body>', 1)

# 项目场景：继承 + 活动记录 + 表格列
s = s.replace("  $('f_opp').value=hit.opp;\n  $('f_dept_ro').value=hit.dept||'';",
              "  $('f_opp').value=hit.opp;\n  $('f_scene_ro').value=hit.scene||'其他';\n  $('f_dept_ro').value=hit.dept||'';", 1)
s = s.replace("    actId:actId, dept:$('f_dept_ro').value, cid:$('f_cid_ro').value.trim(), cust:$('f_cust').value,",
              "    actId:actId, dept:$('f_dept_ro').value, cid:$('f_cid_ro').value.trim(), cust:$('f_cust').value,\n    projectScene:$('f_scene_ro').value,", 1)
s = s.replace("    {key:'opp',   title:'商机名称', w:190, group:'base'},",
              "    {key:'opp',   title:'商机名称', w:190, group:'base'},\n    {key:'projectScene', title:'项目场景（第一层）', w:100, group:'base'},", 1)
s = s.replace("    actId:r.id||'', dept:r.dept||'', cid:'', cust:r.customer||'', opp:(r.opp_names||[])[0]||b.opp||'',",
              "    actId:r.id||'', dept:r.dept||'', cid:'', cust:r.customer||'', opp:(r.opp_names||[])[0]||b.opp||'', projectScene:r.project_scene||a.project_scene||'其他',", 1)

OUT.write_text(s, encoding='utf-8')
print('WROTE', OUT, 'bytes', OUT.stat().st_size)
print('activities', len(d['activities']))
