Files
solution-erp/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/sub-reviewer-1.md
pqhuy1987 bfc7b79f72
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m50s
[CLAUDE] FE-User+FE-Admin: cay 4 folder GD duoi tung goi thau (Duyet NCC / Ke hoach HD / Duyet HD / HD cung) x2 app + 5 fix review F-1/3/4/6/7
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 10:04:32 +07:00

100 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# sub-reviewer-1 — T2 review đối-kháng cây 4-folder GĐ (S162, 2026-07-30)
Ghi-đĩa-trong-lúc-làm (chống #53). Append theo từng mục soi. Verdict cuối ở cuối file.
## Phạm vi đo (từ đĩa, `git status --porcelain` + `git diff --stat`)
- 6 file tracked M: `fe-{user,admin}/src/pages/pe/PurchaseEvaluationsListPage.tsx` · `.../khkk/KhkkListPage.tsx` · `.../contracts/HardCopiesPage.tsx` — tổng +168/16.
- 6 file MỚI untracked: `fe-{user,admin}/src/components/pipeline/{PipelineStageFolders,PipelineTreePanel}.tsx` + `fe-{user,admin}/src/hooks/usePipelineStages.ts`.
- Ngoài src: memory frontend-designer (MEMORY.md + archive/2026-06.md + archive/_INDEX.md), `.session-counter.json`, run-folder artifacts (2 png + 2 harness .txt + sub-frontend-designer-0.md).
## MỤC 1 — Claim "khối `<li>` phiếu giữ nguyên từng byte" — ĐẠT
`git diff -- .../pe/PurchaseEvaluationsListPage.tsx` (cả 2 app, diff IDENTICAL):
- Hunk A `@@ -29,6 +29,8` — +2 import. 0 xoá.
- Hunk B `@@ -47,6 +49,14` — +8 dòng (6 comment + `usePipelineStages()` + `filterActive`). 0 xoá.
- Hunk C `@@ -507,7 +517,12` — xoá ĐÚNG 1 dòng `<ul className="ml-3 divide-y divide-slate-100 border-l border-slate-200">`.
- Hunk D `@@ -607,6 +622,7` — +1 dòng `) })} />`. 0 xoá.
⇒ Tổng xoá = 1 dòng. Khối `<li>` KHÔNG có dòng `-` nào ⇒ CLAIM ĐỨNG. Class cũ không mất: `ml-3 border-l border-slate-200` → prop `className` wrapper; `divide-y divide-slate-100` giữ ở `<ul>` trong.
## MỤC 6 — Biên giới — ĐẠT
- `git diff --name-only` (tracked): 0 file `src/Backend`, 0 `tests/`, 0 seeder/menu (#84), 0 `PePipelineStrip.tsx`. Chỉ 6 file FE + memory/run-artifact ngoài src.
- `grep -rn "export default"` trong 6 file mới ⇒ NONE (named export only, đúng §3.9).
- Nhãn VERBATIM `PipelineStageFolders.tsx:22-27`: `1:'Duyệt NCC'` · `2:'Kế hoạch Hợp đồng'` · `3:'Duyệt Hợp đồng'` · `4:'Hợp đồng cứng'` — khớp lời owner từng chữ.
- Mirror SHA256 6/6 PAIR (tự đo lại, không tin bảng designer):
PipelineStageFolders `05c93c3619a9cfec` · PipelineTreePanel `76e5dafb239e402a` · usePipelineStages `e762d5eb9c3a057a` · PE page `1d8d8f75c7d2cc14` · Khkk page `40a650bf6964ab4b` · HardCopies page `d30a911e7483f96a`.
- `types/contracts.ts` lệch 2 app CHỈ ở giá trị `ContractPhaseColor` (cùng bộ key 1-10,99) ⇒ hook SHA-chung import an toàn, `badgeClass` undefined vẫn có fallback `PipelineStageFolders.tsx:189`.
## MỤC 3 — Permission-gate 2 tầng — 1 LỖ THẬT (latent) + 1 điểm sai luận-cứ
Key FE (`usePipelineStages.ts:41-44`) vs BE:
- `Khkk_List`/`Khkk_G1`/`KeHoachKyKet` — cả 3 tồn tại thật (`DbInitializer.cs:2184-2188` KhkkKeys + `MenuKeys.cs:41`).
- `Hdc_<7 code>` + `HopDongCung` — tồn tại thật (`DbInitializer.cs:1868/2191`).
- `Ct_<code>_List` + `Contracts` — tồn tại thật.
- `can(k)` = `node.canRead` (`usePermission.ts:20`), findNode không thấy ⇒ false ⇒ OR-nhiều-key hợp lệ về mặt đọc cây.
🔴 **F: OR-nhiều-key ĐÁ NHAU với policy API cho GĐ2.** `ContractSigningPlansController.cs:36` = `[Authorize(Policy="KeHoachKyKet.Read")]`, và `MenuPermissionHandler.cs:40` so khớp **CHÍNH XÁC** `p.MenuKey == req.MenuKey` (KHÔNG kế thừa, KHÔNG OR con). ⇒ role có `Khkk_List=1``KeHoachKyKet=0` sẽ: FE cho `canKhkk=true` → gọi API → **403**`khkkFailed` → GĐ2 hiện "—" dưới MỌI gói thầu, và claim "0 rác 403" vỡ. Đo Dev DB: 13/13 role đều có `Contracts=1``KeHoachKyKet=1` ⇒ HIỆN CHƯA vỡ; prod chưa đo được (chỉ localhost+secret). Là lỗ THIẾT KẾ chờ 1 lần chỉnh ma trận quyền.
🟡 luận-cứ trong comment `usePipelineStages.ts:10-13` ("gate phải OR nhiều key vì root có thể canRead=false") đúng cho TẦNG HIỂN THỊ nhưng sai đích cho tầng API: query nào cũng nên gate bằng ĐÚNG key policy của endpoint (`KeHoachKyKet` cho `/contract-signing-plans`).
🟢 `/contracts` KHÔNG có policy action-level (`ContractsController.cs:16-17`, chỉ `[Authorize]` trần class) ⇒ nhánh `enabled: canContract || canHardCopy` KHÔNG sinh 403. Đổi lại: đây là bare-`[Authorize]` sẵn có của hệ (#82), diff này không làm tệ thêm nhưng có nới hiển-thị: user chỉ được cấp `Hdc_ThauPhu` vẫn thấy leaf GĐ4 của MỌI loại HĐ (`usePipelineStages.ts:211`) vì gate là OR-toàn-nhóm chứ không theo `c.type`.
🟢 Không quyền ⇒ `enabled:false` (`:77`, `:91`, `PipelineTreePanel.tsx:108`) ⇒ KHÔNG gọi API; folder khoá render nhánh riêng KHÔNG `<details>` (`PipelineStageFolders.tsx:99-113`). Fetch lỗi ⇒ `failed` ⇒ badge "—" + body "Không tải được…" (`:140-156`) ⇒ cây không vỡ. ĐẠT.
## MỤC 2 — Semantics filter / `hideEmpty` — ĐẠT phần cốt lõi, 3 gợn nhẹ
- Nguồn cờ: PE page `filterActive = search.trim()!=='' || phase!=='' || pendingMe || deletedView` (`PurchaseEvaluationsListPage.tsx:58`); panel `hideEmpty = term.length > 0` (`PipelineTreePanel.tsx:138`). Không lọc ⇒ `buildStages` luôn trả ĐỦ 4 (`usePipelineStages.ts:240`) ⇒ "không filter = đủ 4 folder" ĐÚNG.
- **Ca "GĐ1 rỗng chiếm chỗ" KHÔNG tồn tại:** `wg.items` là nguồn dựng nhóm nên mọi gói thầu có ≥1 phiếu ⇒ `stage1.count = wg.items.length ≥ 1` (`usePipelineStages.ts:153`). Thêm chốt chặn `if (visible.length===0) return null` (`PipelineStageFolders.tsx:220`) ⇒ không có wrapper rỗng, không vỡ layout.
- 🟡 gợn 1: bộ lọc `hideEmpty` chấm bằng `stageCount(s) > 0` (`:219`) nên nó nuốt LUÔN folder `locked` và folder `failed` (cả hai đều count=0). Đang lọc mà GĐ2 lỗi tải ⇒ dòng "—" BIẾN MẤT thay vì báo lỗi (spec dòng 12 muốn "—"). Cũng nuốt folder đang `loading` ⇒ folder nhảy vào sau khi query xong (giật layout nhẹ).
- 🟡 gợn 2: count GĐ2/3/4 KHÔNG chịu bộ lọc của trang (index nạp riêng, không truyền `search`/`phase`) ⇒ "chỉ hiện folder có nội dung khớp" chỉ đúng theo nghĩa YẾU (folder khác rỗng), không phải "nội dung khớp từ khoá".
- 🟡 gợn 3: ở view "Đã xóa" (`deletedView=1`) cây vẫn treo GĐ2/3/4 SỐNG dưới phiếu đã xoá (index không lọc theo phiếu xoá) ⇒ bấm được sang KHKK/HĐ còn sống từ màn thùng rác.
## MỤC 4 — Query hygiene + degrade `pageSize:200`
**Không đụng cache trang cũ — ĐẠT.** Key mới `['pipeline-khkk-index']` / `['pipeline-contract-index']` (`usePipelineStages.ts:70,84`) + `['pipeline-pe-index']` (`PipelineTreePanel.tsx:101`). Key đang sống: `['khkk-list',{…}]` (`KhkkListPage.tsx:70`) · `['hard-copies',{…}]` + `['contract',id]` (`HardCopiesPage.tsx:75,268`) · `['pe-list',{…}]` + `['pe-detail',id]` (`PurchaseEvaluationsListPage.tsx:75,114`) · dashboard `['pipeline-pe-total']`/`['pipeline-contract-total']` (`UserDashboardPage.tsx:906,913`). Tất cả là mảng 1 phần tử CHUỖI KHÁC NHAU ⇒ 0 giao, 0 prefix-match. `invalidateQueries()` trống (nuốt tất) = 0 chỗ trong fe-user.
🟡 mặt trái: `qc.invalidateQueries({queryKey:['hard-copies']})` sau khi lưu bản cứng (`HardCopiesPage.tsx:290`) KHÔNG chạm `pipeline-contract-index` ⇒ badge GĐ4 trên cây giữ số cũ tới lần mount/refocus kế (staleTime 60s, `usePipelineStages.ts:34`).
**Degrade khi vượt 200 — graceful CÓ, im lặng CŨNG CÓ.**
- Kẹp là THẬT: `PagedResult.cs:20` `PageSize = value switch { <1 => 20, >200 => 200, _ => value }`.
- Thứ tự lấy: 3 controller đều mặc định `sortDesc = true` (`ContractsController.cs:19` · `ContractSigningPlansController.cs:45` · `PurchaseEvaluationsController.cs:21`) ⇒ 200 bản **MỚI NHẤT**, không phải 200 bản cũ nhất. Tốt.
- 🔴 nhưng hook VỨT `total`: `.data.items` (`usePipelineStages.ts:76,90` · `PipelineTreePanel.tsx:107`) ⇒ mã KHÔNG BIẾT mình bị cắt ⇒ không đâu có dấu "còn N nữa". GĐ2 mất leaf + tụt count; GĐ4 tụt count; header panel "{packageCount} gói" (`PipelineTreePanel.tsx:188`) cũng tụt. Đây là **sai-số-im-lặng**, không phải chỉ degrade.
- 🔴 GĐ3 tệ hơn 1 nấc: HĐ ngoài index vẫn ra leaf nhưng **DÁN NHÃN SAI** `primary: c?.maHopDong ?? '(chưa cấp mã)'` (`usePipelineStages.ts:204`) — HĐ có mã thật vẫn bị ghi "(chưa cấp mã)". Trộn 2 nguyên nhân khác hẳn nhau (chưa cấp mã ⟂ ngoài cửa sổ 200) vào một câu khẳng định sai.
- 🔴 độc lập với 200: `pe.ContractId = contracts[0].Id` (`CreateContractFromEvaluationFeatures.cs:145`, comment `:143` ghi rõ "N HĐ liên-danh truy qua changelog") ⇒ phiếu multi-winner đẻ N hợp đồng nhưng cây chỉ nối ĐÚNG 1 (`usePipelineStages.ts:197`) ⇒ GĐ3/GĐ4 hụt N1 và badge in "1". Đúng lớp bài `feedback_cardinality_change_grep_consumers` (S87/S88): consumer mới đọc field single trong miền multi.
- Cỡ dữ liệu đo được: Dev `PE=4 · KHKK=0 · Contracts=7 · PE có ContractId=0/4` ⇒ chưa chạm trần 200, nhưng cũng có nghĩa **GĐ3/GĐ4 trên Dev sẽ rỗng ("SẮP") toàn bộ**; prod không đo được từ máy này.
## MỤC 5 — Lưới 19rem 2 trang (KhkkListPage + HardCopiesPage)
- **PageHeader còn full-width — ĐÚNG.** KHKK: header đóng `/>``:133`, lưới mở ở `:138` (ngoài lưới). HardCopies: PageHeader + ghi chú 1-MỐC amber đứng trước, lưới mở sau (`HardCopiesPage.tsx` hunk `@@ -110,6 +111,12`).
- **`min-w-0` có đủ:** cột phải `<div className="min-w-0 space-y-5">` ở cả 2 trang; track lưới dùng `minmax(0,1fr)` ⇒ bảng không thổi vỡ cột. ĐẠT.
- **Mobile:** `<lg` lưới về 1 cột, panel nằm TRÊN; thân panel `mobileOpen ? 'flex' : 'hidden'` + `lg:flex` (`PipelineTreePanel.tsx:200`) với `mobileOpen` mặc định `false` (`:95`) ⇒ mobile chỉ thêm 1 thanh tiêu đề + nút "Hiện cây", bảng không bị đẩy xuống sâu. ĐẠT.
- 🟡 `overflow-x-auto` bọc `<table>` gần như VÔ TÁC DỤNG như lời chú thích: `<table className="w-full">` + `whitespace-nowrap` = **0 lần** ở cả 2 file ⇒ bảng co cột/xuống dòng chứ không sinh cuộn ngang. Vô hại, nhưng chú thích `KhkkListPage.tsx:175` ("bảng 7 cột cuộn ngang trong thẻ") mô tả một hành vi không xảy ra; thứ thật sự chặn tràn là `minmax(0,1fr)`.
- 🟡 đổi hành vi có thật (nhỏ): hàng KpiCard `lg:grid-cols-6``xl:grid-cols-6` (`KhkkListPage.tsx:137`) ⇒ ở dải 10241279px KPI từ 6 cột rớt về 3 cột. Hợp lý vì cột trái ăn 19rem, nhưng đây là thay đổi hiển thị ngoài phạm vi "chỉ thêm cây".
- 🟡 `HardCopyPanel` (`HardCopiesPage.tsx:220`, định nghĩa `:259`) nằm TRONG `<tbody>` nên nay nằm trong lớp `overflow-x-auto` mới. Không mất chức năng (upload/tải/ý kiến vẫn nguyên, 0 dòng `-` trong diff), nhưng nó bị bó vào bề rộng cột phải hẹp hơn trước.
- 🔴 **Bằng chứng thị giác cho 2 trang này = KHÔNG CÓ.** Harness chỉ import `PipelineStageFolders` + `PipelineTreePanel` (`harness-s162.tsx.txt:10-11`), tự "sao markup" vỏ cây (`:61`), và `grep 'grid-cols-\[19rem'` trong harness = **0 hit** ⇒ chưa bao giờ render KhkkListPage/HardCopiesPage/lưới 19rem. Vậy dòng rubric FD4 "responsive ✓ (1440/1366/375 … bảng không bị đẩy)" trong `sub-frontend-designer-0.md:94`**nhãn ĐO mà phép đo không tồn tại** — phải hoặc đo lại, hoặc bỏ dấu ✓ cho trục đó.
- (sạch) Harness đã xoá khỏi `src` 2 app — `ls fe-*/src/harness*` = NONE.
## MỤC 7 — Self-write memory frontend-designer
ĐÚNG SỰ THẬT (tự đo lại, không tin lời khai):
- `PagedResult.cs:20` kẹp ≤200 — ĐÚNG nguyên văn.
- `GetMyMenuTreeQuery.cs:96` = `HasAccess(n) => n.CanRead || n.Children.Any(HasAccess)` — ĐÚNG số dòng.
- "Sau nén 14.6KB" — `wc -c MEMORY.md` = **14.652 B**. ĐÚNG.
- Archive **moved-not-cut** (bài S102): 3 mục chuyển đi đều CÓ MẶT trong `archive/2026-06.md` (`WidgetCard` 3 hit · `OfficeDashboardPage` 3 hit · `Maximize2` 2 hit) + 3 dòng trỏ `substring` còn ở L1. KHÔNG phải cut-giả-vờ-archive.
- "MIRROR đủ 2 app / build PASS ×2 / `<li>` giữ nguyên từng byte" — đã tự chứng ở MỤC 1 + MỤC 6.
2 chỗ SAI/NỬA-ĐÚNG sẽ đóng băng vào trí nhớ vai nếu commit nguyên:
- 🔴 "`enabled:false`**0 rác 403**" viết như tính chất vô điều kiện — sai cho đường KHKK (xem F-1): gate OR `Khkk_List` trong khi policy API là `KeHoachKyKet.Read`.
- 🟡 "`Ct_*` **kế thừa** `Contracts`" — chỉ đúng khi menu con KHÔNG có row quyền (`GetMyMenuTreeQuery.cs:66` `!resolved.ContainsKey(m.Key)`), mà seeder LẠI tạo row cho `Ct_*` (`DbInitializer.cs:2168-2176`) ⇒ trên DB đã seed, kế thừa gần như không bao giờ chạy. Câu này sẽ dẫn sai phiên sau.
- 🟡 nhật ký `sub-frontend-designer-0.md:82` "mount CHÍNH component thật (không vẽ lại markup)" — đúng với 2 component mới, nhưng vỏ cây thì có vẽ lại ("sao markup", `harness-s162.tsx.txt:61`).
---
# VERDICT: PASS-WITH-FLAGS
Hợp đồng spec giữ đúng ở mọi ranh giới kiểm được (nhãn verbatim 4/4 · mirror SHA 6/6 · khối `<li>` phiếu 0 dòng xoá · 0 chạm BE/menu/seeder/strip/tests · named-export) và các đường hỏng đều có nhánh đỡ (khoá/lỗi/rỗng). Không có blocker. 8 flag: nặng nhất là **im lặng về độ chính xác của con số** (cắt 200 không dấu vết, `contracts[0]` nuốt HĐ liên-danh, nhãn "(chưa cấp mã)" sai nguyên nhân) và **1 nhãn ✓ responsive chưa từng được đo**.
- **F-1** `usePipelineStages.ts:42` — gate OR `['Khkk_List','Khkk_G1','KeHoachKyKet']` trong khi API là `[Authorize(Policy="KeHoachKyKet.Read")]` (`ContractSigningPlansController.cs:36`) và `MenuPermissionHandler.cs:40` so khớp CHÍNH XÁC 1 key ⇒ role có `Khkk_List=1`/`KeHoachKyKet=0` sẽ 403 và GĐ2 hiện "—" khắp cây. Dev 13/13 role hiện đều =1 nên chưa lộ.
- **F-2** `usePipelineStages.ts:197` + `CreateContractFromEvaluationFeatures.cs:145` — phiếu liên-danh N HĐ chỉ nối 1 (`pe.ContractId` = HĐ đầu) ⇒ GĐ3/GĐ4 hụt N1 mà badge vẫn in số như thật.
- **F-3** `usePipelineStages.ts:204` — HĐ ngoài cửa sổ 200 bị dán nhãn "(chưa cấp mã)"; trộn "chưa có mã" với "không nằm trong index".
- **F-4** `usePipelineStages.ts:76,90` + `PipelineTreePanel.tsx:107` — vứt `total` ⇒ không phát hiện được việc bị cắt 200 ⇒ mọi count/`{n} gói` tụt im lặng, không dấu "còn N nữa".
- **F-5** `sub-frontend-designer-0.md:94` — rubric "responsive ✓ … bảng không bị đẩy" không có phép đo: harness chỉ mount 2 component (`harness-s162.tsx.txt:10-11`), `grid-cols-[19rem` = 0 hit ⇒ 2 trang lưới chưa từng render.
- **F-6** `PipelineStageFolders.tsx:219``hideEmpty` lọc theo count>0 nên nuốt cả folder `locked`/`failed`/`loading`; đang lọc thì dấu "—" báo lỗi biến mất.
- **F-7** `HardCopiesPage.tsx:290` — invalidate `['hard-copies']` không chạm `['pipeline-contract-index']` ⇒ badge GĐ4 lệch tới lần mount/refocus kế.
- **F-8** memory vai: "0 rác 403" (vô điều kiện — sai theo F-1) + "`Ct_*` kế thừa `Contracts`" (chỉ đúng khi con không có row; seeder có tạo row `Ct_*`) — nên sửa trước khi commit vì đây là trí nhớ dài hạn.
- (nhẹ, không đánh số) `overflow-x-auto` vô tác dụng vì `table w-full` + 0 `whitespace-nowrap`; KPI `lg:``xl:` đổi hiển thị dải 10241279; view "Đã xóa" vẫn treo GĐ2/3/4 sống; 2 query index chạy sớm ngay cả khi người dùng không mở folder nào.