#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""西安佰奥莱博 · 客户360一页纸（A4，自包含 HTML + PDF）
口径：仅西安佰奥莱博；标准销售订单+非赠品；不含佰奥长生任何信息。
"""
import datetime as dt
import json
import os
import sys

BASE = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
sys.path.insert(0, BASE + '/脚本')
import brand_kit as BK  # noqa: E402

P = json.load(open(BASE + '/数据/baiaolab_xian_3y.json', encoding='utf-8'))
Y24, Y25, Y26 = P['2024'], P['2025'], P['2026']
AR = P['ar']
AR_TOT = sum(float(r['amt']) for r in AR)
AR_OD = sum(float(r.get('od_amt') or 0) for r in AR)
AR_MAXD = max([int(r['od_days']) for r in AR if r.get('od_days')] or [0])
COMPS = sorted(P['complaints'], key=lambda c: c.get('created', ''))
TODAY = dt.date(2026, 10, 21)

LOGO = BK.logo_data_uri()


def wan(x):
    return x / 1e4


MONTHS = ['%02d' % i for i in range(1, 13)]


def month_rows(year_key):
    m = {'2024': Y24, '2025': Y25, '2026': Y26}[year_key]['by_month']
    return [m.get('%s-%s' % (year_key, k), 0) for k in MONTHS]


DATA = {
    '2024': month_rows('2024'),
    '2025': month_rows('2025'),
    '2026': month_rows('2026'),
}
MAXV = max(max(v) for v in DATA.values()) / 1e4


def bars():
    out = []
    for i, m in enumerate(MONTHS):
        vals = []
        for y, color in (('2024', '#b9cbe0'), ('2025', '#4b7fb5'), ('2026', '#1F4E79')):
            v = DATA[y][i] / 1e4
            h = max(1.2, v / MAXV * 62) if v > 0 else 0.8
            vals.append('<i style="height:%.1fpx;background:%s" title="%s %s月 %.1f万"></i>' % (h, color, y, int(m), v))
        out.append('<div class="mg"><div class="mb">%s</div><span>%d</span></div>' % (''.join(vals), int(m)))
    return ''.join(out)


# 货号矩阵
def sku_map(y):
    return {s['sku']: s['amt'] for s in y['top_sku']}


M24, M25, M26 = sku_map(Y24), sku_map(Y25), sku_map(Y26)
KEYS = []
for m in (M26, M25, M24):
    for k, v in sorted(m.items(), key=lambda kv: -kv[1]):
        if k not in KEYS:
            KEYS.append(k)
KEYS = KEYS[:8]
NAME = {s['sku']: s['name'] for y in (Y24, Y25, Y26) for s in y['top_sku']}


def clean(n):
    import re
    n = re.sub(r'\s*\(.*$', '', n or '')
    return n.replace('5×', '').replace('2×', '').strip()[:22]


rows = []
for k in KEYS:
    rows.append('<tr><td class="sku">%s</td><td class="pn">%s</td><td>%s</td><td>%s</td><td>%s</td></tr>' % (
        k, clean(NAME.get(k, '')),
        ('%.1f' % wan(M24[k])) if k in M24 else '—',
        ('%.1f' % wan(M25[k])) if k in M25 else '—',
        ('%.1f' % wan(M26[k])) if k in M26 else '—'))

comp_rows = []
for c in COMPS:
    try:
        d = dt.date.fromisoformat(c['created'])
        days = (TODAY - d).days
    except Exception:
        days = 0
    open_ = c.get('status') != '结束'
    comp_rows.append('<tr><td>%s</td><td class="iss">%s</td><td class="%s">%s</td><td class="rt">%d 天</td></tr>' % (
        c.get('created', ''), str(c.get('name', '')).split('-', 2)[-1], 'op' if open_ else 'ok',
        c.get('status', ''), days))

TOT3 = Y24['total'] + Y25['total'] + Y26['total']
YOY = (Y26['total'] / sum(v for k, v in Y25['by_month'].items() if k <= '2025-10') - 1) * 100
G25 = (Y25['total'] / Y24['total'] - 1) * 100

HTML = """<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
<title>西安佰奥莱博 · 客户360</title>
<style>
@page{size:A4;margin:0}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:"PingFang SC","Microsoft YaHei",sans-serif;color:#1b2733;background:#eef2f7;
     -webkit-print-color-adjust:exact;print-color-adjust:exact}
.page{width:210mm;height:297mm;background:#fff;margin:0 auto;padding:9mm 10mm 7mm;position:relative;overflow:hidden}
.brand{display:flex;align-items:center;justify-content:space-between;border-bottom:2px solid #1F4E79;padding-bottom:2.2mm}
.brand img{height:7.2mm}
.brand .r{font-size:7.4pt;color:#6b7c8f;letter-spacing:.4px}
h1{font-size:15.5pt;color:#1F4E79;margin-top:2.6mm;letter-spacing:.5px}
h1 b{font-weight:800}
.meta{font-size:7.6pt;color:#5b6b7c;margin-top:1.2mm}
.kpis{display:grid;grid-template-columns:repeat(6,1fr);gap:2mm;margin-top:2.6mm}
.kpi{border:1px solid #dde5ef;border-top:2.4px solid #1F4E79;border-radius:1mm;padding:1.6mm 2mm}
.kpi.warn{border-top-color:#c0392b}
.kpi .l{font-size:6.9pt;color:#7a8899}
.kpi .v{font-size:12.5pt;font-weight:800;color:#12324f;margin-top:.4mm;line-height:1.1}
.kpi .v small{font-size:7pt;font-weight:600;color:#7a8899;margin-left:.6mm}
.kpi .s{font-size:6.6pt;color:#8b98a8;margin-top:.5mm}
h2{font-size:9.2pt;color:#1F4E79;margin:3.2mm 0 1.4mm;padding-left:2mm;border-left:3px solid #1F4E79}
.two{display:grid;grid-template-columns:1.15fr .85fr;gap:3mm}
.card{border:1px solid #e1e8f1;border-radius:1mm;padding:2mm 2.4mm}
.chart{display:flex;align-items:flex-end;gap:1.1mm;height:70px;border-bottom:1px solid #cfd9e5;padding-bottom:.6mm}
.mg{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%}
.mb{display:flex;align-items:flex-end;gap:.5px;height:62px}
.mb i{display:block;width:2.6px;border-radius:.4px .4px 0 0}
.mg span{font-size:5.6pt;color:#8b98a8;margin-top:.6mm}
.leg{display:flex;gap:3mm;font-size:6.6pt;color:#5b6b7c;margin-top:1mm;justify-content:flex-end}
.leg i{display:inline-block;width:7px;height:7px;border-radius:1px;margin-right:1mm;vertical-align:-.5px}
.yl{display:grid;grid-template-columns:repeat(3,1fr);gap:2mm;margin-top:1mm}
.yl div{border-left:2.6px solid #b9cbe0;padding-left:2mm}
.yl div:nth-child(2){border-left-color:#4b7fb5}
.yl div:nth-child(3){border-left-color:#1F4E79}
.yl .y{font-size:6.8pt;color:#7a8899}
.yl .n{font-size:11pt;font-weight:800;color:#12324f}
.yl .n small{font-size:6.6pt;font-weight:600;color:#7a8899;margin-left:.5mm}
.yl .g{font-size:6.6pt;color:#c0392b;font-weight:700}
table{width:100%;border-collapse:collapse;font-size:7.1pt}
th{background:#1F4E79;color:#fff;font-weight:600;padding:1.1mm 1mm;text-align:left;font-size:6.9pt}
td{padding:1mm;border-bottom:1px solid #edf1f6;vertical-align:middle}
tr:nth-child(even) td{background:#f7f9fc}
td.rt,th.rt{text-align:right}
td.sku{font-family:"SF Mono",Menlo,monospace;font-size:6.8pt;color:#12324f}
td.pn{color:#5b6b7c;font-size:6.7pt}
td.iss{color:#22303f}
td.op{color:#c0392b;font-weight:700}
td.ok{color:#6b8a6b}
.note{font-size:6.5pt;color:#8b98a8;margin-top:1mm;line-height:1.45}
.four{display:grid;grid-template-columns:1fr 1fr;gap:3mm;margin-top:1mm}
.risk li{list-style:none;font-size:7.2pt;line-height:1.55;color:#22303f;padding-left:2.6mm;position:relative}
.risk li:before{content:"·";position:absolute;left:0;color:#c0392b;font-weight:900}
.risk li b{color:#c0392b}
.act li{list-style:none;font-size:7.1pt;line-height:1.5;padding:1.1mm 0;border-bottom:1px dashed #e8eef6;display:flex;justify-content:space-between;gap:2mm}
.act li:last-child{border:0}
.act li .who{color:#7a8899;font-size:6.5pt;white-space:nowrap}
.txt{font-size:7.4pt;line-height:1.62;color:#22303f}
.txt b{color:#12324f}
.foot{position:absolute;left:10mm;right:10mm;bottom:4.5mm;border-top:1px solid #dde5ef;padding-top:1.6mm;
      display:flex;justify-content:space-between;font-size:6.4pt;color:#8b98a8}
.foot .sl{color:#1F4E79;font-weight:700;letter-spacing:.6px}
</style></head><body><div class="page">

<div class="brand"><img src="__LOGO__" alt="BIORI BIOTECH"><div class="r">客户 360 卡　·　内部资料　·　2026-10-21</div></div>

<h1>西安佰奥莱博生物科技有限公司</h1>
<div class="meta">诊断原料 · 大客户销售部　｜　A 类　｜　销售：刘子研　｜　冻干预混液 OEM（兽药 GMP 约束）　｜　数据截止 2026-10-20</div>

<div class="kpis">
  <div class="kpi"><div class="l">三年累计</div><div class="v">__T3__<small>万</small></div><div class="s">2024–2026.10</div></div>
  <div class="kpi"><div class="l">2025 全年</div><div class="v">__T25__<small>万</small></div><div class="s">同比 +__G25__%</div></div>
  <div class="kpi"><div class="l">2026 年 1–10 月</div><div class="v">__T26__<small>万</small></div><div class="s">同比 __YOY__%</div></div>
  <div class="kpi"><div class="l">在采货号</div><div class="v">__SKU__<small>个</small></div><div class="s">2025 年 20 个</div></div>
  <div class="kpi warn"><div class="l">应收未收</div><div class="v">__AR__<small>万</small></div><div class="s">逾期 __OD__ 万 · 最长 __ODD__ 天</div></div>
  <div class="kpi warn"><div class="l">客诉未闭环</div><div class="v">__OPENC__<small>条</small></div><div class="s">共 5 条，最早挂 __MAXC__ 天</div></div>
</div>

<h2>一、销售趋势：先翻倍，再腰斩</h2>
<div class="two">
  <div class="card">
    <div class="chart">__BARS__</div>
    <div class="leg"><span><i style="background:#b9cbe0"></i>2024</span><span><i style="background:#4b7fb5"></i>2025</span><span><i style="background:#1F4E79"></i>2026</span></div>
  </div>
  <div class="card">
    <div class="yl">
      <div><div class="y">2024</div><div class="n">__T24__<small>万</small></div><div class="g">基数年</div></div>
      <div><div class="y">2025</div><div class="n">__T25__<small>万</small></div><div class="g">+__G25__%</div></div>
      <div><div class="y">2026（1–10 月）</div><div class="n">__T26__<small>万</small></div><div class="g">__YOY__%</div></div>
    </div>
    <div class="txt" style="margin-top:1.6mm">2025 年是靠 <b>FMD5084-000901、FMD2011</b> 两个直扩货号冲起来的；今年这两个货号同步回落，8、9 月直接零采购，10 月只发了 2.6 万。</div>
  </div>
</div>

<h2>二、主销货号：从常规 RT 预混切到直扩冻干</h2>
<table>
<tr><th style="width:24%">货号</th><th style="width:30%">产品</th><th class="rt">2024</th><th class="rt">2025</th><th class="rt">2026（1–10 月）</th></tr>
__SKUROWS__
</table>
<div class="note">单位：万元。2024 年主力是 FM513（16.8 万，占 42%）；2025 年 FMD5084-000901 从 0 做到 36.4 万、FMD2011 26.3 万，两个直扩货号合计 57%；2026 年这两个货号回落到 15.3 万和 9.2 万，FM5144-007701-TP 反而成了第三名。</div>

<h2>三、客诉：5 条，4 条已关，1 条还挂着</h2>
<div class="four">
  <div class="card">
    <table>
    <tr><th style="width:19%">日期</th><th>问题</th><th style="width:22%">状态</th><th class="rt" style="width:14%">已挂</th></tr>
    __COMPROWS__
    </table>
    <div class="note">FM5144 一个货号占了 3 条（两次沉淀、一次漏液），是当前最需要盯的质量点。</div>
  </div>
  <div>
    <div class="card" style="margin-bottom:2mm">
      <div style="font-size:7.6pt;font-weight:700;color:#c0392b;margin-bottom:1mm">风险</div>
      <ul class="risk">
        <li>应收 <b>__AR__ 万</b>，其中逾期 <b>__OD__ 万</b>，最长 __ODD__ 天；占今年销售额 __ARP__%</li>
        <li>8–9 月零采购，原因是客户研发离职 + 搬厂房，恢复时间取决于对方</li>
        <li>收入 100% 来自单一主体、单一产品线，没有第二增长点</li>
      </ul>
    </div>
    <div class="card">
      <div style="font-size:7.6pt;font-weight:700;color:#1F4E79;margin-bottom:1mm">下一步</div>
      <ul class="act">
        <li><span>① 逾期款逐票对账，锁定付款计划</span><span class="who">刘子研 · 10 月底</span></li>
        <li><span>② FM5144-007701 漏液客诉闭环</span><span class="who">质量部 · 11 月中</span></li>
        <li><span>③ 搬厂后恢复供货，锁 Q4 最低量</span><span class="who">刘子研 · 11 月中</span></li>
        <li><span>④ 宠物真菌、马 ACTB 出配方结论</span><span class="who">技术支持 · 10 月底</span></li>
      </ul>
    </div>
  </div>
</div>

<div class="foot"><span class="sl">守护生命健康，缔造美好生活</span><span>数据：ERP 订单（标准单、非赠品）+ 客诉库 + 应收账龄｜仅西安佰奥莱博单一主体｜含客户与金额信息，禁止对外</span></div>
</div></body></html>"""

rep = {
    '__LOGO__': LOGO,
    '__T3__': '%.1f' % wan(TOT3), '__T24__': '%.1f' % wan(Y24['total']),
    '__T25__': '%.1f' % wan(Y25['total']), '__T26__': '%.1f' % wan(Y26['total']),
    '__G25__': '%.1f' % G25, '__YOY__': '%.1f' % YOY,
    '__SKU__': str(Y26['sku_count']), '__AR__': '%.1f' % wan(AR_TOT), '__OD__': '%.1f' % wan(AR_OD),
    '__ODD__': str(AR_MAXD), '__OPENC__': str(len([c for c in COMPS if c.get('status') != '结束'])),
    '__MAXC__': str(max((TODAY - dt.date.fromisoformat(c['created'])).days for c in COMPS)),
    '__ARP__': '%.0f' % (AR_TOT / Y26['total'] * 100),
    '__BARS__': bars(), '__SKUROWS__': ''.join(rows), '__COMPROWS__': ''.join(comp_rows),
}
for k, v in rep.items():
    HTML = HTML.replace(k, v)

out = BASE + '/销售分析/客户360_西安佰奥莱博_2026-10-21.html'
open(out, 'w', encoding='utf-8').write(HTML)

from playwright.sync_api import sync_playwright
pdf = out.replace('.html', '.pdf')
with sync_playwright() as p:
    b = p.chromium.launch(channel='chrome')
    pg = b.new_page(viewport={'width': 794, 'height': 1123})
    pg.goto('file://' + out)
    pg.wait_for_timeout(500)
    pg.pdf(path=pdf, format='A4', print_background=True, prefer_css_page_size=True)
    b.close()

print('SAVED', out, os.path.getsize(out))
print('PDF', pdf, os.path.getsize(pdf))
print('T3,T24,T25,T26,YOY,G25,AR,OD', round(wan(TOT3), 1), round(wan(Y24['total']), 1),
      round(wan(Y25['total']), 1), round(wan(Y26['total']), 1), round(YOY, 1), round(G25, 1),
      round(wan(AR_TOT), 1), round(wan(AR_OD), 1))
