Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-gd3d.md
2026-08-01 16:51:09 +07:00

19 KiB
Raw Blame History

GATE-GD3D: PASS-WITH-FLAGS-10 — Đúng phương án D, khuôn nguyên vẹn (SHA-pair identical), import sạch; 0 blocker, nhưng max-h của khuôn vượt chiều cao Panel-3 trên InboxPage (~26px) đẻ 2 thanh cuộn lồng nhau + 2 class khuôn chết âm thầm.

Sub-reviewer gate GĐ3-D (S168, 2026-08-01)

Scope: 3 file FE uncommitted, phương án D "cây thay chỗ Panel-3 khi chưa chọn HĐ". Spec nguồn: .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-gd3.md §1 bảng 4 phương án (D). Vai: reviewer đối-kháng, propose-only — không sửa code, không commit.


Đo trước (baseline)

  • git status --porcelain toàn repo: 5 M.claude/WAL.md, .claude/agent-memory/memory-budget.json, + 3 file FE.
  • git status --porcelain --untracked-files=all -- fe-admin/src fe-user/src: ĐÚNG 3 file, 0 untracked.
  • git diff --stat: mỗi file FE 13 dòng đổi (+6/7), tổng 3 file.

R1 — Đúng-D (điều kiện render) → PASS

Cả 3 file giữ NGUYÊN cặp điều kiện:

{!selectedId && <PipelineTreePanel currentStage={3} />}
{selectedId && detail.data && <WorkflowHistoryPanel contract={detail.data} />}
  • Cây CHỈ render khi !selectedId — đúng D.
  • Chọn HĐ → WorkflowHistoryPanel như cũ; vế điều kiện không đổi một ký tự.
  • Trạng thái "đã chọn HĐ nhưng detail đang tải" render RỖNG ở CẢ bản cũ lẫn bản mới (bản cũ: !selectedId sai nên ô dashed tắt, detail.data chưa có nên panel trống) ⇒ hành vi loading giữa chừng KHÔNG đổi.
  • currentStage={3} đúng GĐ Hợp đồng cho cả 3 trang (xem F-10 cho lưu ý riêng về InboxPage).

R2 — 0-đụng-khuôn → PASS

sha256sum (16 ký tự đầu), fe-admin ⟂ fe-user:

File admin user Kết quả
components/pipeline/PipelineTreePanel.tsx d70ac315bae485e6 d70ac315bae485e6 IDENTICAL
components/pipeline/PipelineStageFolders.tsx bed52f39d595c86a bed52f39d595c86a IDENTICAL
hooks/usePipelineStages.ts 76e1823345d4e78b 76e1823345d4e78b IDENTICAL

git status --porcelain trên 4 đường dẫn khuôn = RỖNG ⇒ khuôn chưa bị sửa. SHA-pair identical vừa chứng mirror 2 app còn khớp, vừa chứng không ai chạm vào khuôn trong đợt này.

R3 — Unused-import XPASS

  • Cây làm việc: 0 hit mã ở cả 3 file. Hit duy nhất là chuỗi type=X trong comment ContractsListPage.tsx:6 (không phải icon).
  • Control-âm: cùng regex chạy trên git show HEAD:<file> cho 2 hit/file (dòng import … X … + <X className="mx-auto mb-2 h-5 w-5" />) ⇒ phép đo CÓ răng, không phải 0-hit vì regex hỏng.
  • Không gỡ nhầm icon khác: FileText / Search / Plus / Inbox / AlertTriangle / Clock / ClipboardList đều còn nguyên trong dòng import mới.

FINDINGS

F-1 (MAJOR) — R4: max-h của khuôn hiệu-chỉnh cho host page-flow, vượt chiều cao Panel-3 trên InboxPage ⇒ 2 thanh cuộn lồng nhau

Chiều cao TỐI ĐA của khuôn khi đặt ở lg:

Thành phần File:dòng Lớp CSS px
Đầu panel PipelineTreePanel.tsx:181 px-3 py-2.5 (20) + icon-chip h-7! (28) + border-b (1) 49
Ô lọc PipelineTreePanel.tsx:204 p-2 (16) + input h-8 (32) + border-b (1) 49
Thân cây PipelineTreePanel.tsx:215 lg:max-h-[calc(100vh-16rem)] 100vh 256
Tổng 100vh 158

Chiều cao KHẢ DỤNG của Panel-3 = 100vh 4rem (vỏ trang) đầu trang dải phụ p-4 (32):

Trang Vỏ / đầu trang Đầu trang Dải phụ Khả dụng So với 100vh158
fe-user/src/pages/contracts/MyContractsPage.tsx :105 / :107 py-3 + Button size=md h-8 + border 57px 0 100vh 153 thừa 5px (khít)
fe-admin/src/pages/contracts/ContractsListPage.tsx :106 / :107 py-3 + h1 24px + border 49px 0 100vh 145 thừa 13px
fe-user/src/pages/InboxPage.tsx :166 / :168 py-3 + StatPill 34px (:50 px-3 py-1.5 + h-5) + border 59px 29px — dải "Vai trò bạn đang xử lý" :183-187 (py-1.5 + text-[11px] + border) 100vh 184 THIẾU ~26px

Hệ quả trên /inbox: <aside className="hidden overflow-y-auto … p-4 lg:block"> mọc thanh cuộn dọc thứ hai bao ngoài thanh cuộn của thân cây, tức trong cột nội dung rộng 328px (360 2×16) có hai thanh cuộn lồng nhau. Nội dung vẫn với tới được (không vỡ layout, không mất dữ liệu), nhưng người dùng phải cuộn hai tầng, và đầu panel cùng ô lọc trôi khỏi tầm nhìn khi cuộn tầng ngoài.

Dải "Vai trò" chỉ render khi user?.roles.lengthđo prod: /api/roles trả 13 vai, mọi tài khoản thật đều thuộc ít nhất một vai ⇒ nhánh này gần như LUÔN bật, không phải ca hiếm.

Hai trang còn lại chỉ thừa 5px và 13px ⇒ mong manh: bất kỳ thay đổi nào ở đầu trang (đổi Button sang size=lg = h-10, hoặc thêm một chip trạng thái làm flex-wrap xuống dòng ở bề rộng 1024px) đều đẩy chúng sang đúng trạng thái của InboxPage.

Tiêu chí nghiệm thu đề nghị: ở 1280×800 và 1366×768, mở /inbox khi chưa chọn HĐ — số thanh cuộn dọc trong vùng Panel-3 phải bằng 1, và cuộn tới đáy cây không cần cuộn <aside> ngoài. Lặp lại phép đo cho /my-contracts và fe-admin /contracts.

F-2 (MINOR) — R4: hai class của khuôn chết ÂM THẦM trong host mới

PipelineTreePanel.tsx:177cn('card-accent flex flex-col self-start overflow-hidden lg:sticky lg:top-4', className):

  • self-startalign-self:flex-start, chỉ có tác dụng khi cha là flex hoặc grid. Host mới có cha là <aside … lg:block>display:blockvô hiệu.
  • lg:sticky lg:top-4: tổ tiên cuộn gần nhất chính là <aside overflow-y-auto>, mà cây lại là con DUY NHẤT của nó ⇒ khối chứa gần bằng chính nó ⇒ quãng "dính" xấp xỉ 0 ⇒ vô hiệu.

Ở ba host cũ (KhkkListPage.tsx:197HardCopiesPage.tsx:116, đều là lg:grid-cols-[19rem_minmax(0,1fr)]) cha là grid và trang cuộn ở <main className="flex-1 overflow-auto">, nên cả hai class đều sống. Đây không phải lỗi chức năng, nhưng là dấu hiệu khuôn đang chạy ngoài giả định thiết kế của nó — cùng gốc với F-1. Nên ghi một dòng chú thích ở chỗ cắm để người sau không tưởng rằng cây có bám dính ở đây.

F-3 (MINOR) — R4 / a11y: <aside> lồng trong <aside>

Host Panel-3 là <aside>, và gốc khuôn cũng là <aside> (PipelineTreePanel.tsx:176) có kèm aria-label="Cây toàn trình theo gói thầu" (:179). Theo HTML-AAM, một aside nằm trong sectioning content chỉ tụt xuống role=generic khi KHÔNG có tên khả truy cập; ở đây nó có tên nên giữ nguyên role=complementary ⇒ sinh ra hai landmark complementary lồng nhau. Điều này hợp lệ về HTML nhưng gây nhiễu khi người dùng điều hướng theo landmark bằng trình đọc màn hình. Ba host cũ không dính vì cha của chúng là <div className="grid …">.

F-4 (MINOR) — R5: !canPe đổi thông điệp Panel-3 từ chỉ dẫn sang than phiền quyền; đo prod cho thấy hiện chưa vai nào dính

Đường quyền, đọc thẳng từ mã chứ không suy:

  • usePipelineStages.ts:83canPe = PE_MENU_KEYS.some(k => can(k)), với PE_MENU_KEYS:55['PurchaseEvaluations','Pe_DuyetNcc_List','Pe_DuyetNccPhuongAn_List'].
  • Khi canPe sai, PipelineTreePanel.tsx:111 đặt enabled: canPe nên truy vấn pipeline-pe-index không chạykhông sinh rác 403, đúng ý đồ đã khai ở usePipelineStages.ts:5-8.
  • Nhưng Panel-3 khi đó hiển thị khung cây rỗng, huy hiệu "0 gói", và câu ở :216-220: "Bạn chưa có quyền xem phiếu Duyệt NCC nên cây toàn trình chưa hiển thị được." Câu này nằm trên trang Hợp đồng, thay cho câu cũ "Quy trình duyệt sẽ hiện khi chọn HĐ." ⇒ người dùng mất chỉ dẫn hữu ích và nhận về một lời than phiền lạc đề.

Đo prod /api/permissions/by-role/{id} trên 13 vai (tất cả trả http=200): PurchaseEvaluations.canRead bằng True ở 13/13 vai ⇒ nhánh !canPe hiện không xảy ra với vai nào. Control-âm của cùng phép đo: Permissions.canRead và Users.canRead bằng False ở 12/13 vai (chỉ Admin True; số dòng quyền là 169 so với 191) ⇒ phép đo có phân biệt thật, không phải "True tuốt vì đọc sai trường". Kết luận: F-4 là rủi ro tương lai (nếu ai đó gỡ quyền PE của một vai), không phải lỗi đang chảy máu. Không chặn commit.

F-5 (MINOR) — R5: cây bơm thêm tối đa ba truy vấn index vào lượt tải đầu của ba trang

PipelineTreePanel gọi usePipelineStages, hàm này gọi usePipelineLinkage, sinh ra ba truy vấn — pipeline-pe-index (PipelineTreePanel.tsx:100), pipeline-khkk-index (usePipelineStages.ts:90) và pipeline-contract-index (:108) — mỗi truy vấn lấy pageSize=200.

Trên InboxPage, MyContractsPage và ContractsListPage, truy vấn pipeline-contract-index gọi GET /contracts?pageSize=200 song song với truy vấn danh sách của chính trang (/contracts/inboxInboxPage.tsx:71, /contracts ở MyContractsPage) — cùng một tài nguyên nhưng khác queryKey, nên mỗi lần vào trang mà chưa chọn HĐ là hai lần nạp danh sách hợp đồng. Đo prod cho thấy 13/13 vai đều có đủ canPe, canKhkkcanContractcả ba truy vấn đều bắn cho mọi vai, không vai nào được miễn. Mức này chấp nhận được vì staleTime là 60 giây và đây là hệ quả nội tại của phương án D, nhưng nên biết trước để không bất ngờ khi UAT phản ánh trang tải chậm hơn.

F-6 (MINOR) — R1 phụ: trade-off của D khai thiếu một vế — ô lọc cây mất chữ khi chọn HĐ rồi bỏ chọn

Vì điều kiện là {!selectedId && <PipelineTreePanel …>}, việc chọn một HĐ sẽ gỡ bỏ (unmount) component. Hệ quả:

  • expandedSet sống sót nhờ localStorage (PipelineTreePanel.tsx:119-126 đọc, :170 ghi).
  • search (:94) và mobileOpen (:95) là state thuần nên mất trắng: người dùng gõ "FLOCK" để lọc cây, bấm xem một HĐ, quay lại thì ô lọc rỗng và cây bung về nhánh mặc định.

Spec D ở sub-implfe-gd3.md §1 chỉ khai trade-off "cây nhảy chỗ khi chọn HĐ", không khai việc mất chữ đang lọc. Đề nghị bổ sung vế này khi báo owner để owner chốt trên thông tin đầy đủ.

F-7 (MINOR) — R8 mobile: không hồi quy, nhưng lệch với ba host cũ

  • Host là <aside className="hidden overflow-y-auto … lg:block"> nên dưới 1024px Panel-3 không render. Bản cũ (ô dashed) nằm trong đúng cùng <aside> đó, vì vậy không có hồi quy trên mobile.
  • Nhưng khuôn có sẵn cả một nhánh mobile riêng: nút "Hiện cây / Ẩn cây" gắn lg:hidden (:193-200), state mobileOpen khống chế thân cây (:203), và max-h-[28rem] dành cho mobile (:215). Trong host này toàn bộ nhánh mobile của khuôn chết.
  • Hệ quả nghiệp vụ: người dùng mobile CÓ cây ở GĐ2 (KhkkListPage) và GĐ4 (HardCopiesPage, hai trang page-flow nên cây xếp ở trên), nhưng không có cây ở GĐ3.

Điều này đúng chủ đích của HOST (bố cục 3-panel vốn chỉ dành cho desktop) nhưng lệch chủ đích của KHUÔN. Owner cần biết để chốt, đây không phải lỗi code.

F-8 (INFO — phán quyết: lead ĐÚNG) — R6: WorkflowMatrixViewPage không áp D là quyết định đúng

fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:104 cho thấy gốc trang là <div className="space-y-4 px-2 py-5">: đây là trang page-flow, không có <aside>, không có lưới 3-panel, không có Panel-3. Vì vậy "cây thay chỗ Panel-3" không có chỗ cắm, và bỏ qua trang này là đúng theo đúng định nghĩa của phương án D. Đây không phải chỗ lead bỏ sót.

Tuy nhiên có một câu cần owner chốt: trang này được mount ở ba route (fe-user/src/App.tsx:71/purchase-evaluations/workflow-matrix, :73/contracts/workflow-matrix, :83/khkk/workflow-matrix), tức nó là trang giai đoạn của cả ba GĐ. Nếu ý owner khi nói "(a) cứ mở rộng" là "mọi trang giai đoạn đều phải có cây", thì đây là trang duy nhất còn thiếu, và cách cắm đúng phải là lưới page-flow theo khuôn KhkkListPage.tsx:197 chứ không phải D. Việc này nằm ngoài phạm vi wave hiện tại, nêu ra để owner quyết.

F-9 (INFO — phán quyết: KHÔNG mâu thuẫn K6) — R7: sửa trang đang bị ẩn khỏi sidebar

  • fe-admin/src/components/Layout.tsx:227-240 — tập ADMIN_HIDDEN_KEYS có chứa 'Contracts'; thêm nữa, :248 trong isAdminHidden còn chặn mọi key bắt đầu bằng Ct_. Vì vậy admin không nhìn thấy mục Hợp đồng ở sidebar.
  • Route thì vẫn sống: fe-admin/src/App.tsx:82 map /contracts sang ContractsListPage, kèm :83 cho /contracts/new:84 cho /contracts/:id.
  • Có tiền lệ khớp: key 'HopDongCung' cũng nằm trong ADMIN_HIDDEN_KEYS (:231), vậy mà HardCopiesPage bên fe-admin vẫn được S162 cắm cây (fe-admin/src/pages/contracts/HardCopiesPage.tsx:117) và route của nó vẫn sống (App.tsx:87).

Kết luận: K6 ẩn phần hiển thị, chứ không xoá trang. Sửa một trang bị ẩn chính là giữ mirror theo docs/rules.md §3.9, nên không có mâu thuẫn. Một lưu ý thực dụng: thay đổi bên fe-admin gần như vô hình với admin vì phải gõ thẳng URL mới tới được. Giá trị của nó là parity giữa hai app, nên đừng tính nó vào "ba trang người dùng nhìn thấy" khi báo owner, và UAT phải làm ở fe-user chứ không phải fe-admin.

F-10 (INFO) — R1 phụ: currentStage={3} trên InboxPage, trong khi hộp thư trộn GĐ1 và GĐ3

fe-user/src/pages/InboxPage.tsx:71 gọi /contracts/inbox:79 gọi /purchase-evaluations/inbox, nên trang liệt kê cả hợp đồng lẫn phiếu Duyệt NCC (bấm vào phiếu PE thì :283 điều hướng sang /purchase-evaluations/:id). Giá trị currentStage={3} làm cây mở sẵn folder GĐ3.

Cách đặt này vẫn hợp lý, vì Panel-3 là chỗ dành cho HĐ đang chọn (selectedId được dùng cho /contracts/${selectedId}:84), tức ngữ cảnh của panel đúng là GĐ3. Nêu ra để tránh hiểu nhầm nếu sau này owner coi Inbox là trang GĐ1. Không tính là lỗi.


Tổng kết theo trục

Trục Kết quả Ghi chú
R1 Đúng-D + loading không đổi PASS điều kiện render không đổi một ký tự; kèm F-6 và F-10 dạng thông tin
R2 0-đụng-khuôn + SHA-pair PASS 3/3 cặp IDENTICAL, git status trên khuôn rỗng
R3 Unused-import X PASS 0 hit mã, control-âm cho 2 hit/file ở HEAD
R4 Ngữ cảnh render / CSS FLAG F-1 (MAJOR), F-2, F-3
R5 Permission-path / 403 rác PASS có cờ không sinh rác 403; F-4 là rủi ro tương lai (prod 13/13 chưa dính), F-5 là chi phí truy vấn
R6 Trang không áp PASS F-8 — quyết định của lead là đúng
R7 fe-admin ẩn sidebar PASS F-9 — không mâu thuẫn K6, có tiền lệ HardCopiesPage
R8 Mobile PASS F-7 — không hồi quy, nhưng nhánh mobile của khuôn chết trong host này

Blocker: 0. Không finding nào chặn commit. F-1 là thứ đáng xử lý nhất trước khi UAT, hoặc phải khai rõ với owner như một trade-off đã biết.

Ranh vai: không sửa file mã nào, không commit, không push. Mọi finding đều ở dạng mô tả vấn đề kèm tiêu chí nghiệm thu.

END sub-reviewer-gate-gd3d — VERDICT=PASS-WITH-FLAGS-10 — TOTAL=10 finding


LEAD DISPOSITION @S168 (từng dòng — không số tổng)

F Xử Chi tiết
F-1 MAJOR Nới khuôn đúng ngoại-lệ "thiếu prop cho GĐ3" artifact lane khoanh: PipelineTreePanel +prop bodyMaxHClass? (default GIỮ NGUYÊN — 3 host giấy-cuộn cũ 0 đổi hành vi), 3 host Panel-3 pass max-h-[28rem] lg:max-h-nonepanel cuộn = đúng 1 thanh, bền mọi trang (không phụ thuộc 5px/13px mong manh). Mirror ×2 app SHA-pair mới 7baf6247af3d. Lệch tiêu-chí-CHỮ của gate ("cuộn tới đáy cây không cần cuộn aside ngoài") — đạt tiêu-chí-Ý (1 thanh cuộn, không lồng): chọn panel-cuộn thay cây-tự-cuộn vì bền cấu-trúc, đầu cây + ô lọc trôi theo là trade-off khai
F-2 MINOR VÁ (comment) Chú thích tại 3 chỗ cắm: sticky/self-start vô hiệu ở host display:block — by-design
F-3 MINOR 🔸 CHẤP NHẬN-khai aside-lồng-aside 2 landmark complementary: hợp lệ HTML, nhiễu screen-reader — không đổi khuôn lượt này (đổi root khuôn = đụng 4 host); ghi nợ a11y cùng lô a11y-<a>-label K5
F-4 MINOR 🔸 CHẤP NHẬN !canPe than-phiền-quyền thay chỉ-dẫn: prod 13/13 vai có PE.Read (gate đo + control-âm) — rủi ro tương lai, không chảy máu
F-5 MINOR 🔸 CHẤP NHẬN +tối đa 3 query index lượt tải đầu (staleTime 60s, enabled canPe) — giá của D
F-6 MINOR 🔸 CHẤP NHẬN-khai chọn HĐ → cây unmount → bỏ chọn → ô lọc mất chữ (state trong component) — trade-off D "cây nhảy chỗ" đã khai với owner
F-7 MINOR 🔸 CHẤP NHẬN mobile: panel-3 hidden dưới lg = khuôn 3-panel by-design (mobile đi fullpage route)
F-8 INFO ✓ (gate phán lead ĐÚNG) WorkflowMatrixView không áp D
F-9 INFO ✓ (gate phán KHÔNG mâu-thuẫn K6) fe-admin /contracts ẩn sidebar — route sống, tiền lệ HardCopiesPage
F-10 INFO 🔸 GIỮ currentStage={3} /inbox trộn GĐ1+GĐ3 nhưng trang chủ-đích là HĐ Pending; PE item navigate sang trang PE riêng

Vá F-1/F-2 nằm trong commit GĐ3-D (build+test re-verify sau vá). 6 finding chấp-nhận đi vào UAT-NÓI-TRƯỚC.