Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum2.md
2026-07-31 13:21:20 +07:00

17 KiB
Raw Blame History

SUB-INVEST /fable-real — B2 CỤM-2: SPEC CHI TIẾT K4 + K5 + K6

Run 2026-07-31-S164-4gd-khkk-fanout · agent investigator-codebase (Fable) · luật ĐAN XEN: đọc đủ 1 section → viết ngay section đó.

K4 Sidebar 8 nhóm × 6 leaf

K4a — 49 row menu + MenuKeys.All + grant 3-site (0 phụ thuộc — chạy song song cụm-1)

  1. MenuKeys.cs — 49 key VÀO MenuKeys.All (vá-1 cụm-1, 3-lane hội tụ): 7 group Khkk_G2..G8 + 42 leaf Khkk_G{n}_{WfView|List|Create|Pending|Approved|Deleted} (n=2..8). Đề xuất factory KhkkGroupKey(n)/KhkkLeafKey(n,action) mirror khuôn ContractTypeGroup. G1 GIỮ NGUYÊN 7 key cũ không-infix (Khkk_G1 + Khkk_WfView… — seed DbInitializer.cs:1780-1787): đổi key = mồ côi permission-row (S155, 13 row/key). ⚠️ Khai precedent-split để reviewer khỏi tưởng mâu thuẫn: Ct_*/Pe_* leaf CỐ Ý ngoài All (DbInitializer.cs:2193-2195 "All… KHÔNG chứa Pe_* leaf") ⟂ catalog/AwV2 leaf TRONG All — cụm-2 đi nhánh TRONG-All theo vá-1 (ít site mirror hơn: Admin tự grant). Policy sinh thêm 49×4 registered-unused = vô hại, endpoint vẫn chỉ dùng KeHoachKyKet.* (#85).
  2. Seed menu-row: thêm loop G2..G8 cạnh :1780-1787, khuôn PE-loop :1896-1913 — group (parent KeHoachKyKet, Order n, icon FileCheck) + 6 leaf y nhãn/icon/Order G1 (:1782-1787: Network/List/Plus/CheckCircle2/CheckCheck/Trash2, Order 1-6). Upsert :1926-1943: key mới đi nhánh INSERT; key cũ chỉ re-set Order.
  3. labelBackfill G1 (dict :1953-1995, apply :1997-2005 — sửa chữ trong tree.Add VÔ TÁC DỤNG DB đang chạy, comment :1959-1961): Khkk_G1 "1. Kế hoạch ký kết HĐ (NCC-TP)" → nhãn nhóm 1. Bảng nhãn 8 nhóm (đề xuất từ danh-muc-sp002-transcribe.md:5-16; owner chỉnh CHỮ được, KEY không đổi):
    Key Nhãn đề xuất
    Khkk_G1 (labelBackfill) 1. Thiết bị, máy móc, vật tư phụ (A1)
    Khkk_G2 2. Kết cấu, hạ tầng & DV hỗ trợ thi công (A2+A3)
    Khkk_G3 3. Hoạt động văn phòng (A4)
    Khkk_G4 4. Vật tư (B1)
    Khkk_G5 5. Thầu phụ xây dựng (B2)
    Khkk_G6 6. Thầu phụ MEP (B3)
    Khkk_G7 7. BCH công trường (C1-C3)
    Khkk_G8 8. HĐ chưa ủy quyền (CEO)
  4. Grant 3-site — QUYẾT: KHÔNG retire site nào, EXTEND cả 3. Evidence:
    • Site-1 SeedAdminPermissionsAsync loop MenuKeys.All :2058 chỉ Admin-role (:2049-2050) → 49 key vào All ⇒ Admin TỰ có row, 0 code thêm.
    • Site-2 KhkkKeys() :2185-2189 (feed reviewKeys :2216 → CanRead MỌI role, skip-existing): extend += 7 group + 42 leaf. KHÔNG retire — group-key CẦN CanRead row để render mà site-3 CỐ Ý không cấp group (comment :2418-2419 "Khkk_G1 = group điều hướng, CanRead-all đã có từ nhánh read-only").
    • Site-3 SeedKeHoachKyKetAccessAsync :2415-2472 (UPGRADE-if-exists, R+C+U 13/13 role, Delete=Drafter+Admin :2437): keys :2420-2425 extend += 42 leaf (giữ convention không cấp cờ ghi cho group). KHÔNG retire — All-loop chỉ Admin; reviewKeys chỉ CanRead; cờ GHI chỉ site-3 phát.
  5. 🔴 Sửa grounding đề bài (evidence ngược): bảng kỳ-vọng 13 role KHÔNG phải "7 vai có grant + 6 vai 0-leaf". SeedKeHoachKyKetAccessAsync loop roleManager.Roles :2427 = 13/13 role R+C+U (O-A "hiển thị hết để góp ý" :2408-2410). "7 vai" là seeder PE :2669-2675 — không áp KHKK. Bảng kỳ-vọng đúng: 13/13 thấy đủ 48 leaf; cột Delete ✓ chỉ Drafter+Admin.
  6. Drift khai cùng commit (bài cụm-1): STATUS 2 row — Menu keys 64→113 (55→64 đã khai cụm-1) · Policies 256→452 (+49×4). MenuItems DB +49 (Khkk% tổng 56 row). Route-map ×2 app khai trong PR (K4c).

K4b — 4 leaf list nhận ?group= (CHỜ K2: param group BE ContractSigningPlanFeatures.cs:622-625 + DTO approvalGroup)

  1. KhkkListPage.tsx ×2 app (mirror SHA — header tự khai :3-4): đọc searchParams.get('group') cạnh filter/view :60-61 → truyền group vào params API :75-82, CẢ nhánh /deleted :74. Chip lọc giữ state-only :11-12 (không ghi URL — bug UAT S155); group nằm trong URL leaf nên NavLink active-logic ăn tự nhiên.
  2. 6 URL leaf G1 CŨ phải mang group=1 (phiếu cũ backfill ApprovalGroup=1 — QĐ6 spec-tổng :16): sửa staticMap fe-user Layout.tsx:59-62 + :82-83 (List /khkk/list?group=1 · Pending ?group=1&filter=ChoDuyet · Approved ?group=1&filter=DaDuyet · Deleted ?group=1&view=deleted · Create /khkk/create?group=1 · WfView ?type=10&group=1). Root KeHoachKyKet:79 GIỮ /khkk/list không-group = xem tất. Query-string là DANH TÍNH leaf (active-check :341-347 + bài TRANSIENT :318-331) → group phân biệt G1..G8 sạch.
  3. Nếu thiếu K2-param mà ship K4b: leaf G2..G8 List hiển thị TẤT phiếu (silent-wrong, không lỗi build) → acceptance mục 2 bắt buộc đo SAU K2.

K4c — regex staticMap + WfView/Create (CHỜ K3: 8 Code KHKK-N1..N8)

  1. fe-user Layout.tsx: regex MỚI sau staticMap[key] :131, khuôn Pe-regex :154: /^Khkk_G([1-8])_(WfView|List|Create|Pending|Approved|Deleted)$/ → List /khkk/list?group=n · Pending +&filter=ChoDuyet · Approved +&filter=DaDuyet · Deleted +&view=deleted · Create /khkk/create?group=n · WfView /khkk/workflow-matrix?type=10&group=n. (G1 dùng key cũ nên đi staticMap mục 8; regex [1-8] phòng key G1-infix tương lai, không hại.)
  2. WfView lọc theo nhóm (vá l2-F3 B1b): WorkflowMatrixViewPage hiện chỉ lọc type (:52-71) → đọc group → chỉ hiện workflow Code === 'KHKK-N'+n (convention Code do K3 seed; map group↔Code = 1 chỗ export const dùng chung).
  3. Create preset: KhkkCreatePage ×2 app đọc group → preselect picker nhóm (picker + body approvalGroup đã là việc vá-4 cụm-1; đây CHỈ preset giá trị).
  4. fe-admin mirror — ĐÃ CHỐT bằng K6-read (không còn treo): fe-admin Layout.tsx Pe-regex :134 CỐ Ý không WfView (:129-133 thiếu WorkflowMatrixViewPage toàn cục — lead chốt 2026-07-27 giữ lệch) VÀ K6 ẩn root KeHoachKyKet khỏi sidebar admin ⇒ fe-admin KHÔNG nới regex Khkk_G{n}_* (dead-by-K6). Chỉ fe-user Layout đổi ở K4b/c; KhkkListPage/KhkkCreatePage fe-admin VẪN sửa (mirror SHA — sống qua deep-link). Ghi comment khai lệch tại regex fe-user.

Acceptance K4 (đo được, UI-first — bài cụm-1)

  • A1. Login Admin ×2 app: root GĐ2 → 8 group × 6 leaf = 48 leaf HIỆN sidebar (đếm từ UI, không chỉ curl).
  • A2. Login vai thường (nv.test Drafter): CŨNG 48 leaf (13/13 grant — mục 5); bấm TỪNG LOẠI leaf ở nhóm ≠1: List/Pending/Approved/Deleted lọc đúng group+phase; Create preset đúng nhóm; WfView chỉ matrix KHKK-N{n}.
  • A3. Bảng 13 role × {R,C,U,D} khớp seeder (R/C/U ✓ 13/13 · D ✓ Drafter+Admin): PermissionsPage matrix + 1 curl DELETE vai thường → 403 (2-tầng #82).
  • A4. Key cũ 0 mồ côi: SELECT DISTINCT MenuKey FROM Permissions WHERE MenuKey LIKE 'Khkk%'MenuItems.Key; 7 key G1 cũ nguyên row.
  • A5. Restart API ×2 lần: MenuItems Khkk% = 56 row đúng (7 cũ + 49 mới), không nhân; labelBackfill đổi đúng 1 nhãn G1.
  • A6. Drift 2 row STATUS (Menu keys 113 · Policies 452) cùng commit + route-map ×2 app khai PR.

K5 Cây sub-folder

QUYẾT khuôn: thêm groups?: PipelineGroup[] mang leaves[] THẬT — KHÔNG đi đường content. Evidence bẫy: stageCount (PipelineStageFolders.tsx:74-76) — nhánh content bắt truyền count tay (:68-69 "Bắt buộc khi dùng content — component không đếm hộ được") còn nhánh leaves tự đếm; content còn mất luôn hideEmpty/failed/leaf-styling. groups với leaves thật ⇒ count DERIVED, bất biến cấu trúc stage.leaves == concat(groups[].leaves) ⇒ Σ đúng by-construction.

  1. Type (PipelineStageFolders.tsx vùng :58-72): export type PipelineGroup = { key: string; label: string; leaves: PipelineLeaf[] } + PipelineStage.groups?: PipelineGroup[]. GIỮ stage.leaves = flat tổng (nguồn stageCount:74-76 KHÔNG đổi — badge folder cha đếm như cũ).
  2. Render: trong khối ml-3 border-l (:156), nếu stage.groups?.length → mỗi group 1 sub-<details> (chevron + nhãn + chip đếm g.leaves.length, tái dùng token STAGE_STYLE:39-44), ruột = đúng <ul> leaf hiện tại (:169-202) tách thành component nội bộ LeafList dùng chung 2 nhánh. Group RỖNG không render (cây per-gói-thầu — hiện đủ 8 sub-folder rỗng là noise; nhãn/thứ tự khớp sidebar KHI hiện). Không đụng hideEmpty:228.
  3. Builder GĐ2 (usePipelineStages.ts:189-206): group khkkLeaves theo k.approvalGroup (DTO field K2 — cụm-1 K2 chủ), sort n tăng dần; approvalGroup null/lạ → group fallback "(chưa phân nhóm)" HIỆN CUỐI, KHÔNG rơi (chống rơi-im-lặng NULL-group; sau backfill=1 fallback đóng vai canary lệch backfill).
  4. Builder GĐ3/GĐ4 (:211-243): group theo c.type 7 loại (c.type đã dùng :240). 🔴 GĐ3 leaf "ngoài chỉ mục 200" (:222-224 không có c ⇒ KHÔNG biết type) → vào fallback "(ngoài chỉ mục)" — đừng ép vào loại. GĐ4 luôn có c (:231 cần c?.hasSealedCopy) ⇒ đủ 7-loại sạch. Nhãn loại tái dùng map sẵn trong types/contracts (đối chiếu khi build; thiếu thì thêm map 7 nhãn "01.-07." khớp label menu Ct_*DbInitializer.cs:1988-1994).
  5. Single-source nhãn nhóm + Code (dùng chung K4c/K5): types/khkk.ts ×2 app export KHKK_GROUP_LABELS: Record<1|..|8, string> (đúng bảng nhãn K4a-3) + khkkGroupWorkflowCode(n) = 'KHKK-N'+n. Sidebar + cây + WfView filter cùng 1 nguồn — không drift chữ.
  6. Mirror ×2 app: 2 file đích danh SHA-identical (usePipelineStages.ts header :3 + PipelineStageFolders.tsx header :10 tự khai luật mirror) + types/khkk.ts. ⚠️ Bộ pipeline có file thứ 3 PipelineTreePanel.tsx (l3-F19 gọi tên nó): K5 dự kiến KHÔNG đụng — verify bằng git diff; NẾU đụng thì SHA-compare luôn.

Acceptance K5

  • B1. Cây trên trang GĐ2/GĐ3 hiện sub-folder đúng nhóm/loại, nhãn + thứ tự khớp sidebar (cùng KHKK_GROUP_LABELS).
  • B2. certutil -hashfile (hoặc Get-FileHash) từng cặp file ×2 app: usePipelineStages.ts · PipelineStageFolders.tsx · types/khkk.ts — SHA256 identical; PipelineTreePanel.tsx không đổi (git diff) hoặc SHA-compare nếu đổi.
  • B3. npm run build ×2 app PASS.
  • B4. Đếm: badge folder cha == Σ chip sub-folder (kể cả "(chưa phân nhóm)"/"(ngoài chỉ mục)") — thử 1 gói thầu có phiếu 2 nhóm + 1 HĐ ngoài index; KHÔNG leaf nào biến mất so với trước K5.

K6 Admin thu gọn

Hiện trạng: isAdminHidden fe-admin Layout.tsx:182-184 = đúng 1 luật key.startsWith('Ct_'); filterForAdmin:186-190 lọc ĐỆ QUY mọi tầng ⇒ ẩn 1 root = cả subtree chết theo — tập key chỉ cần liệt ROOT (+2 con của System), không phải từng leaf.

  1. Mở rộng tập key tường minh (QĐ7 — GIỮ blacklist, KHÔNG đảo whitelist):
    const ADMIN_HIDDEN_KEYS = new Set<string>([
      'PurchaseEvaluations',  // GĐ1 root (:1797) — Pe_* subtree chết theo
      'KeHoachKyKet',         // GĐ2 root (:1775) — Khkk_* 8 nhóm chết theo
      'Contracts',            // GĐ3 root (:1771) — Ct_* đã có prefix-rule, root nay ẩn nốt
      'HopDongCung',          // GĐ4 root (:1776) — Hdc_* chết theo
      'Forms',                // :1788
      'Reports',              // :1789
      'Master',               // Danh mục root (:1760) — Suppliers/Projects/Departments/Catalogs/HrmConfig(:1816 parent Master) chết theo
      'Hrm',                  // :1809
      'Off',                  // :1827
      'Personal',             // :1853
      'Workflows',            // :1795 — CON của System, phải liệt riêng (Wf_* chết theo)
      'PeWorkflows',          // :1798 — CON của System (PeWf_* chết theo)
    ])
    function isAdminHidden(key: string): boolean {
      return ADMIN_HIDDEN_KEYS.has(key) || key.startsWith('Ct_')
    }
    
    CHỪA (không vào set): Dashboard (QĐ7 giữ) · System + 4 leaf quản trị Users/Roles/Permissions/MenuVisibility (:1791-1794) · ApprovalWorkflowsV2 (:1801) + subtree AwV2 (2 leaf seed :1802-1803 + 8 designer K3).
  2. Sửa comment ĐỦ KHỐI :177-184: khối cũ khai "[S57] BỎ ẩn Danh mục — admin quản master trực tiếp" (:179-181) — QĐ7 ĐẢO lời đó ⇒ comment phải viết lại (khai đảo + trỏ spec S164), kẻo lượt sau đọc comment cũ tưởng bug (bài cụm-1 "đọc comment đủ khối").
  3. Display-only, KHÔNG đụng quyền (2 tầng #82): route fe-admin còn nguyên — admin deep-link /purchase-evaluations… vẫn 200. Thu gọn = menu, không phải revoke; khai rõ trong PR để không ai tưởng mất quyền.
  4. Hệ quả cho K4c (chốt nghi vấn WfView): fe-admin Pe-regex :134 CỐ Ý không WfView (:129-133 — fe-admin KHÔNG có WorkflowMatrixViewPage, lead chốt 2026-07-27 "giữ lệch, đừng vá") + K6 ẩn KeHoachKyKet root ⇒ fe-admin Layout KHÔNG cần nới regex Khkk_G{n}_* (leaf không bao giờ render admin-side). Ghi 1 comment tại chỗ regex fe-user để khai lệch có chủ đích.

Acceptance K6

  • C1. Liệt kê đúng N mục LÁ còn thấy (đủ tên — dư/thiếu 1 = FAIL). Công thức: N = Dashboard + Users + Roles + Permissions + MenuVisibility + |leaf AwV2 trong DB|. Seed đọc được 2 leaf AwV2 (:1802-1803); ⚠️ uncertain: row AwV2_Contract có thể tồn tại DB ngoài seed (regex :166-172 nhận code Contract) — đo trước bằng SELECT [Key] FROM MenuItems WHERE [Key] LIKE 'AwV2%' rồi chốt N: 7 (nếu 2 AwV2) / 8 (nếu 3); SAU K3 = N+8 designer KHKK-N1..N8.
  • C2. fe-user KHÔNG đổi: git diff --stat <base>..<head> -- fe-user/src = 0 file (đo diff, KHÔNG đo bundle-hash — #69/l3-F12).
  • C3. l3-F9: K6 kéo sớm trước K3 ⇒ mục "đếm được 8 designer trong sidebar admin" DỜI sang acceptance K3 (không đo ở K6).
  • C4. Deep-link admin 1 route bị ẩn menu (vd /purchase-evaluations) vẫn 200 — chứng display-only.

Thứ tự + phụ thuộc

Vẽ lại (trả lời câu "K4a chạy được sau K1 hay cần K2 DTO?"): K4a KHÔNG cần cả K1 lẫn K2 — menu-row + grant + MenuKeys.All thuần DbInitializer/MenuKeys.cs, 0 đụng schema/DTO. Nhưng ship K4a MỘT MÌNH thì 42 leaf trỏ ?group=n mà BE chưa hiểu param ⇒ list hiện TẤT phiếu (silent-wrong, mục K4b-9) — leaf RENDER được, LỌC chưa đúng.

Cụm-1:   K1 ──► K2 ──► K3 (K3 đang treo OG-9)
                │        │
Cụm-2:   K6 ───┐│        │   (độc lập 100% — kéo SỚM NHẤT: còn rút việc fe-admin
         K4a ──┼┘        │    khỏi K4c [K6-4]; đổi lại C3/l3-F9: đếm-8-designer dời sau K3)
         K4b ◄─┘ (cần K2: param group :622-625 + DTO approvalGroup + backfill=1)
         K5  ◄── (cần K2: approvalGroup trên KhkkListItemDto; GĐ3/4 group-by c.type
                  tự do nhưng CÙNG FILE — làm 1 lượt sau K2 cho đỡ 2 lần mirror-SHA)
         K4c ◄── (cần K3: 8 Code KHKK-N1..N8 cho WfView filter + preset Create)

Thứ tự ship đề xuất: K6 → K4a → [K2 land] → K4b + K5 → [K3 land] → K4c.

  • K6 + K4a chạy song song cụm-1 ngay hôm nay (0 chặn).
  • K4a land trước K2 = chấp nhận cửa sổ leaf-lọc-chưa-đúng trên Dev; nếu sợ UAT hiểu nhầm bug thì gộp K4a ship cùng K4b sau K2 (đo acceptance A2 luôn thể). Acceptance A1/A3-A6 đo được NGAY sau K4a; A2 BẮT BUỘC sau K2.
  • K4c KHÔNG ship-trước-degrade: filter WfView theo Code KHKK-N{n} khi chưa seed 8 workflow ⇒ matrix RỖNG (tệ hơn không filter) — đợi K3.

Checklist tổng cụm-2

  • 0 EF migration toàn cụm-2 — menu = seed DB + restart API (bài S155); có ai đề mig là sai khuôn.
  • K4: 13 việc (K4a 1-6 · K4b 7-9 · K4c 10-13) + acceptance A1-A6 (A2 đo sau K2).
  • K5: 6 việc + acceptance B1-B4 (SHA-identical 3 file ×2 app · build ×2 PASS · Σ sub-folder == folder cha).
  • K6: 4 việc + acceptance C1-C4 (liệt đúng N lá kèm tên · fe-user git-diff 0 file · 8-designer dời sau K3).
  • Drift khai CÙNG COMMIT K4a: STATUS 2 row Menu keys 64→113 · Policies 256→452 (nối tiếp 55→64 cụm-1) — đừng Goodhart giữ-số-đứng-yên.
  • Test: +1 BE (extend test seed-grant cụm-1 phủ 49 key mới — khai k=1, tổng ≥590+k' cụm-1 +1); còn lại là acceptance UI/SQL đo tay (14 mục A/B/C).
  • Mọi wave: cicd PASS · wave FE-ship byte-verify #77 (K4b/c, K5, K6) · wave BE-only smoke API (K4a) — l3-F17.
  • Nhãn 8 nhóm (K4a-3) = đề xuất từ transcribe — owner chỉnh CHỮ tự do, KEY không đổi; không chặn wave nào (khác OG-6 vốn chỉ chặn K1-seed).

END sub-invest-fable-b2-cum2 — TOTAL: K4 13v · K5 6v · K6 4v · 1 test