from pathlib import Path
from datetime import datetime, date, timedelta
from collections import Counter, defaultdict
from difflib import SequenceMatcher
import json, re, hashlib, math, unicodedata
import xlsxwriter
import openpyxl

ROOT = Path(__file__).resolve().parent
LOGS = json.loads((ROOT / 'source/logs.json').read_text())
START, END = date(2026, 8, 17), date(2026, 10, 8)
DAYS = [START + timedelta(days=i) for i in range((END - START).days + 1)]
PEOPLE = sorted({x['name'] for x in LOGS})

# Alias groups are grounded in the exported logs. Ambiguous customers remain separate.
PROJECTS = [
 ('滨州健康科技职业学院','滨州|滨职|滨州职校','学校','餐厅上线与就餐保障'),
 ('中央美术学院','中央美术学院|中央美院|央美|美院','学校','餐厅建设与一卡通对接'),
 ('江西206',r'江西.?206|(?<!\d)206(?!\d)|江西二零六','政企','餐厅设备与网络运维'),
 ('四方达','四方达','企业','智慧健康餐厅建设'),
 ('三全','三全','企业','餐厅与内部系统对接'),
 ('江苏国信','江苏国信|国信','企业','消费策略与安全整改'),
 ('中央网信办','中央网信办|中央网信|网信办','政企','国产数据库迁移与安全备案'),
 ('哈尔滨膳质舍','哈尔滨|膳质舍','企业','健康餐厅与体检设备交付'),
 ('安庆部队','安庆','部队','餐厅施工与设备交付'),
 ('安徽部队（具体点位待确认）','安徽某部队|安徽部队|安徽项目','部队','餐厅部署与需求支持'),
 ('安徽体育职业技术学院','安徽体职|安徽体育职业','学校','体能设备交付'),
 ('安徽体科所','安徽体科所','体育机构','检测数据支持'),
 ('重庆环卫','重庆环卫','政企','营养推荐与定制页面'),
 ('国康医院','国康','医院','称重结算与防逃单'),
 ('张家港农商行','张家港','企业','支付对接与现场设备交付'),
 ('金斯瑞','金斯瑞','企业','外卖取餐与餐厅设备'),
 ('北京城市副中心','城市副中心|副中心','政企','大屏与餐厅运行支持'),
 ('南京建宁中学（莱迪森）','建宁中学|莱迪森','学校','账户余额与流水查询'),
 ('保康中学','保康','学校','食安与进销存系统部署'),
 ('国防科技大学','国防科技大学|国防科大|国防项目','学校','体能测试与系统交付'),
 ('北京体育大学','北体大|北京体育大学|北体项目','学校','体测设备与餐厅支持'),
 ('广东体科所','广东体科所','体育机构','设备数据对接'),
 ('广东反兴奋剂','广东反兴奋剂|广东体科.*反兴奋剂','体育机构','训练系统与检测支持'),
 ('江西体科所','江西体科所','体育机构','科训与体重管理'),
 ('甘肃体育局','甘肃体育局|甘肃项目','体育机构','科训与体能系统支持'),
 ('陕西举摔柔中心','举重摔跤柔道|陕西.*柔道','体育机构','科训系统部署'),
 ('首通智城','首通智城','企业','食安与学校系统支持'),
 ('大兴项目','大兴','政企','建设方案与专家评审'),
 ('雄安宣武医院','雄安宣武|雄安项目','医院','食安售前与部署方案'),
 ('宿迁公安局','宿迁','政企','餐厅售前方案'),
 ('大庆油田','大庆油田','企业','餐厅投标与售前方案'),
 ('昌平项目（客户待确认）','昌平区项目|昌平项目','政企','售前方案'),
 ('某县级教育局平台','县级教育局','教育监管','食品安全与膳食营养方案'),
 ('河北体校','河北体校','学校','进销存投标响应'),
 ('河北餐厅（点位待确认）','河北餐厅','待确认','食安投标参数'),
 ('山西渠道食安','山西.*食安','渠道','食安设备方案'),
 ('无锡餐饮公司','无锡餐饮|新吴区','企业','售前与系统支持'),
 ('中央国债','中央国债|中债','企业','设备与移动端售后'),
 ('先农坛体校','先农坛','学校','训练设备维修'),
 ('大杜社中学','大杜社','学校','消费机安装与培训'),
 ('林秀嘉园幼儿园','林秀嘉园','学校','消费机安装与培训'),
 ('人大附中','人大附中','学校','餐厅设备与AI功能支持'),
 ('江南训练基地','江南训练基地','体育机构','设备故障支持'),
 ('江南大学','江南大学','学校','智慧餐厅售前'),
 ('冬奥村','冬奥村','体育机构','演示设备准备'),
 ('旧宫中学','旧宫','学校','留样设备售后'),
 ('崇明','崇明','待确认','一体机售后'),
 ('赛迪','赛迪','企业','消费策略支持'),
 ('机场（具体客户待确认）','机场','企业','消费与账户支持'),
 ('什刹海','什刹海','体育机构','体能系统支持'),
 ('伊春二中','伊春二中','学校','餐厅售前支持'),
 ('广州城市（机构待确认）','广州城市','学校','设备维修'),
 ('中国银行（版本/点位待确认）','中国银行','企业','体测与移动端产品介绍'),
 ('食堂食安传感器（客户待确认）','食堂食安传感器','待确认','传感器配置与售前'),
 ('大型区政府（客户待确认）','大型区政府','政企','服务器架构方案'),
]
PRODS = [
 ('学校膳食营养','学校线|学校版|膳食营养|学校营养|周排菜|周复制|特殊供餐|食谱|采购需求|营养总览'),
 ('食品安全系统','食品安全|食安|foodsafe|food_safety|留样|晨检|明厨亮灶|物联网|传感器'),
 ('进销存与采购','进销存|供应商接单|商品比价|供应商.*比价|出入库|采购单'),
 ('称重取餐设备','410|450|称重台|称重设备|称体|秤体|单秤|双秤|膳识智能台|德沃|验收秤'),
 ('消费机与支付结算','消费机|结算台|结算机|一卡通|读卡器|PSAM|支付宝|微信支付|补单|逃单|退款|提现|订单|绑盘'),
 ('健康档案与体检','体检|健康档案|体成分|体测仪|460|上禾|骨密度|体重管理|脉全|欧姆龙'),
 ('AI营养与健康助手','AI.?问答|AI.?助手|营养师|权益|会员|积分|菜品识别|AI.?一下|食物成分|营养标签|营养推荐|推荐取用量'),
 ('AI跑步与训练助手','跑步|训练计划|补给计划|心率六区|全马|半马|RAG|华为.*运动|竞技小程序'),
 ('科训与体能系统','科训|体能|VALD|vald|功率车|kBox|kPulley|kplley|monark|运动员|训练历史'),
 ('餐厅大屏与物料','大屏|一体机|易拉宝|海报|宣发|宣导|软装|餐垫|提示纸'),
 ('标准餐厅管理平台','标准产品|标准版|store|ai_api|ai_store|ai_web|商户端|菜品|菜谱|餐单|团餐|食堂'),
 ('AI工具与知识资产','Skill|skill|Agent|agent|Codex|Coze|coze|工作流|知识库|知识服务|VDB|自动化'),
]
TYPES = [
 ('安全整改与认证','补漏洞、准备等保/密评/软著等材料，让系统满足安全和资质要求','安全|漏洞|等保|密评|软著|备案|网安|整改回函|数据安全|认证|证书'),
 ('调研选型与竞品研究','比较产品、厂家和客户需求，判断应该做什么、买什么','竞品|调研|访谈|选型|测评|评估.*硬件|厂家.*参数|硬件.*评估|市场.*分析'),
 ('售前方案与报价投标','在签约或增购前说明方案、范围、价格和投标参数','报价|投标|应标|售前|控标|建设方案|配置清单|工时.*评估|估时|费用.*沟通'),
 ('产品设计与需求确认','把客户想法变成清楚的功能、流程、页面和开发要求','原型|需求分析|需求评审|功能评审|需求讲解|产品设计|业务逻辑.*梳理|确认.*需求|需求.*确认|需求.*边界|流程.*设计|方案制定'),
 ('修复问题与运行保障','查清故障并修好，让客户现场或线上系统恢复正常','排查|修复|故障|闪退|异常|问题处理|解决.*问题|问题.*解决|售后|开餐|跟餐|值守|巡检|维修|检修|检漏'),
 ('数据整理与系统迁移','导入人员、菜品和余额，核对数据，迁移或备份数据库','数据库迁移|数据迁移|数据导入|导入.*数据|人员.*录入|菜品.*录入|备份|导.*余额|导出数据|数据.*核对|数据.*整理|数据.*修正|商品比价|食物成分.*合并'),
 ('第三方接口与设备对接','让我们的系统与一卡通、支付、厂家设备或客户系统互通','对接|联调|接口|协议|SDK|sdk|SOAP|回调|串口|接入|PSAM|一卡通|同步.*人员|人员.*同步'),
 ('开发新功能与改进体验','写代码或修改功能，让软件/设备多一个能力或更好用','开发|代码|重构|新增|增加.*功能|实现|功能.*优化|页面.*调整|样式.*调整|完善.*功能|适配|功能改造'),
 ('测试验证与代码审查','通过测试、走查或审核检查功能是否符合要求','测试|验证|自测|回归|走查|Review|review|审核代码|代码审核|代码审查|ApiFox|apifox|压力|压测|验机'),
 ('系统部署与上线发布','准备服务器、安装软件并发布版本，形成能使用的环境','部署|上线|发版|发布|服务器|Docker|docker|打包|安装包|升级|Linux|linux|麒麟|环境配置|云服务|域名'),
 ('现场安装与项目交付','到现场施工、布线、安装调试设备并推动验收','安装|施工|布线|勘测|进场|岛台|设备.*搬运|设备.*发货|到货|拆箱|验收|交付完成|完成.*交付|设备配对|出差'),
 ('培训手册与宣传材料','教别人使用，制作手册、操作视频、产品介绍和现场指引','手册|培训|操作视频|说明书|产品.*介绍|PPT|ppt|易拉宝|海报|宣导|宣发|软装|流程图|卖点|一页纸|图册|讲解|录制'),
 ('AI工具与方法沉淀','建设知识库、Skill和自动化方法，提高后续工作的可复用性','Skill|skill|工作流|知识库|知识服务|方法论|方法沉淀|AI资产|自动化|工作台|Agent|agent|复用'),
 ('项目协调与计划管理','协调客户、销售、研发和厂家，确认进度、责任、排期和卡点','沟通|协调|跟进|推进|排期|计划|会议|周报|汇报|预算|同步|梳理|整理|方案|规划|进度|需求'),
 ('内部行政与费用事务','处理报销、合同审批、入职、账号申请等内部事务','报销|发票|合同评审|续签|入职|新人|盖章|对公|行政|开通.*权限|账号.*申请'),
 ('学习休假与笼统记录','学习/请假或只有笼统表述，未能识别具体产出','请假|婚假|休假|学习|熟悉|公司交办|相关工作|按事业部|完成问题记录'),
 ('待确认工作类型','原文信息不足，需要补充才能准确判断',''),
]

def hits(text, defs):
    return [d[0] for d in defs if re.search(d[1], text, re.I)]

def redact(text):
    text = str(text or '')
    text = re.sub(r'https?://[^\s<>）)]+', '[原文链接已隐藏]', text)
    text = re.sub(r'\b(?:\d{1,3}\.){3}\d{1,3}\b', '[地址已隐藏]', text)
    text = re.sub(r'(?i)(?<![\w-])(?:[\w-]+\.)+(?:com|cn|net|org|cloud)(?::\d+)?(?:/[^\s]*)?', '[域名已隐藏]', text)
    text = re.sub(r'(?i)\b(?:admin|root|administrator)\s*[/：:]\s*[^\s，。,;；]+', '[账号口令已隐藏]', text)
    text = re.sub(r'(?i)\bCpt@\d+\b', '[口令已隐藏]', text)
    text = re.sub(r'(?:密码|口令|token|secret|access_key|api_key)\s*[:：=]\s*[^\s，。；;]+', '[敏感值已隐藏]', text, flags=re.I)
    text = re.sub(r'(?<!\d)1[3-9]\d{9}(?!\d)', '[手机号已隐藏]', text)
    return text

def norm(text):
    return re.sub(r'[\s\W_]+', '', text).lower()

def clean(line):
    return re.sub(r'^\s*(?:[-•·]|\d+(?:\.\d+)*[、.．）)]|[一二三四五六七八九十]+[、.．])\s*', '', line).strip()

def generic(t):
    return bool(re.fullmatch(r'(?:无|暂无|无新增进展|已完成|完成|\d+\s*[%％]|\.\.\.|……|项目|产品|其他|完成问题记录|按事业部规划完成|配合销售完成相关工作|完成公司交办其他工作|公司交办的其他工作|今日无新增进展)[。.!！\s]*', t)) or t == '公司交办其他工作' or t in ['响应销售同事的问题，协助提供支持。','及时完成，无无故逾期或失联','交付流程疏通']

def sector(projects):
    return '；'.join(sorted({p[2] for p in PROJECTS if p[0] in projects})) or '待确认'

def classify(t, context):
    # Actual action verbs override subjects, so an interface test is not counted as development.
    strong = [
      ('内部行政与费用事务', r'报销|发票|续签|入职培训|新人培训|预算编制|合同评审|候选人面试|权限.*开通'),
      ('安全整改与认证', r'漏洞|等保|密评|软著|备案|网安|安全评估|高危|漏扫'),
      ('修复问题与运行保障', r'排查|修复|维修|检修|故障|跟餐|值守|开餐保障|问题.*(?:解决|处理)|(?:解决|处理).*?(?:异常|问题)|(?:调试|判断).*?(?:网络|原因)|恢复|(?:异常|无法).*?(?:调试|原因|解决)'),
      ('调研选型与竞品研究', r'竞品|访谈|选型|硬件.*评估|市场.*分析|调研'),
      ('售前方案与报价投标', r'报价|投标|应标|控标|建设方案|估时|售前|售前.*配置'),
      ('培训手册与宣传材料', r'手册|说明书|操作视频|PPT|ppt|易拉宝|海报|宣导|流程图|卖点|图册|一页纸|操作文档|物料|公发画面|展示.*界面图'),
      ('AI工具与方法沉淀', r'(?:skill|Skill).*?(?:沉淀|设计|编制|提交|形成)|知识库.*?(?:文档|入库|建设)|(?:方法|AI资产).*沉淀'),
      ('产品设计与需求确认', r'原型|需求评审|需求分析|需求讲解|产品设计|功能评审|需求.*(?:讲解|确认|梳理|边界)|(?:确认|梳理).*需求|方案.*确认|算法.*确认|页面.*设计|概念设计|应用设计'),
      ('测试验证与代码审查', r'专项测试|压力测试|测试报告|(?:开展|完成|进行|通过|兼容性|端到端|专项|回归|全流程|核对).*?(?:测试|验证)|代码.*(?:审核|审查)|Review|review|走查|assertions|tests|用例|评审优化|查看.*代码'),
      ('数据整理与系统迁移', r'数据(?:库)?迁移|(?:导入|录入|导出|核对|清理|收集|查找|获取).*?(?:数据|人员|余额|菜品|用户信息|食材|食物成分)|备份|商品比价|菜品建立|菜品信息.*导入'),
      ('开发新功能与改进体验', r'开发|代码.*实现|新增|功能改造|重构|新增.*功能|(?:增加|完善|修正|更改|修改|变更|调整|补充|封装|渲染|优化).*?(?:功能|流程|规则|页面|逻辑|显示|字段|接口|策略|体验)|(?:页面|逻辑|订单|功能|内容|流程|字段).*?(?:优化|调整|修改|补充|增强)|改为|完成.*(?:核心链路|快捷登录|主题模式|功能|逻辑|PC 端)|自动生成.*改造'),
      ('第三方接口与设备对接', r'对接|联调|回调|SOAP|协议|SDK|sdk|接口|接入'),
      ('系统部署与上线发布', r'部署|上线|发版|软件安装|系统安装|Docker|docker|服务器.*配置|环境.*配置|版本.*更新|软件.*更新|云服务|域名|系统.*切换|网络配置|初始化|软件.*配对'),
      ('现场安装与项目交付', r'安装|施工|布线|岛台|搬运|设备.*发货|到货|拆箱|验收|交付完成|完成.*交付|出差|设备接货|(?:设备|秤体|扫码器|固定器|红外).*?(?:调试|更换|调整)|(?:更换|调试).*?(?:设备|屏幕|消费机|秤|扫码器|大屏)|布展|物资转运|打胶|驻场|收尾工作|设备.*(?:布置|拆除)'),
      ('培训手册与宣传材料', r'培训|指导.*使用|介绍.*材料|产品.*资料|演示.*流程|获取演示'),
    ]
    found = [n for n, pat in strong if re.search(pat, t, re.I)]
    if not found:
        found = [d[0] for d in TYPES[:-1] if re.search(d[2],t,re.I)]
    primary = found[0] if found else '待确认工作类型'
    return primary, '；'.join(dict.fromkeys(found)), primary

def state_tag(t):
    if re.search(r'待测试|待验收|待审核|待客户确认|等待|未完成|未解决|尚未|无法|未闭环', t): return '待测试/确认/处理（据原文）'
    if re.search(r'进行中|进度|推进|跟进|继续|\d+[%％]', t): return '进行中（据原文）'
    if re.search(r'已完成|完成|已上线|已解决|通过|已提交', t): return '声称完成/已上线（未独立验收）'
    return '有行动描述，结果未说明'

def stage(t, typ):
    if typ in ['售前方案与报价投标','调研选型与竞品研究']: return '售前/研究'
    if typ=='产品设计与需求确认': return '需求/设计'
    if typ in ['开发新功能与改进体验','第三方接口与设备对接']: return '开发/对接'
    if typ=='测试验证与代码审查': return '测试/评审'
    if typ in ['系统部署与上线发布','现场安装与项目交付']: return '部署/交付'
    if typ=='修复问题与运行保障': return '运行保障/整改'
    if typ=='安全整改与认证': return '安全/认证'
    if typ=='培训手册与宣传材料': return '使用培训/资料建设'
    return '协调/支撑'

def objects(t):
    fields=[('硬件设备','设备|扫码器|读卡器|摄像头|秤|消费机|410|450'),('前端页面','页面|小程序|H5|手机端|前端|UI|原型'),('后端接口','后端|接口|API|api|回调|数据库|SQL'),('资金与订单','支付|充值|退款|提现|余额|补贴|订单|一卡通'),('营养与健康','营养|体检|体重|菜品|食谱|食材|健康|训练'),('服务器与网络','服务器|网络|数据库|VPN|vpn|Docker|部署'),('文档与物料','文档|方案|PPT|报告|手册|海报|易拉宝|报价|说明书'),('组织与流程','会议|协调|排期|报销|发票|审批')]
    return '；'.join(hits(t,fields)) or '待确认'

items=[]
for log in sorted(LOGS,key=lambda x:(x['time'],x['name'],x['id'])):
    log['key']=f"L{len({q['log'] for q in items})+1:04}" if items else 'L0001'
    workall='\n'.join(w['text'] for w in log['work'])+'\n'+log['other']
    unique_proj=hits(workall,PROJECTS)
    # Split each source field, preserving source cell, section and project/product headings.
    chunks=[(w['text'],w['metric'],w['row'],f"I{w['row']}") for w in log['work']]
    chunks += [(log['other'],'其他重点工作',log['source_row'],f"J{log['source_row']}")]
    for raw,metric,srow,cell in chunks:
        ctx_proj=[]; ctx_prod=[]; ctx_title=''; mode='今日行动'; stated_day=''; last=None
        for line in re.split(r'\n+',raw):
            t=clean(line)
            if not t: continue
            if t.startswith(('http://','https://')) or re.fullmatch(r'/[\w/.-]+',t):
                continue
            if re.fullmatch(r'(?:.*阶段|.*准备任务|.*测试任务|【其他】.*|目前已完成以下工作[:：]|完成以下工作[:：])',t) and len(t)<35:
                ctx_title=t
                continue
            # Reports explicitly containing two business days are split without inferring midnight ownership.
            daymatch=re.fullmatch(r'【?\s*(\d{1,2})月(\d{1,2})日\s*】?',t)
            if daymatch:
                stated_day=f"2026-{int(daymatch[1]):02}-{int(daymatch[2]):02}"; continue
            if re.search(r'【(?:下一步计划|项目待办|明日计划|明日核心目标|预计完成时间|项目问题|项目卡点|项目风险|项目卡点|卡点)|^明日计划[:：]|^下一步|^卡点[:：]',t):
                mode='未来计划' if re.search(r'计划|待办|预计|明日|下一步',t) else '风险/卡点'
            elif re.search(r'【(?:今日进展|今日完成|交付结果|今日处理进展|sop阶段/任务|SOP阶段/任务)|^完成部分[:：]',t):
                mode='今日行动'
            elif re.search(r'【(?:产品差异性|AI赋能|AI 赋能)|经验沉淀|素材沉淀|资产沉淀',t):
                mode='复述/经验'
            explicit=hits(t,PROJECTS)
            # A compact project header starts a new context; long multi-project prose is never inherited.
            header=(len(t)<55 and (t.endswith(('项目','项目：','项目:','：',':')) or t.startswith(('【项目】','【项目名称】','【产品】','【安庆','【中央','【张家港')) or (explicit and len(t)<30)))
            if explicit and header:
                ctx_proj=explicit; ctx_prod=[];ctx_title=t
                if mode!='今日行动': mode='今日行动'
            elif re.match(r'^(?:标准产品|标准版|产品标准化|【产品】|一、标准产品|产品资料)',t):
                ctx_proj=[];ctx_prod=hits(t,PRODS);ctx_title=t;mode='今日行动'
            ptag=hits(t,PRODS)
            if not explicit and ptag and len(t)<38 and t.endswith(('：',':')):
                ctx_prod=ptag;ctx_title=t
            isheader=bool(re.fullmatch(r'【[^】]+】[:：]?',t)) or (header and not re.search(r'完成|输出|开发|沟通|安装|部署|排查|测试|推进|支持|改造|调试|跟进|分析|解决|确认|升级',t))
            if isheader or re.fullmatch(r'(?:第\d+[阶段任务]+[:：]?.*|[一二三四五六七八九十]、.*)',t) and len(t)<35 and not re.search(r'完成|开发|部署|排查|推进',t):
                ctx_title=t
                continue
            category, alltypes, _=classify(t,ctx_title)
            if category=='待确认工作类型':
                if re.search(r'更新.*(?:清单|资料)|功能点 Excel|软件功能描述',t):
                    category='培训手册与宣传材料';alltypes=category
                elif re.search(r'复盘|周会|答疑|返程|签收单|卫生打扫|现场.*网络|收集.*问题',t):
                    category='项目协调与计划管理';alltypes=category
                elif re.search(r'调试|更改|控制|创建|核查',t):
                    category='开发新功能与改进体验';alltypes=category
            if category=='待确认工作类型':
                if re.search(r'同步|确认|会议|例会|解答|支持|汇报|配合|拜访|交流|行程|处理|需求|调研|接收|提供',t):
                    category='项目协调与计划管理';alltypes=category
                elif re.search(r'输出|资料|材料|文档|截图|方案|介绍|工作簿|资产|沉淀|整理',t):
                    category='培训手册与宣传材料';alltypes=category
                elif re.search(r'优化|调整|修改|更换|增加|补充|配置|更正|渲染|代码|后台|前端|后端|绑定|校验|完善|封装|界面',t):
                    category='开发新功能与改进体验';alltypes=category
            pr=explicit or ctx_proj
            basis='原句明确提及' if explicit else ('同段项目标题' if ctx_proj else '未说明')
            if not pr and len(unique_proj)==1 and not re.search(r'标准产品|标准版|产品标准化|【产品】',ctx_title):
                pr=unique_proj; basis='整份汇报唯一项目（待核对）'
            if not pr and category in ['内部行政与费用事务','AI工具与方法沉淀','学习休假与笼统记录']:
                pr=['内部运营/通用工作'];basis='依据工作内容归入内部支撑'
            if not pr and re.search(r'标准产品|标准版|产品标准化|【产品】|学校线|学校版|营养师|跑步助手',t+' '+ctx_title):
                pr=['标准产品工作（未指向客户）'];basis='原文明确产品工作'
            products=ptag or ctx_prod
            if not products and last and last['projects']==('；'.join(pr) or '待确认/未说明') and last['products']!='待确认/未说明':
                products=last['products'].split('；')
            if not products and category=='安全整改与认证' and re.search(r'等保|密评|软著',t): products=['安全认证与通用资质']
            isgeneric=generic(t) or (len(t)<8 and not re.search(r'开发|部署|联调|报销|测试|采购|上线|发布|排查|更换|安装|培训|调试|增加|资料|学习',t))
            if isgeneric:
                category='学习休假与笼统记录'; alltypes=category
            if '假' in t and re.search(r'婚假|请假|休假',t):mode='休假/非工作事项'
            outcome=bool(re.search(r'手册|方案|清单|报告|原型|代码|PPT|视频|报价|图纸|安装包|资料|文档|截图|部署包',t,re.I))
            counts = int(mode=='今日行动' and not isgeneric)
            if re.search(r'^未完成|待完成$',t):
                counts=0;mode='未来计划'
            if category=='待确认工作类型' and re.search(r'完成',t) and len(t)<35:
                counts=0;mode='状态摘要（动作未说明）'
            if category=='待确认工作类型' and not re.search(r'完成|开展|进行|推进|解决|支持|取用|查找|查看|获取|录入|生成|建设|收集|更新',t):
                counts=0
                mode='背景/描述（动作不足）'
            flag=[]
            if not pr:flag.append('项目未说明')
            if not products:flag.append('产品未说明')
            if basis.startswith('整份'):flag.append('项目上下文推断')
            if category=='待确认工作类型':flag.append('类型待确认')
            if log['time'][11:13] in ['00','01','02','03','04','05']:flag.append('凌晨提交，工作日期待核对')
            if stated_day:flag.append('原文明示工作日')
            scope='内部支撑' if pr==['内部运营/通用工作'] else ('标准产品建设' if pr==['标准产品工作（未指向客户）'] else ('客户项目' if pr else '归属待确认'))
            item={'log':log['id'],'date':log['date'],'workdate':stated_day,'name':log['name'],'department':log['department'].split('/')[-1], 'text':t,'heading':ctx_title,'metric':metric,'mode':mode,'category':category,'alltypes':alltypes,'projects':'；'.join(pr) or '待确认/未说明','products':'；'.join(products) or '待确认/未说明','sector':sector(pr),'basis':basis,'state':state_tag(t),'stage':stage(t,category),'objects':objects(t),'count':counts,'source':log['source'],'cell':cell,'flags':'；'.join(flag),'duplicate':'','result':'原文有交付物关键词' if outcome else '未明确命名交付物','scope':scope}
            items.append(item);last=item
            if mode=='背景/描述（动作不足）':mode='今日行动'
    # Remove repeated text/near exact reformulations from the same report only.
    seen=[]
    for it in [x for x in items if x['log']==log['id']]:
        n=norm(it['text'])
        if not n:continue
        duplicate=None
        for other,on in seen:
            same_proj=it['projects']==other['projects'] or '待确认/未说明' in [it['projects'],other['projects']]
            ratio=SequenceMatcher(None,n,on).ratio() if abs(len(n)-len(on))<max(len(n),len(on))*0.25 else 0
            if same_proj and (n==on or (len(n)>=24 and len(on)>=24 and (n in on or on in n)) or (same_proj and len(n)>=20 and ratio>=0.9)):
                duplicate=other;break
        if duplicate:
            it['duplicate']='同份汇报复述：'+duplicate['cell'];it['count']=0
        else:seen.append((it,n))

# Stable IDs and accounting base: one retained current-action segment = one item, not one hour.
for i,it in enumerate(items,1):it['key']=f'W{i:05}'
ACTIVE=[x for x in items if x['count']]
by_log={x['id']:x for x in LOGS}
by_day_person=defaultdict(list)
for it in ACTIVE:by_day_person[(it['date'],it['name'])].append(it)
logdp=Counter((x['date'],x['name']) for x in LOGS)
day_types=Counter((x['date'],x['category']) for x in ACTIVE)
person_types=Counter((x['name'],x['category']) for x in ACTIVE)
PERSONNOTES={
 '侯世伟':'现场交付、施工与设备安装；涉及滨州、央美、安庆和张家港，重点是现场条件与交付节点。',
 '张帅A':'设备安装调试、客户培训和售后；同时覆盖餐厅、食品安全、体测和训练设备。',
 '赵野':'多项目现场推进、网络/设备排查和客户沟通；四方达、三全、哈尔滨等项目经常并行。',
 '王金洋':'滨州就餐现场保障占据大量日志，也承担设备测试、更换、消费机升级及金斯瑞支持。',
 '柴玉龙':'服务器部署、项目上线、远程技术保障；较多项目之间切换，并协同交付人员处理现场条件。',
 '兰海军':'后端功能、数据及接口对接、安全整改；后期出现达梦数据库适配与迁移。',
 '赖清涛':'私有化部署、接口对接、代码审核与项目技术方案；跨多个餐厅项目进行技术协作。',
 '郭东伟':'PC/小程序页面、设备数据展示与功能开发；后期参与食安和前厅达梦适配。',
 '孙倩倩':'菜品识别、消费功能和学校膳食营养开发与测试；也处理食安部署及设备接口问题。',
 '尹旭亮':'设备端软件、称重/刷卡/支付兼容、硬件问题定位和厂家技术沟通。',
 '田亚辉':'初期小程序事项，后期主要是三全的主数据、钉钉/魔点回调、成本中心和订单对接。',
 '王玉':'AI跑步/训练助手的后端、知识库、训练计划和小程序联调；包含入职学习与服务配置。',
 '李潇':'客户需求确认、产品原型、标准产品资料、营养设计和多个项目的优先级协调。',
 '孙海琴':'产品原型、用户手册、宣传物料及项目需求跟进；同时有报销和知识资料沉淀。',
 '王朔A':'售前方案与投标报价、安全认证材料、学校膳食营养设计及产品资料建设。',
}

OUT=ROOT/'团队工作日志与通俗工作分类_20260817-20261008.xlsx'
wb=xlsxwriter.Workbook(OUT,{'strings_to_formulas':False,'strings_to_urls':False})
wb.set_properties({'title':'团队工作日志与通俗工作分类','subject':'企业微信原始日志、日期人员视图与项目产品分类','author':'Codex','comments':'内容标签为分析判断，不能替代工时或绩效考核。'})
fmt=wb.add_format({'font_name':'微软雅黑','font_size':12,'valign':'top','text_wrap':True})
titlefmt=wb.add_format({'font_name':'微软雅黑','font_size':20,'bold':True,'font_color':'163B56'})
headfmt=wb.add_format({'font_name':'微软雅黑','font_size':12,'bold':True,'font_color':'FFFFFF','bg_color':'163B56','valign':'vcenter','text_wrap':True})
numfmt=wb.add_format({'font_name':'微软雅黑','font_size':12,'valign':'top','num_format':'0'})
pctfmt=wb.add_format({'font_name':'微软雅黑','font_size':12,'num_format':'0.0%'})
datefmt=wb.add_format({'font_name':'微软雅黑','font_size':12,'num_format':'yyyy-mm-dd','valign':'top'})
warnfmt=wb.add_format({'font_name':'微软雅黑','font_size':12,'bg_color':'FFF1CF','font_color':'835817','text_wrap':True,'valign':'top'})
greenfmt=wb.add_format({'font_name':'微软雅黑','font_size':12,'bg_color':'E8F3EE','font_color':'226D51','text_wrap':True,'valign':'top'})
linkfmt=wb.add_format({'font_name':'微软雅黑','font_size':12,'font_color':'1677C8','underline':1})

def sheet(name,headers,rows,widths=None):
    ws=wb.add_worksheet(name)
    ws.hide_gridlines(2);ws.freeze_panes(1,3 if len(headers)>3 else 1)
    ws.set_row(0,44,headfmt)
    ws.set_column(0,len(headers)-1,20,fmt)
    for col,width in (widths or {}).items():ws.set_column(col,col,width,fmt)
    ws.write_row(0,0,headers,headfmt)
    for i,row in enumerate(rows,1):
        for j,v in enumerate(row):
            if isinstance(v,(int,float)):ws.write_number(i,j,v,numfmt)
            elif isinstance(v,date):ws.write_datetime(i,j,datetime.combine(v,datetime.min.time()),datefmt)
            else:ws.write_string(i,j,redact(v),fmt)
        linecount=1
        for j,v in enumerate(row):
            width=(widths or {}).get(j,20)
            wrapped=sum(max(1,math.ceil(sum(2 if unicodedata.east_asian_width(ch) in 'WF' else 1 for ch in line)/max(10,width-2))) for line in str(v).split('\n'))
            linecount=max(linecount,wrapped)
        ws.set_row(i,min(150,max(30,18*linecount+8)))
    if rows:ws.add_table(0,0,len(rows),len(headers)-1,{'columns':[{'header':h} for h in headers],'style':'Table Style Medium 2'})
    ws.set_landscape();ws.fit_to_pages(1,0);ws.repeat_rows(0);ws.set_paper(9)
    return ws

overview=wb.add_worksheet('先看这里')
overview.hide_gridlines(2);overview.set_column('A:A',4);overview.set_column('B:B',32,fmt);overview.set_column('C:H',20,fmt)
overview.merge_range('B2:H3','团队每天都在做什么',titlefmt)
overview.merge_range('B5:H6','2026-08-17 至 2026-10-08｜当前账号收到的「工作过程线上化--工作日志」｜后台分段导出',fmt)
summary=[('原始汇报数',len(LOGS)),('涉及人员',len(PEOPLE)),('有提交记录的日期',len({x['date'] for x in LOGS})),('日历范围天数',len(DAYS)),('保留用于统计的今日行动片段',len(ACTIVE)),('同人同日多份汇报组合',sum(n>1 for n in logdp.values())),('凌晨提交记录（工作日期待核对）',sum(x['time'][11:13] in ['00','01','02','03','04','05'] for x in LOGS))]
for r,(label,n) in enumerate(summary,8):overview.write(r,1,label,fmt);overview.write_number(r,2,n,numfmt)
overview.merge_range('B17:H19','阅读顺序：先看「每日人员矩阵」找到某天某人；再看「日期人员日览」了解当天做了什么；「事项标签明细」可按工作类型、项目、产品、状态筛选；「人员工作画像」查看每个人的主要工作；「类型说明与标签」用普通话解释分类。',fmt)
overview.merge_range('B21:H23','统计口径：一个计入片段是日志中的一段可识别行动，绝不等于一个任务、一小时或绩效。一条片段只分配一个主类型，辅助标签可以有多个。事项写得详细的人会有更多片段，不能据此比较忙碌程度。',warnfmt)
overview.merge_range('B25:H28','日期按导出文件的「汇报时间」归属。实际工作日期只有原文明确写出时才另列；凌晨补报不自动移到前一天。同人同日不同汇报均保留。同份日志中的高度相似复述不重复计数。没有记录仅表示本次导出未发现，不能认定没工作、缺报或旷工。',warnfmt)
overview.merge_range('B30:H33','采集范围完整性：第一段316份、第二段147份，合计463份，编号互不重复，原始数据1218行均保留。关联结果表当前无权限，无法补核工作日归属。附件列标注「已删除」，本次没有附件内容。今天截至导出时点未发现提交记录。',warnfmt)
overview.merge_range('B35:H37','标签是基于日志内容的语义归类与可复算规则，未使用工时估算，也未向外部分类服务发送原文。项目别名依同一客户名称归并；安徽/安庆等不能证实相同的项目分别保留。含账号口令、服务器地址、手机号及原文网址的字段在分析表中隐藏，未经独立核验的完成情况仅标「据原文」。',fmt)
for i,n in enumerate(['每日人员矩阵','日期人员日览','人员工作画像','工作类型总览','项目汇总','产品汇总','事项标签明细','原始日志汇总','原始字段逐行','类型说明与标签','质量与采集核验'],40):overview.write_url(i,1,f"internal:'{n}'!A1",linkfmt,n)

rawrows=[]
for i,l in enumerate(sorted(LOGS,key=lambda x:(x['time'],x['name'])),1):
    l['localid']=f'L{i:04}'
    work='\n\n'.join(f"[{w['number']} {w['metric']}]\n{w['text']}" for w in l['work'])
    flags=[]
    if logdp[(l['date'],l['name'])]>1:flags.append('同人同日多份：均保留')
    if l['time'][11:13] in ['00','01','02','03','04','05']:flags.append('凌晨提交：工作日待确认')
    rawrows.append([l['localid'],l['date'],l['name'],l['time'],l['department'].split('/')[-1],work,l['other'],l['tomorrow'],l['learning'],'；'.join(flags),l['source'],l['source_row']])
sheet('原始日志汇总',['日志编号','提交日期','人员','汇报时间','部门','重点工作原文（敏感值隐藏）','其他重点工作原文（敏感值隐藏）','明日目标原文','学习与反思原文','日期/重复提示','来源文件','源首行'],rawrows,{5:90,6:90,7:65,8:55,10:47})
source_rows=[]
for l in sorted(LOGS,key=lambda x:(x['time'],x['name'])):
    for idx,row in enumerate(l['all_rows']):source_rows.append([l['localid'],l['date'],l['name'],l['source'],l['source_row']+idx]+row[6:])
sheet('原始字段逐行',['日志编号','提交日期','人员','来源文件','源行号','重点序号','绩效指标原文','今日进度原文（敏感值隐藏）','其他重点工作原文','明日目标原文','学习反思原文','评论原文'],source_rows,{3:45,6:42,7:100,8:90,9:70,10:60,11:60})
ih=['事项编号','日志编号','提交日期','人员','部门','主工作类型','辅助工作类型','客户项目/工作归属','产品/能力','客户领域','阶段','工作对象','内容属性','原文片段（敏感值隐藏）','上下文标题','日志填报栏目','结果状态','交付物提示','项目判定依据','原文明示工作日期','计入今日行动(1/0)','重复说明','待核验提示','来源文件','源单元格','工作范围']
ir=[[x['key'],by_log[x['log']]['localid'],x['date'],x['name'],x['department'],x['category'],x['alltypes'],x['projects'],x['products'],x['sector'],x['stage'],x['objects'],x['mode'],x['text'],x['heading'],x['metric'],x['state'],x['result'],x['basis'],x['workdate'],x['count'],x['duplicate'],x['flags'],x['source'],x['cell'],x['scope']] for x in items]
sheet('事项标签明细',ih,ir,{5:28,6:45,7:40,8:42,13:100,14:55,15:43,16:40,18:35,21:45,22:45,23:45})

daily=[]
matrix=[]
for d in DAYS:
    ds=d.isoformat();mr=[d,['周一','周二','周三','周四','周五','周六','周日'][d.weekday()]]
    for name in PEOPLE:
        its=by_day_person[(ds,name)];n=logdp[(ds,name)]
        cats=Counter(x['category'] for x in its)
        typ='；'.join(f'{c}({nn})' for c,nn in cats.most_common())
        pj=sorted({p for x in its for p in x['projects'].split('；') if p!='待确认/未说明'})
        pd=sorted({p for x in its for p in x['products'].split('；') if p!='待确认/未说明'})
        ex='\n'.join(f"• {x['text']}" for x in sorted(its,key=lambda x:len(x['text']),reverse=True)[:5])
        status='有日志但无具体行动片段' if n and not its else ('有日志' if n else '本次导出无记录（非缺勤判定）')
        daily.append([d,name,status,n,len(its),typ,'；'.join(pj) or '待确认/未说明','；'.join(pd) or '待确认/未说明',ex,'；'.join(x['localid'] for x in LOGS if x['name']==name and x['date']==ds)])
        mr.append(f"{n}份｜"+' / '.join(c for c,_ in cats.most_common(3)) if n else '无导出记录')
    matrix.append(mr)
ws=sheet('日期人员日览',['提交日期','人员','日志状态','汇报份数','今日行动片段数','当天工作类型（片段数）','项目','产品/能力','具体工作举例（最多5段）','原始日志编号'],daily,{2:39,5:65,6:60,7:60,8:110,9:35})
mx=sheet('每日人员矩阵',['提交日期','星期']+PEOPLE,matrix,{i:35 for i in range(2,17)})
mx.freeze_panes(1,2);mx.set_zoom(70)
mx.conditional_format(1,2,len(matrix),len(PEOPLE)+1,{'type':'text','criteria':'containing','value':'无导出记录','format':warnfmt})

portraits=[]
for name in PEOPLE:
    own=[x for x in ACTIVE if x['name']==name];ls=[x for x in LOGS if x['name']==name]
    ct=Counter(x['category'] for x in own)
    pjs=Counter(p for x in own for p in x['projects'].split('；') if p!='待确认/未说明')
    prs=Counter(p for x in own for p in x['products'].split('；') if p!='待确认/未说明')
    portraits.append([name,ls[0]['department'].split('/')[-1],len(ls),len({x['date'] for x in ls}),len(own),PERSONNOTES.get(name,''),'；'.join(f'{c}({n})' for c,n in ct.most_common(5)),'；'.join(p for p,_ in pjs.most_common(6)),'；'.join(p for p,_ in prs.most_common(5)),'日志自述画像；片段数不用于工时或绩效排名'])
sheet('人员工作画像',['人员','部门','汇报份数','有提交日期数','具体行动片段数','通俗工作画像（分析）','主要工作类型及片段数','经常涉及的项目','经常涉及的产品/能力','使用边界'],portraits,{5:95,6:70,7:65,8:65,9:65})

endrow=len(items)+1
countformula=f"'事项标签明细'!$U$2:$U${endrow}"
catformula=f"'事项标签明细'!$F$2:$F${endrow}"
personformula=f"'事项标签明细'!$D$2:$D${endrow}"
dateformula=f"'事项标签明细'!$C$2:$C${endrow}"
type_rows=[]
total=len(ACTIVE)
typect=Counter(x['category'] for x in ACTIVE)
for name,ex,pat in TYPES:
    its=[x for x in ACTIVE if x['category']==name]
    type_rows.append([name,ex,len(its),len(its)/total if total else 0,'；'.join(sorted({x['name'] for x in its})),'\n'.join(x['text'][:150] for x in its[:3])])
tw=sheet('工作类型总览',['主要工作类型','外行解释','今日行动片段数','片段占比（非工时）','涉及人员','真实日志举例'],type_rows,{0:29,1:80,4:75,5:100})
for i,row in enumerate(type_rows,1):
    tw.write_formula(i,2,f'=SUMIFS({countformula},{catformula},A{i+1})',numfmt,row[2])
    tw.write_formula(i,3,f'=IFERROR(C{i+1}/SUM(C$2:C${len(type_rows)+1}),0)',pctfmt,row[3])
chart=wb.add_chart({'type':'bar'})
chart.add_series({'name':'今日行动片段','categories':['工作类型总览',1,0,len(TYPES),0],'values':['工作类型总览',1,2,len(TYPES),2],'fill':{'color':'258D9B'}})
chart.set_title({'name':'工作类型分布（按日志片段，非工时）'});chart.set_legend({'none':True});chart.set_size({'width':900,'height':580});tw.insert_chart('H2',chart)

ptrows=[]
for name in PEOPLE:
    ptrows.append([name]+[person_types[(name,t[0])] for t in TYPES]+[sum(person_types[(name,t[0])] for t in TYPES)])
ps=sheet('人员类型交叉表',['人员']+[t[0] for t in TYPES]+['片段合计'],ptrows,{i:25 for i in range(1,len(TYPES)+2)})
for ri,row in enumerate(ptrows,1):
    for ci,t in enumerate(TYPES,1):
        from xlsxwriter.utility import xl_col_to_name
        ps.write_formula(ri,ci,f'=SUMIFS({countformula},{personformula},$A{ri+1},{catformula},{xl_col_to_name(ci)}$1)',numfmt,row[ci])
    ps.write_formula(ri,len(TYPES)+1,f'=SUM(B{ri+1}:{xl_col_to_name(len(TYPES))}{ri+1})',numfmt,row[-1])
ps.conditional_format(1,1,len(PEOPLE),len(TYPES),{'type':'3_color_scale','min_color':'FFFFFF','mid_color':'D9EEF0','max_color':'258D9B'})

dtrows=[]
for d in DAYS:
    ds=d.isoformat();ct=[day_types[(ds,t[0])] for t in TYPES]
    dtrows.append([ds,sum(x['date']==ds for x in LOGS),len({x['name'] for x in LOGS if x['date']==ds})]+ct+[sum(ct)])
dsheet=sheet('每日类型交叉表',['提交日期','汇报份数','提交人数']+[t[0] for t in TYPES]+['片段合计'],dtrows,{i:25 for i in range(3,len(TYPES)+4)})
for ri,row in enumerate(dtrows,1):
    for ci,t in enumerate(TYPES,3):dsheet.write_formula(ri,ci,f'=SUMIFS({countformula},{dateformula},$A{ri+1},{catformula},{xl_col_to_name(ci)}$1)',numfmt,row[ci])
    dsheet.write_formula(ri,len(TYPES)+3,f'=SUM(D{ri+1}:{xl_col_to_name(len(TYPES)+2)}{ri+1})',numfmt,row[-1])
dsheet.conditional_format(1,3,len(DAYS),len(TYPES)+2,{'type':'3_color_scale','min_color':'FFFFFF','mid_color':'D9EEF0','max_color':'258D9B'})

def aggregate(field):
    rows=[]
    vals=sorted({p for x in ACTIVE for p in x[field].split('；')})
    for p in vals:
        own=[x for x in ACTIVE if p in x[field].split('；')]
        types=Counter(x['category'] for x in own)
        other='products' if field=='projects' else 'projects'
        rows.append([p,'；'.join(sorted({x['sector'] for x in own})),len(own),len({x['log'] for x in own}),len({x['name'] for x in own}),len({x['date'] for x in own}),'；'.join(sorted({x['name'] for x in own})),'；'.join(f'{t}({n})' for t,n in types.most_common(5)),'；'.join(sorted({z for x in own for z in x[other].split('；')})),'\n'.join(x['text'][:150] for x in own[:3]),'多标签可重复关联，不能把各行数量相加作为总工作量'])
    return rows
sheet('项目汇总',['项目（统一别名）','客户领域','关联行动片段数','涉及汇报份数','涉及人数','涉及日期数','参与人员','常见工作类型','产品/能力','日志例子','统计说明'],aggregate('projects'),{0:42,6:70,7:75,8:75,9:100,10:70})
sheet('产品汇总',['产品/能力','关联客户领域','关联行动片段数','涉及汇报份数','涉及人数','涉及日期数','参与人员','常见工作类型','涉及项目','日志例子','统计说明'],aggregate('products'),{0:35,6:70,7:75,8:90,9:100,10:70})

dictionary=[['工作性质',n,note,'按实际行动词判定，主类型唯一，辅助类型可多选'] for n,note,_ in TYPES]
dictionary += [['项目',n,f'别名识别规则：{pat}',f'客户领域：{sec}；常见主题：{topic}；只用原句或清楚的上下文，不按人员岗位推断'] for n,pat,sec,topic in PROJECTS]
dictionary += [['产品/能力',n,'涵盖关键词：'+pat,'按具体能力识别，可以多选；未说明时留待确认'] for n,pat in PRODS]
dictionary += [['内容属性',n,note,'只有今日行动中的具体且未重复片段计入统计'] for n,note in [('今日行动','原文描述的当日动作，不等于独立验收完成'),('未来计划','明日、下一步、预计完成或项目待办'),('风险/卡点','客户条件、故障和未解决的约束'),('复述/经验','产品差异性、经验总结等，避免与行动重复'),('休假/非工作事项','请假和休假记录保留，但不算具体工作')]]
sheet('类型说明与标签',['标签维度','标签名称','普通话解释/识别依据','使用规则'],dictionary,{0:20,1:44,2:95,3:95})

quality=[]
for p,expected in [('01-原始日志-20260817-20260916.xlsx',316),('02-原始日志-20260917-20261008.xlsx',147)]:
    actual=sum(x['source']==p for x in LOGS)
    quality.append(['采集对账',p,f'UI显示{expected}份；文件读取{actual}份','通过' if actual==expected else '不一致','来源文件汇报编号逐份计数'])
quality += [
 ['合并去重','汇报编号',f'{len(LOGS)}份 / {len({x["id"] for x in LOGS})}个唯一编号','通过','不同编号的同人同日记录均保留'],
 ['逐行保留','原始字段',f'{len(source_rows)}个源数据行','通过','2个文件870/350行，扣除各1行表头：1218'],
 ['范围','提交日期','2026-08-17至2026-10-07有数据；10-08截至导出时点无记录','已说明','53个日历日全部出现在日览和矩阵中'],
 ['日期归属','实际工作日期','导出没有单独的工作所属日期字段；凌晨提交与补报需核对','待核验','原文明示的日期另列，不自动把凌晨提交移到前一天'],
 ['权限','关联结果表','当前账号无权限；未点击申请权限','不影响现有日志分析','不能借关联表补核日期或缺报'],
 ['缺报','应报人员及日历','没有完整应报人员/休假/日报计划的系统证据','不作缺报率','无记录不等于没工作'],
 ['附件','原导出附件列','来源列为附件（已删除）','无附件内容','分析只覆盖导出中的文本'],
 ['隐私','敏感字段','分析工作簿隐藏账号口令、地址、手机号和原文网址','已隐藏','原始文件仅本地保存，不上传、发消息或纳入Git'],
 ['算法','分类方法','文本语义分组+本地规则+上下文继承+同份近似复述去重','可复算','未使用外部向量/分类服务；此报告非独立绩效审核'],
]
for l in LOGS:
    n=logdp[(l['date'],l['name'])]
    if n>1:quality.append(['同日多份',l['name']+' '+l['date'],l['localid'],'已保留','不同编号，不自动删；实际日期需核对'])
    if l['time'][11:13] in ['00','01','02','03','04','05']:quality.append(['凌晨提交',l['name']+' '+l['date'],l['localid']+' '+l['time'],'工作日期待确认','统计仍按提交日期，原文如明示日期则另列'])
sheet('质量与采集核验',['核验维度','对象','事实/数量','结果或限制','说明'],quality,{0:22,1:50,2:95,3:35,4:100})

for s in wb.worksheets():s.set_tab_color('258D9B' if s.name in ['先看这里','每日人员矩阵','人员工作画像'] else '163B56')
wb.close()
(ROOT/'source/items.json').write_text(json.dumps(items,ensure_ascii=False,indent=2))
facts={'reports':len(LOGS),'people':len(PEOPLE),'calendar_days':len(DAYS),'report_days':len({x['date'] for x in LOGS}),'raw_data_rows':len(source_rows),'segments':len(items),'counted_segments':len(ACTIVE),'duplicates_removed_from_counts':sum(bool(x['duplicate']) for x in items),'type_counts':dict(typect),'people_reports':dict(Counter(x['name'] for x in LOGS)),'unclassified_active':sum(x['category']=='待确认工作类型' for x in ACTIVE),'project_unknown_active':sum(x['projects']=='待确认/未说明' for x in ACTIVE),'product_unknown_active':sum(x['products']=='待确认/未说明' for x in ACTIVE),'workbook':str(OUT),'sheets':len(wb.worksheets())}
(ROOT/'validation.json').write_text(json.dumps(facts,ensure_ascii=False,indent=2))
print(json.dumps(facts,ensure_ascii=False,indent=2))
