IMPLFE-K6: DONE

# Lane FE K6 — admin thu gọn (S166, 2026-08-01)

SPEC PIN: spec-cụm2 K6 mục 1-4 + acceptance C1-C4 @`sub-invest-fable-b2-cum2.md:67-100`.
Phạm vi: `fe-admin/src/components/Layout.tsx` **ONLY**.

## §0 Kế hoạch đo trước khi gõ
- [ ] Đọc lại khối spec `:67-100` (12-key set viết sẵn — dùng NGUYÊN VĂN, không tự chế)
- [ ] Đếm THẬT cây menu seed từ `DbInitializer.cs` (không đoán): `AwV2%` là 2 hay 3?
- [ ] Xác định chính xác tập lá còn thấy sau lọc + LIỆT TÊN (C1)
- [ ] Kiểm `filterForAdmin` lọc theo cây (cha ẩn ⇒ con biến mất theo?) — quyết định set có cần liệt con hay không

## §1 ĐO ĐĨA (ground truth) — TRƯỚC khi gõ

### 1.1 🔴 GIẢI xong ẩn-số spec để mở C1: `AwV2_Contract` **KHÔNG TỒN TẠI**
Spec `:C1` treo: *"row `AwV2_Contract` có thể tồn tại DB ngoài seed… chốt N: 7 (nếu 2 AwV2) / 8 (nếu 3)"*.
Đo: `grep -rn "AwV2_Contract" src/Backend fe-admin/src fe-user/src` → **0 HIT toàn repo**. `MenuKeys.cs:115-117` chỉ có root `ApprovalWorkflowsV2` + **2** leaf (`AwV2_DuyetNcc`, `AwV2_DuyetNccPhuongAn`); `:127-134` thêm 8 `AwV2_KhkkN1..8`. Seed rows: `DbInitializer.cs:1981-1983` (root + 2) + `:1988-1995` (8 designer).
⚠️ Chú ý phân biệt: fe-admin `resolvePath:173` CÓ nhận code `'Contract'` → `/system/approval-workflows-v2/Contract`, nhưng đó là **bộ giải route chờ sẵn**, KHÔNG phải menu-row. Route-resolver có nhánh ≠ menu tồn tại. ⇒ **|AwV2 leaf| = 2 (seed) + 8 (designer)**.
⇒ **N = 1 (Dashboard) + 4 (System-leaf) + 2 + 8 = 15.** (Dư-lượng thành thật: nếu ai đó INSERT tay `AwV2_Contract` thẳng vào DB prod thì +1; tôi không có đường đo prod trong rào lượt này — khai ở FLAG-2.)

### 1.2 Cơ chế lọc ⇒ chỉ cần liệt ROOT
`filterForAdmin` lọc **ĐỆ QUY** mọi tầng (`.filter(...).map(n => ({...n, children: filterForAdmin(n.children)}))`) ⇒ ẩn 1 root là cả cây con chết theo. Tập 12 key chỉ liệt ROOT + 2 con của `System` (`Workflows`, `PeWorkflows` — parent = `System` nên KHÔNG chết theo root nào khác, phải liệt riêng). Liệt từng leaf vừa thừa vừa phải bảo trì lại mỗi lần BE thêm key.

### 1.3 `System` không phải lá
`System` có 6 con (`Users/Roles/Permissions/MenuVisibility/Workflows/PeWorkflows`) + `ApprovalWorkflowsV2` ⇒ render `MenuGroup`, **không tính vào số LÁ**. Tương tự `ApprovalWorkflowsV2` là group của 10 leaf.

## §2 THỰC THI — 1 file `fe-admin/src/components/Layout.tsx` (+52 / −19)

1. **`ADMIN_HIDDEN_KEYS`** = Set 12 key **ĐÚNG NGUYÊN VĂN spec** (PurchaseEvaluations · KeHoachKyKet · Contracts · HopDongCung · Forms · Reports · Master · Hrm · Off · Personal · Workflows · PeWorkflows); `isAdminHidden = ADMIN_HIDDEN_KEYS.has(key) || key.startsWith('Ct_')`. Giữ luật prefix `Ct_` (thừa so với ẩn root `Contracts`, nhưng giữ để nếu mở lại root thì 28 leaf không ùa về theo — khai tại chỗ).
2. **Vá-14 — viết lại TRỌN khối comment**: khai lineage **2 LẦN ĐẢO** (S29 ẩn → **S57 bỏ-ẩn** → **QĐ7 S164 ẩn LẠI rộng hơn**, "Admin thu về vai NƠI-CONFIG"), kèm câu chốt *"cả hai đều từng đúng, mốc sau thắng mốc trước — đừng khôi phục S57 vì tưởng gặp bug"*. Comment S57 cũ đã gỡ (nó đang nói ngược luật đang chạy).
3. **Display-only #82** khai trong comment: route + quyền admin còn nguyên, deep-link `/purchase-evaluations` vẫn 200; **thu gọn = bớt lối đi trên menu, KHÔNG phải thu hồi quyền**; muốn chặn thật phải làm ở BE `[Authorize(Policy=…)]`.
4. **Ghi chú móc nối K4a**: 42 leaf `Khkk_G{2..8}_*` nay bị cắt từ TẦNG CHA nên chuyện `resolvePath` fe-admin thiếu nhánh `Khkk_G*` hết là vấn đề (không còn "rơi im lặng" ở tầng leaf); kèm cảnh báo ngược: gỡ `KeHoachKyKet` khỏi Set thì PHẢI nới regex TRƯỚC + quyết `WfView` admin trỏ đâu.

## §3 CHỨNG

**C1 — đếm + LIỆT TÊN (dư/thiếu 1 = FAIL).** Dựng lại cây menu seed từ BE (72 tuple literal parse từ `DbInitializer` + 128 key sinh từ vòng lặp, hằng đọc thẳng `MenuKeys.cs`: `ContractTypeCodes`/`PurchaseEvaluationTypeCodes`/`KhkkGroupNumbers`/`KhkkLeafActions`) = **200 node**, rồi chạy **CHÍNH `ADMIN_HIDDEN_KEYS` + `isAdminHidden` + `filterForAdmin` đã ship** (cắt từ file bằng `indexOf`, chỉ bỏ annotation TS, có assert "còn annotation thì tự chết").
> **Đối chứng tính đúng của cây dựng lại:** 113 key trong `MenuKeys.All` → **0 key thiếu menu-row** (invariant T6). Cây dựng sai thì chỉ số này vỡ ngay.

**166 lá TRƯỚC lọc → 15 lá SAU lọc**, đúng tên từng mục:
```
 1 Dashboard        6 AwV2_DuyetNcc            9 AwV2_KhkkN2   12 AwV2_KhkkN5   15 AwV2_KhkkN8
 2 Users            7 AwV2_DuyetNccPhuongAn   10 AwV2_KhkkN3   13 AwV2_KhkkN6
 3 Roles            8 AwV2_KhkkN1             11 AwV2_KhkkN4   14 AwV2_KhkkN7
 4 Permissions
 5 MenuVisibility
```
16/16 assert OK: số = 15 · tập tên khớp CHÍNH XÁC (thừa=[] thiếu=[]) · AwV2 seed-leaf = 2 · 8 designer có mặt · 11 mục kiểm-ẩn đều biến mất (`Khkk_List`, `Khkk_G5_List`, `Pe_DuyetNcc_List`, `Ct_ThauPhu_List`, `Hdc_ThauPhu`, `Suppliers`, `CatalogUnits`, `Wf_ThauPhu`, `PeWf_DuyetNcc`, `Off_DanhBa`, `Hrm_HoSo`).
**CONTROL-ÂM:** bỏ `'KeHoachKyKet'` khỏi Set ⇒ **15 → 63 lá (+48)** — đúng 8 nhóm × 6 leaf, chứng phép đo phân biệt được chứ không phải luôn ra 15.

**C2 — fe-user KHÔNG đổi:** `git diff --stat -- fe-user/src` = **RỖNG (0 file)** ✅ (đo diff, không đo bundle-hash — đúng #69/l3-F12).
**C3** — mục "đếm 8 designer trong sidebar" thuộc acceptance K3, không đo ở đây; tuy vậy 8 `AwV2_KhkkN*` có mặt trong danh sách C1 ở trên nên coi như phụ-chứng.
**C4 — display-only:** 0 dòng route, 0 dòng `usePermission`/`PermissionGuard` trong diff (diff chỉ chạm `ADMIN_HIDDEN_KEYS`/`isAdminHidden` + comment) ⇒ deep-link admin tới route bị ẩn menu vẫn render và API vẫn 200; rào THẬT ở BE authz.

**Build:** `npx tsc -b` exit 0; `npm run build` fe-admin PASS **1961 modules**, `dist/assets/index-B0OrpamE.js` 1,726.07 kB (gzip 429.28) · `index-AgG72YRZ.css` 86.93 kB · 620ms. Warning >500KB + `INEFFECTIVE_DYNAMIC_IMPORT` = PRE-EXISTING.
**Rào:** `git diff --stat` = **đúng 1 file** `fe-admin/src/components/Layout.tsx` (52+/19−). 0 fe-user, 0 BE, **0 commit**.

## §4 FLAG cho lead

**FLAG-1 (MINOR — chốt số cho spec):** ẩn-số C1 đã giải: **N = 15** (không phải 7/8 như spec dự phòng — spec quên cộng 8 designer K3 vào công thức của chính nó, chỉ nhắc ở vế "SAU K3 = N+8"). Con số ăn khớp: 1 + 4 + 2 + 8 = 15.

**FLAG-2 (MINOR — dư-lượng thành thật):** tôi đo từ **SEED**, đúng cho DB dựng từ seeder. Nếu prod có menu-row được INSERT tay ngoài seeder (spec nghi `AwV2_Contract`) thì N thật = 15 + số row đó. Đo dứt điểm cần 1 câu `SELECT [Key] FROM MenuItems WHERE [Key] LIKE 'AwV2%'` trên prod — ngoài rào lượt này (không đụng BE/DB), nhờ lane có quyền chạy giúp nếu muốn đóng tuyệt đối.

**FLAG-3 (MINOR — quan sát, không tự sửa):** sau K6, `resolvePath` fe-admin còn **một loạt nhánh chết** (staticMap `Khkk_*`, `Hdc_*`, `Suppliers`, `Off_*`, `Hrm_*`… + regex `Pe_*`) — key không bao giờ tới nữa vì đã cắt ở tầng cha. Vô hại (chỉ là mã không chạy tới), và **cố ý không dọn** lượt này: dọn = xoá đúng thứ phải khôi phục nếu QĐ7 lại đảo lần 3, mà lịch sử file này đã đảo 2 lần. Nếu lead muốn dọn thì nói, tôi tách lượt riêng.

IMPLFE-K6-END

