#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""产品系列资料补齐 —— 排期 + 甘特图（Excel & HTML）

排期口径（2026-10-26 用户拍板）：
- 时间窗 2026-11-01 ~ 2027-01-31
- 责任人先用占位符（研发A/B/C、产品D/E）
- 工作日 = 周一至周五，跳过 2027-01-01 元旦
- 优先级 P0 → P1 → P2，同优先级按销售额降序
- 每个任务由 1 个占位资源独占完成，资源空闲即接下一项
"""
import base64
import datetime as dt
import json
import os
import sys

import openpyxl
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.utils import get_column_letter

sys.path.insert(0, '/Users/liuxinyuan/Desktop/Hermes输出-工作类/脚本')
import brand_kit as BK

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
SRC = os.path.join(ROOT, '销售分析', '产品系列资料归档与缺口清单_v3_2026-10-26.xlsx')
OUT_XLSX = os.path.join(ROOT, '经营分析', '产品系列资料补齐_排期与甘特图_2026-11至2027-01.xlsx')
OUT_HTML = os.path.join(ROOT, '销售分析', '产品系列资料补齐_甘特图_2026-11至2027-01.html')
START, END = dt.date(2026, 11, 1), dt.date(2027, 1, 31)
HOLIDAYS = {dt.date(2027, 1, 1)}
RESOURCES = ['研发-占位A', '研发-占位B', '研发-占位C', '产品-占位D', '产品-占位E']


def workdays(a, b):
    d, out = a, []
    while d <= b:
        if d.weekday() < 5 and d not in HOLIDAYS:
            out.append(d)
        d += dt.timedelta(days=1)
    return out


def load_tasks():
    ws = openpyxl.load_workbook(SRC)['系列缺口任务清单']
    rows = []
    for r in ws.iter_rows(min_row=2, values_only=True):
        rows.append({'id': r[0], 'rep': r[1], 'series': r[2], 'cat': r[3], 'finished': r[4],
                     'sales_wan': r[5], 'customers': r[6], 'sole_share': r[7], 'docs': r[8],
                     'missing': r[9], 'special': r[10], 'priority': r[11], 'action': r[12],
                     'dept': r[13], 'days': float(r[14] or 0)})
    return rows


def schedule(tasks):
    cal = workdays(START, END)
    free = {r: 0 for r in RESOURCES}   # 资源 → 下一个可用日索引
    order = {'P0': 0, 'P1': 1, 'P2': 2}
    tasks.sort(key=lambda t: (order[t['priority']], -t['sales_wan']))
    for t in tasks:
        res = min(free, key=lambda r: (free[r], RESOURCES.index(r)))
        s = free[res]
        dur = max(1, int(round(t['days'])))
        e = min(s + dur - 1, len(cal) - 1)
        t['owner'] = res
        t['start'], t['end'] = cal[s], cal[e]
        t['dur'] = e - s + 1
        free[res] = e + 1
    return tasks, free


def build_xlsx(tasks):
    wb = openpyxl.Workbook()
    H, F = Font(color='FFFFFF', bold=True), PatternFill('solid', fgColor='1F4E79')
    ws = wb.active
    ws.title = '甘特数据'
    heads = ['任务ID', '优先级', '系列', '代表货号', '任务内容', '缺失项', '专项参数', '预计人日', '排定工期(工作日)',
             '负责人(占位)', '开始', '完成', '2026销售额(万)', '客户数', '单客金额占比%', '资料状态依据']
    ws.append(heads)
    for t in tasks:
        ws.append([t['id'], t['priority'], t['series'], t['rep'], t['action'], t['missing'], t['special'],
                   t['days'], t['dur'], t['owner'], t['start'], t['end'], t['sales_wan'], t['customers'],
                   t['sole_share'], '未收录' if t['docs'] == 0 else '部分'])
    for c in ws[1]:
        c.font = H; c.fill = F; c.alignment = Alignment(horizontal='center', wrap_text=True)
    widths = [8, 7, 30, 12, 46, 26, 18, 8, 10, 12, 11, 11, 13, 7, 11, 9]
    for i, w in enumerate(widths, 1):
        ws.column_dimensions[get_column_letter(i)].width = w
    ws.freeze_panes = 'C2'

    # 日历视图
    ws2 = wb.create_sheet('日历视图')
    weeks = []
    d = START
    while d <= END:
        ws2.cell(2, 3 + len(weeks)).value = d.strftime('%m/%d')
        ws2.cell(2, 3 + len(weeks)).font = Font(size=8)
        weeks.append(d)
        d += dt.timedelta(days=1)
    ws2.cell(1, 1, '任务'); ws2.cell(1, 2, '负责人'); ws2.cell(1, 3, '工期/起止')
    ws2.cell(2, 1, ''); ws2.cell(2, 2, '')
    for i, t in enumerate(tasks, 3):
        ws2.cell(i, 1, f"{t['id']} {t['rep']}").font = Font(size=9, bold=True)
        ws2.cell(i, 2, t['owner']).font = Font(size=8)
        ws2.cell(i, 3, f"{t['dur']}d").font = Font(size=8)
        for j, d in enumerate(weeks):
            c = ws2.cell(i, 4 + j)
            if t['start'] <= d <= t['end']:
                c.fill = PatternFill('solid', fgColor={'P0': 'C0392B', 'P1': 'E67E22', 'P2': '2E86C1'}[t['priority']])
    ws2.column_dimensions['A'].width = 26
    ws2.column_dimensions['B'].width = 12
    ws2.column_dimensions['C'].width = 7
    for j in range(len(weeks)):
        ws2.column_dimensions[get_column_letter(4 + j)].width = 2.4
    ws2.freeze_panes = 'D3'

    ws3 = wb.create_sheet('口径与资源')
    for line in [
        ['时间窗', '2026-11-01 ~ 2027-01-31'],
        ['工作日', '周一至周五，跳过 2027-01-01'],
        ['排期规则', 'P0 → P1 → P2；同优先级按 2026 销售额降序；每项由 1 名占位资源独占完成'],
        ['资源', '、'.join(RESOURCES) + '（占位，待你指派真人后一键替换）'],
        ['负载', ' / '.join('%s %d项' % (r, sum(1 for t in tasks if t['owner'] == r)) for r in RESOURCES)],
        ['总人日', '%.0f 人日' % sum(t['days'] for t in tasks)],
        ['最晚完成', max(t['end'] for t in tasks).isoformat()],
        ['说明', '人日=按缺失项 2 天/项 + 无资料建档 2 天 + DG/NA 专项 2 天/项 估算；排定工期按 1 人独占折算工作日'],
    ]:
        ws3.append(line)
    ws3.column_dimensions['A'].width = 12
    ws3.column_dimensions['B'].width = 110

    wb.save(OUT_XLSX)


def build_html(tasks):
    logo = base64.b64encode(open(os.path.expanduser('~/work/公司资料/logo_transparent.png'), 'rb').read()).decode()
    cal = workdays(START, END)
    idx = {d: i for i, d in enumerate(cal)}
    months = []
    for d in cal:
        key = (d.year, d.month)
        if not months or months[-1][0] != key:
            months.append([key, 0])
        months[-1][1] += 1

    colors = {'P0': '#c0392b', 'P1': '#e67e22', 'P2': '#2e86c1'}
    rows = []
    for i, t in enumerate(tasks):
        s, e = idx[t['start']], idx[t['end']]
        rows.append({
            'id': t['id'], 'rep': t['rep'], 'series': t['series'], 'pri': t['priority'],
            'owner': t['owner'], 'start': t['start'].isoformat(), 'end': t['end'].isoformat(),
            's': s, 'w': e - s + 1, 'days': t['dur'], 'act': t['action'], 'sales': t['sales_wan'],
            'cust': t['customers'], 'sole': t['sole_share'], 'missing': t['missing'],
            'color': colors[t['priority']],
        })
    n = len(cal)
    month_html = ''.join(
        '<div class="mh" style="width:%dpx">%d年%d月</div>' % (cnt * 9, k[0], k[1]) for k, cnt in months)
    week_html = ''
    for i, d in enumerate(cal):
        cls = 'wk' if d.weekday() == 0 else ''
        week_html += '<div class="tw %s" style="left:%dpx">%s</div>' % (cls, i * 9, d.strftime('%d') if d.weekday() == 0 else '')

    row_html = ''
    for r in rows:
        row_html += f'''
        <div class="row" data-pri="{r['pri']}" data-owner="{r['owner']}" data-text="{r['rep']} {r['series']}">
          <div class="task">
            <div class="tname"><span class="pri p{r['pri']}">{r['pri']}</span> {r['rep']}
              <span class="tser">{r['series']}</span></div>
            <div class="tmeta">{r['owner']} · {r['days']}d · {r['start'][5:]}→{r['end'][5:]} · {r['sales']}万 · {r['cust']}家客户</div>
          </div>
          <div class="lane">
            <div class="bar p{r['pri']}" style="left:{r['s']*9}px;width:{r['w']*9-2}px"
              title="{r['rep']}｜{r['series']}&#10;{r['act']}&#10;{r['owner']}｜{r['start']} → {r['end']}｜{r['days']} 工作日">
              <span>{r['days']}d</span>
            </div>
          </div>
        </div>'''

    html = f'''<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>产品系列资料补齐 · 甘特图（2026-11 ~ 2027-01）</title>
<style>
*{{box-sizing:border-box;margin:0;padding:0}}
html,body{{max-width:100%;overflow-x:hidden}}
body{{font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;background:#f4f7fb;color:#1c2b3a}}
.wrap{{max-width:1500px;margin:0 auto;padding:18px 22px 60px;overflow:hidden}}
h1{{font-size:22px;color:#12446e;margin:6px 0 4px}}
.sub{{color:#5b7186;font-size:13px;margin-bottom:14px}}
.kpis{{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin:14px 0 18px}}
.kpi{{background:#fff;border-radius:10px;padding:14px 16px;box-shadow:0 1px 3px rgba(16,42,67,.08);border-left:4px solid #2e86c1}}
.kpi b{{display:block;font-size:23px;color:#12446e}}
.kpi span{{font-size:12px;color:#6b8199}}
.ctrl{{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:0 0 12px}}
.ctrl button{{border:1px solid #cfdce8;background:#fff;color:#33566f;padding:6px 14px;border-radius:16px;font-size:13px;cursor:pointer}}
.ctrl button.on{{background:#12446e;color:#fff;border-color:#12446e}}
.ctrl input{{border:1px solid #cfdce8;border-radius:16px;padding:6px 14px;font-size:13px;width:220px}}
.legend{{margin-left:auto;font-size:12px;color:#5b7186}}
.dot{{display:inline-block;width:9px;height:9px;border-radius:2px;margin:0 4px 0 10px}}
.board{{background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(16,42,67,.07);overflow:hidden;max-width:100%}}
.months{{display:flex;border-bottom:1px solid #e3ecf4;background:#f9fbfd}}
.mh{{flex:none;text-align:center;font-size:12px;color:#41637e;padding:7px 0;border-right:1px solid #e3ecf4;font-weight:600}}
.scroll{{overflow-x:auto;max-width:100%;-webkit-overflow-scrolling:touch}}
.grid{{position:relative;width:max-content;min-width:100%}}
.timeline{{position:relative;height:26px;border-bottom:1px solid #e7eef6;background:#fbfdff;margin-left:360px}}
.tw{{position:absolute;top:6px;font-size:10px;color:#8ba2b8;transform:translateX(2px)}}
.tw.wk{{color:#41637e;font-weight:700;border-left:1px solid #dbe6f0;height:26px;top:0;padding-top:8px}}
.row{{display:flex;border-bottom:1px solid #eef4f9;min-height:48px}}
.row:hover{{background:#f6faff}}
.task{{width:360px;flex:none;padding:8px 12px;border-right:1px solid #e7eef6}}
.tname{{font-size:13px;font-weight:600;color:#17364f}}
.tser{{font-weight:400;color:#7690a6;font-size:11px;margin-left:4px}}
.tmeta{{font-size:11px;color:#8aa1b6;margin-top:3px}}
.pri{{display:inline-block;font-size:10px;color:#fff;border-radius:3px;padding:1px 5px;margin-right:4px}}
.pP0{{background:#c0392b}}.pP1{{background:#e67e22}}.pP2{{background:#2e86c1}}
.lane{{position:relative;flex:1;min-width:{n*9}px}}
.bar{{position:absolute;top:13px;height:20px;border-radius:4px;color:#fff;font-size:11px;display:flex;align-items:center;justify-content:flex-end;padding-right:5px;opacity:.92;box-shadow:0 1px 2px rgba(0,0,0,.12)}}
.bar:hover{{opacity:1;outline:2px solid #12446e}}
.pP0.bar{{background:#c0392b}}.pP1.bar{{background:#e67e22}}.pP2.bar{{background:#2e86c1}}
.foot{{margin-top:18px;font-size:12px;color:#7d93a8;display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px}}
@media(max-width:760px){{
 .kpis{{grid-template-columns:repeat(2,1fr)}}
 .task{{width:170px}}
 .corner{{width:170px !important}}
 .timeline{{margin-left:170px}}
 .tname{{font-size:12px}} .tser{{display:none}} .tmeta{{font-size:10px}}
 .ctrl input{{width:130px}}
 .wrap{{padding:14px 12px 40px}}
 h1{{font-size:18px}} .sub{{font-size:12px}}
 .bior-tag{{display:none}} .bior-slogan{{font-size:11px;letter-spacing:1px}}
}}
</style></head>
<body><div class="wrap">
<h1>产品系列资料补齐 · 排期甘特图</h1>
<div class="sub">时间窗 2026-11-01 → 2027-01-31 ｜ 责任人暂用占位符（研发A/B/C、产品D/E），指派真人后一键替换 ｜ 数据源：产品系列资料归档与缺口清单 v3</div>

<div class="kpis">
  <div class="kpi"><b>{len(tasks)}</b><span>任务数（P0 {sum(1 for t in tasks if t['priority']=='P0')} / P1 {sum(1 for t in tasks if t['priority']=='P1')} / P2 {sum(1 for t in tasks if t['priority']=='P2')}）</span></div>
  <div class="kpi"><b>{sum(t['days'] for t in tasks):.0f}</b><span>总人日（估算）</span></div>
  <div class="kpi"><b>{max(t['end'] for t in tasks).strftime('%m-%d')}</b><span>最晚完成日</span></div>
  <div class="kpi"><b>{len(RESOURCES)}</b><span>占位资源</span></div>
  <div class="kpi"><b>{sum(1 for t in tasks if t['docs']==0)}</b><span>需先建档（未收录）</span></div>
</div>

<div class="ctrl">
  <button class="on" onclick="flt(this,'all')">全部</button>
  <button onclick="flt(this,'P0')">P0</button>
  <button onclick="flt(this,'P1')">P1</button>
  <button onclick="flt(this,'P2')">P2</button>
  <input id="q" placeholder="搜索货号 / 系列…" oninput="srch()">
  <span class="legend"><span class="dot" style="background:#c0392b"></span>P0 千万级
  <span class="dot" style="background:#e67e22"></span>P1 30–100万
  <span class="dot" style="background:#2e86c1"></span>P2 &lt;30万</span>
</div>

<div class="board"><div class="scroll"><div class="grid">
  <div class="months"><div class="corner" style="width:360px;flex:none;border-right:1px solid #e3ecf4"></div>{month_html}</div>
  <div class="timeline">{week_html}</div>
  {row_html}
</div></div></div>

<div class="foot"><span>排期规则：P0→P1→P2，同优先级按销售额降序；每项 1 名资源独占，工作日=周一至周五（跳元旦）。</span><span>宝锐生物 · 营销中心</span></div>
</div>
<script>
function flt(btn,p){{document.querySelectorAll('.ctrl button').forEach(b=>b.classList.remove('on'));btn.classList.add('on');
 document.querySelectorAll('.row').forEach(r=>{{r.style.display=(p==='all'||r.dataset.pri===p)?'flex':'none';}});}}
function srch(){{const q=document.getElementById('q').value.trim().toLowerCase();
 document.querySelectorAll('.row').forEach(r=>{{r.style.display=(!q||r.dataset.text.toLowerCase().includes(q))?'flex':'none';}});}}
</script>
</body></html>'''
    html = BK.inject(html, theme='light', tag='营销中心 · 产品资料')
    open(OUT_HTML, 'w', encoding='utf-8').write(html)


if __name__ == '__main__':
    tasks = load_tasks()
    tasks, free = schedule(tasks)
    build_xlsx(tasks)
    build_html(tasks)
    print('任务 %d ｜人日 %.0f' % (len(tasks), sum(t['days'] for t in tasks)))
    for r in RESOURCES:
        ts = [t for t in tasks if t['owner'] == r]
        print('  %s: %d 项，%s → %s' % (r, len(ts), ts[0]['start'] if ts else '-', max(t['end'] for t in ts) if ts else '-'))
    print('最晚完成', max(t['end'] for t in tasks))
    print('✅', OUT_XLSX)
    print('✅', OUT_HTML)
