#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""P1 前端补丁：工具/活动录入.html —— 活动形式 + 关联订单 + 待办确认清单。

改动面：
  F1  ST 增加 orderRefs / todoDrafts
  F2  「活动形式」必填下拉（线下拜访 / 线上跟进）
  F3  新增「关联订单」与「待办确认」两张卡片
  F4  新增 JS 块：订单检索 / 待办草稿生成与确认清单
  F5  collect() 带 channel / order_refs / draft_id / todos（只带已确认）
  F6  validate() 必填 channel + 提示未确认条数
  F7  showResult() 展示活动形式与待办落库结果
  F8  addPlan() 给计划行分配稳定 uid（待办指纹锚在它上面）
  F9  草稿保存/恢复/清空 覆盖新字段
每处断言命中次数 == 1。
"""
import io
import os
import re
import shutil
import sys
import time

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
F = os.path.join(ROOT, '工具', '活动录入.html')

F1_OLD = "var ST = {blocks:[], draftId:'', busy:false};"
F1_NEW = "var ST = {blocks:[], draftId:'', busy:false, orderRefs:[], todoDrafts:[], uidSeq:0};"

F2_OLD = '      <div class="f" id="wrapDate"><label>活动日期</label><input type="date" id="date"></div>'
F2_NEW = '''      <div class="f" id="wrapDate"><label>活动日期</label><input type="date" id="date"></div>
      <div class="f" id="wrapChannel"><label>活动形式 <span style="color:#d93025">*</span> <span style="font-weight:400;color:var(--grey)">（拜访视图按此计线下/线上次数）</span></label>
        <select id="channel">
          <option value="">请选择…</option>
          <option value="offline">线下拜访</option>
          <option value="online">线上跟进</option>
        </select></div>'''

F3_OLD = '  <div id="blocksHost"></div>'
F3_NEW = '''  <div id="blocksHost"></div>

  <div class="card" id="cardOrders">
    <h3><span class="n">＋</span>关联订单 <span style="font-weight:400;color:var(--grey);font-size:12px">（发货订单 / 测试订单；类型由系统按单据性质判定，不用手选）</span></h3>
    <div class="hint">活动相当于一条「对外待办」，关联订单后可回溯到具体发货单与测试单</div>
    <div class="f" style="position:relative">
      <label>检索订单编号或客户名</label>
      <input type="text" id="ordRefQ" placeholder="如 ZHBR260921046" autocomplete="off">
      <div class="slist" id="ordRefList"></div>
    </div>
    <div id="ordRefChips" style="display:flex;flex-wrap:wrap;gap:6px;margin-top:10px"></div>
  </div>

  <div class="card" id="cardTodo">
    <h3><span class="n">✓</span>待办确认 <span style="font-weight:400;color:var(--grey);font-size:12px">（提交前逐条确认 · 未确认的不落库）</span></h3>
    <div class="hint">点「生成待办预览」从「下一步计划」生成待办，并把「问题现象」派生为一条客诉跟进；可改可删，<b>只有勾了确认的才会写入工作台待办</b></div>
    <div class="addrow" style="display:flex;align-items:center;gap:10px">
      <button class="btn sm" type="button" onclick="genTodoDrafts(true)">生成待办预览</button>
      <span id="todoSummary" style="font-size:12px;color:var(--grey)"></span>
    </div>
    <div id="todoConfirmHost"></div>
  </div>'''

F4_ANCHOR = "/* ════════════ 关联商机 ════════════ */"

F4_JS = '''/* ════════════ P1：关联订单（发货单 / 测试单）════════════ */
var ordRefTimer = null;
function searchOrderRefs(q){
  var box = $('ordRefList'); if (!box) return;
  q = (q||'').trim();
  if (!q){ box.classList.remove('on'); return; }
  apiGet('/api/activity/orders', {q:q, limit:20}).then(function(j){
    var rows = (j && j.orders) || [];
    box.innerHTML = rows.map(function(o){
      return '<div onclick="addOrderRef(&quot;'+esc(o.no)+'&quot;)"><b>'+esc(o.no)+'</b> <small>'+esc(o.customer)+
             ' · '+esc(o.date)+' · '+(o.is_test?'<span style="color:#e37400">测试单</span>':'发货单')+'</small></div>';
    }).join('') || '<div class="hint" style="padding:8px">无匹配订单</div>';
    box.classList.add('on');
  }).catch(function(){ box.classList.remove('on'); });
}
function addOrderRef(no){
  if (!no) return;
  ST.orderRefs = ST.orderRefs || [];
  if (ST.orderRefs.indexOf(no) < 0) ST.orderRefs.push(no);
  var q = $('ordRefQ'); if (q) q.value = '';
  var b = $('ordRefList'); if (b) b.classList.remove('on');
  renderOrderRefs(); saveDraftSoon(); validate();
}
function delOrderRef(no){
  ST.orderRefs = (ST.orderRefs||[]).filter(function(x){ return x !== no; });
  renderOrderRefs(); saveDraftSoon();
}
function renderOrderRefs(){
  var h = $('ordRefChips'); if (!h) return;
  var list = ST.orderRefs || [];
  h.innerHTML = list.length ? list.map(function(no){
    return '<span class="chip">'+esc(no)+' <b style="cursor:pointer" onclick="delOrderRef(&quot;'+esc(no)+'&quot;)">×</b></span>';
  }).join('') : '<span style="font-size:12px;color:var(--grey)">尚未关联订单</span>';
}

/* ════════════ P1：待办确认清单（生成 ≠ 落库）════════════ */
var TD_TYPES = [
  ['external_visit','🔵 拜访计划'],['external_test_req','🧪 测试需求'],
  ['external_test_result','✅ 测试结果'],['external_project','🟢 项目进度'],
  ['external_appointment','📅 邀约及计划'],
  ['internal_project','🎯 立项跟进'],['internal_complaint','🔴 客诉跟进'],
  ['internal_test_plan','🧪 测试方案'],['internal_other','⚪ 其他（客户需求）']
];
function tdMerge(o, patch){
  var out = {}, k;
  for (k in o){ if (Object.prototype.hasOwnProperty.call(o,k)) out[k] = o[k]; }
  for (k in patch){ if (Object.prototype.hasOwnProperty.call(patch,k)) out[k] = patch[k]; }
  return out;
}
function genTodoDrafts(manual){
  syncFromDom();
  var old = {}; (ST.todoDrafts||[]).forEach(function(t){ old[t.uid] = t; });
  var out = [], defOwner = (USER && USER.name) || '';
  (ST.blocks||[]).forEach(function(b){
    (b.plan||[]).forEach(function(p){
      if (p.del || !(p.item||'').trim()) return;
      var uid = p.uid || ('p-'+b.k+'-'+out.length);
      p.uid = uid;
      var patch = {text:(p.item||'').trim(), owner:(p.owner||defOwner), due:p.due||''};
      out.push(old[uid] ? tdMerge(old[uid], patch)
                        : {uid:uid, text:patch.text, owner:patch.owner, due:patch.due,
                           type:(isPurchase()?'external_visit':'external_project'),
                           tech:false, confirmed:false, removed:false, from:'下一步计划'});
    });
    var iss = (b.issue||'').trim();
    if (iss){
      var uid2 = 'i-'+b.k, txt = iss.replace(/\\s+/g,' ').slice(0,80);
      out.push(old[uid2] ? tdMerge(old[uid2], {text:txt})
                         : {uid:uid2, text:txt, owner:defOwner, due:'',
                            type:'internal_complaint', tech:false, confirmed:false, removed:false, from:'问题现象'});
    }
  });
  ST.todoDrafts = out;
  renderTodoConfirm();
  if (manual) toast(out.length ? ('已生成 ' + out.length + ' 条待办草稿，请逐条确认') : '暂无可生成的待办（请先填「下一步计划」或「问题现象」）');
}
var TD_COLS = 'padding:5px 6px;border-bottom:1px solid #f1f3f4;vertical-align:top';
function renderTodoConfirm(){
  var h = $('todoConfirmHost'); if (!h) return;
  var list = (ST.todoDrafts||[]).filter(function(t){ return !t.removed; });
  var unc = list.filter(function(t){ return !t.confirmed; }).length;
  var sum = $('todoSummary');
  if (sum) sum.textContent = list.length
      ? ('共 ' + list.length + ' 条 ｜ 已确认 ' + (list.length-unc) + ' 条' + (unc ? (' ｜ ⚠ 未确认 ' + unc + ' 条（不会落库）') : ''))
      : '';
  if (!list.length){
    h.innerHTML = '<div class="hint" style="margin-top:8px">点上方「生成待办预览」——从「下一步计划」生成待办，并把「问题现象」派生为一条客诉跟进</div>';
    return;
  }
  var opts = function(sel){ return TD_TYPES.map(function(x){
      return '<option value="'+x[0]+'"'+(sel===x[0]?' selected':'')+'>'+x[1]+'</option>'; }).join(''); };
  var inp = 'style="width:100%;padding:4px 6px;font-size:13px"';
  h.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:13px;margin-top:8px">' +
    '<thead><tr>' +
      ['确认','事项','负责人','截止','类型','需技术协同',''].map(function(t,i){
        return '<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;font-size:12px;color:#5f6368;width:'+
               [44,0,110,130,150,80,44][i]+'px">'+t+'</th>'; }).join('') +
    '</tr></thead><tbody>' +
    list.map(function(t){
      return '<tr>' +
        '<td style="'+TD_COLS+'"><input type="checkbox"'+(t.confirmed?' checked':'')+
          ' onchange="tdConfirm(&quot;'+esc(t.uid)+'&quot;,this.checked)"></td>' +
        '<td style="'+TD_COLS+'"><input type="text" '+inp+' value="'+esc(t.text)+
          '" oninput="tdEdit(&quot;'+esc(t.uid)+'&quot;,&quot;text&quot;,this.value)">' +
          '<div style="font-size:11px;color:var(--grey);margin-top:2px">来源：'+esc(t.from||'')+'</div></td>' +
        '<td style="'+TD_COLS+'"><input type="text" list="userAll" '+inp+' value="'+esc(t.owner)+
          '" oninput="tdEdit(&quot;'+esc(t.uid)+'&quot;,&quot;owner&quot;,this.value)"></td>' +
        '<td style="'+TD_COLS+'"><input type="date" '+inp+' value="'+esc(t.due||'')+
          '" onchange="tdEdit(&quot;'+esc(t.uid)+'&quot;,&quot;due&quot;,this.value)"></td>' +
        '<td style="'+TD_COLS+'"><select '+inp+' onchange="tdEdit(&quot;'+esc(t.uid)+'&quot;,&quot;type&quot;,this.value)">'+opts(t.type)+'</select></td>' +
        '<td style="'+TD_COLS+';text-align:center"><input type="checkbox"'+(t.tech?' checked':'')+
          ' onchange="tdEdit(&quot;'+esc(t.uid)+'&quot;,&quot;tech&quot;,this.checked)"></td>' +
        '<td style="'+TD_COLS+'"><button class="btn sm" type="button" onclick="tdDel(&quot;'+esc(t.uid)+'&quot;)">删</button></td>' +
      '</tr>';
    }).join('') + '</tbody></table>';
}
function tdFind(uid){
  var l = ST.todoDrafts || [];
  for (var i=0;i<l.length;i++) if (l[i].uid === uid) return l[i];
  return null;
}
function tdConfirm(uid, on){ var t = tdFind(uid); if (t){ t.confirmed = !!on; renderTodoConfirm(); validate(); saveDraftSoon(); } }
function tdEdit(uid, f, v){
  var t = tdFind(uid); if (!t) return;
  if (f === 'tech'){ t.tech = !!v; }
  else { t[f] = v; }
  saveDraftSoon();
  if (f === 'text' || f === 'owner') validate();
}
function tdDel(uid){
  var t = tdFind(uid); if (!t) return;
  t.removed = true; renderTodoConfirm(); validate(); saveDraftSoon();
}
function p1Boot(){
  var q = $('ordRefQ');
  if (q) q.addEventListener('input', function(){
    clearTimeout(ordRefTimer);
    var v = this.value;
    ordRefTimer = setTimeout(function(){ searchOrderRefs(v); }, 260);
  });
  document.addEventListener('click', function(ev){
    var b = $('ordRefList');
    if (b && b.classList.contains('on') && !b.contains(ev.target) && ev.target !== q) b.classList.remove('on');
  });
  renderOrderRefs(); renderTodoConfirm();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', p1Boot); else p1Boot();

/* ════════════ 关联商机 ════════════ */'''

F5_OLD = """    blocks: blocks,
    order_no: first.test_order && first.test_order.no || ''
  };"""
F5_NEW = """    blocks: blocks,
    order_no: first.test_order && first.test_order.no || '',
    channel: (($('channel')||{}).value) || '',
    order_refs: (ST.orderRefs||[]).slice(),
    draft_id: (ST.draftId||''),
    todos: (ST.todoDrafts||[]).filter(function(t){ return t.confirmed && !t.removed; })
             .map(function(t){ return {uid:t.uid, text:t.text, owner:t.owner, due:t.due,
                                       type:t.type, tech_support:!!t.tech, confirmed:true}; })
  };"""

F6_OLD = "  if (!d.customer) miss.push('客户');"
F6_NEW = """  if (!d.customer) miss.push('客户');
  if (!d.channel) miss.push('活动形式（线下拜访 / 线上跟进）');"""

F6B_OLD = "  else { m.className='msg ok'; m.textContent = '信息齐备，可提交（' + d.blocks.length + ' 套跟进内容）'; }"
F6B_NEW = """  else {
    var _unc = (ST.todoDrafts||[]).filter(function(t){ return !t.removed && !t.confirmed; }).length;
    var _ok = d.todos.length;
    m.className='msg ok';
    m.textContent = '信息齐备，可提交（' + d.blocks.length + ' 套跟进内容）'
      + (_unc ? ('｜⚠ 还有 ' + _unc + ' 条待办未确认，将不会落库') : (_ok ? ('｜待办已确认 ' + _ok + ' 条') : ''));
  }"""

F7_OLD = "    '<span class=\"k\">跟进内容</span><span>'+(d.blocks||[]).length+' 套</span></div>' +"
F7_NEW = """    '<span class="k">跟进内容</span><span>'+(d.blocks||[]).length+' 套</span>' +
    '<span class="k">活动形式</span><span>'+(d.channel==='offline'?'线下拜访':(d.channel==='online'?'线上跟进':'—'))+'</span>' +
    '<span class="k">关联订单</span><span>'+((d.order_refs||[]).length ? esc(d.order_refs.join('、')) : '—')+'</span>' +
    '<span class="k">待办落库</span><span>'+((j.todos&&!j.todos.error) ? ('新建 '+j.todos.created+' ｜ 更新 '+j.todos.updated+' ｜ 跳过 '+j.todos.skipped) : '—')+'</span></div>' +"""

F8_OLD = "  if (typeof bindIdx !== 'number') b.plan.push({item:item||'', owner:owner||(USER&&USER.name)||'', due:due||''});"
F8_NEW = ("  if (typeof bindIdx !== 'number') b.plan.push({item:item||'', owner:owner||(USER&&USER.name)||'',\n"
          "      due:due||'', uid:'r'+((ST.uidSeq=(ST.uidSeq||0)+1))+'-'+k, tech:false});")

F9_OLD = """      customer:$('customer').value, contact:$('contact').value, date:$('date').value,
      type:$('type').value, subtype:$('subtype').value,
      blocks: ST.blocks"""
F9_NEW = """      customer:$('customer').value, contact:$('contact').value, date:$('date').value,
      type:$('type').value, subtype:$('subtype').value,
      channel:(($('channel')||{}).value)||'',
      orderRefs: ST.orderRefs, todoDrafts: ST.todoDrafts, uidSeq: ST.uidSeq,
      blocks: ST.blocks"""

F10_OLD = """  $('type').value = v.type||'项目跟进'; $('subtype').value = v.subtype||'';
  ST.blocks = (v.blocks||[]).length ? v.blocks : [];
  if (!ST.blocks.length) ST.blocks = [newBlock(null)];
  renderBlocks();
  toast('草稿已恢复');"""
F10_NEW = """  $('type').value = v.type||'项目跟进'; $('subtype').value = v.subtype||'';
  if ($('channel')) $('channel').value = v.channel||'';
  ST.orderRefs = v.orderRefs || []; ST.todoDrafts = v.todoDrafts || []; ST.uidSeq = v.uidSeq || 0;
  ST.blocks = (v.blocks||[]).length ? v.blocks : [];
  if (!ST.blocks.length) ST.blocks = [newBlock(null)];
  renderBlocks(); renderOrderRefs(); renderTodoConfirm();
  toast('草稿已恢复');"""

F11_OLD = """  $('date').value = today(); $('type').value='项目跟进'; $('subtype').value='';
  ST.blocks = [newBlock(null)];
  renderBlocks(); validate();"""
F11_NEW = """  $('date').value = today(); $('type').value='项目跟进'; $('subtype').value='';
  if ($('channel')) $('channel').value = '';
  ST.orderRefs = []; ST.todoDrafts = []; ST.uidSeq = 0;
  ST.blocks = [newBlock(null)];
  renderBlocks(); renderOrderRefs(); renderTodoConfirm(); validate();"""

PAIRS = [
    ('F1 ST 新字段', F1_OLD, F1_NEW),
    ('F2 活动形式下拉', F2_OLD, F2_NEW),
    ('F3 两张新卡片', F3_OLD, F3_NEW),
    ('F4 JS 新块', F4_ANCHOR, F4_JS),
    ('F5 collect 新字段', F5_OLD, F5_NEW),
    ('F6 validate 必填', F6_OLD, F6_NEW),
    ('F6b validate 文案', F6B_OLD, F6B_NEW),
    ('F7 showResult', F7_OLD, F7_NEW),
    ('F8 addPlan uid', F8_OLD, F8_NEW),
    ('F9 草稿保存', F9_OLD, F9_NEW),
    ('F10 草稿恢复', F10_OLD, F10_NEW),
    ('F11 清空表单', F11_OLD, F11_NEW),
]


def main():
    apply = '--apply' in sys.argv
    s = io.open(F, encoding='utf-8').read()
    print('P1 录入页补丁 —— %s\n' % ('APPLY' if apply else 'DRY-RUN'))
    ok = True
    for label, old, new in PAIRS:
        n = s.count(old)
        if n != 1:
            ok = False
        print('  %s %-22s 命中 %d' % ('✅' if n == 1 else '❌', label, n))
        if n == 1:
            s = s.replace(old, new, 1)
    if not ok:
        print('\n有未命中 → 不落盘')
        return 1
    if apply:
        ts = time.strftime('%Y%m%d-%H%M%S')
        bak = os.path.join(ROOT, '_bak_p1_entry_%s' % ts)
        os.makedirs(bak, exist_ok=True)
        shutil.copy2(F, os.path.join(bak, os.path.basename(F)))
        io.open(F, 'w', encoding='utf-8').write(s)
        print('\n已写（备份 %s）｜大小 %d → %d' % (bak, os.path.getsize(F), len(s.encode('utf-8'))))
    else:
        print('\n干跑通过，未落盘。')
    return 0


sys.exit(main())
