# -*- coding: utf-8 -*-
"""生成 v2 看板：对照《经营管理指标日常播报》大区表的结构与口径"""
import json, datetime

P = json.load(open('南部大区城市看板/daqu_parsed.json'))
D = json.load(open('南部大区城市看板/dashboard_data.json'))
E = json.load(open('南部大区城市看板/extra_data.json'))

meta, region, award = P['meta'], P['region'], P['award']
# 经营奖起算线按季度目标 100%（2026-08-19 用户口径修正，原播报表为 80%）
award['start_line'] = award['q_targets']['合计']
award['achieve_progress'] = award['achieve_gp']['合计'] / award['start_line']
# 时间进度按日历自算（季度首日到 as_of 的天数/季度总天数），不信源字段（源 6/7 口径错误，2026-08-19 修正）
_as_of = datetime.date.fromisoformat(meta['as_of'])
_q_start = datetime.date(_as_of.year, (_as_of.month - 1) // 3 * 3 + 1, 1)
_q_end_m = _q_start.month + 2
_q_end = datetime.date(_as_of.year, _q_end_m, [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][_q_end_m - 1])
award['time_progress'] = ((_as_of - _q_start).days + 1) / ((_q_end - _q_start).days + 1)
award['gap_vs_time'] = award['achieve_gp']['合计'] - award['start_line'] * award['time_progress']
# 剩余天数/需日均/季末预测（2026-08-19 修正：源 rest_daily 按 13 天除，错误）
award['elapsed_days'] = (_as_of - _q_start).days + 1
award['total_days'] = (_q_end - _q_start).days + 1
award['remaining_days'] = award['total_days'] - award['elapsed_days']
award['rest_daily'] = -(abs(award['target_gap']) / award['remaining_days'])
award['current_level'] = region['毛利额']['t1']
award['projected_total'] = award['achieve_gp']['合计'] + award['current_level'] * award['remaining_days']
award['projected_gap'] = award['projected_total'] - award['start_line']
cities12 = P['cities']
region_daily = P['region_daily']
city_daily = P['city_daily']

DIRECT = list(cities12.keys())
LIANYING = [r['city'] for r in D['city_rows'] if r['type'] != '直营']
trend, city_rows = D['trend'], {r['city']: r for r in D['city_rows']}

# 最近14天大区日序列
last14 = region_daily[-14:]

payload = {
    'meta': meta, 'region': region, 'award': award,
    'cities12': cities12, 'region_daily14': last14,
    'city_daily14': {c: rows[-14:] for c, rows in city_daily.items()},
    'city_ext': P['city_ext'], 'contrib': P['contrib'], 'risk': P['risk'],
    'lianying': [],
    'months': D['months'], 'mlabel': D['mlabel'],
    'agg_trend': D['agg_trend'], 'city_trend': trend,
    'agg_cards': D['cards'],
    'gp_user_core': E['gp_user_core'], 'ly_modes': E['ly_modes'], 'targets': E['targets'],
}

for c in LIANYING:
    jul = city_rows.get(c, {})
    aug = trend.get(c, {}).get('202608', {})
    payload['lianying'].append({
        'city': c,
        'mode': E['ly_modes'].get(c, '-'),
        'target': E['targets']['lianying'].get(c),
        'aug_rev': aug.get('rev'), 'aug_gpm': aug.get('gpm'),
        'aug_users': aug.get('users'), 'aug_kedanjia': aug.get('kedanjia'),
        'jul_rev': jul.get('rev'), 'jul_gp': jul.get('gp'), 'jul_gpm': jul.get('gpm'),
        'jul_users': jul.get('users'), 'jul_rev_mom': jul.get('rev_mom'),
        'jul_gp_user': jul.get('gp_user'), 'jul_kedanjia': jul.get('kedanjia'),
    })
payload['lianying'].sort(key=lambda x: -(x['aug_rev'] or 0))

# ---------- 不达标城市归因 ----------
tp_v = meta['time_progress']
_agg = {}
for _k in ['收入', '租金', '电费', '全职人力&资产折旧', '安装成本', '其他成本', '营销费', '毛利额']:
    _agg[_k] = sum((v.get(_k) or {}).get('mtd') or 0 for v in cities12.values())
BENCH = {k: _agg[k] / _agg['收入'] for k in ['租金', '电费', '全职人力&资产折旧', '安装成本', '其他成本', '营销费']}
BENCH['gpm'] = _agg['毛利额'] / _agg['收入']
BENCH['kd'] = region_daily[-1]['kedanjia_noka']

attribution = []
for c, v in cities12.items():
    g = lambda k, f='mtd': (v.get(k) or {}).get(f) if isinstance(v.get(k), dict) else None
    vs = v.get('月度目标达成进度VS时间进度')
    if vs is None or vs >= 0:
        continue
    rev, gp = g('收入'), g('毛利额')
    gpm, gpm_tgt, gp_tgt = g('毛利率'), v.get('毛利率目标'), v.get('毛利额目标')
    imp_tgt = gp_tgt / gpm_tgt if gpm_tgt else None
    rev_ach = rev / imp_tgt if imp_tgt else None
    rev_drag = (rev_ach - tp_v) * 100 if rev_ach else 0
    gpm_drag = (gpm - gpm_tgt) * 100 if gpm and gpm_tgt else 0
    if rev_drag < -1 and gpm_drag < -1:
        verdict = '收入与毛利率双拖'
    elif gpm_drag < -1:
        verdict = '成本侧拖累为主（毛利率不达标）'
    else:
        verdict = '收入/规模拖累为主（量不够）'
    drags = []
    for k in ['电费', '租金', '全职人力&资产折旧', '安装成本', '其他成本', '营销费']:
        m = g(k)
        if not m:
            continue
        rate = m / rev
        tgt, lab = (0.30, '目标') if k == '电费' else (BENCH[k], '基准')
        d = rate - tgt
        if d > 0.003:
            drags.append({'name': k, 'rate': rate, 'bench': tgt, 'lab': lab, 'd': d, 'impact': -d * rev})
    drags.sort(key=lambda x: x['impact'])
    dl = city_daily.get(c, [])[-14:]
    u_chg = (dl[-1]['users'] - dl[0]['users']) / dl[0]['users'] if dl and dl[0]['users'] else None
    kd = g('客单价（去KA）', 't1')
    jul = city_rows.get(c, {})
    attribution.append({
        'city': c, 'vs': vs, 'gp_gap': v.get('毛利额GAP'),
        'rev_ach': rev_ach, 'rev_drag': rev_drag, 'imp_tgt': imp_tgt, 'rev': rev,
        'gpm': gpm, 'gpm_tgt': gpm_tgt, 'gpm_drag': gpm_drag,
        'verdict': verdict, 'drags': drags[:3],
        'u_chg': u_chg, 'kd': kd, 'kd_bench': BENCH['kd'],
        'jul_gpm': jul.get('gpm'), 'jul_gpm_mom': jul.get('gpm_mom'),
    })
attribution.sort(key=lambda x: x['vs'])
payload['attribution'] = attribution

# ---------- 城市 T-1 vs T-2 日环比（明细展开用） ----------
city_dod = {}
for c, rows in city_daily.items():
    if len(rows) < 2:
        continue
    a, b = rows[-1], rows[-2]
    if not a.get('rev') or not b.get('rev'):
        continue
    hr = lambda r: r['renli'] + r['btrydep'] + r['cabtdep']
    city_dod[c] = {
        '租金': a['zujin'] - b['zujin'], '电费': a['dianfei'] - b['dianfei'],
        '电费费率': a['dianfei'] / a['rev'] - b['dianfei'] / b['rev'],
        '单用户用电量': (a.get('kwh_user') or 0) - (b.get('kwh_user') or 0),
        '全职人力&资产折旧': hr(a) - hr(b),
        '安装成本': a['anzhuang'] - b['anzhuang'], '其他成本': a['qita_cost'] - b['qita_cost'],
        '营销费': a['yingxiao'] - b['yingxiao'],
        '营销费率': a['yingxiao'] / a['rev'] - b['yingxiao'] / b['rev'],
    }
payload['city_dod'] = city_dod

JSON_DATA = json.dumps(payload, ensure_ascii=False)

html = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>南部大区经营管理看板 · 8月MTD播报</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
:root{--bg:#f4f6f9;--card:#fff;--ink:#1c2733;--sub:#7b8a9d;--line:#e5eaf1;
--blue:#1f6fe0;--blue-d:#155ab6;--green:#18a058;--red:#d64545;--orange:#ef9f2e;--purple:#7a5af5;}
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:"PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--ink);font-size:13px;}
.wrap{max-width:1480px;margin:0 auto;padding:14px 18px 40px;}
header{background:linear-gradient(120deg,#14335c,#1f6fe0 60%,#2f8ef0);color:#fff;border-radius:12px;padding:16px 22px;margin-bottom:12px;}
header h1{font-size:20px;font-weight:600;}
header .sub{margin-top:5px;font-size:12px;opacity:.85;}
header .sub b{color:#ffe9a8;}
.tprog{margin-top:9px;display:flex;align-items:center;gap:10px;font-size:12px;}
.tprog .bar{flex:1;max-width:420px;height:8px;background:rgba(255,255,255,.25);border-radius:5px;overflow:hidden;}
.tprog .bar i{display:block;height:100%;background:#ffd666;border-radius:5px;}
.tabs{display:flex;gap:6px;margin:0 0 12px;}
.tabs button{border:1px solid var(--line);background:#fff;padding:8px 18px;border-radius:8px;cursor:pointer;font-size:13px;color:var(--sub);font-weight:500;}
.tabs button.on{background:var(--blue);border-color:var(--blue);color:#fff;}
.tab{display:none;} .tab.on{display:block;}
.grid{display:grid;gap:10px;}
.kpis{grid-template-columns:repeat(3,1fr);}
.card{background:var(--card);border-radius:10px;padding:12px 14px;box-shadow:0 1px 3px rgba(20,40,70,.05);}
.kpi .t{font-size:12px;color:var(--sub);display:flex;justify-content:space-between;}
.kpi .v{font-size:22px;font-weight:700;margin:4px 0 2px;letter-spacing:-.5px;}
.kpi .v small{font-size:12px;font-weight:400;color:var(--sub);}
.kpi .s{font-size:11.5px;color:var(--sub);line-height:1.7;}
.kpi .s b{font-weight:600;color:var(--ink);}
.up{color:var(--green);} .down{color:var(--red);}
.kpi .s b.up{color:var(--green);} .kpi .s b.down{color:var(--red);}
.tag{display:inline-block;padding:1px 7px;border-radius:9px;font-size:11px;font-weight:600;}
.tag.g{background:#e5f6ec;color:var(--green);} .tag.r{background:#fdecec;color:var(--red);}
.tag.o{background:#fdf3e2;color:var(--orange);}
.panel{background:var(--card);border-radius:10px;padding:14px 16px;margin-bottom:12px;box-shadow:0 1px 3px rgba(20,40,70,.05);}
.panel h3{font-size:14px;margin-bottom:10px;display:flex;align-items:center;gap:8px;}
.panel h3 .hint{font-size:11px;color:var(--sub);font-weight:400;}
.award-grid{display:grid;grid-template-columns:repeat(4,1fr) 1.6fr;gap:10px;}
.award-cell{background:#f8fafd;border:1px solid var(--line);border-radius:8px;padding:9px 12px;}
.award-cell .t{font-size:11px;color:var(--sub);}
.award-cell .v{font-size:16px;font-weight:700;margin-top:3px;}
.award-cell .v small{font-size:11px;color:var(--sub);font-weight:400;}
.pb{height:9px;background:#edf1f6;border-radius:5px;overflow:hidden;margin-top:6px;position:relative;}
.pb i{display:block;height:100%;border-radius:5px;}
.pb .mk{position:absolute;top:-2px;width:2px;height:13px;background:var(--ink);opacity:.55;}
table{width:100%;border-collapse:separate;border-spacing:0;font-size:12px;}
th{background:#f2f6fb;color:#46586e;font-weight:600;padding:7px 8px;text-align:right;white-space:nowrap;border-bottom:2px solid var(--line);position:sticky;top:0;z-index:5;}
th:first-child,td:first-child{text-align:left;}
td{padding:6.5px 8px;text-align:right;border-bottom:1px solid #f0f3f7;white-space:nowrap;}
tr.clickable{cursor:pointer;}
tr.clickable:hover{background:#f6f9fe;}
tr.detail{background:#fafcff;} tr.detail td{padding:10px 12px;}
.cname{font-weight:600;}
.pill{padding:2px 8px;border-radius:9px;font-weight:600;font-size:11.5px;}
.pill.g{background:#e5f6ec;color:var(--green);} .pill.r{background:#fdecec;color:var(--red);} .pill.o{background:#fdf3e2;color:var(--orange);}
.spark{width:120px;height:34px;display:inline-block;vertical-align:middle;}
.chart{height:300px;}
.two{grid-template-columns:1.25fr 1fr;}
.note{font-size:11px;color:var(--sub);margin-top:8px;line-height:1.6;}
.detail-grid{display:grid;grid-template-columns:1.1fr 1fr 1fr;gap:14px;}
.subtabs{display:flex;gap:6px;margin-bottom:10px;border-bottom:1px solid var(--line);padding-bottom:8px;}
.subtabs button{border:1px solid var(--line);background:#fff;padding:5px 14px;border-radius:7px;cursor:pointer;font-size:12px;color:var(--sub);}
.subtabs button.on{background:#2f6db8;border-color:#2f6db8;color:#fff;font-weight:600;}
.subp{display:none;} .subp.on{display:block;}
.chips{display:flex;gap:4px;flex-wrap:wrap;justify-content:flex-end;}
.chip{display:inline-block;padding:1px 7px;border-radius:9px;font-size:10.5px;font-weight:600;white-space:nowrap;}
.chip.r{background:#fdecec;color:var(--red);} .chip.o{background:#fdf3e2;color:var(--orange);} .chip.g{background:#e5f6ec;color:var(--green);}
.fc-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-bottom:8px;}
.fc-cell{background:#f8fafd;border:1px solid var(--line);border-radius:8px;padding:7px 10px;}
.fc-cell .t{font-size:10.5px;color:var(--sub);}
.fc-cell .v{font-size:15px;font-weight:700;margin-top:2px;}
.fc-cell .v small{font-size:10.5px;color:var(--sub);font-weight:400;}
.spark{width:90px;height:26px;display:inline-block;vertical-align:middle;}
.mini-t td,.mini-t th{padding:4px 7px;font-size:11.5px;}
tr.attr-row td{background:#fff7f7;padding:8px 14px 10px;border-bottom:2px solid #f3d9d9;}
.attr-head{display:flex;align-items:center;gap:10px;margin-bottom:5px;flex-wrap:wrap;}
.attr-head .verdict{display:inline-block;background:#fdecec;color:var(--red);font-weight:600;font-size:12px;padding:3px 10px;border-radius:6px;}
.attr-row ul{list-style:none;display:grid;grid-template-columns:1fr 1fr;gap:1px 26px;}
.attr-row li{font-size:11.5px;line-height:1.7;padding-left:13px;position:relative;color:var(--ink);}
.attr-row li::before{content:'';position:absolute;left:2px;top:8px;width:6px;height:6px;border-radius:50%;background:var(--sub);}
.attr-row li.bad::before{background:var(--red);}
.attr-row li.ok::before{background:var(--green);}
.attr-row li.mut{color:var(--sub);}
.attr-row li.mut::before{background:var(--line);}
@media(max-width:1100px){.attr-row ul{grid-template-columns:1fr;}}
footer{text-align:center;color:var(--sub);font-size:11px;margin-top:18px;line-height:1.8;}
@media(max-width:1100px){.kpis{grid-template-columns:repeat(2,1fr);}.award-grid{grid-template-columns:repeat(2,1fr);}.two,.detail-grid{grid-template-columns:1fr;}}
@media print{@page{size:A4 landscape;margin:8mm}body{background:#fff}.wrap{max-width:none;padding:0}.tabs{display:none}.tab{display:block!important}.chart{height:260px}header{padding:12px 18px}}
</style>
</head>
<body>
<div class="wrap">
<header>
  <h1>南部大区 · 经营管理指标看板</h1>
  <div class="sub" id="hdrSub"></div>
  <div class="tprog"><span id="tpTxt"></span><div class="bar"><i id="tpBar"></i></div><span id="tpPct"></span></div>
</header>
<div class="tabs">
  <button class="on" data-t="t1">大区日播报</button>
  <button data-t="t2">直营城市播报</button>
  <button data-t="t3">联营城市</button>
  <button data-t="t4">月度趋势</button>
</div>

<div id="t1" class="tab on">
  <div class="grid kpis" id="kpiRow"></div>
  <div class="panel" style="margin-top:12px;">
    <h3>Q3换电毛利额目标进度 <span class="hint">毛利额口径：7月+8月(≤8/20)为管报口径，8/21起为线上数据</span></h3>
    <div id="awardBody"></div>
  </div>
  <div class="grid two">
    <div class="panel"><h3>近14天 收入 / 毛利额 / 毛利率</h3><div id="ch14" class="chart"></div></div>
    <div class="panel"><h3>近14天 换电服务用户数 / 单用户毛利</h3><div id="ch14b" class="chart"></div></div>
  </div>
</div>

<div id="t2" class="tab">
  <div class="panel">
    <h3>直营12城 · 8月MTD播报 <span class="hint">点行展开：经营趋势与预估 / 电费异动 / 成本结构 · 进度VS时间&lt;0 的城市行下附归因 · 进度VS时间 = 毛利目标达成率 − 时间进度(<span id="tpHint"></span>) · 毛利GAP = MTD毛利 − 月目标×时间进度</span></h3>
    <table id="tblDirect"></table>
  </div>
  <div class="panel">
    <h3>大区城市变化贡献排名（T-1 vs T-2 日环比） <span class="hint">按城市变化额排名 · 贡献占比 = 城市变化 ÷ 大区合计变化 · 覆盖南部全部城市</span></h3>
    <div id="contribBody" class="grid" style="grid-template-columns:1fr 1fr;gap:14px;"></div>
  </div>
  <div class="panel">
    <h3>经营风险矩阵 <span class="hint">X=毛利率 MTD vs 目标缺口（无目标城市按25%参考线） · Y=近7日收入周环比 · 气泡=收入MTD规模 · 左下=缺口大且走弱（重点风险）</span></h3>
    <div id="chRisk" class="chart" style="height:380px;"></div>
  </div>
</div>

<div id="t3" class="tab">
  <div class="grid kpis" id="lyKpi" style="grid-template-columns:repeat(4,1fr);"></div>
  <div class="panel" style="margin-top:12px;">
    <h3>联营城市 · 8月MTD + 7月对照 <span class="hint">8月为MTD（截至""" + str(int(meta['mtd_end'][6:])) + """日），点列头可排序 · 模式＝联营分润模式（空值记“标准”）</span></h3>
    <table id="tblLy"></table>
  </div>
</div>

<div id="t4" class="tab">
  <div class="panel"><h3>大区月度走势（5–7月全月 · 8月MTD+月末预测）</h3><div id="chMonth" class="chart" style="height:340px;"></div>
    <div class="note">8月柱＝MTD实绩（深色）＋月末预测（浅色），预测＝MTD÷已过天数×全月天数；毛利率/换电用户为MTD实绩口径。</div></div>
  <div class="panel"><h3>单城市月度走势 <select id="citySel" style="margin-left:10px;padding:4px 8px;border-radius:6px;border:1px solid var(--line);"></select></h3>
    <div id="chCity" class="chart" style="height:320px;"></div></div>
</div>

<footer>
数据来源：伽利略（Polaris）经营管理报表_核心指标 + 《经营管理指标日常播报-26年08》大区表（截至 """ + meta['as_of'] + """）<br>
口径说明：收入为换电均摊口径（税后）；毛利率=毛利额/收入；进度VS时间=毛利达成率−时间进度。生成于 """ + datetime.datetime.now().strftime('%Y-%m-%d %H:%M') + """
</footer>
</div>
<script>
const DATA = """ + JSON_DATA + """;
const tp = DATA.meta.time_progress;
document.getElementById('tpHint').textContent = (tp*100).toFixed(1) + '%';

/* ---------- helpers ---------- */
const wan = v => {
  if (v==null||isNaN(v)) return '-';
  const a = Math.abs(v);
  if (a>=1e8) return (v/1e8).toFixed(2)+'亿';
  if (a>=1e4) return (v/1e4).toFixed(a>=1e6?0:1)+'万';
  return v.toFixed(0);
};
const pct = (v,d=1) => v==null||isNaN(v)?'-':(v*100).toFixed(d)+'%';
const pp  = (v,d=2) => v==null||isNaN(v)?'-':(v>=0?'+':'')+(v*100).toFixed(d)+'pp';
const sgn = v => v==null||isNaN(v)?'-':(v>=0?'+':'')+wan(v);
const cls = v => v==null?'':(v>=0?'up':'down');
const tag = v => v==null?'<span class="tag">-</span>':`<span class="tag ${v>=0?'g':'r'}">${pp(v)}</span>`;
const dstr = ds => ds.slice(4,6)+'/'+ds.slice(6,8);
const sparkSVG = arr => {
  if(!arr||arr.length<2) return '-';
  const w=88,h=24,mn=Math.min(...arr),mx=Math.max(...arr),rg=(mx-mn)||1;
  const pts=arr.map((v,i)=>`${(2+i/(arr.length-1)*(w-4)).toFixed(1)},${(h-2-(v-mn)/rg*(h-6)).toFixed(1)}`).join(' ');
  return `<svg class="spark" width="${w}" height="${h}"><polyline points="${pts}" fill="none" stroke="${arr[arr.length-1]>=arr[0]?'#18a058':'#d64545'}" stroke-width="1.6"/></svg>`;
};
const isRateK = k => (k==='毛利率'||k==='电费率');
const fmtCmp = (k,c) => {
  const val = c.t1==null?'-': isRateK(k)? pct(c.t1,2) : k==='换电用户'? Math.round(c.t1).toLocaleString()
    : k==='单用户收入'? c.t1.toFixed(2)+'元' : wan(c.t1);
  const ch = v => v==null?'-': isRateK(k)? pp(v) : (v>=0?'+':'')+(v*100).toFixed(2)+'%';
  return [val, ch(c.dod), ch(c.wowsame), ch(c.w7)];
};

/* ---------- header ---------- */
document.getElementById('hdrSub').innerHTML =
  `数据区间 <b>${DATA.meta.as_of.slice(0,8)}01 ~ ${DATA.meta.as_of}</b>（8月MTD，全月${DATA.meta.days_in_month}天）· 时间进度 <b>${pct(tp)}</b> · 口径：经营管理报表_核心指标`;
document.getElementById('tpTxt').textContent = '本月时间进度';
document.getElementById('tpBar').style.width = (tp*100).toFixed(1)+'%';
document.getElementById('tpPct').textContent = pct(tp);

/* ---------- tabs ---------- */
document.querySelectorAll('.tabs button').forEach(b=>b.onclick=()=>{
  document.querySelectorAll('.tabs button').forEach(x=>x.classList.remove('on'));
  document.querySelectorAll('.tab').forEach(x=>x.classList.remove('on'));
  b.classList.add('on'); document.getElementById(b.dataset.t).classList.add('on');
  setTimeout(()=>{ document.querySelectorAll('.chart').forEach(el=>{
    const c = echarts.getInstanceByDom(el); c && c.resize(); }); }, 60);
  if(window.__tabExtra) window.__tabExtra(b.dataset.t);
});

/* ---------- T1: KPI ---------- */
(function(){
  const R = DATA.region;
  const k = (title, mtd, unit, t1, dod, gap, gapIsPp, extra='') => {
    const dodTxt = unit==='ratio'? pp(dod) : sgn(dod);
    const gapTxt = gap==null? '' : `<br>目标达成Gap <b class="${cls(gap)}">${gapIsPp?pp(gap):sgn(gap)}</b>`;
    return `<div class="card kpi"><div class="t"><span>${title}</span><span>8月MTD</span></div>
      <div class="v">${mtd}<small> ${unit==='ratio'?'':'元'}</small></div>
      <div class="s">T-1 <b>${t1}</b> · 日环比 <b class="${cls(dod)}">${dodTxt}</b>${gapTxt}${extra}</div></div>`;
  };
  let cards = [
    k('收入', wan(R['收入'].mtd), 'yuan', wan(R['收入'].t1), R['收入'].dod, R['收入'].gap, true),
    k('毛利额', wan(R['毛利额'].mtd), 'yuan', wan(R['毛利额'].t1), R['毛利额'].dod, R['毛利额'].gap, true),
    k('毛利率', pct(R['毛利率'].mtd), 'ratio', pct(R['毛利率'].t1), R['毛利率'].dod, R['毛利率'].gap, true),
    (()=>{ const G=DATA.gp_user_core, d=G.t1-G.t2;
      return `<div class="card kpi"><div class="t"><span>单用户毛利额</span><span>8月MTD</span></div>
        <div class="v">${G.mtd.toFixed(1)}<small> 元</small></div>
        <div class="s">T-1 <b>${G.t1.toFixed(2)}</b> · 日环比 <b class="${cls(d)}">${(d>=0?'+':'')+d.toFixed(2)}</b>
        <br>口径：核心指标 · 日维度截至${G.asof.slice(5).replace('-','/')}</div></div>`; })(),
    (()=>{ const T=DATA.targets, ach=T.region_total_t1/T.region_total;
      return `<div class="card kpi"><div class="t"><span>换电用户目标（总）</span><span>8月</span></div>
        <div class="v">${(T.region_total/1e4).toFixed(2)}<small> 万</small></div>
        <div class="s">T-1换电 <b>${(T.region_total_t1/1e4).toFixed(2)}万</b> · 达成 <b class="${cls(ach-1)}">${pct(ach)}</b>
        <br>直营 <b>${(T.direct_total/1e4).toFixed(2)}万</b> · 联营 <b>${(T.ly_total/1e4).toFixed(2)}万</b></div></div>`; })(),
    k('联营毛利额', wan(R['联营毛利额'].mtd), 'yuan', wan(R['联营毛利额'].t1), R['联营毛利额'].dod, R['联营毛利额'].gap, true,
      `<br>本月毛利目标 <b>${wan(R['联营毛利额'].target)}</b>`),
  ];
  document.getElementById('kpiRow').innerHTML = cards.join('');
})();

/* ---------- T1: award ---------- */
(function(){
  const A = DATA.award;
  const cell = (t,v,sub='') => `<div class="award-cell"><div class="t">${t}</div><div class="v">${v}${sub}</div></div>`;
  const q = A.q_targets;
  const achJul = A.achieve_gp['202607'], achAug = A.achieve_gp['202608'], achTot = A.achieve_gp['合计'];
  let h = `<div class="award-grid">
    ${cell('7月目标', wan(q['202607']))}${cell('8月目标', wan(q['202608']))}
    ${cell('9月目标', wan(q['202609']))}${cell('季度合计', wan(q['合计']))}
    <div class="award-cell"><div class="t">已达成毛利（7月+8月MTD）</div>
      <div class="v">${wan(achTot)} <small>7月 ${wan(achJul)} · 8月 ${wan(achAug)}</small></div></div>
    ${cell('经营奖起算线', wan(A.start_line), '<small>＝季度目标100%</small>')}
    ${cell('目标GAP（剩余）', `<span class="${cls(A.target_gap)}">${sgn(A.target_gap)}</span>`)}
    ${cell('剩余天数需日均', `<span class="${A.current_level>=Math.abs(A.rest_daily)?'up':'down'}">${wan(Math.abs(A.rest_daily))}/天</span>`, `<small>当前水位约 ${wan(A.current_level)}/天 · 剩${A.remaining_days}天</small>`)}
    ${cell('预估季末达成', wan(A.projected_total), `<small>vs起算线 <b class="${cls(A.projected_gap)}">${sgn(A.projected_gap)}</b> · ${A.projected_gap>=0?'达标在望':'有风险'}</small>`)}
  </div>
  <div style="margin-top:12px;">
    <div style="font-size:12px;color:var(--sub);margin-bottom:3px;">达成进度（达成毛利 ÷ 起算线） <b style="color:var(--ink)">${pct(A.achieve_progress)}</b>
    　·　时间进度 <b style="color:var(--ink)">${pct(A.time_progress)}</b>
    　·　进度差 <b class="${cls(A.gap_vs_time)}">${sgn(A.gap_vs_time)}</b></div>
    <div class="pb"><i style="width:${Math.min(100,A.achieve_progress*100)}%;background:${A.achieve_progress>=A.time_progress?'var(--green)':'var(--orange)'}"></i>
      <span class="mk" style="left:${A.time_progress*100}%"></span></div>
    <div class="note">口径：时间进度＝已过 ${A.elapsed_days} 天 ÷ 全季 ${A.total_days} 天；剩余需日均＝剩余GAP ÷ ${A.remaining_days} 天；预估季末＝已达成＋当前水位(T-1毛利)×剩余天数，仅供参考。</div>
  </div>`;
  document.getElementById('awardBody').innerHTML = h;
})();

/* ---------- T1: 14天图 ---------- */
(function(){
  const ds = DATA.region_daily14.map(d=>dstr(d.date));
  const rev = DATA.region_daily14.map(d=>+(d.rev/1e4).toFixed(1));
  const gp  = DATA.region_daily14.map(d=>+(d.gp/1e4).toFixed(1));
  const gpm = DATA.region_daily14.map(d=>+(d.gpm*100).toFixed(2));
  echarts.init(document.getElementById('ch14')).setOption({
    tooltip:{trigger:'axis'},legend:{data:['收入','毛利额','毛利率'],top:0,textStyle:{fontSize:11}},
    grid:{left:46,right:46,top:32,bottom:24},
    xAxis:{type:'category',data:ds,axisLabel:{fontSize:10}},
    yAxis:[{type:'value',name:'万元',axisLabel:{fontSize:10}},{type:'value',name:'%',min:v=>Math.floor(v.min-1),axisLabel:{fontSize:10}}],
    series:[
      {name:'收入',type:'bar',data:rev,itemStyle:{color:'#bfd7f7'},barWidth:9},
      {name:'毛利额',type:'bar',data:gp,itemStyle:{color:'#5b8def'},barWidth:9},
      {name:'毛利率',type:'line',yAxisIndex:1,data:gpm,itemStyle:{color:'#ef9f2e'},lineStyle:{width:2},symbolSize:5},
    ]});
  const users = DATA.region_daily14.map(d=>d.users);
  const gpu = DATA.region_daily14.map(d=>+(d.gp_user||0).toFixed(1));
  echarts.init(document.getElementById('ch14b')).setOption({
    tooltip:{trigger:'axis'},legend:{data:['换电服务用户','单用户毛利'],top:0,textStyle:{fontSize:11}},
    grid:{left:56,right:46,top:32,bottom:24},
    xAxis:{type:'category',data:ds,axisLabel:{fontSize:10}},
    yAxis:[{type:'value',min:v=>Math.floor((v.min-4000)/10000)*10000,axisLabel:{fontSize:10}},{type:'value',name:'元',axisLabel:{fontSize:10}}],
    series:[
      {name:'换电服务用户',type:'line',data:users,itemStyle:{color:'#7a5af5'},areaStyle:{opacity:.08},symbolSize:4},
      {name:'单用户毛利',type:'line',yAxisIndex:1,data:gpu,itemStyle:{color:'#18a058'},lineStyle:{width:2},symbolSize:5},
    ]});
})();

/* ---------- T2: 直营城市 ---------- */
(function(){
  const cols = ['城市','换电用户','用户目标','收入 MTD','收入 T-1','换电收入','其他收入','客单价(去KA)','成本','毛利额','毛利率','毛利率目标','单户毛利','毛利目标','毛利GAP','进度VS时间','近7日毛利趋势','月底预估','异常标签'];
  let h = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead><tbody>`;
  const list = Object.entries(DATA.cities12);
  list.sort((a,b)=> (b[1]['收入']?.mtd||0)-(a[1]['收入']?.mtd||0));
  const ATTR = Object.fromEntries(DATA.attribution.map(a=>[a.city,a]));
  const tpPct = (tp*100).toFixed(1);
  for (const [name, v] of list){
    const g = k => v[k];
    const gv = (k,f='mtd') => g(k)? g(k)[f] : null;
    const hd = gv('换电用户'), hdDod = gv('换电用户','dod');
    const rev = gv('收入'), revT1 = gv('收入','t1'), revDod = gv('收入','dod');
    const gp = gv('毛利额'), gpm = gv('毛利率'), gpu = gv('本月单用户毛利额');
    const tgt = g('毛利额目标'), gap = g('毛利额GAP'), vs = g('月度目标达成进度VS时间进度');
    const vsPill = vs==null?'-':`<span class="pill ${vs>=0.02?'g':vs>=-0.02?'o':'r'}">${pp(vs)}</span>`;
    h += `<tr class="clickable" data-c="${name}">
      <td class="cname">${name}<div style="font-size:10.5px;color:var(--sub);font-weight:400;">环比 <span class="${cls(hdDod)}">${sgn(hdDod)}</span>人</div></td>
      <td>${hd?hd.toLocaleString():'-'}<div style="font-size:10.5px;color:var(--sub);">含单次 ${gv('换电用户含单次')?.toLocaleString()||'-'}</div></td>
      <td>${DATA.targets.direct[name]?DATA.targets.direct[name].toLocaleString():'-'}${(DATA.targets.direct[name]&&hd)?`<div style="font-size:10.5px;" class="${cls(hd-DATA.targets.direct[name])}">${sgn(hd-DATA.targets.direct[name])}</div>`:''}</td>
      <td><b>${wan(rev)}</b></td>
      <td>${wan(revT1)}<div style="font-size:10.5px;" class="${cls(revDod)}">${sgn(revDod)}</div></td>
      <td>${wan(gv('换电收入'))}</td>
      <td>${wan(gv('其他收入'))}</td>
      <td>${gv('客单价（去KA）','t1')?.toFixed(1)||'-'}</td>
      <td>${wan(gv('成本'))}</td>
      <td><b>${wan(gp)}</b><div style="font-size:10.5px;color:var(--sub);">T-1 ${wan(gv('毛利额','t1'))}</div></td>
      <td>${pct(gpm)}</td>
      <td style="color:var(--sub)">${pct(v['毛利率目标'])}</td>
      <td>${gpu?.toFixed(1)||'-'}</td>
      <td>${wan(tgt)}</td>
      <td class="${cls(gap)}"><b>${sgn(gap)}</b></td>
      <td>${vsPill}</td>
      <td>${sparkSVG((DATA.city_ext[name]||{}).spark7)}</td>
      <td>${(()=>{const fc=(DATA.city_ext[name]||{}).fc; return fc? `<span class="pill ${fc.gpm_ach>=1?'g':fc.gpm_ach>=0.95?'o':'r'}">${pct(fc.gpm_ach,0)}</span>
        <div style="font-size:10.5px;color:var(--sub);">毛利率 ${pct(fc.gpm_proj,1)} · GAP <span class="${cls(fc.gp_gap_proj)}">${sgn(fc.gp_gap_proj)}</span></div>` : '-';})()}</td>
      <td>${(()=>{const tg=(DATA.city_ext[name]||{}).tags; return tg&&tg.length? `<div class="chips">${tg.map(([t,cl])=>`<span class="chip ${cl}">${t}</span>`).join('')}</div>` : '<span style="color:var(--sub)">-</span>';})()}</td></tr>`;
    // 归因子行（不达标城市）
    const A_ = ATTR[name];
    if (A_){
      const uTxt = A_.u_chg==null?'-':`${(A_.u_chg>=0?'+':'')+(A_.u_chg*100).toFixed(2)}%`;
      const kdD = A_.kd!=null ? (A_.kd-A_.kd_bench)/A_.kd_bench : null;
      const dragTxt = A_.drags.length
        ? A_.drags.map(d=>`${d.name}率 <b>${pct(d.rate)}</b>（${d.lab||'基准'}${pct(d.bench)}，${pp(d.d)}，侵蚀 <b>${wan(d.impact)}</b>）`).join('；')
        : '各项费率均不高于目标/基准';
      h += `<tr class="attr-row"><td colspan="19">
        <div class="attr-head"><span class="verdict">归因 · ${A_.verdict}</span>
          <span style="font-size:11px;color:var(--sub);">毛利GAP ${sgn(A_.gp_gap)} · 电费费率目标30% · 其余基准=直营12城合计费率</span></div>
        <ul>
          <li class="${A_.rev_drag<0?'bad':'ok'}">收入达成 <b>${pct(A_.rev_ach)}</b> vs 时间进度 <b>${tpPct}%</b>（${pp(A_.rev_drag/100)}），隐含月目标 ${wan(A_.imp_tgt)}、MTD ${wan(A_.rev)}</li>
          <li class="${A_.gpm_drag<0?'bad':'ok'}">毛利率 <b>${pct(A_.gpm,2)}</b> vs 目标 <b>${pct(A_.gpm_tgt,2)}</b>（${pp(A_.gpm_drag/100)}，约影响毛利 <b>${wan(A_.gpm_drag/100*A_.rev)}</b>）</li>
          <li class="${A_.drags.length?'bad':'ok'}">成本拖累：${dragTxt}</li>
          <li class="${(A_.u_chg!=null&&A_.u_chg<0)||(kdD!=null&&kdD<-0.02)?'bad':'ok'}">量价：近14天换电用户 <b>${uTxt}</b>；客单价(去KA) <b>${A_.kd?.toFixed(0)||'-'}</b> 元 vs 基准 ${A_.kd_bench.toFixed(0)} 元${kdD==null?'':'（'+pp(kdD,1)+'）'}</li>
          ${A_.jul_gpm!=null?`<li class="mut">背景：7月毛利率 ${pct(A_.jul_gpm,2)}，环比 ${pp(A_.jul_gpm_mom)}</li>`:''}
        </ul></td></tr>`;
    }
    // detail row（三子页：经营趋势与预估 / 电费异动 / 成本结构）
    h += `<tr class="detail" id="d-${name}" style="display:none;"><td colspan="19">
      <div class="subtabs" id="stb-${name}">
        <button class="on" data-s="0">经营趋势与预估</button>
        <button data-s="1">电费异动</button>
        <button data-s="2">成本结构</button>
      </div>
      <div class="subp on" id="st0-${name}"></div>
      <div class="subp" id="st1-${name}"></div>
      <div class="subp" id="st2-${name}"></div>
    </td></tr>`;
  }
  const TG=DATA.targets;
  h += `<tr style="font-weight:700;background:#f6f9fd;"><td class="cname">直营合计</td>
    <td>${TG.direct_total_t1.toLocaleString()}<div style="font-size:10.5px;color:var(--sub);font-weight:400;">套内 ${TG.direct_taonei_t1.toLocaleString()}</div></td>
    <td>${TG.direct_total.toLocaleString()}<div style="font-size:10.5px;" class="${cls(TG.direct_total_t1-TG.direct_total)}">${sgn(TG.direct_total_t1-TG.direct_total)}</div></td>
    <td colspan="16" style="text-align:left;color:var(--sub);">用户目标GAP＝T-1换电用户−目标 · 目标来源：用户规模日报 · 月底预估：变动成本=近7日电费费率×预估收入、固定成本按天计提、一次性成本按实际，详见展开页</td></tr>`;
  document.getElementById('tblDirect').innerHTML = h + '</tbody>';

  /* ---- 城市三子页构建 ---- */
  const built = {}, shown = {};
  function buildSub0(name){
    const EXT = DATA.city_ext[name]||{}, cmp = EXT.cmp||{}, fc = EXT.fc;
    const METRICS = ['收入','毛利额','毛利率','换电用户','单用户收入','电费率'];
    let t = `<div style="font-weight:600;margin-bottom:6px;">经营趋势对比（T-1 = ${dstr(DATA.meta.mtd_end)}）</div>
      <table class="mini-t"><thead><tr><th style="text-align:left">指标</th><th>T-1</th><th>日环比<br>(T-1 vs T-2)</th><th>较上周同日<br>(T-1 vs T-8)</th><th>近7日周环比<br>(7日均 vs 前7日均)</th></tr></thead><tbody>`;
    for(const k of METRICS){
      const c = cmp[k];
      if(!c){ t += `<tr><td style="text-align:left;color:var(--sub)">${k}</td><td colspan="4">-</td></tr>`; continue; }
      const [val,a,b2,c2] = fmtCmp(k,c);
      t += `<tr><td style="text-align:left;color:var(--sub)">${k}${isRateK(k)?' <span style="font-size:10px">· 变化为pp</span>':''}</td>
        <td><b>${val}</b></td><td class="${cls(c.dod)}">${a}</td><td class="${cls(c.wowsame)}">${b2}</td><td class="${cls(c.w7)}">${c2}</td></tr>`;
    }
    t += '</tbody></table>';
    let f = '';
    if(fc){
      const cell = (tt,vv,s='',cl='')=>`<div class="fc-cell"><div class="t">${tt}</div><div class="v ${cl}">${vv}${s}</div></div>`;
      const revMtd = (DATA.cities12[name]['收入']||{}).mtd;
      f = `<div style="font-weight:600;margin:12px 0 6px;">月底预估（8月全月）</div>
      <div class="fc-grid">
        ${cell('预估收入', wan(fc.rev_proj), `<small>MTD ${wan(revMtd)} + 近7日run-rate</small>`)}
        ${cell('预估毛利额', wan(fc.gp_proj))}
        ${cell('预估毛利率', pct(fc.gpm_proj,2), `<small>目标 ${pct(fc.gpm_tgt,2)}</small>`)}
        ${cell('毛利达成率', pct(fc.gpm_ach,1), '', fc.gpm_ach>=1?'up':'down')}
        ${cell('预估毛利GAP', sgn(fc.gp_gap_proj), '<small>预估毛利−预估收入×目标毛利率</small>', cls(fc.gp_gap_proj))}
      </div>
      <table class="mini-t"><thead><tr><th style="text-align:left">成本分项</th><th>预估金额</th><th style="text-align:left">预估方法</th></tr></thead><tbody>
        ${[['变动成本(电费)预估','变动'],['固定成本计提','固定'],['一次性成本(安装)按实际','一次性'],['营销费预估','营销']].map(([k2,mk3])=>`<tr><td style="text-align:left;color:var(--sub)">${k2}</td><td>${wan(fc.parts[k2])}</td><td style="text-align:left;color:var(--sub)">${fc.methods[mk3]||''}</td></tr>`).join('')}
      </tbody></table>
      <div class="note">预估方法：收入=MTD+近7日run-rate×剩余天数；变动成本(电费)=预估收入×近7日电费费率；固定成本(租金+人力折旧+其他)=按天线性计提；一次性成本(安装)=按MTD实际、不外推；营销费=近7日run-rate。各类成本方法不同，未用「MTD÷时间进度」一刀切。</div>`;
    }
    document.getElementById('st0-'+name).innerHTML = t + f + `
      <div class="grid" style="grid-template-columns:1fr 1fr;gap:10px;margin-top:12px;">
        <div><div style="font-weight:600;margin-bottom:6px;">近14天收入（万）</div><div id="sp-${name}" class="chart" style="height:170px;"></div></div>
        <div><div style="font-weight:600;margin-bottom:6px;">近14天毛利率（%）</div><div id="sg-${name}" class="chart" style="height:170px;"></div></div>
      </div>`;
  }
  function buildSub1(name){
    const box = document.getElementById('st1-'+name);
    const el = (DATA.city_ext[name]||{}).ele;
    if(!el){ box.innerHTML = '<div class="note">日数据不足14天，暂无法分析电费异动</div>'; return; }
    const a0 = el.a0, a1 = el.a1, p2 = v => v==null?'-':(v>=0?'+':'')+(v*100).toFixed(2)+'pp';
    const f3 = (a,b,d) => a==null||b==null?'-':((b-a)>=0?'+':'')+(b-a).toFixed(d);
    box.innerHTML = `
    <div class="grid" style="grid-template-columns:1.15fr 1fr;gap:10px;">
      <div><div style="font-weight:600;margin-bottom:6px;">近14天 电费率 / 单用户用电量 / 度电单价</div><div id="chE1-${name}" class="chart" style="height:210px;"></div></div>
      <div><div style="font-weight:600;margin-bottom:6px;">日均温度 / 降雨量（天气·辅助相关因素）</div><div id="chE2-${name}" class="chart" style="height:210px;"></div></div>
    </div>
    <div class="grid" style="grid-template-columns:1fr 1.15fr;gap:10px;margin-top:12px;">
      <div>
        <div style="font-weight:600;margin-bottom:6px;">电费率三因子（近7日日均 vs 前7日日均）</div>
        <table class="mini-t"><tbody>
          <tr><th style="text-align:left">因子</th><th>前7日</th><th>近7日</th><th>变化</th></tr>
          <tr><td style="text-align:left;color:var(--sub)">电费率（实际）</td><td>${pct(a0.rate,2)}</td><td><b>${pct(a1.rate,2)}</b></td><td class="${cls(a1.rate-a0.rate)}">${p2(a0.rate!=null&&a1.rate!=null?a1.rate-a0.rate:null)}</td></tr>
          <tr><td style="text-align:left;color:var(--sub)">单用户用电量（度/人·日）</td><td>${a0.k?.toFixed(2)||'-'}</td><td>${a1.k?.toFixed(2)||'-'}</td><td class="${cls((a1.k??0)-(a0.k??0))}">${f3(a0.k,a1.k,2)}</td></tr>
          <tr><td style="text-align:left;color:var(--sub)">度电单价（元/度）</td><td>${a0.p?.toFixed(3)||'-'}</td><td>${a1.p?.toFixed(3)||'-'}</td><td class="${cls((a1.p??0)-(a0.p??0))}">${f3(a0.p,a1.p,3)}</td></tr>
          <tr><td style="text-align:left;color:var(--sub)">单用户收入（元/人·日）</td><td>${a0.rpu?.toFixed(2)||'-'}</td><td>${a1.rpu?.toFixed(2)||'-'}</td><td class="${cls((a1.rpu??0)-(a0.rpu??0))}">${f3(a0.rpu,a1.rpu,2)}</td></tr>
        </tbody></table>
        <div class="note">异动拆分（顺序替代法，三项之和=Δ电费率）：<br>
          用电量影响 <b class="${cls(el.d_usage)}">${p2(el.d_usage)}</b> · 度电单价影响 <b class="${cls(el.d_price)}">${p2(el.d_price)}</b> · 收入分母影响 <b class="${cls(el.d_denom)}">${p2(el.d_denom)}</b></div>
      </div>
      <div><div style="font-weight:600;margin-bottom:6px;">电费率变化分解（近7日 vs 前7日，pp）</div><div id="chED-${name}" class="chart" style="height:210px;"></div></div>
    </div>
    <div style="margin-top:10px;">
      <div style="font-weight:600;margin-bottom:6px;">异常日（近7日：电费率高于7日均值1pp以上，或日环比升1pp以上）</div>
      ${el.anom_days.length? `<div class="chips" style="justify-content:flex-start;">${el.anom_days.map(a=>`<span class="chip r">${dstr(a.date)} 电费率${pct(a.rate,1)}${a.temp!=null?' · '+a.temp+'℃':''}${a.rain!=null?' · 降雨'+a.rain+'mm':''}</span>`).join('')}</div>` : '<span style="color:var(--sub)">近7日无显著异常日</span>'}
      <div class="note">天气仅作辅助相关因素：近7日均温 <b>${el.weather.l7.temp?.toFixed(1)??'-'}℃</b>（前7日 ${el.weather.p7.temp?.toFixed(1)??'-'}℃），累计降雨 <b>${el.weather.l7.rain?.toFixed(1)??'-'}mm</b>（前7日 ${el.weather.p7.rain?.toFixed(1)??'-'}mm）。相关≠因果，不得将天气直接判定为电费异动的确定原因。</div>
    </div>`;
  }
  function buildSub2(name){
    const v = DATA.cities12[name];
    const gv2 = (k,f='mtd') => v[k]? v[k][f] : null;
    document.getElementById('st2-'+name).innerHTML = `<div style="max-width:600px;">
      <div style="font-weight:600;margin-bottom:6px;">成本结构（MTD）</div>
      <table class="mini-t"><tbody>
      <tr><th style="text-align:left;">项目</th><th>MTD</th><th>T-1</th><th>日环比 T-1vsT-2</th></tr>
      ${[['租金','租金'],['电费','电费'],['- 电费费率','-电费费率','p'],['- 单用户用电量','-单用户用电量','f2','度/人'],['全职人力&资产折旧','全职人力&资产折旧'],['安装成本','安装成本'],['其他成本','其他成本'],['营销费','营销费'],['- 营销费率','-营销费率','p']]
        .map(([lab,key,fmt,unit])=>{
          const m=gv2(key), t=gv2(key,'t1'), dd=(DATA.city_dod[name]||{})[key.replace(/^-/,'')];
          let vs2 = fmt==='p'? pct(m) : fmt==='f2'? (m?.toFixed(2)+(unit||'')) : wan(m);
          let ts = fmt==='p'? pct(t) : fmt==='f2'? t?.toFixed(2) : wan(t);
          let ds3 = dd==null?'-': fmt==='p'? pp(dd) : fmt==='f2'? (dd>=0?'+':'')+dd.toFixed(2) : sgn(dd);
          return `<tr><td style="text-align:left;color:var(--sub);">${lab}</td><td><b>${vs2}</b></td><td style="color:var(--sub);">T-1 ${ts||'-'}</td><td class="${cls(dd)}">${ds3}</td></tr>`;}).join('')}
      </tbody></table></div>`;
  }
  function showSub(name,i){
    const subEl = document.getElementById('st'+i+'-'+name);
    const visq = (el,fn)=>{ if(el.offsetWidth>0){fn();return;} let n=0;
      const t=setInterval(()=>{ if(el.offsetWidth>0){clearInterval(t);fn();} else if(++n>60)clearInterval(t); },100); };
    const key = name+'_'+i;
    if(shown[key]){ subEl.querySelectorAll('.chart').forEach(el=>{const c=echarts.getInstanceByDom(el); c&&c.resize();}); return; }
    shown[key] = 1;
    if(i===0){
      const dl = DATA.city_daily14[name]||[], xs = dl.map(d=>dstr(d.date));
      visq(document.getElementById('sp-'+name), ()=>{
      echarts.init(document.getElementById('sp-'+name)).setOption({
        grid:{left:44,right:8,top:8,bottom:20},tooltip:{trigger:'axis'},
        xAxis:{type:'category',data:xs,axisLabel:{fontSize:9},boundaryGap:false},
        yAxis:{type:'value',axisLabel:{fontSize:9},scale:true},
        series:[{type:'line',data:dl.map(d=>+((d.rev||0)/1e4).toFixed(1)),itemStyle:{color:'#5b8def'},lineStyle:{width:2.2},symbolSize:6,areaStyle:{opacity:.10}}]});
      echarts.init(document.getElementById('sg-'+name)).setOption({
        grid:{left:40,right:8,top:8,bottom:20},tooltip:{trigger:'axis'},
        xAxis:{type:'category',data:xs,axisLabel:{fontSize:9}},
        yAxis:{type:'value',axisLabel:{fontSize:9,formatter:'{value}%'}},
        series:[{type:'line',data:dl.map(d=>d.gpm==null?null:+(d.gpm*100).toFixed(2)),itemStyle:{color:'#ef9f2e'},symbolSize:4}]});
      });
    }
    if(i===1){
      const el = (DATA.city_ext[name]||{}).ele;
      if(!el) return;
      visq(document.getElementById('chE1-'+name), ()=>{
      const xs = el.series.map(s=>dstr(s.date));
      echarts.init(document.getElementById('chE1-'+name)).setOption({
        tooltip:{trigger:'axis'},legend:{data:['电费率','单用户用电量','度电单价'],top:0,textStyle:{fontSize:10}},
        grid:{left:42,right:42,top:30,bottom:22},
        xAxis:{type:'category',data:xs,axisLabel:{fontSize:9}},
        yAxis:[{type:'value',axisLabel:{fontSize:9},min:v=>Math.floor(v.min-1)},{type:'value',axisLabel:{fontSize:9}}],
        series:[
          {name:'电费率',type:'line',data:el.series.map(s=>s.rate==null?null:+(s.rate*100).toFixed(2)),itemStyle:{color:'#d64545'},lineStyle:{width:2},symbolSize:4},
          {name:'单用户用电量',type:'line',yAxisIndex:1,data:el.series.map(s=>s.kwh==null?null:+s.kwh.toFixed(2)),itemStyle:{color:'#1f6fe0'},symbolSize:4},
          {name:'度电单价',type:'line',yAxisIndex:1,data:el.series.map(s=>s.p==null?null:+s.p.toFixed(3)),itemStyle:{color:'#ef9f2e'},symbolSize:4},
        ]});
      echarts.init(document.getElementById('chE2-'+name)).setOption({
        tooltip:{trigger:'axis'},legend:{data:['日均温度','降雨量'],top:0,textStyle:{fontSize:10}},
        grid:{left:38,right:42,top:30,bottom:22},
        xAxis:{type:'category',data:xs,axisLabel:{fontSize:9}},
        yAxis:[{type:'value',name:'℃',axisLabel:{fontSize:9}},{type:'value',name:'mm',axisLabel:{fontSize:9}}],
        series:[
          {name:'日均温度',type:'line',data:el.series.map(s=>s.temp),itemStyle:{color:'#ef9f2e'},symbolSize:4,lineStyle:{width:2}},
          {name:'降雨量',type:'bar',yAxisIndex:1,data:el.series.map(s=>s.rain),itemStyle:{color:'#9ec5f5'},barWidth:7},
        ]});
      const dd = [el.d_usage,el.d_price,el.d_denom].map(v=>v==null?0:+(v*100).toFixed(2));
      echarts.init(document.getElementById('chED-'+name)).setOption({
        tooltip:{trigger:'axis',formatter:p=>p[0].name+'：'+p[0].value+'pp'},
        grid:{left:90,right:30,top:16,bottom:24},
        xAxis:{type:'value',axisLabel:{fontSize:10,formatter:'{value}pp'}},
        yAxis:{type:'category',data:['收入分母影响','度电单价影响','用电量影响'],axisLabel:{fontSize:11}},
        series:[{type:'bar',data:dd.map(v=>({value:v,itemStyle:{color:v>=0?'#d64545':'#18a058'}})),barWidth:18,
          label:{show:true,position:'right',fontSize:10,formatter:p=>p.value+'pp'}}]});
      });
    }
  }
  document.querySelectorAll('#tblDirect tr.clickable').forEach(tr=>{
    tr.onclick = ()=>{
      const name = tr.dataset.c, dr = document.getElementById('d-'+name);
      const open = dr.style.display!=='none';
      document.querySelectorAll('#tblDirect tr.detail').forEach(x=>x.style.display='none');
      if(open) return;
      dr.style.display='';
      if(!built[name]){
        built[name] = 1;
        buildSub0(name); buildSub1(name); buildSub2(name);
        document.querySelectorAll('#stb-'+name+' button').forEach(b=>b.onclick=ev=>{
          ev.stopPropagation();
          document.querySelectorAll('#stb-'+name+' button').forEach(x=>x.classList.remove('on'));
          b.classList.add('on');
          [0,1,2].forEach(j=>document.getElementById('st'+j+'-'+name).classList.toggle('on', j==+b.dataset.s));
          showSub(name, +b.dataset.s);
        });
      }
      showSub(name, 0);
    };
  });

  /* ---- 大区城市变化贡献排名 ---- */
  (function(){
    const one = mk => {
      const arr = DATA.contrib[mk]||[];
      if(!arr.length) return '<div class="note">无数据</div>';
      const tbl = (rows,title)=>`<div><div style="font-weight:600;margin-bottom:5px;">${title}</div>
        <table class="mini-t"><thead><tr><th style="text-align:left">城市</th><th>变化额</th><th>贡献占比</th></tr></thead><tbody>
        ${rows.map(r=>`<tr><td style="text-align:left">${r.city}</td><td class="${cls(r.delta)}">${sgn(r.delta)}</td><td class="${cls(r.share)}">${r.share==null?'-':(r.share*100).toFixed(1)+'%'}</td></tr>`).join('')}
        </tbody></table></div>`;
      return tbl(arr.slice(0,5),'拖累 TOP5（降幅最大）') + tbl(arr.slice(-5).reverse(),'拉动 TOP5（增幅最大）');
    };
    document.getElementById('contribBody').innerHTML = ['收入','毛利额'].map(mk=>
      `<div class="panel" style="margin-bottom:0;padding:10px 12px;"><h3 style="font-size:13px;">${mk}</h3>
       <div class="grid" style="grid-template-columns:1fr 1fr;gap:12px;">${one(mk)}</div></div>`).join('');
  })();

  /* ---- 经营风险矩阵（t2 显示时惰性初始化） ---- */
  window.__tabExtra = t => {
    if(t!=='t2' || window.__riskDrawn) return;
    window.__riskDrawn = true;
    const rs = DATA.risk.filter(r=>r.x!=null && r.y!=null);
    const mk2 = type => rs.filter(r=>(r.type||'')===type).map(r=>[+(r.x*100).toFixed(2), +(r.y*100).toFixed(2), r.rev, r.city, r.has_tgt]);
    echarts.init(document.getElementById('chRisk')).setOption({
      tooltip:{formatter:p=>`<b>${p.data[3]}</b>（${p.seriesName}）${p.data[4]?'':'·25%参考线'}<br>毛利率vs目标缺口 ${p.data[0]}pp<br>近7日收入周环比 ${p.data[1]}%<br>收入MTD ${wan(p.data[2])}`},
      legend:{data:['直营','联营'],top:0,textStyle:{fontSize:11}},
      grid:{left:56,right:26,top:34,bottom:44},
      xAxis:{type:'value',name:'毛利率缺口 (pp)',nameLocation:'middle',nameGap:28,axisLabel:{fontSize:10},splitLine:{lineStyle:{type:'dashed'}}},
      yAxis:{type:'value',name:'近7日收入周环比 (%)',axisLabel:{fontSize:10},splitLine:{lineStyle:{type:'dashed'}}},
      series:[
        {name:'直营',type:'scatter',data:mk2('直营'),symbolSize:d=>Math.max(9,Math.sqrt(d[2])/80),itemStyle:{color:'#1f6fe0',opacity:.78},
         markLine:{silent:true,symbol:'none',lineStyle:{color:'#aab7c8',type:'dashed'},label:{show:false},data:[{xAxis:0},{yAxis:0}]}},
        {name:'联营',type:'scatter',data:mk2('联营'),symbolSize:d=>Math.max(6,Math.sqrt(d[2])/80),itemStyle:{color:'#ef9f2e',opacity:.72}},
      ]});
  };
})();

/* ---------- T3: 联营 ---------- */
(function(){
  const LY = DATA.region['联营毛利额'];
  const totAug = DATA.lianying.reduce((s,r)=>s+(r.aug_rev||0),0);
  const totJul = DATA.lianying.reduce((s,r)=>s+(r.jul_rev||0),0);
  document.getElementById('lyKpi').innerHTML = [
    `<div class="card kpi"><div class="t"><span>联营毛利额 8月MTD</span></div><div class="v">${wan(LY.mtd)}<small> 元</small></div>
      <div class="s">T-1 <b>${wan(LY.t1)}</b> · 本月目标 <b>${wan(LY.target)}</b><br>目标进度Gap <b class="${cls(LY.gap)}">${pp(LY.gap)}</b></div></div>`,
    `<div class="card kpi"><div class="t"><span>联营收入 8月MTD</span></div><div class="v">${wan(totAug)}<small> 元</small></div>
      <div class="s">7月全月 <b>${wan(totJul)}</b></div></div>`,
    `<div class="card kpi"><div class="t"><span>联营城市数</span></div><div class="v">${DATA.lianying.length}<small> 个</small></div>
      <div class="s">8月有收入城市 <b>${DATA.lianying.filter(r=>r.aug_rev>0).length}</b> 个 · 用户目标 <b>${(DATA.targets.ly_total/1e4).toFixed(2)}万</b></div></div>`,
    `<div class="card kpi"><div class="t"><span>8月MTD毛利率>30%城市</span></div><div class="v">${DATA.lianying.filter(r=>r.aug_gpm>=0.3).length}<small> 个</small></div>
      <div class="s">毛利率为负 <b class="down">${DATA.lianying.filter(r=>r.aug_gpm<0).length}</b> 个</div></div>`,
  ].join('');
  const cols = [['city','城市'],['mode','模式'],['aug_rev','收入 8月MTD'],['aug_gpm','毛利率 8月'],['aug_users','换电用户'],['target','换电用户目标'],['aug_kedanjia','客单价'],
                ['jul_rev','收入 7月'],['jul_rev_mom','7月环比'],['jul_gpm','毛利率 7月'],['jul_gp_user','单户毛利 7月']];
  const tbl = document.getElementById('tblLy');
  let sortKey = 'aug_rev', sortDesc = true;
  function render(){
    const rows = [...DATA.lianying].sort((a,b)=>{
      let x=a[sortKey], y=b[sortKey];
      if(sortKey==='city'||sortKey==='mode') return sortDesc? String(y).localeCompare(String(x),'zh'): String(x).localeCompare(String(y),'zh');
      x=x??-Infinity; y=y??-Infinity;
      return sortDesc? y-x : x-y;
    });
    let h = `<thead><tr>${cols.map(([k,t])=>`<th data-k="${k}" style="cursor:pointer;">${t}${k===sortKey?(sortDesc?' ↓':' ↑'):''}</th>`).join('')}</tr></thead><tbody>`;
    for(const r of rows){
      const momTag = r.jul_rev_mom==null?'-':`<span class="${cls(r.jul_rev_mom)}">${(r.jul_rev_mom>=0?'+':'')+(r.jul_rev_mom*100).toFixed(1)}%</span>`;
      h += `<tr><td class="cname">${r.city}</td>
        <td><span class="tag o">${r.mode}</span></td>
        <td><b>${wan(r.aug_rev)}</b></td><td>${pct(r.aug_gpm)}</td>
        <td>${r.aug_users?.toLocaleString()||'-'}</td><td>${r.target?r.target.toLocaleString():'-'}</td><td>${r.aug_kedanjia?.toFixed(0)||'-'}</td>
        <td>${wan(r.jul_rev)}</td><td>${momTag}</td>
        <td>${pct(r.jul_gpm)}</td><td>${r.jul_gp_user?.toFixed(1)||'-'}</td></tr>`;
    }
    const T=DATA.targets;
    h += `<tr style="font-weight:700;background:#f6f9fd;"><td class="cname">联营已开城</td><td>-</td>
      <td colspan="2" style="text-align:left;color:var(--sub);">用户目标 <b style="color:var(--ink)">${T.ly_total.toLocaleString()}</b></td>
      <td>${T.ly_total_t1.toLocaleString()}</td><td class="${cls(T.ly_total_t1-T.ly_total)}">${sgn(T.ly_total_t1-T.ly_total)}</td>
      <td colspan="5" style="text-align:left;color:var(--sub);">南部合计目标 <b style="color:var(--ink)">${T.region_total.toLocaleString()}</b> · T-1 ${T.region_total_t1.toLocaleString()}</td></tr>`;
    tbl.innerHTML = h+'</tbody>';
    tbl.querySelectorAll('th').forEach(th=>th.onclick=()=>{
      const k = th.dataset.k;
      if(sortKey===k) sortDesc=!sortDesc; else {sortKey=k; sortDesc = k!=='city';}
      render();
    });
  }
  render();
})();

/* ---------- T4: 月度趋势 ---------- */
(function(){
  const ms = DATA.months, LAST = ms[ms.length-1];
  const augD = parseInt(DATA.meta.mtd_end.slice(6,8)), dim = DATA.meta.days_in_month, f = dim/augD;
  const lb = ms.map(m=> m===LAST? `8月·${augD}天+预测` : DATA.mlabel[m]);
  const rev = ms.map(m=>+(DATA.agg_trend[m].rev/1e4).toFixed(0));
  const revP = ms.map(m=> (m===LAST)? +((DATA.agg_trend[m].rev*(f-1))/1e4).toFixed(0) : 0);
  const totLabel = p => wan((rev[p.dataIndex]+revP[p.dataIndex])*1e4)+(ms[p.dataIndex]===LAST?' 预':'');
  const gpm = ms.map(m=>+(DATA.agg_trend[m].gpm*100).toFixed(2));
  const users = ms.map(m=>DATA.agg_trend[m].users);
  echarts.init(document.getElementById('chMonth')).setOption({
    tooltip:{trigger:'axis'},legend:{data:['收入','月末预测','毛利率','换电用户'],top:0},
    grid:{left:56,right:56,top:36,bottom:26},
    xAxis:{type:'category',data:lb},
    yAxis:[{type:'value',name:'万元'},{type:'value',name:'%',min:20,max:34}],
    series:[
      {name:'收入',type:'bar',stack:'rev',data:rev,itemStyle:{color:'#5b8def'},barWidth:34},
      {name:'月末预测',type:'bar',stack:'rev',data:revP,itemStyle:{color:'rgba(91,141,239,.35)'},barWidth:34,
       label:{show:true,position:'top',formatter:totLabel,fontSize:10}},
      {name:'毛利率',type:'line',yAxisIndex:1,data:gpm,itemStyle:{color:'#ef9f2e'},lineStyle:{width:2.5},symbolSize:7,
       label:{show:true,formatter:p=>p.value+'%',fontSize:10}},
      {name:'换电用户',type:'line',data:users,itemStyle:{color:'#7a5af5'},symbolSize:6,
       label:{show:true,formatter:p=>(p.value/1e4).toFixed(1)+'万',fontSize:10}},
    ]});
  const sel = document.getElementById('citySel');
  const allCities = Object.keys(DATA.city_trend).sort((a,b)=>
    (DATA.city_trend[b]['202607']?.rev||0)-(DATA.city_trend[a]['202607']?.rev||0));
  sel.innerHTML = allCities.map(c=>`<option>${c}</option>`).join('');
  const chC = echarts.init(document.getElementById('chCity'));
  function drawCity(c){
    const t = DATA.city_trend[c]||{};
    chC.setOption({
      tooltip:{trigger:'axis'},legend:{data:['收入','月末预测','毛利率'],top:0},
      grid:{left:56,right:52,top:34,bottom:26},
      xAxis:{type:'category',data:lb},
      yAxis:[{type:'value',name:'万元'},{type:'value',name:'%',axisLabel:{formatter:'{value}%'}}],
      series:[
        {name:'收入',type:'bar',stack:'rev',data:ms.map(m=>t[m]?+(t[m].rev/1e4).toFixed(1):null),itemStyle:{color:'#5b8def'},barWidth:30},
        {name:'月末预测',type:'bar',stack:'rev',data:ms.map(m=>(m===LAST&&t[m])?+((t[m].rev*(f-1))/1e4).toFixed(1):0),
         itemStyle:{color:'rgba(91,141,239,.35)'},barWidth:30,
         label:{show:true,position:'top',fontSize:10,formatter:p=>{const m=ms[p.dataIndex];const rv=t[m]?t[m].rev:0;return rv?wan(rv/1e4*1e4*(m===LAST?f:1))+(m===LAST?' 预':''):'';}}},
        {name:'毛利率',type:'line',yAxisIndex:1,data:ms.map(m=>t[m]?+(t[m].gpm*100).toFixed(2):null),itemStyle:{color:'#ef9f2e'},lineStyle:{width:2.5},symbolSize:7,
         label:{show:true,formatter:p=>p.value+'%',fontSize:10}},
      ]}, true);
  }
  sel.onchange = ()=>drawCity(sel.value);
  drawCity(allCities[0]);
})();
</script>
</body>
</html>"""

open('南部大区城市看板/南部大区经营看板_v2.html', 'w', encoding='utf-8').write(html)
print('written', len(html), 'chars')
