#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""新增 LS 专属 Tab「课题组经营」（KPI 1转化率/2沉睡/3成交额同比/4复购/6单组产出）。
两入口同改；KPI 按可见课题组现算（销售只看本人，管理员看全量）。"""
import io
import os
import re
import subprocess
import sys
import tempfile

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
FILES = [os.path.join(ROOT, '工具/index.html'), os.path.join(ROOT, '工具/app.html')]

TAB_ANCHOR = "  { id: 'groups', group: 'cust', label: '课题组', icon: '🔬', lines: ['ls'] },\n"
TAB_NEW = TAB_ANCHOR + "  { id: 'lsKpi', group: 'cust', label: '课题组经营', icon: '🧮', lines: ['ls'] },\n"

CASE_ANCHOR = "    case 'groups': renderGroups(main); break;\n"
CASE_NEW = CASE_ANCHOR + "    case 'lsKpi': renderLsKpi(main); break;\n"

FN_ANCHOR = "/* ---------- 课题组 (Life Science) ---------- */"

FN_NEW = r"""/* ---------- 课题组经营（LS 专属 KPI）----------
   口径（构建期算入 数据/ls_groups.json 的 kpi 段，前端只做汇总）：
   ① 转化率 = 有成交课题组 / 有订单课题组
   ② 沉睡   = 最近一次订单(含测试/赠品)距今 > 180 天（基准 = 数据截止日 kpi.as_of）
   ③ 成交额 = 课题组口径(标准单&已审核&非赠品&剔宝锐系) 累计；同比按【同期 1/1~10/8】比
   ④ 复购   = 成交订单数 ≥ 2 的课题组
   ⑤ 单组产出 = 累计成交额 / 有成交课题组
   销售只见本人（与「课题组」Tab 同一过滤），管理员见全量。 */
function renderLsKpi(main) {
  var gd = (state.data || {}).lsGroups;
  if (!gd || !gd.kpi) { main.innerHTML = '<div class="error-box"><h3>⚠️ 课题组经营数据未加载</h3><p>数据源：数据/ls_groups.json 的 kpi 段</p></div>'; return; }
  var k = gd.kpi, user = state.user || {};
  var isAdmin = user.role === 'admin' || user.role === 'gm';
  var all = gd.groups || [];
  var groups = (isAdmin ? all : all.filter(function(g) { return g.sales === user.name; }))
                  .filter(function(g) { return (g.orders || 0) > 0; });

  var asOf = new Date(k.as_of + 'T00:00:00');
  var sleepDays = k.sleep.days || 180;
  var paidAmts = [], sleeping = [], amtSum = 0, y25 = 0, y26 = 0, rep = 0, sleepPaid = 0, sleepAmt = 0;
  groups.forEach(function(g) {
    var a = parseFloat(g.amt) || 0;
    amtSum += a; y25 += parseFloat(g.amt_p25) || 0; y26 += parseFloat(g.amt_p26) || 0;
    if ((g.orders_paid || 0) >= 2) rep++;
    if (a > 0) paidAmts.push(a);
    var lt = g.last_any ? new Date(g.last_any + 'T00:00:00') : null;
    if (lt && (asOf - lt) / 86400000 > sleepDays) {
      sleeping.push(g);
      if (a > 0) { sleepPaid++; sleepAmt += a; }
    }
  });
  var sorted = paidAmts.slice().sort(function(x, y) { return x - y; });
  var median = sorted.length ? (sorted.length % 2 ? sorted[(sorted.length - 1) / 2]
            : (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2) : 0;
  var convRate = groups.length ? 100 * paidAmts.length / groups.length : 0;
  var yoy = y25 ? 100 * (y26 - y25) / y25 : null;
  var perGroup = paidAmts.length ? amtSum / paidAmts.length : 0;
  var repRate = groups.length ? 100 * rep / groups.length : 0;
  sleeping.sort(function(x, y) { return (parseFloat(y.amt) || 0) - (parseFloat(x.amt) || 0); });

  function wan(v) { return (v / 10000).toFixed(2); }
  function yuan(v) { return '¥' + (Math.round(v) || 0).toLocaleString('en-US'); }
  function card(v, label, sub, color) {
    return '<div class="stat-card"><div class="value"' + (color ? ' style="color:' + color + '"' : '') + '>' + v + '</div>'
         + '<div class="label">' + label + '</div>'
         + (sub ? '<div style="font-size:11px;color:var(--sub);margin-top:3px;line-height:1.5">' + sub + '</div>' : '')
         + '</div>';
  }
  var html = '';
  html += '<div style="font-size:15px;font-weight:800;color:var(--pri);margin:2px 0 4px">🧮 课题组经营（生命科学）</div>';
  html += '<div style="font-size:11px;color:var(--sub);margin-bottom:12px">'
       + '数据截止 <b>' + esc(k.as_of) + '</b>　｜　口径：' + esc(gd.amt_caliber || '') + '　｜　'
       + (isAdmin ? '全量' : '仅本人（' + esc(user.name || '') + '）') + '</div>';

  html += '<div class="stats-grid">';
  html += card(convRate.toFixed(2) + '<span style="font-size:13px">%</span>', '🎯 测试→成交转化率',
               '有成交 <b>' + paidAmts.length + '</b> / 有订单 <b>' + groups.length + '</b> 个课题组');
  html += card(sleeping.length, '😴 沉睡课题组',
               '末单 > ' + sleepDays + ' 天　含曾成交 <b>' + sleepPaid + '</b>（' + wan(sleepAmt) + '万）', '#C0392B');
  html += card(wan(amtSum) + '<span style="font-size:13px">万</span>', '💵 累计成交额',
               yoy === null ? '同比 —' : ('同期同比 <b style="color:' + (yoy >= 0 ? '#0E7A45' : '#C0392B') + '">'
                 + (yoy >= 0 ? '+' : '') + yoy.toFixed(1) + '%</b>（' + wan(y25) + '万 → ' + wan(y26) + '万）'));
  html += '</div>';
  html += '<div class="stats-grid">';
  html += card(rep, '🔁 复购课题组', '成交订单 ≥ 2 次　占 <b>' + repRate.toFixed(1) + '%</b>',
               rep ? '#0E7A45' : null);
  html += card(yuan(perGroup), '📦 单组产出', '累计成交额 / 有成交课题组　中位 ' + yuan(median));
  html += '</div>';

  html += '<div style="background:#F2F6FD;border-radius:9px;padding:10px 13px;font-size:11.5px;color:#475467;line-height:1.9;margin:12px 0">'
       + '① <b>转化率</b>＝有成交课题组 ÷ 有订单课题组（这条线 <b>' + (k.gift_ratio != null ? k.gift_ratio : '—')
       + '%</b> 的记录是赠品、<b>' + (k.test_ratio != null ? k.test_ratio : '—') + '%</b> 是测试单，转化率才是真业绩口径）<br>'
       + '② <b>沉睡</b>＝最近一次订单（含测试/赠品）距今 > ' + sleepDays + ' 天，基准日 ' + esc(k.as_of)
       + '；近 90 天活跃 ' + k.sleep.active90 + ' 个<br>'
       + '③ <b>成交额</b>＝' + esc(gd.amt_caliber || '') + '；<b>同比取同期 ' + esc(k.amt.yoy_period)
       + '</b>（拿 2025 全年比 2026 的 10 个月会低估增长）<br>'
       + '④ <b>复购</b>＝成交订单数 ≥ 2 的课题组（非「全部订单」，避免测试单虚增）<br>'
       + '⑤ <b>单组产出</b>＝累计成交额 ÷ 有成交课题组数</div>';

  html += '<div class="card"><div class="card-header"><h3>😴 沉睡课题组 · 按历史成交额（TOP ' + Math.min(20, sleeping.length) + '）</h3></div>';
  html += '<div class="card-body" style="overflow-x:auto"><table class="data-table"><thead><tr>'
       + '<th>课题组</th><th>销售员</th><th>区域</th><th style="text-align:right">历史成交额</th>'
       + '<th style="text-align:center">订单数</th><th>末单日期</th><th style="text-align:right">沉睡天数</th>'
       + '<th style="text-align:center">客户数</th></tr></thead><tbody>';
  if (!sleeping.length) {
    html += '<tr><td colspan="8" style="text-align:center;color:var(--sub)">无沉睡课题组</td></tr>';
  }
  sleeping.slice(0, 20).forEach(function(g) {
    var lt = new Date(g.last_any + 'T00:00:00');
    var days = Math.round((asOf - lt) / 86400000);
    var custs = g.customers || [];
    html += '<tr><td>' + esc(g.name) + '</td><td>' + esc(g.sales || '') + '</td><td>' + esc(g.region || '—') + '</td>'
         + '<td style="text-align:right;font-weight:700;color:var(--pri)">' + yuan(parseFloat(g.amt) || 0) + '</td>'
         + '<td style="text-align:center">' + (g.orders || 0) + '</td>'
         + '<td style="white-space:nowrap">' + esc(g.last_any || '') + '</td>'
         + '<td style="text-align:right;color:#C0392B">' + days + ' 天</td>'
         + '<td style="text-align:center">' + custs.length + '</td></tr>';
  });
  html += '</tbody></table></div></div>';
  html += '<div style="font-size:11px;color:var(--sub);margin-top:10px">📌 建议：沉睡清单可直接转成跟进待办；'
       + '「🚫 未成交」' + (k.repurchase ? k.repurchase.never : '') + ' 个课题组从未成交，属测试后未转化，宜单独复盘。</div>';
  main.innerHTML = html;
}

"""

old = {}
for path in FILES:
    s = io.open(path, encoding='utf-8').read()
    n_tab, n_case, n_fn = s.count(TAB_ANCHOR), s.count(CASE_ANCHOR), s.count(FN_ANCHOR)
    assert n_tab == 1, 'TABS 锚点命中 %d 次: %s' % (n_tab, path)
    assert n_case == 1, 'dispatch 锚点命中 %d 次: %s' % (n_case, path)
    assert n_fn == 1, '函数锚点命中 %d 次: %s' % (n_fn, path)
    assert "id: 'lsKpi'" not in s, '已存在 lsKpi Tab（勿重复插入）: %s' % path
    s = s.replace(TAB_ANCHOR, TAB_NEW, 1).replace(CASE_ANCHOR, CASE_NEW, 1).replace(FN_ANCHOR, FN_NEW + FN_ANCHOR, 1)
    io.open(path, 'w', encoding='utf-8').write(s)
    old[os.path.basename(path)] = len(s)
    print('✅ 已改 %-14s（Tab + dispatch + renderLsKpi）→ %d 字符' % (os.path.basename(path), len(s)))

print('\n── node --check ──')
bad = 0
for path in FILES:
    blocks = re.findall(r'<script[^>]*>(.*?)</script>', io.open(path, encoding='utf-8').read(), re.S)
    fails = []
    for b in blocks:
        if not b.strip():
            continue
        fd, p = tempfile.mkstemp(suffix='.js')
        os.write(fd, b.encode('utf-8'))
        os.close(fd)
        r = subprocess.run(['node', '--check', p], capture_output=True, text=True)
        if r.returncode:
            fails.append(r.stderr.strip().splitlines()[-1][:130])
        os.unlink(p)
    print(('✅' if not fails else '❌'), '%-14s %d 块' % (os.path.basename(path), len(blocks)), '全通过' if not fails else fails[:1])
    bad += len(fails)

for path in FILES:
    s = io.open(path, encoding='utf-8').read()
    need = ["id: 'lsKpi'", "case 'lsKpi'", 'function renderLsKpi', '测试→成交转化率', '沉睡课题组', '单组产出', '复购课题组']
    miss = [x for x in need if x not in s]
    print(('✅' if not miss else '❌'), '%-14s 关键词%s' % (os.path.basename(path), '齐' if not miss else '缺 ' + str(miss)))
sys.exit(1 if bad else 0)
