diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k4b.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k4b.md index f640602..b143b15 100644 --- a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k4b.md +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k4b.md @@ -14,3 +14,24 @@ K4a (MenuKeys/DbInitializer/Layout-regex/types/STATUS/test) ĐÃ qua gate K4a ## Ghi TỪNG MỤC (append khi đo xong, không soạn-gộp) +### M1 — Đo nền (tự chạy, không tin claim) +- `dotnet build SolutionErp.slnx` — **Build succeeded, 0 Warning, 0 Error** (56.4s, tôi tự chạy). +- `npx tsc -b` fe-admin — **EXIT=0**; fe-user — **EXIT=0** (tôi tự chạy, không đọc log nền của lane). +- WAL 6 commit `7a903cf..HEAD` chỉ chạm `.claude/**` (đo `git diff --stat`) ⇒ toàn bộ code K4a+K4b nằm ở working-tree. Mốc so sánh dùng `7a903cf` là ĐÚNG về nội dung code dù HEAD ghi khác. + +### M2 — Mirror 2 app (sha256, 16 ký tự đầu) +| file | fe-admin | fe-user | | +|---|---|---|---| +| `pages/khkk/KhkkListPage.tsx` | `fa4c7aa268f35eeb` | `fa4c7aa268f35eeb` | MATCH (khớp con số `fa4c7aa2` spec nêu) | +| `pages/khkk/KhkkCreatePage.tsx` | `545b4c2fe0ebae88` | `545b4c2fe0ebae88` | MATCH | +| `types/khkk.ts` | `7a99b86518948093` | `7a99b86518948093` | MATCH | + +`Layout.tsx` lệch by-design (đã khai trong cả 2 file: admin `Khkk_WfView` → Designer, không mang `group`; admin không nới regex `Khkk_G*`). + +### M3 — FLAG-1 (code LEAD tự viết) — BE `/deleted` + group +- Ký-pháp: `public record ListDeletedContractSigningPlansQuery(int? ApprovalGroup = null) : PagedRequest, ...`. `PagedRequest` là `abstract record` (`Common/Models/PagedResult.cs:15`) với 4 property `init` ⇒ trộn positional-ctor + object-initializer HỢP LỆ (record kế thừa record; nếu `PagedRequest` là `class` thì đã không biên dịch được). Build 0E xác nhận. +- Controller mirror List **ĐÚNG TỪNG NÉT**: `[FromQuery(Name = "group")] int? group = null` đặt ở vị trí tham số đầu, `new Query(group) { Page = …, PageSize = …, Search = …, SortDesc = … }` — cùng khuôn `List` (`:42`, anchor này ĐÚNG, tôi đếm lại). +- Handler: vế lọc `if (request.ApprovalGroup is not null) q = q.Where(p => p.ApprovalGroup == request.ApprovalGroup)` nằm **SAU** khối IDOR non-admin và **TRƯỚC** `Search` — đúng thứ tự list sống (`:755-758`). `IgnoreQueryFilters().Where(x => x.IsDeleted)` giữ nguyên phía trên ⇒ lọc nhóm không đụng vế soft-delete. +- `ContractSigningPlan.ApprovalGroup` là `int` **không nullable**, default `= 1` (`ContractSigningPlan.cs:41`) ⇒ so sánh `== request.ApprovalGroup` (int? vs int) dịch SQL thành so sánh giá trị, không sinh ca NULL-semantics. +- **Không có validator biên 1..8** cho `/deleted` — nhưng list SỐNG cũng không có ⇒ **consistent**, ghi nhận chứ không tính lỗi. `?group=999` = lọc rỗng (200 + `items: []`, `total: 0`), không 500. `?group=abc` = model-binding fail ⇒ 400 do `[ApiController]`. Cả hai hành vi giống hệt list sống. +- FE gửi `group` cho **cả 2 nhánh** (`KhkkListPage.tsx:124`, nằm ngoài tam-nguyên `deletedView` nên áp cho cả `/contract-signing-plans` lẫn `/deleted`) ⇒ khớp BE mới. Title/subtitle màn "Đã xóa" in tên nhóm — hết là lời nói dối vì server đã lọc thật.