11 KiB
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.tsxTRỌN vùng state/query ×2 app — có mirror không (SHA) - Đọc
KhkkCreatePage.tsxvùng stateapprovalGroup×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ừsearchParamsmỗi render. Bộ lọc chip chuyển sang lớp overridechipPhase: KhkkPhaseValue|null|undefined(3 trạng thái,undefined≠null— xem §1.2),phase = chipPhase !== undefined ? chipPhase : urlPhase. ResetchipPhase+pagekhinavKeyđổi, điều chỉnh state trong render (khuôn React chính thống) chứ khônguseEffect—useEffectsẽ 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ửiparams.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}+ subtitlekhkkGroupMenuLabel(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/Deletedmanggroup=1;Khkk_WfViewKHÔ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