IMPLFE-K4A: DONE

# Lane FE K4a — route regex 42 leaf + single-source nhãn nhóm (S166, 2026-08-01)

SPEC PIN: `spec-cum2-chi-tiet-31-07-2026.md` vá 1/8/15 + `sub-invest-fable-b2-cum2.md:37` (K4c-10 regex ship CÙNG K4a) + `:58` (acceptance-5 single-source) + `:94` (fe-admin KHÔNG nới regex).

## §0 Kế hoạch đo trước khi gõ (bài S164: đo đĩa TRƯỚC)
- [ ] Đọc route THẬT `fe-user/src/App.tsx` (grep KhkkListPage / KhkkCreatePage / WorkflowMatrixViewPage)
- [ ] Đọc query-param THẬT trong KhkkListPage (`filter` / `view`)
- [ ] Đọc khuôn Pe-regex trong `fe-user/src/components/Layout.tsx`
- [ ] Đọc nhãn nhóm gốc `DbInitializer.cs` dict `khkkGroupLabels` (Read-only, KHÔNG sửa)
- [ ] Đọc `types/khkk.ts` ×2 app (nền F-1 `7a903cf`)

## §1 ĐO ĐĨA (ground truth) — TRƯỚC khi gõ 1 dòng nào

### 1.1 Route THẬT (KHÔNG tin literal spec)
`fe-user/src/App.tsx` — 3 route KHỚP spec, 0 lệch:
- `:81 /khkk/list` → `KhkkListPage` · `:82 /khkk/create` → `KhkkCreatePage` · `:83 /khkk/workflow-matrix` → `WorkflowMatrixViewPage` (import `:21` từ `@/pages/pe/`)
- `fe-admin/src/App.tsx` `:107-110`: có `/khkk`, `/khkk/list`, `/khkk/create`, `/khkk/:id` — **KHÔNG có `/khkk/workflow-matrix`**, 0 import `WorkflowMatrixViewPage` ⇒ xác nhận spec `:94` (fe-admin cố ý lệch).

### 1.2 Query-param THẬT (`KhkkListPage.tsx`)
`:61 searchParams.get('filter')` → `FILTER_TO_PHASE` (`:40-46`, key hợp lệ: `DangSoanThao|ChoDuyet|DaDuyet|TraLai|TuChoi`) · `:62 searchParams.get('view') === 'deleted'`. ⇒ `filter=ChoDuyet` / `filter=DaDuyet` / `view=deleted` ĐÚNG từng ký tự.
🔴 `group` **CHƯA có consumer** ở KhkkListPage lẫn KhkkCreatePage (K4b/K5 lane) — leaf trỏ đúng URL nhưng trang chưa lọc. Không phải lỗi K4a, là bàn giao (ghi ở §4 FLAG-2).

### 1.3 Active-state (Layout `queryMatches` `:315-336`)
`TRANSIENT_QUERY_KEYS = {id,q,editHeader,page,phase,awId}` `:311` — **`group`/`filter`/`view` KHÔNG transient** ⇒ 6 URL/nhóm phân biệt được, không sáng nhầm (bài S155 `view=approved`). KhkkListPage KHÔNG ghi `filter` ngược lên URL (chỉ đọc lúc init) ⇒ highlight ổn định khi user đổi dropdown.

### 1.4 Bộ key BE (`MenuKeys.cs` — Read-only)
`KhkkGroupNumbers = [2..8]` `:72` · `KhkkLeafActions = [WfView,List,Create,Pending,Approved,Deleted]` `:73-74` · `KhkkLeaf(n,a) => Khkk_G{n}_{a}` `:77`.
🔴 **42 leaf infix = nhóm 2..8 (7×6)**. Nhóm 1 GIỮ 7 key CŨ **KHÔNG-infix** (`Khkk_G1` + `Khkk_WfView`…`Khkk_Deleted`) — đã nằm sẵn trong staticMap ×2 app. ⇒ regex `([1-8])` của spec **rộng hơn tập seed 1 nấc** (`Khkk_G1_List` không tồn tại) — giữ nguyên `[1-8]` theo spec, vô hại + phòng nếu sau này nhóm 1 đổi sang khuôn infix.

### 1.5 Nhãn nhóm BE (`DbInitializer.cs` — Read-only)
- `:1960` `Khkk_G1` = `"1. Thiết bị, máy móc, vật tư phụ (A1)"` (+ `labelBackfill :2235` cùng chuỗi)
- `:2127-2136` dict `khkkGroupLabels` nhóm 2..8:
  `2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)` · `3. Hoạt động văn phòng (A4)` · `4. Vật tư (B1)` · `5. Thầu phụ xây dựng (B2)` · `6. Thầu phụ MEP (B3)` · `7. BCH công trường (C1-C3)` · `8. HĐ chưa ủy quyền — CEO (5.2)`
- Workflow code: `^KHKK-N([1-8])$` — `ContractSigningPlanFeatures.cs:420` (regex BE thật) ⇒ `khkkGroupWorkflowCode` khớp.

### 1.6 🔴 TIỀN-ĐỀ LEAD SAI ×2 (đo ra, không suy)
**(a) `KHKK_GROUP_LABELS` — map nhãn ĐÃ TỒN TẠI.** `types/khkk.ts:129 KHKK_APPROVAL_GROUP_LABELS: Record<number,string>` (K2 S164 land), **5 call-site đang dùng**: `KhkkListPage:243` · `KhkkDetailPage:301` · `KhkkCreatePage:232,285`. Đẻ thêm map tên `KHKK_GROUP_LABELS` = **PHÁ acceptance-5** (thành 2 nguồn). Hơn nữa comment K2 `:125-127` đã uỷ quyền sẵn cho K4a: *"nếu K4a land nhãn khác thì SỬA Ở ĐÂY (1 const, 2 app), đừng vá rải trong page"*.
**Quan hệ đo được:** nhãn BE ≡ `` `${n}. ${KHKK_APPROVAL_GROUP_LABELS[n]}` `` cho **8/8 nhóm** (đối chiếu từng ký tự §1.5 ↔ `:130-137`). ⇒ giải bằng **DERIVE**, không hardcode lần 2.

**(b) `isAdminHidden` KHÔNG hề ẩn `KeHoachKyKet`.** `fe-admin/Layout.tsx:194-196` = `return key.startsWith('Ct_')` — chỉ ẩn `Ct_*`. ⇒ mệnh đề "gỡ `KeHoachKyKet` khỏi ẩn-admin ⇒ phải nới regex trước" **mô tả một điều kiện tương lai KHÔNG tồn tại**; ràng buộc thật đã **ĐANG hiệu lực NGAY** (fe-admin đang hiện KHKK). Comment phải viết theo đĩa, không theo tiền-đề.

## §2 THỰC THI

### F1 — `fe-user/src/components/Layout.tsx` (+34 / −0)
Nhánh `khkkMatch` đặt **SAU** khối `peMatch`, **TRƯỚC** `return null` cuối `resolvePath`. Regex ĐÚNG literal spec `/^Khkk_G([1-8])_(WfView|List|Create|Pending|Approved|Deleted)$/`. 6 route (đọc từ App.tsx thật, không tin literal spec):
`WfView → /khkk/workflow-matrix?type=10&group={n}` · `List → /khkk/list?group={n}` · `Create → /khkk/create?group={n}` · `Pending → …&filter=ChoDuyet` · `Approved → …&filter=DaDuyet` · `Deleted → …&view=deleted`.
+comment khai lệch fe-admin (không có `WorkflowMatrixViewPage`, lead chốt 2026-07-27) + khai `[1-8]` rộng hơn tập seed `[2-8]` là cố ý + khai `group` không transient nên active-state phân biệt được.

### F2 — `fe-admin/src/components/Layout.tsx` (+13 / −0, COMMENT-ONLY)
Ràng buộc ngược đặt ngay trên `isAdminHidden` `:194`. **KHÔNG nới regex** (diff 0 dòng xoá, 0 dòng code — chỉ comment). Nội dung viết theo ĐĨA chứ không theo tiền-đề lead (xem §1.6b).

### F3 — `types/khkk.ts` ×2 app (+25 / +25, SHA-pair)
KHÔNG đẻ `KHKK_GROUP_LABELS` (sẽ thành nguồn thứ 2 — xem §1.6a). Thay bằng 2 export DERIVE trên map sẵn có:
- `khkkGroupMenuLabel(n)` → `` `${n}. ${KHKK_APPROVAL_GROUP_LABELS[n]}` `` , fallback `Nhóm {n}`. Comment khai TO: **không phải nguồn sidebar** (sidebar đọc `/api/menus/me` → `MenuItems.Label`, admin còn override `displayLabel` Mig 27); dùng cho cây K5 / leaf-title / WfView.
- `khkkGroupWorkflowCode(n) => \`KHKK-N${n}\`` (O-2) — khớp regex BE `^KHKK-N([1-8])$` `Features.cs:420`.
0 call-site cũ bị đụng ⇒ 5 chỗ đang dùng `KHKK_APPROVAL_GROUP_LABELS` giữ nguyên hành vi, ô select `N{g} - {label}` không bị lặp số.

## §3 CHỨNG (đo bằng lệnh, kèm control-âm)

**A. Nhãn byte-exact 8/8** — script trích chuỗi từ `DbInitializer.cs` (`Khkk_G1` tuple `:1960` + dict `:2127-2136`) và từ `types/khkk.ts`, so `"{n}. "+short` với BE:
```
OK N1..N8  ->  --- byte-exact 8/8 khop, 0 lech
CONTROL(âm giả, thêm 'x' vào N4): OK-phep-do-bat-duoc-lech
```
N2 có "phá dỡ" ✅. Control-âm chứng phép so có RĂNG (không phải happy-path mù).

**B. Regex ăn đủ 42 leaf** — `new Function()` chạy CHÍNH khối text đã ship trong `Layout.tsx` (không chép tay), key sinh từ `KhkkGroupNumbers`/`KhkkLeafActions` đọc thẳng `MenuKeys.cs`:
```
BE nhom=[2,3,4,5,6,7,8] actions=[WfView,List,Create,Pending,Approved,Deleted]
--- 42 leaf, 0 tra null (drop im lang)
CONTROL Khkk_G9_List  -> null
CONTROL Khkk_G2_Bogus -> null
CONTROL Khkk_List     -> null  (đúng: nhóm 1 không-infix đi staticMap, chặn trước regex)
```

**C. SHA-pair types (acceptance-5)**
| file | sha256 |
|---|---|
| baseline trước K4a (cả 2 app) | `eba649cc6fc7eccfeec5262f2e4d58265dbf8caaac4d53c6bc7120fc8065976d` |
| `fe-user/src/types/khkk.ts` | `7a99b865189480933c57a46a9d9aac9be525d14a78e7743f3a82293b706f22b6` |
| `fe-admin/src/types/khkk.ts` | `7a99b865189480933c57a46a9d9aac9be525d14a78e7743f3a82293b706f22b6` |
→ **IDENTICAL**. Layout.tsx 2 app KHÁC NHAU by-design (fe-user +34 có regex · fe-admin +13 comment-only) — không mirror, đúng chủ đích.

**D. Build ×2 PASS, 0 TS error** (`npm run build` = `tsc -b && vite build`, `noUnusedLocals:true` ⇒ build LÀ phép thử TS6133 thật, không tin IDE — gotcha #68)
| app | modules | bundle js | css | time |
|---|---|---|---|---|
| fe-user | 1950 | `index-B2v0kn5F.js` 1,640.21 kB (gzip 414.27) | `index-my6T4K7V.css` 92.62 kB | 20.75s |
| fe-admin | 1961 | `index-84zDBHE-.js` 1,723.20 kB (gzip 428.44) | `index-Ct6-3cYU.css` 86.80 kB | 8.17s |
Warning >500KB + `INEFFECTIVE_DYNAMIC_IMPORT realtime.ts` = PRE-EXISTING cả 2 app.

**E. Rào** — `git status` sau lượt: 4 file của tôi. `MenuKeys.cs` + `DbInitializer.cs` M nhưng là **lane BE land trước** (tôi chỉ Read/Grep, 0 Edit). `KhkkListPage.tsx` · `KhkkCreatePage.tsx` · `KhkkWorkflowPanel.tsx` · `KhkkDetailPage.tsx` **KHÔNG có trong diff**. `KhkkGroupMenuSeedTests.cs` untracked = lane test. **0 commit.**

## §4 FLAG cho lead (3 — cần quyết, KHÔNG tự quyết)

**FLAG-1 (MAJOR, đã tự bẻ — báo để lead soát lại spec):** đề bài yêu cầu export `KHKK_GROUP_LABELS` mới, nhưng đĩa đã có `KHKK_APPROVAL_GROUP_LABELS` (`types/khkk.ts:129`, K2 land, 5 call-site). Đẻ map thứ 2 là **phá đúng acceptance-5 mà nó phục vụ**. K2 còn để sẵn uỷ quyền trong comment `:125-127` ("nếu K4a land nhãn khác thì SỬA Ở ĐÂY (1 const, 2 app)"). Tôi đi đường DERIVE. **Nếu lead vẫn muốn đúng tên `KHKK_GROUP_LABELS`** thì đó là đổi TÊN của const sẵn có (+ sửa 5 import), không phải thêm const — 1 lượt nhỏ, nói là làm.

**FLAG-2 (MAJOR, bàn giao K4b/K5):** `?group=n` **chưa có ai đọc**. `KhkkListPage:61-62` chỉ đọc `filter` + `view`; `KhkkCreatePage` lấy nhóm từ state default, không đọc URL. ⇒ 42 leaf điều hướng đúng + sáng đúng, nhưng trang **hiện toàn bộ nhóm**. Câu hỏi treo cho K4b: **leaf "Danh sách" của NHÓM 1 (`Khkk_List` → `/khkk/list`, KHÔNG có `group=`) nghĩa là "nhóm 1" hay "tất cả"?** Nếu K4b coi vắng-`group` = nhóm 1 thì mất đường xem-tất-cả; nếu coi = tất cả thì leaf nhóm 1 nói dối tên nó. Đây là quyết định UX ⇒ tôi KHÔNG tự chọn, và cố ý KHÔNG sửa 6 dòng staticMap nhóm 1.

**FLAG-3 (MINOR, hệ quả đã biết của vá-15/`:94`):** `isAdminHidden` chỉ ẩn `Ct_*` ⇒ fe-admin ĐANG hiện root KHKK. Sau khi BE seed land, sidebar admin sẽ có **7 nhóm rỗng** (`Khkk_G2..G8` render header nhưng 42 leaf con trả null → thân rỗng), vì `MenuGroup` chỉ xét `children.length > 0` chứ không xét con có resolve được không. Nhóm 1 vẫn đủ 6 mục. Nếu lead thấy 7 nhóm rỗng là nhiễu thì có 2 đường (đều 1 lượt nhỏ): (a) nới regex admin, `WfView` trỏ Designer `AwV2_KhkkN{n}`; (b) thêm `Khkk_G` vào `isAdminHidden`. **Không tự chọn.**

IMPLFE-K4A-END

