#!/usr/bin/env python3 """cd_checklist.py — trạng thái TICK checklist (do[]) các bước Customer Development, MỨC CÔNG TY. Company-shared: company_kb/_cd_checklist.json (giữ qua update). Chỉ ghi file này, chỉ bật/tắt boolean theo (stage, step, index) — validate đối chiếu customer_development.json. Đây là ngoại lệ GHI có kiểm soát của dashboard read-only (auth-gated ở server.py; không đụng dữ liệu nhạy cảm). CLI: python3 lib/cd_checklist.py status -> {ok, items, by_step} python3 lib/cd_checklist.py toggle <0|1> [by] """ import os import re import sys import json import datetime def _bundle_root(): return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOT = _bundle_root() KB = os.path.join(ROOT, "company_kb") STORE = os.path.join(KB, "_cd_checklist.json") CD = os.path.join(ROOT, "content", "customer_development.json") def _safe(s): return re.sub(r"[^a-z0-9_-]", "", (s or "").lower()) def _step_key(stp): return stp.get("id") or re.sub(r"(^-|-$)", "", re.sub(r"[^a-z0-9]+", "-", (stp.get("name") or "").lower())) def _key(stage, step, path): return "%s/%s/%s" % (stage, step, path) def _leaves(stp): """Danh sách path hợp lệ của 1 bước: 'g-i' nếu có checklist[] (nhóm), hoặc 'i' nếu chỉ có do[].""" cl = stp.get("checklist") if cl: return ["%d-%d" % (g, i) for g, grp in enumerate(cl) for i in range(len(grp.get("items", [])))] return [str(i) for i in range(len(stp.get("do") or []))] def _cd(): with open(CD, encoding="utf-8") as f: return json.load(f) def _find_step(stage_id, step_id): for s in _cd().get("stages", []): if s.get("id") != stage_id: continue for row in s.get("flow", []): for stp in row.get("steps", []): if _step_key(stp) == step_id: return stp return None def _load(): try: with open(STORE, encoding="utf-8") as f: d = json.load(f) d.setdefault("items", {}) return d except Exception: return {"version": 1, "items": {}} def _save(st): os.makedirs(KB, exist_ok=True) st["updated_at"] = datetime.datetime.now().isoformat(timespec="seconds") tmp = STORE + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(st, f, ensure_ascii=False, indent=2) os.replace(tmp, STORE) def toggle(stage, step, path, done, by="dashboard"): stage, step = _safe(stage), _safe(step) path = re.sub(r"[^0-9-]", "", str(path)) # chỉ cho 'i' hoặc 'g-i' stp = _find_step(stage, step) if not stp: return {"ok": False, "error": "unknown_step"} leaves = _leaves(stp) if path not in leaves: return {"ok": False, "error": "unknown_item"} st = _load() k = _key(stage, step, path) if str(done) in ("1", "true", "True", "on"): st["items"][k] = {"done": True, "ts": datetime.datetime.now().isoformat(timespec="seconds"), "by": (by or "")[:40]} else: st["items"].pop(k, None) _save(st) done_n = sum(1 for p in leaves if _key(stage, step, p) in st["items"]) return {"ok": True, "stage": stage, "step": step, "path": path, "done": k in st["items"], "step_done": done_n, "step_total": len(leaves)} def status(): st = _load() items = st.get("items", {}) roll = {} try: cd = _cd() except Exception: return {"ok": True, "items": items, "by_step": {}} for s in cd.get("stages", []): for row in s.get("flow", []): for stp in row.get("steps", []): sk = _step_key(stp) leaves = _leaves(stp) if not leaves: continue d = sum(1 for p in leaves if _key(s["id"], sk, p) in items) roll["%s/%s" % (s["id"], sk)] = {"done": d, "total": len(leaves)} return {"ok": True, "items": items, "by_step": roll, "updated_at": st.get("updated_at")} def main(): a = sys.argv[1:] cmd = a[0] if a else "status" g = lambda i: a[i] if len(a) > i else "" if cmd == "toggle": out = toggle(g(1), g(2), g(3), g(4), g(5) or "dashboard") elif cmd == "status": out = status() else: out = {"ok": False, "error": "unknown cmd: %s" % cmd} print(json.dumps(out, ensure_ascii=False)) return 0 if __name__ == "__main__": sys.exit(main())