Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k4b.md
2026-08-01 09:52:41 +07:00

11 KiB
Raw Blame History

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|undefinedundefined = "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? groupListContractSigningPlansQuery(..., 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, undefinednull — 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 useEffectuseEffect 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/6Khkk_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ã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[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ó consumerWorkflowMatrixViewPage 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