IMPLFE-K4B: DONE

# Lane FE K4b — nối `?group=` + vá-3/5/6 (S166, 2026-08-01)

Nối finding-A của gate K4a ("`?group=` chết-lâm-sàng" = FLAG-2 lượt trước: leaf trỏ đúng URL nhưng KHÔNG ai đọc).
SPEC PIN: spec-cụm2 vá 3 (queryKey 2 khoá) · vá 5 (`phase` derive thay mount-only) · vá 6 (Create preset từ URL) + K4b-7 (URL G1 mang `group=1`).

## §0 Kế hoạch đo trước khi gõ (khuôn S164/S166)
- [ ] Đọc `KhkkListPage.tsx` TRỌN vùng state/query ×2 app — có mirror không (SHA)
- [ ] Đọc `KhkkCreatePage.tsx` vùng state `approvalGroup` ×2 app
- [ ] Grep staticMap 6 leaf G1 ở fe-admin (đề bài dặn: có thì mirror, không thì khai)
- [ ] Xác nhận BE nhận `?group=` (K2 wire `[FromQuery(Name="group")]`) — Read-only

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

### 1.1 Baseline SHA (cả 2 page ĐANG mirror ×2 app ⇒ được phép `cp`)
| file | sha256 trước K4b |
|---|---|
| `KhkkListPage.tsx` ×2 | `80ebd79dba1356e8f1512f798ab105eca312d84aae2744b7ed852ff667f60f3b` |
| `KhkkCreatePage.tsx` ×2 | `677f4ab803b0b7ac73ede9b2d5871a3243f4cbe90153a218b320d642c6431268` |

### 1.2 🔴 TIỀN-ĐỀ ĐỀ-BÀI CẦN BẺ: vá-5 "derive thay useState" nguyên văn sẽ **GIẾT bộ lọc chip**
`phase` KHÔNG chỉ đến từ URL — nó còn là state do người dùng bấm: `KpiCard onClick :153-156 setPhase(c.value)` (6 chip Tất cả/Soạn/Chờ/Đã/Trả lại/Từ chối). Comment `:11-12` khai rõ chủ đích: *"Chip lọc chỉ đổi STATE, KHÔNG ghi lại URL — giữ nguyên URL của leaf đang đứng nên menu không sáng nhầm"* (bài S155).
⇒ Nếu làm `const phase = urlPhase` thuần thì `setPhase` mất chỗ đứng, **6 chip thành nút chết** — hỏng CÂM, build vẫn xanh.
⇒ Giải đúng = **state override + reset theo danh tính URL** (khuôn React "adjust state during render", KHÔNG `useEffect`, không nháy 2 pass):
`chipPhase: KhkkPhaseValue|null|undefined` — `undefined` = "chưa bấm chip, dùng URL"; `null` = "user chủ động chọn Tất cả" (2 nghĩa khác nhau, không gộp được). URL đổi ⇒ `chipPhase=undefined` + `page=1`.

### 1.3 🔴 BE `/deleted` KHÔNG nhận `group` (rào: không sửa BE)
`ContractSigningPlansController.cs:59-66 ListDeleted(page, pageSize, search, sortDesc)` — **0 tham số group**, khác hẳn `List :42 [FromQuery(Name="group")] int? group`.
⇒ `Khkk_G{n}_Deleted → /khkk/list?group=n&view=deleted` sẽ hiện phiếu xóa CỦA MỌI NHÓM. Không client-side lọc được vì `total`/phân trang do server đếm trên tập chưa lọc ⇒ lọc ở FE sẽ ra số trang nói dối. ⇒ Xử: **gửi `group` chỉ ở nhánh không-xóa** + **không để tiêu đề CLAIM nhóm ở màn đã-xóa** (không nói dối) + FLAG lane BE. Xem FLAG-1.

### 1.4 fe-admin CÓ 6 leaf G1 trong staticMap, nhưng `Khkk_WfView` ĐI ĐƯỜNG KHÁC
`fe-admin/Layout.tsx:35` `Khkk_WfView → /system/approval-workflows-v2/ContractSigningPlan` (Designer), 5 leaf còn lại `:36-40` trỏ `/khkk/*` như fe-user.
⇒ mirror `?group=1` cho **5 leaf**; `Khkk_WfView` admin **KHÔNG thêm** `group` (Designer không đọc tham số này — chính ghi chú K4a đã khai). Khai lệch, không mirror mù.

### 1.5 Xác nhận đường BE cho nhánh chính
`List :42` `[FromQuery(Name="group")] int? group` → `ListContractSigningPlansQuery(..., group)` `:49`. ⇒ FE gửi `params.group` là ăn.

## §2 THỰC THI (6 file — 3 cặp mirror)

### F1 — `KhkkListPage.tsx` ×2 (+89 / −18 mỗi app, SHA-pair)
- **(a) vá-5:** `urlFilter`/`urlPhase`/`deletedView`/`group` đọc THẲNG từ `searchParams` mỗi render. Bộ lọc chip chuyển sang lớp override `chipPhase: KhkkPhaseValue|null|undefined` (3 trạng thái, `undefined` ≠ `null` — xem §1.2), `phase = chipPhase !== undefined ? chipPhase : urlPhase`. Reset `chipPhase`+`page` khi `navKey` đổi, **điều chỉnh state trong render** (khuôn React chính thống) chứ không `useEffect` — `useEffect` sẽ vẽ 1 lượt bằng dữ liệu cũ rồi mới sửa (nháy).
- **(b)** `group` = `?group=` lọc `^[1-8]$`, ngoài dải/vắng ⇒ `null` = xem tất cả nhóm. Gửi `params.group` (chỉ nhánh không-xóa, §1.3).
- **(c) vá-3:** `queryKey: ['khkk-list', { group, phase, search, page, deletedView }]`.
- **(d)** `group !== null` ⇒ title `… — N{group}` + subtitle `khkkGroupMenuLabel(group)` (helper K4a). Nút "Lập kế hoạch" mang `?group=` sang màn tạo (bắt tay vá-6).
- Header file cập nhật bảng URL 6 leaf + khai lý do "8 nhóm chung 1 route ⇒ phải đọc lại mỗi render".

### F2 — `KhkkCreatePage.tsx` ×2 (+24 / −2 mỗi app, SHA-pair)
vá-6: `presetGroup` từ `?group=` (lọc `^[1-8]$`) làm giá trị khởi tạo `approvalGroup`; +đồng bộ lại khi THAM SỐ URL đổi (cùng khuôn F1 — 8 leaf "Thao tác" cũng chung 1 route). 🔴 So với `lastPresetGroup` chứ KHÔNG so với `approvalGroup`: so với `approvalGroup` thì user tự đổi nhóm sẽ bị kéo ngược về nhóm của URL, ô select thành bấm-không-được. `firstFreeGroup`/`selectPe` giữ nguyên (URL là gợi ý, không phải khoá — PE đã dùng nhóm đó vẫn tự nhảy sang nhóm trống).

### F3 — `Layout.tsx` staticMap (KHÔNG mirror, cố ý)
- fe-user: **6/6** leaf G1 mang `group=1` (`Khkk_WfView → /khkk/workflow-matrix?type=10&group=1`).
- fe-admin: **5/6** — `Khkk_List/Create/Pending/Approved/Deleted` mang `group=1`; `Khkk_WfView` **KHÔNG** vì bên admin nó trỏ Designer `/system/approval-workflows-v2/ContractSigningPlan`, không phải trang danh sách (§1.4). Khai trong comment tại chỗ.

## §3 CHỨNG (mỗi phép đo kèm CONTROL-ÂM)

**A. Acceptance "leaf→leaf đổi kết quả không F5"** — mô phỏng 1 instance KHÔNG remount, chạy **CHÍNH đoạn text đã ship** (`indexOf('const navKey')` → slice → `new Function`), không chép tay:
```
R1  ?group=2                   phase=null        (mọi trạng thái)
    [user bấm chip "Đã duyệt"]
R2  ?group=2 (URL giữ nguyên)  phase=3           ⇒ CHIP CÒN SỐNG (derive không giết nó)
R3  ?group=5&filter=ChoDuyet   phase=2 page=1    ⇒ RE-LỌC NGAY, không F5
CONTROL-ÂM (gỡ khối reset ra khỏi chính đoạn trên): phase=3 ⇒ kết dính bộ lọc cũ
   = ĐÚNG căn bệnh vá-5 ⇒ phép đo CÓ RĂNG
```
🔴 Bẫy harness đã sập rồi bò ra, ghi lại vì dễ tái phạm: lượt chạy đầu ra `phase=3` ở R3 và tôi suýt đọc thành PASS-nhầm-hướng. Nguyên nhân: `setState` gọi TRONG lúc render thì React **vứt bỏ render đó và render lại ngay với state mới trước khi commit**; harness bản đầu chỉ đo cái render BỊ VỨT. Sửa harness thành vòng lặp tới khi state đứng yên (đúng ngữ nghĩa React) mới ra `phase=2`. **Đo nhầm cái render bị vứt = kết luận ngược.**

**B. vá-3 queryKey** (đọc thẳng dòng trong file):
```
queryKey: ['khkk-list', { group, phase, search, page, deletedView }],
CÓ group · CÓ phase · CÓ search · CÓ page · CÓ deletedView
CONTROL-ÂM khoá bịa "nhomX" → không khớp ⇒ phép đo bắt được cái thiếu
```
Ghi chú spec: đề bài nói "chứa cả `group` lẫn `filter`". Trục `?filter=` nằm trong khoá dưới dạng **đã giải mã** là `phase` (`FILTER_TO_PHASE` đơn ánh, và `phase` cũng chính là tham số gửi lên server) ⇒ khoá phủ đủ 2 trục. Nhét thêm `filter` thô chỉ tách đôi ô cache cho 2 request y hệt nhau. Khai để gate soát (FLAG-2).

**C. SHA-pair (3 cặp)**
| file | trước K4b | sau K4b | ×2 app |
|---|---|---|---|
| `KhkkListPage.tsx` | `80ebd79d…` | **`fc3944df7af22bd3c2d29d191b31af7dc13c8377ad1f943e92902da1e13c3b10`** | IDENTICAL |
| `KhkkCreatePage.tsx` | `677f4ab8…` | **`545b4c2fe0ebae88c0d8ffcd4118f85e71b3e6ac2dfe6833d9e6028765f3d564`** | IDENTICAL |
| `types/khkk.ts` (K4a, không sửa lượt này) | — | `7a99b865…` | IDENTICAL (đối chứng dương) |
`Layout.tsx` 2 app KHÁC NHAU by-design (user +50 · admin +28).

**D. Build ×2 PASS, 0 TS error** (`tsc -b && vite build`, `noUnusedLocals` ⇒ build là phép thử TS6133 thật)
| app | bundle js | css | time |
|---|---|---|---|
| fe-user | `index-C4F7kHKT.js` 1,640.77 kB (gzip 414.52) | `index-my6T4K7V.css` 92.62 kB | 3.57s |
| fe-admin | `index-kv90BBYQ.js` 1,723.75 kB (gzip 428.71) | `index-Ct6-3cYU.css` 86.80 kB | 0.86s |
Warning >500KB + `INEFFECTIVE_DYNAMIC_IMPORT` = PRE-EXISTING.

**E. Rào** — 0 chạm `MenuKeys.cs`/`DbInitializer.cs` (2 file M sẵn từ lane BE trước lượt tôi), 0 chạm `KhkkWorkflowPanel.tsx`/`KhkkDetailPage.tsx` (không có trong `git diff --name-only`), 0 chạm `tests/`, **0 commit**.

## §4 FLAG cho lead

**FLAG-1 (MAJOR — cần lane BE, ngoài rào của tôi):** `GET /contract-signing-plans/deleted` **không nhận `group`** (`ContractSigningPlansController.cs:61-66`, đối chiếu `List:42` có `[FromQuery(Name="group")]`). ⇒ 8 leaf "Đã xóa" trỏ 8 URL khác nhau nhưng trả CÙNG một tập phiếu-của-mọi-nhóm. Không vá được ở FE: lọc client-side sẽ làm `total`/số trang nói dối (server đếm trên tập chưa lọc). Xử tạm trong lượt này = **không để UI claim nhóm ở màn đã-xóa** (title/subtitle giữ nguyên bản "đã xóa", không in tên nhóm) ⇒ sai-thầm-lặng biến thành thiếu-tính-năng-nhìn-thấy-được. Fix thật: thêm `int? group` vào `ListDeletedContractSigningPlansQuery` + controller, 1 lượt nhỏ.

**FLAG-2 (MINOR — chờ gate phán):** queryKey mang `phase` (dạng đã giải mã của `filter`) chứ không mang chuỗi `filter` thô — xem §3B. Nếu gate muốn khớp LITERAL chữ `filter` trong khoá thì nói, tôi thêm (chi phí: 2 ô cache cho cùng 1 request).

**FLAG-3 (MINOR — bàn giao K5):** `?group=` trên `/khkk/workflow-matrix?type=10&group={n}` hiện **chưa có consumer** — `WorkflowMatrixViewPage` chỉ đọc `type`. Leaf "Luồng duyệt" của cả 8 nhóm vì thế hiển thị cùng một ma trận. Tham số vẫn cần có (nó là danh tính để sidebar sáng đúng leaf). Nối thật = K5 hoặc lượt sau: lọc workflow theo `Code = KHKK-N{n}` — helper `khkkGroupWorkflowCode(n)` đã có sẵn trong `types/khkk.ts` từ K4a, chờ người gọi.

**FLAG-4 (MINOR — đã quyết, báo để khỏi tưởng sót):** `search` CỐ Ý không reset khi đổi leaf (đang dò một mã rồi nhảy nhóm để tìm tiếp là thao tác có thật); chỉ `chipPhase` + `page` reset. Nếu owner muốn reset cả `search` thì thêm 1 dòng.

IMPLFE-K4B-END

