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

16 KiB
Raw Blame History

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=1KeHoachKyKet=0 sẽ: FE cho canKhkk=true → gọi API → 403khkkFailed → 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=1KeHoachKyKet=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-6xl: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:94nhã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:false0 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:219hideEmpty 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.