from __future__ import annotations

from copy import copy
from html import escape
from pathlib import Path

from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "deliverables"
XLSX = OUT / "央美项目未完成事项与工期估算_20260904.xlsx"
MD = OUT / "cafa-unfinished-work-and-estimate.md"
HTML = OUT / "cafa-unfinished-work-and-estimate.html"

NAVY = "17324D"
BLUE = "2F5D8C"
ORANGE = "F28C28"
PALE_BLUE = "DDEBF7"
PALE_GREEN = "E2F0D9"
PALE_YELLOW = "FFF2CC"
PALE_RED = "F4CCCC"
PALE_GRAY = "E7E6E6"
WHITE = "FFFFFF"
TEXT = "243447"
THIN = Side(style="thin", color="CBD5E1")

ITEMS = [
    {
        "id": "WP-01",
        "priority": "P1",
        "target": "计量称重一体机",
        "work": "补齐消费界面信息展示",
        "type": "开发+联调+证据",
        "judgment": "原表明确标红；可能是字段已存在但终端未完整展示，需先做同版本核验。",
        "done": "同一笔消费实时展示姓名/余额、菜品、单价、克重、金额、当餐合计及四类营养数据；异常数据有占位；形成加盖公章所需截图。",
        "owner": "终端研发/后端/测试/产品",
        "dependency": "接口字段、营养计算口径、测试账号与菜品数据",
        "optimistic": 3,
        "likely": 4,
        "conservative": 6,
        "external": "0",
        "critical": "否",
        "source": "D2：功能要求#2",
    },
    {
        "id": "WP-02",
        "priority": "P1",
        "target": "计量称重一体机",
        "work": "补齐异常报警与文字提示",
        "type": "开发+真机测试",
        "judgment": "原表明确标红；需覆盖多种异常状态，不能只证明声音或灯光单点可用。",
        "done": "余额不足、托盘未绑定、未放托盘、压秤等异常均有正确状态判断、文字提示和声光反馈；恢复后状态自动解除；真机回归通过。",
        "owner": "终端研发/硬件/测试",
        "dependency": "报警灯控制、称重状态、绑盘状态、余额接口",
        "optimistic": 3,
        "likely": 5,
        "conservative": 8,
        "external": "0",
        "critical": "否",
        "source": "D2：功能要求5",
    },
    {
        "id": "WP-03",
        "priority": "P0",
        "target": "计量称重一体机",
        "work": "实现断网取餐、联网补传与冲突恢复",
        "type": "架构+开发+压力/异常测试",
        "judgment": "原表明确标红；历史资料只有离线补传线索，尚无本项目同版本闭环证据。",
        "done": "断网可读取受控本地数据完成取餐；本地订单防重复、防越权；联网自动补传；重复、乱序、失败重试、余额冲突均有可验证恢复策略；形成证明材料。",
        "owner": "终端研发/后端/测试/架构",
        "dependency": "离线授权边界、缓存策略、幂等键、扣费与对账规则",
        "optimistic": 8,
        "likely": 12,
        "conservative": 18,
        "external": "0-5工作日（规则确认）",
        "critical": "是",
        "source": "D2：功能要求★6",
    },
    {
        "id": "WP-04",
        "priority": "P2",
        "target": "餐盘身份绑定终端",
        "work": "绑定成功后实时显示绑定状态",
        "type": "开发+真机测试",
        "judgment": "原表明确标红；与现有绑定流程接近，预计为小范围终端交互补齐。",
        "done": "人脸、IC卡、扫码等绑定成功后立即展示人员/托盘/状态；失败、重复绑定、托盘移走场景提示正确。",
        "owner": "终端研发/测试",
        "dependency": "绑定结果事件与页面状态模型",
        "optimistic": 1,
        "likely": 2,
        "conservative": 3,
        "external": "0",
        "critical": "否",
        "source": "D3：功能要求#4",
    },
    {
        "id": "WP-05",
        "priority": "P1",
        "target": "餐盘身份绑定终端",
        "work": "增加余额阈值条件绑定",
        "type": "规则+开发+测试",
        "judgment": "原表明确标红；需要后台参数、终端校验及异常提示一致。",
        "done": "后台可配置最小余额；终端绑定前校验；不足时拒绝并提示；边界值、参数未下发、断网缓存及规则变更均有测试。",
        "owner": "终端研发/后端/测试/产品",
        "dependency": "阈值口径、参数下发机制、离线规则",
        "optimistic": 2,
        "likely": 3,
        "conservative": 5,
        "external": "0-2工作日（规则确认）",
        "critical": "否",
        "source": "D3：功能要求#5",
    },
    {
        "id": "WP-06",
        "priority": "P0",
        "target": "绑盘终端+管理系统",
        "work": "闭环无感支付并取得同版本第三方评测报告",
        "type": "跨端开发+联调+外部评测",
        "judgment": "D3与D4为同一业务能力，合并为一个工作包；功能完成与评测报告到手是两个门禁。",
        "done": "完成绑盘—取餐—自动结算—扣费—对账—失败补偿全流程；重复扣费、余额不足、撤销/退款及断网恢复可测；第三方评测报告与交付版本一一对应。",
        "owner": "终端/后端/支付接口/测试/投标",
        "dependency": "支付与一卡通契约、异常补偿规则、评测机构档期与样品版本",
        "optimistic": 8,
        "likely": 12,
        "conservative": 20,
        "external": "10-20工作日（第三方评测，可并行）",
        "critical": "是",
        "source": "D3：★10；D4：★17",
    },
    {
        "id": "WP-07",
        "priority": "P1",
        "target": "智慧餐厅管理系统",
        "work": "完成十类平台能力的同版本核验与补缺",
        "type": "功能盘点+补开发+证据",
        "judgment": "历史矩阵显示多数能力已有基础；不能把整条标红直接当作十个模块均未开发，应先逐项核验。",
        "done": "食堂、餐品、账户、托盘、排餐、营养、订单、对账、设备监控、统计分析逐项有页面/API/数据/权限/异常/导出证据；缺口开发完成。",
        "owner": "产品/PC研发/后端/测试/投标",
        "dependency": "冻结交付版本、测试数据、证据模板与盖章流程",
        "optimistic": 3,
        "likely": 6,
        "conservative": 12,
        "external": "1-3工作日（盖章/材料）",
        "critical": "否",
        "source": "D4：平台具备#1",
    },
    {
        "id": "WP-08",
        "priority": "P1",
        "target": "智慧餐厅管理系统",
        "work": "补齐托盘绑定记录、手动解绑与自动扣款",
        "type": "后台+接口+账务测试",
        "judgment": "原表明确标红；涉及账务结果，需比普通后台功能增加幂等与对账回归。",
        "done": "实时/历史绑定记录可查；管理员权限受控；手动解绑触发一次且仅一次正确扣款；失败可重试/回滚；审计日志和对账一致。",
        "owner": "PC研发/后端/测试/财务接口",
        "dependency": "解绑扣费时点、权限、冲正/退款与对账规则",
        "optimistic": 3,
        "likely": 5,
        "conservative": 8,
        "external": "0-3工作日（账务规则确认）",
        "critical": "否",
        "source": "D4：#10",
    },
    {
        "id": "WP-09",
        "priority": "P1",
        "target": "智慧餐厅手机管理端",
        "work": "交付手机管理端指定功能与证明材料",
        "type": "移动端+接口+测试+证据",
        "judgment": "原表明确标红；目前未见本项目同版本手机端完整证据。",
        "done": "可查看分时段营收、商品、交易流水、设备运行和当日菜品销量；权限/数据范围/刷新/空态/异常正确；形成项目版本截图。",
        "owner": "移动端/后端/测试/产品",
        "dependency": "移动端技术基线、角色权限、统计口径、接口复用程度",
        "optimistic": 8,
        "likely": 12,
        "conservative": 18,
        "external": "0",
        "critical": "是",
        "source": "D4：★18",
    },
    {
        "id": "WP-10",
        "priority": "P0",
        "target": "学校一卡通/身份介质接口",
        "work": "兼容校园实体卡、电子码、人脸与NFC绑盘",
        "type": "外部接口+终端适配+端到端测试",
        "judgment": "原表明确标红；现有接口文档、测试环境、介质能力和财务规则均未冻结，是最大外部依赖。",
        "done": "四类介质能力矩阵经校方确认；账户/身份映射、绑盘、扣费、退款/冲正、对账、幂等、超时重试与降级均通过端到端测试；不新增独立消费介质。",
        "owner": "接口研发/终端/测试/校方一卡通/财务",
        "dependency": "厂商接口文档、测试环境、联调人、样卡/电子码、人脸/NFC能力、财务规则",
        "optimistic": 10,
        "likely": 15,
        "conservative": 25,
        "external": "5-15工作日或更长（校方/厂商输入）",
        "critical": "是",
        "source": "D5：技术对接3",
    },
]

RAW = [
    ("R-01", "计量称重一体机", "功能#2", "完整展示身份、余额、消费、当餐合计及营养信息", "WP-01"),
    ("R-02", "计量称重一体机", "功能5", "余额不足、托盘未绑定、未放托盘、压秤等异常报警和文字提示", "WP-02"),
    ("R-03", "计量称重一体机", "功能★6", "断网取餐，联网后自动同步后台", "WP-03"),
    ("R-04", "餐盘身份绑定终端", "功能#4", "绑定成功后实时显示绑定状态", "WP-04"),
    ("R-05", "餐盘身份绑定终端", "功能#5", "余额阈值条件绑定", "WP-05"),
    ("R-06", "餐盘身份绑定终端", "功能★10", "无感支付并提供第三方软件评测报告", "WP-06"),
    ("R-07", "智慧餐厅管理系统", "平台#1", "十类平台能力及证明材料", "WP-07"),
    ("R-08", "智慧餐厅管理系统", "功能#10", "绑定记录、管理员手动解绑并自动扣款", "WP-08"),
    ("R-09", "智慧餐厅管理系统", "功能★17", "无感支付并提供第三方软件评测报告", "WP-06"),
    ("R-10", "智慧餐厅管理系统", "功能★18", "手机管理端营收、商品、流水、设备和销量", "WP-09"),
    ("R-11", "系统技术对接", "接口3", "实体卡、电子码、人脸、NFC等现有身份介质绑盘", "WP-10"),
]

ASSUMPTIONS = [
    "1人日按1名有效人员工作8小时计算；人日是投入量，不等于自然日。",
    "估算包含需求澄清、开发、自测、联调、系统测试及截图/测试记录；不包含商务签认、生产上线、现场停业窗口和最终验收。",
    "常规估算假设现有称重、绑盘、管理后台与接口基线可复用，开发/测试环境可用，且主要业务规则在启动后2个工作日内冻结。",
    "第三方评测报告、校方一卡通文档/测试环境/联调人属于外部等待；可与开发并行，不能用内部加人直接压缩。",
    "WP-07先做同版本核验。若十类能力大多已有，接近乐观值；若需新建多个模块，可能超过保守值。",
    "没有完成同版本页面/API/数据/权限/异常/证据回读的事项，一律不按‘已完成’统计。",
]


def style_title(ws, title: str, subtitle: str, max_col: int) -> None:
    ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=max_col)
    ws.cell(1, 1, title)
    ws.cell(1, 1).font = Font(name="Microsoft YaHei", size=18, bold=True, color=WHITE)
    ws.cell(1, 1).fill = PatternFill("solid", fgColor=NAVY)
    ws.cell(1, 1).alignment = Alignment(vertical="center")
    ws.row_dimensions[1].height = 34
    ws.merge_cells(start_row=2, start_column=1, end_row=2, end_column=max_col)
    ws.cell(2, 1, subtitle)
    ws.cell(2, 1).font = Font(name="Microsoft YaHei", size=10, color=TEXT)
    ws.cell(2, 1).fill = PatternFill("solid", fgColor=PALE_BLUE)
    ws.cell(2, 1).alignment = Alignment(vertical="center", wrap_text=True)
    ws.row_dimensions[2].height = 32


def style_header(ws, row: int, max_col: int) -> None:
    for cell in ws[row][:max_col]:
        cell.font = Font(name="Microsoft YaHei", size=10, bold=True, color=WHITE)
        cell.fill = PatternFill("solid", fgColor=BLUE)
        cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
        cell.border = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
    ws.row_dimensions[row].height = 34


def style_body(ws, start: int, end: int, max_col: int) -> None:
    for row in ws.iter_rows(min_row=start, max_row=end, min_col=1, max_col=max_col):
        for cell in row:
            cell.font = Font(name="Microsoft YaHei", size=10, color=TEXT)
            cell.alignment = Alignment(vertical="top", wrap_text=True)
            cell.border = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)


def configure_print(ws, area: str, landscape: bool = True) -> None:
    ws.sheet_view.showGridLines = False
    ws.freeze_panes = "A5"
    ws.print_area = area
    ws.page_setup.orientation = "landscape" if landscape else "portrait"
    ws.page_setup.fitToWidth = 1
    ws.page_setup.fitToHeight = 1
    ws.sheet_properties.pageSetUpPr.fitToPage = True
    ws.oddFooter.center.text = "央美项目未完成事项与工期估算｜内部评审"


def build_xlsx() -> None:
    wb = Workbook()
    ws = wb.active
    ws.title = "00-结论"
    style_title(ws, "央美项目未完成事项与工期估算", "基于用户提供的《央美项目交付差异点.xlsx》与项目内既有差距矩阵；标红代表待闭环，不自动等同于从零开发", 8)
    ws["A4"] = "核心结论"
    ws["A4"].font = Font(name="Microsoft YaHei", size=13, bold=True, color=NAVY)
    cards = [
        ("A6", "原表差异", "11", "条"),
        ("C6", "独立工作包", "10", "个"),
        ("E6", "常规投入", "='01-未完成清单'!K15", "人日"),
        ("G6", "现实日历工期", "25-35", "工作日"),
    ]
    for cell, label, value, unit in cards:
        col = ws[cell].column
        ws.cell(6, col, label)
        ws.cell(7, col, value)
        ws.cell(8, col, unit)
        for r in (6, 7, 8):
            ws.merge_cells(start_row=r, start_column=col, end_row=r, end_column=col + 1)
            c = ws.cell(r, col)
            c.alignment = Alignment(horizontal="center", vertical="center")
            c.fill = PatternFill("solid", fgColor=PALE_BLUE if col < 7 else PALE_YELLOW)
            c.font = Font(name="Microsoft YaHei", size=10 if r != 7 else 18, bold=r != 8, color=NAVY)
    ws["A10"] = "建议判断"
    ws["A10"].font = Font(name="Microsoft YaHei", size=13, bold=True, color=NAVY)
    findings = [
        "按4—5人并行、外部输入及时的常规情景，预计25—35个工作日（约5—7周）完成开发、联调、系统测试和内部证据闭环。",
        "若多数标红项实际上已有功能、只缺同版本核验与盖章材料，可压缩到5—8个工作日；必须先做两天内的差距校准。",
        "若需新做第三方评测，或学校一卡通厂商文档/测试环境迟到，整体更可能是35—50个工作日以上（约7—10周+）。",
        "当前不能稳妥承诺20个自然日交完；只有评测报告可复用、接口条件首日齐备且多人并行时，才有机会进入20个工作日附近。",
    ]
    for idx, line in enumerate(findings, 11):
        ws.merge_cells(start_row=idx, start_column=1, end_row=idx, end_column=8)
        ws.cell(idx, 1, line)
        ws.cell(idx, 1).font = Font(name="Microsoft YaHei", size=10, bold=idx == 14, color="9C0006" if idx == 14 else TEXT)
        ws.cell(idx, 1).fill = PatternFill("solid", fgColor=PALE_RED if idx == 14 else PALE_GREEN)
        ws.cell(idx, 1).alignment = Alignment(vertical="center", wrap_text=True)
        ws.row_dimensions[idx].height = 42
    ws["A17"] = "关键路径"
    ws["A17"].font = Font(name="Microsoft YaHei", size=13, bold=True, color=NAVY)
    paths = [
        "WP-10 一卡通/身份介质接口：必须先拿到厂商文档、测试环境、样卡/码和财务规则。",
        "WP-06 无感支付：跨终端、后台、接口与对账，且第三方评测报告存在独立周期。",
        "WP-03 离线取餐与补传：涉及本地授权、幂等、重复扣费和冲突恢复，不能只测正常路径。",
        "WP-09 手机管理端：若无可复用端，需要独立移动端与接口联调周期。",
    ]
    for idx, line in enumerate(paths, 18):
        ws.merge_cells(start_row=idx, start_column=1, end_row=idx, end_column=8)
        ws.cell(idx, 1, line)
        ws.cell(idx, 1).font = Font(name="Microsoft YaHei", size=10, color=TEXT)
        ws.cell(idx, 1).fill = PatternFill("solid", fgColor=PALE_YELLOW)
        ws.cell(idx, 1).alignment = Alignment(vertical="center", wrap_text=True)
        ws.row_dimensions[idx].height = 34
    ws.column_dimensions["A"].width = 18
    for col in "BCDEFGH":
        ws.column_dimensions[col].width = 15
    configure_print(ws, "A1:H22")

    ws = wb.create_sheet("01-未完成清单")
    headers = ["工作包", "优先级", "标的/模块", "未完成事项", "工作类型", "当前判断", "完成标准", "建议主责", "前置/外部依赖", "乐观人日", "常规人日", "保守人日", "外部等待", "关键路径", "原表依据"]
    style_title(ws, "央美项目未完成事项清单", "人日是内部投入量；外部等待单列。完成标准未满足前，不按已完成统计。", len(headers))
    ws.append([])
    ws.append(headers)
    for item in ITEMS:
        ws.append([
            item["id"], item["priority"], item["target"], item["work"], item["type"], item["judgment"], item["done"], item["owner"], item["dependency"],
            item["optimistic"], item["likely"], item["conservative"], item["external"], item["critical"], item["source"],
        ])
    total_row = 5 + len(ITEMS)
    ws.cell(total_row, 1, "合计")
    ws.merge_cells(start_row=total_row, start_column=1, end_row=total_row, end_column=9)
    ws.cell(total_row, 10, f"=SUM(J5:J{total_row-1})")
    ws.cell(total_row, 11, f"=SUM(K5:K{total_row-1})")
    ws.cell(total_row, 12, f"=SUM(L5:L{total_row-1})")
    ws.merge_cells(start_row=total_row, start_column=13, end_row=total_row, end_column=15)
    ws.cell(total_row, 13, "外部等待可与开发部分并行，不直接与人日相加")
    style_header(ws, 4, len(headers))
    style_body(ws, 5, total_row, len(headers))
    for cell in ws[total_row]:
        cell.fill = PatternFill("solid", fgColor=PALE_BLUE)
        cell.font = Font(name="Microsoft YaHei", size=10, bold=True, color=NAVY)
    for row in range(5, total_row):
        fill = PALE_RED if ws.cell(row, 2).value == "P0" else PALE_YELLOW if ws.cell(row, 2).value == "P1" else PALE_GREEN
        ws.cell(row, 2).fill = PatternFill("solid", fgColor=fill)
        if ws.cell(row, 14).value == "是":
            ws.cell(row, 14).fill = PatternFill("solid", fgColor=PALE_RED)
            ws.cell(row, 14).font = Font(name="Microsoft YaHei", size=10, bold=True, color="9C0006")
    widths = [10, 8, 18, 24, 20, 38, 46, 24, 36, 10, 10, 10, 24, 10, 20]
    for idx, width in enumerate(widths, 1):
        ws.column_dimensions[get_column_letter(idx)].width = width
    for row in range(5, total_row):
        ws.row_dimensions[row].height = 105
    ws.auto_filter.ref = f"A4:O{total_row-1}"
    configure_print(ws, f"A1:O{total_row}")

    ws = wb.create_sheet("02-原始差异映射")
    headers = ["原始差异ID", "标的名称", "条款位置", "原表标红差异摘要", "归并工作包", "处理说明"]
    style_title(ws, "原始差异到工作包映射", "原表共11条差异；两处‘无感支付’属于同一业务闭环，合并为WP-06，因此形成10个独立工作包。", len(headers))
    ws.append([])
    ws.append(headers)
    for rid, target, clause, summary, wp in RAW:
        note = "与另一处无感支付条款合并，避免重复估工" if wp == "WP-06" else "一条原始差异对应一个工作包"
        ws.append([rid, target, clause, summary, wp, note])
    style_header(ws, 4, len(headers))
    style_body(ws, 5, 4 + len(RAW), len(headers))
    for col, width in enumerate([13, 22, 16, 62, 14, 34], 1):
        ws.column_dimensions[get_column_letter(col)].width = width
    for row in range(5, 5 + len(RAW)):
        ws.row_dimensions[row].height = 54
    ws.auto_filter.ref = f"A4:F{4+len(RAW)}"
    configure_print(ws, f"A1:F{4+len(RAW)}")

    ws = wb.create_sheet("03-排期假设")
    style_title(ws, "工期口径、情景与启动门禁", "本表是粗估，不替代研发、校方、评测机构与投标负责人的排期签认。", 7)
    ws["A4"] = "估算假设"
    ws["A4"].font = Font(name="Microsoft YaHei", size=13, bold=True, color=NAVY)
    for idx, line in enumerate(ASSUMPTIONS, 5):
        ws.merge_cells(start_row=idx, start_column=1, end_row=idx, end_column=7)
        ws.cell(idx, 1, f"{idx-4}. {line}")
        ws.cell(idx, 1).font = Font(name="Microsoft YaHei", size=10, color=TEXT)
        ws.cell(idx, 1).fill = PatternFill("solid", fgColor=PALE_GRAY if idx % 2 else PALE_BLUE)
        ws.cell(idx, 1).alignment = Alignment(vertical="center", wrap_text=True)
        ws.row_dimensions[idx].height = 36
    ws["A12"] = "交付情景"
    ws["A12"].font = Font(name="Microsoft YaHei", size=13, bold=True, color=NAVY)
    ws.append([])
    ws.append(["情景", "前提", "内部人日", "日历工期", "团队配置", "结论", "风险"])
    scenarios = [
        ["A 证据补齐为主", "多数功能已有，只缺同版本核验、少量修正、截图/盖章", "约25-40", "5-8工作日", "产品1+研发2+测试1", "可快速闭环", "一旦发现实质功能缺口即切换B"],
        ["B 常规混合（推荐）", "按本清单常规值；4-5人并行；接口条件及时", "约76", "25-35工作日", "终端2+后端1+移动/PC1+测试1", "约5-7周", "共享后端、接口和测试环境会限制并行"],
        ["C 外部依赖延迟", "第三方评测新做；一卡通文档/环境/联调人迟到", "76+返工", "35-50工作日以上", "内部团队同B", "约7-10周+", "外部等待不可通过内部加人直接消除"],
    ]
    for row in scenarios:
        ws.append(row)
    style_header(ws, 14, 7)
    style_body(ws, 15, 17, 7)
    for row in range(15, 18):
        ws.row_dimensions[row].height = 72
    ws["A19"] = "启动门禁（前2个工作日内关闭）"
    ws["A19"].font = Font(name="Microsoft YaHei", size=13, bold=True, color=NAVY)
    gates = [
        "逐项演示现有版本，确认10个工作包中哪些是‘缺功能’、哪些只是‘缺同版本证据’。",
        "拿到学校一卡通厂商接口文档、测试环境、联调人、样卡/电子码及财务对账规则。",
        "确认无感支付第三方评测报告是否已有且覆盖拟交付版本；若无，立即预约机构并冻结样品版本。",
        "冻结离线取餐的授权、余额、扣费、幂等、冲突和恢复规则。",
        "确认手机管理端是复用现有端、适配现有端，还是新建端；三种情景工期差异很大。",
    ]
    for idx, line in enumerate(gates, 20):
        ws.merge_cells(start_row=idx, start_column=1, end_row=idx, end_column=7)
        ws.cell(idx, 1, f"□ {line}")
        ws.cell(idx, 1).font = Font(name="Microsoft YaHei", size=10, color=TEXT)
        ws.cell(idx, 1).fill = PatternFill("solid", fgColor=PALE_YELLOW)
        ws.cell(idx, 1).alignment = Alignment(vertical="center", wrap_text=True)
        ws.row_dimensions[idx].height = 38
    for col, width in enumerate([22, 46, 14, 18, 32, 18, 42], 1):
        ws.column_dimensions[get_column_letter(col)].width = width
    configure_print(ws, "A1:G24")

    wb.calculation.fullCalcOnLoad = True
    wb.calculation.forceFullCalc = True
    wb.calculation.calcMode = "auto"
    for sheet in wb.worksheets:
        for row in sheet.iter_rows():
            for cell in row:
                if cell.value is None:
                    continue
                font = copy(cell.font)
                font.name = "STSong"
                cell.font = font
    OUT.mkdir(parents=True, exist_ok=True)
    wb.save(XLSX)


def build_markdown() -> None:
    rows = []
    for x in ITEMS:
        rows.append(f"| {x['id']} | {x['priority']} | {x['target']} | {x['work']} | {x['likely']} | {x['external']} | {x['critical']} |")
    content = f"""# 央美项目未完成事项与工期估算

更新时间：2026-09-04

来源：用户提供《央美项目交付差异点.xlsx》（SHA256 `9f646323a04dfe1253314440b277baccafce318fdf5f33aa7863fc7f4854ad2e`）及项目内既有差距矩阵。

证据边界：表中标红内容视为待闭环事项，不自动等同于从零开发；完成状态必须以同版本页面、API、数据、权限、异常、真机或证明材料回读为准。

## 结论

- 原表有 11 条差异；两处“无感支付”合并后，共 10 个独立工作包。
- 内部投入粗估：乐观 49 人日、常规 76 人日、保守 123 人日。
- 4—5 人并行且外部输入及时：约 25—35 个工作日（5—7 周）。
- 若多数能力已有、只缺核验和材料：约 5—8 个工作日。
- 若第三方评测新做或一卡通外部输入延迟：约 35—50 个工作日以上（7—10 周+）。
- 当前不能稳妥承诺 20 个自然日全部交完。

## 独立清单

| 工作包 | 优先级 | 标的/模块 | 未完成事项 | 常规人日 | 外部等待 | 关键路径 |
| --- | --- | --- | --- | ---: | --- | --- |
{chr(10).join(rows)}

## 关键路径

1. `WP-10` 学校一卡通/身份介质接口。
2. `WP-06` 无感支付与同版本第三方评测报告。
3. `WP-03` 离线取餐、自动补传与冲突恢复。
4. `WP-09` 手机管理端。

## 启动建议

先用 2 个工作日做“差距校准”：逐项演示当前版本，拿到一卡通接口条件，确认评测报告能否复用，并冻结离线与扣费规则。校准后再把本粗估转成正式排期。

## 人工门禁

交付周期、第三方评测、接口可用性、盖章材料与最终投标承诺仍需产品、研发、测试、校方、投标和项目负责人签认；本分析不构成最终交付承诺。
"""
    MD.write_text(content, encoding="utf-8")


def build_html() -> None:
    body_rows = "".join(
        f"<tr><td>{escape(x['id'])}</td><td><span class='pill {x['priority'].lower()}'>{escape(x['priority'])}</span></td><td>{escape(x['target'])}</td><td><strong>{escape(x['work'])}</strong><small>{escape(x['judgment'])}</small></td><td>{x['likely']}</td><td>{escape(x['external'])}</td><td>{escape(x['critical'])}</td></tr>"
        for x in ITEMS
    )
    HTML.write_text(f"""<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>央美项目未完成事项与工期估算</title>
<style>
:root{{--navy:#17324d;--blue:#2f5d8c;--orange:#f28c28;--paper:#f6f7f9;--ink:#243447;--muted:#64748b;--line:#dbe2ea}}
*{{box-sizing:border-box}} body{{margin:0;background:var(--paper);color:var(--ink);font-family:"Microsoft YaHei","PingFang SC",sans-serif}}
main{{max-width:1280px;margin:auto;padding:40px 28px 72px}} header{{background:linear-gradient(135deg,var(--navy),var(--blue));color:white;padding:38px;border-radius:22px}}
h1{{margin:0 0 10px;font-size:34px}} header p{{margin:0;opacity:.86;line-height:1.7}} .cards{{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:20px 0}}
.card{{background:white;border:1px solid var(--line);border-radius:16px;padding:18px;box-shadow:0 8px 24px #17324d0d}} .card b{{display:block;font-size:28px;color:var(--navy)}} .card span{{color:var(--muted)}}
.warning{{background:#fff3e0;border-left:5px solid var(--orange);padding:18px 20px;border-radius:10px;margin:20px 0;line-height:1.7}}
.table-wrap{{overflow:auto;background:white;border:1px solid var(--line);border-radius:16px}} table{{border-collapse:collapse;width:100%;min-width:1050px}} th{{background:var(--navy);color:white;text-align:left;padding:13px;position:sticky;top:0}} td{{padding:13px;border-bottom:1px solid var(--line);vertical-align:top;line-height:1.55}} tr:hover td{{background:#f8fbff}} small{{display:block;color:var(--muted);margin-top:6px;max-width:440px}}
.pill{{display:inline-block;padding:3px 9px;border-radius:999px;font-weight:700}} .p0{{background:#fee2e2;color:#991b1b}} .p1{{background:#fef3c7;color:#92400e}} .p2{{background:#dcfce7;color:#166534}}
section{{margin-top:26px}} h2{{color:var(--navy)}} li{{margin:8px 0;line-height:1.6}} footer{{margin-top:28px;color:var(--muted);font-size:13px}}
@media(max-width:760px){{.cards{{grid-template-columns:1fr 1fr}} main{{padding:18px}} header{{padding:24px}} h1{{font-size:26px}}}}
</style></head><body><main>
<header><h1>央美项目未完成事项与工期估算</h1><p>标红代表待闭环，不自动等同于从零开发。先核验现有版本，再进入正式排期。</p></header>
<div class="cards"><div class="card"><b>11</b><span>原表差异</span></div><div class="card"><b>10</b><span>独立工作包</span></div><div class="card"><b>76 人日</b><span>常规内部投入</span></div><div class="card"><b>25–35 天</b><span>4—5人并行日历工期</span></div></div>
<div class="warning"><strong>管理判断：</strong>现实情景约 5—7 周；若多数功能已有、只缺同版本核验与材料，可压缩到 5—8 个工作日；若第三方评测或一卡通外部输入延迟，可能达到 7—10 周以上。当前不宜直接承诺 20 个自然日全部交完。</div>
<section><h2>未完成清单</h2><div class="table-wrap"><table><thead><tr><th>工作包</th><th>优先级</th><th>标的/模块</th><th>未完成事项与判断</th><th>常规人日</th><th>外部等待</th><th>关键路径</th></tr></thead><tbody>{body_rows}</tbody></table></div></section>
<section><h2>先做这五件事</h2><ol><li>两天内逐项演示现有版本，区分“缺功能”和“缺证据”。</li><li>取得学校一卡通接口文档、测试环境、联调人和样卡/码。</li><li>确认无感支付第三方评测报告能否覆盖拟交付版本。</li><li>冻结离线取餐、扣费、幂等和冲突恢复规则。</li><li>确认手机管理端是复用、适配还是新建。</li></ol></section>
<footer>内部粗估｜来源：用户提供Excel与项目内既有差距矩阵｜最终工期、证书、接口与投标承诺须人工签认</footer>
</main></body></html>""", encoding="utf-8")


if __name__ == "__main__":
    build_xlsx()
    build_markdown()
    build_html()
    print(XLSX)
    print(MD)
    print(HTML)
