Files
solution-erp/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/sub-reviewer-diff-tongquan.md
2026-07-29 11:44:52 +07:00

16 KiB
Raw Blame History

sub-reviewer-diff-tongquan — soi đối kháng diff fe-user/src/pages/UserDashboardPage.tsx (S159 L8)

Run: 2026-07-29-S159-tong-quan-pipeline-menu · vai: reviewer (adversarial, READ-only) Vật soi: git diff -- fe-user/src/pages/UserDashboardPage.tsx — 728 dòng (585+/143), 1 file. 🔴 Ghi đĩa TRONG LÚC LÀM (chống #53). Mỗi FLAG ghi ngay khi tìm ra, không đợi cuối. 🔴 Giới hạn đã khai: KHÔNG live-verify (route /dashboard cần login, FE chưa deploy) ⇒ toàn bộ kết luận là soi TĨNH trên diff + đọc file nguồn BE/FE + đọc route/type. Mọi phát biểu về hành vi runtime là suy luận từ mã, đã đánh dấu rõ chỗ nào chưa chứng được.


0 — Xác nhận phạm vi (đã chạm đĩa, không tin lời khai)

  • git diff --statđúng 1 file fe-user/src/pages/UserDashboardPage.tsx, 585+/143. khớp lệnh.
  • git status --porcelain → các file M khác (WAL.md, docs/STATUS.md, docs/HANDOFF.md, scripts/*, .claude/commands/*) không thuộc vật soi, đúng như FD khai là của lead. 0 file fe-admin/** bị chạm. ràng buộc "fe-user ONLY" THOẢ.
  • Named export UserDashboardPage giữ nguyên (dòng context không đổi trong hunk @@ -113,20 +351,180 @@ phía trên).
  • 0 dependency mới (chỉ thêm icon lucide-react đã có sẵn).

A — Những claim của FD tôi đã ĐỘC LẬP kiểm và XÁC NHẬN ĐÚNG (không phải tin lời)

Claim Kiểm bằng Kết quả
Paged<T>total, khôngtotalCount fe-user/src/types/master.ts:1-9 ĐÚNG — lead brief ghi totalCountSAI, FD đính chính đúng
phase là query-param BE thật PurchaseEvaluationsController.cs:23 [FromQuery] PurchaseEvaluationPhase? phase
PurchaseEvaluationPhase.DaDuyet === 7 fe-user/src/types/purchaseEvaluation.ts:28
pendingMe KHÔNG phải query-param BE; /inbox trả mảng phẳng PurchaseEvaluationsController.cs:30-35 trả List<...> (không PagedResult) — dùng .length là đúng
/reports/my-dashboard chỉ đếm bảng Contracts ⇒ gán GĐ3 đúng nghĩa GetMyDashboardQuery.cs — mọi query đi từ db.Contracts
.icon-chip có size nội tại 2.25rem ⇒ bỏ h-9 w-9 là no-op index.css:128-137
h1..h4 + .stat-value là rule unlayered ⇒ phải dùng ! để đè màu index.css:79-83 + :140-146 — cả 2 set color, KHÔNG set font-size và FD áp ! ĐÚNG CHỖ: text-brand-800! / text-brand-700! / text-accent-600! / text-slate-600!. text-base / text-[15px] không cần ! vì 2 rule đó không set font-size — tôi đã đọc để bác giả thuyết "chữ phình to".
Mọi stop màu dùng đều CÓ THẬT trong @theme index.css:7-51 — brand 50-900 · teal 50/100/500/600/700 · violet · amberx · accent 500/600 0 stop ma (không có -800 của teal/violet/amberx nào bị dùng)
.label-eyebrow = brand-600 chỉ 4.42:1 tự tính lại luminance từ #1f7dc1 4.41 — số FD khai đo được và đúng, né sang text-brand-700 là hợp lý
slate-500 = 4.76:1 (FD ghi 4.8) tự tính từ #64748b
Buttonsize="sm" + gap-1.5 sẵn ⇒ bỏ mr-1 là đúng fe-user/src/components/ui/button.tsx
1 file, 0 file fe-admin/**, 0 dep mới, named export giữ git diff --stat + đọc file

Giả thuyết tôi TỰ ĐẶT RA rồi TỰ BÁC (ghi lại để khỏi ai đào lại):

  • "Số GĐ1 là toàn hệ thống còn GĐ3 là của riêng tôi ⇒ lệch phạm vi, user đọc sai tỷ lệ rơi rụng"BÁC: cả 2 endpoint đều IDOR-scoped per-user (PurchaseEvaluationFeatures.cs:596-614 + ContractFeatures.cs:297-303). Cùng phạm vi ⇒ so sánh được.
  • "Chờ tôi duyệt GĐ1 = inbox.length sẽ LỚN HƠN số ở màn đích vì màn đích lọc thêm client-side DaGuiDuyet" (PurchaseEvaluationsListPage.tsx:211-212) → BÁC: BE inbox chỉ trả phase ∈ {ChoPurchasing, ChoDuAn, ChoCCM, ChoCEODuyetPA, ChoCEODuyetNCC, ChoDuyet} (PurchaseEvaluationFeatures.cs:731-742,762), tất cả đều map về DaGuiDuyet (purchaseEvaluation.ts:105-110) ⇒ bộ lọc client là no-op, 2 số khớp nhau.
  • "?phase=7 / ?pendingMe=1 là URL bịa, màn đích không đọc"BÁC: PurchaseEvaluationsListPage.tsx:37,45,95 đọc cả 2. Route /purchase-evaluations, /my-contracts, /inbox đều có thật trong App.tsx:50,58,59.
  • "queryKey mới đè cache trang list"BÁC: trang list dùng ['pe-list', {...}] / ['pe-detail', id]; key mới là ['pipeline-pe-approved'] / ['pipeline-pe-inbox'] — không giao.
  • "bắn request bão"BÁC: main.tsx:7-14 refetchOnWindowFocus:false, retry:1; +2 request đúng trần ≤3.
  • "chia 0 / NaN khi rỗng"BÁC: diff không có phép chia nào; value ?? 0 chặn undefined.
  • "skeleton che cả pipeline"BÁC: loading là per-row (StageStatRow), khung 4 GĐ luôn render.

B — FLAG

FLAG-1 [M] — a11y AA: hai nút khác đích nhưng TRÙNG tên đọc được

fe-user/src/pages/UserDashboardPage.tsx:449 (GĐ1 → /purchase-evaluations?pendingMe=1) và :495 (GĐ3 → /inbox) đều là <button> có tên khả truy cập y hệt nhau: "Chờ tôi duyệt". Thêm nút thứ ba cùng nhãn ở KPI (:538). Ngữ cảnh phân biệt (tiêu đề "Duyệt NCC" / "Duyệt Hợp đồng") chỉ tồn tại bằng thị giác<h3> của thẻ giai đoạn (:153) không được liên kết chương trình với nhóm nút, nên người dùng trình đọc màn hình duyệt theo danh sách nút sẽ thấy 2-3 mục trùng tên dẫn tới 3 nơi khác nhau. Spec yêu cầu a11y AA nên đây là khoản nợ thật, không phải chuyện thẩm mỹ. Fix đề nghị: thêm aria-label đầy đủ cho StageStatRow (ví dụ aria-label={${title} — ${label}}), hoặc bọc cụm số liệu bằng role="group" + aria-labelledby trỏ vào id của <h3> giai đoạn.

FLAG-2 [M] — lọt chữ tiếng Anh ra giao diện (ràng buộc "100% tiếng Việt")

Tôi rút toàn bộ chuỗi hiển thị MỚI từ diff (git diff | grep '^+' | grep -oE '>...<|label="..."|title="..."|description="..."') rồi soi từng chuỗi. Có đúng 2 chuỗi lọt tiếng Anh:

  • :405<h2>Pipeline 4 giai đoạn · 21 bước</h2>: "Pipeline". Nó lại nằm ngay dưới dòng eyebrow "Quy trình mua sắm → ký kết hợp đồng" (:402-404) nên vừa là tiếng Anh vừa lặp ý.
  • :520description="Upload bản hợp đồng đã ký cứng.": "Upload".

Mọi chuỗi còn lại (14 nhãn/tiêu đề) đều thuần Việt, kể cả "Giai đoạn {n}", "Sắp triển khai", các thông báo lỗi. Lưu ý bối cảnh: "upload các file đã ký cứng" là chữ owner dùng trong lời thoại gốc (run.md:10), nên đây là câu hỏi biên tập cho owner chứ không phải implementer làm sai ý; còn "Pipeline" thì owner chỉ nói trong lời trao đổi nội bộ (run.md:14 — "Pipeline 4 GĐ đầy đủ"), không phải chốt nhãn hiển thị. Fix đề nghị: :405 → "Quy trình 4 giai đoạn · 21 bước" (bỏ luôn trùng ý với eyebrow); :520 → "Tải lên bản hợp đồng đã ký cứng." — hoặc owner xác nhận giữ nguyên chữ "Upload".

FLAG-3 [L] — câu "sẵn sàng nối tiếp" ở GĐ2 nói QUÁ điều dữ liệu chứng minh được

:467-471 — dòng gợi ý của GĐ2 in ${peApproved} phiếu Duyệt NCC đã duyệt sẵn sàng nối tiếp, trong đó peApprovedtổng mọi phiếu phase=DaDuyet. Nhưng phiếu đã duyệt có thể ĐÃ sinh hợp đồng rồi: entity có sẵn cột PurchaseEvaluation.ContractId (src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs:31"FK Contracts — set khi user gen HĐ từ phiếu"), và endpoint POST /purchase-evaluations/{id}/contracts-from-evaluation (PurchaseEvaluationsController.cs:340) chính là đường sinh ra nó. Vậy con số đang gộp cả phiếu "đã nối tiếp xong" vào nhóm "sẵn sàng nối tiếp" — càng dùng lâu số càng phồng và càng sai nghĩa. Không thể lọc bằng endpoint hiện có (spec cấm thêm endpoint), nên cách rẻ nhất là sửa câu chữ. Fix đề nghị: đổi thành "{n} phiếu Duyệt NCC đã duyệt" (bỏ mệnh đề "sẵn sàng nối tiếp"), hoặc chờ GĐ2 có thật rồi thêm tham số lọc hasContract=false ở BE.

FLAG-4 [L] — bấm số ở GĐ3 sẽ ra màn hình có số KHÁC (lệch có sẵn từ trước, nay bị nhân đôi độ nổi bật)

Hai ô của GĐ3 lấy số từ /reports/my-dashboard nhưng lại dẫn sang /inbox — hai nguồn đếm theo hai luật khác nhau:

  • :495-501 "Chờ tôi duyệt" = pendingMyApproval, handler loại trừ hợp đồng do chính tôi soạn (GetMyDashboardQuery.cseligiblePhases.Contains(c.Phase) && c.DrafterUserId != userId). Còn /contracts/inbox không loại trừ (ContractFeatures.cs:391-401 chỉ lọc eligiblePhases.Contains(c.Phase)), lại còn .Take(100).
  • :503-510 "Đã quá hạn" = overdue tính trên hợp nhất hợp đồng tôi soạn hợp đồng chờ tôi duyệt; phần "tôi soạn" không bao giờ xuất hiện trong hộp thư đến.

Nghĩa là số 3 trên trang chủ có thể mở ra hộp thư 7 dòng, hoặc 0 dòng quá hạn. Đây là lệch có sẵn trước diff (thẻ KPI cũ :542, :557 cũng trỏ /inbox), nên tôi không tính là hồi quy — nhưng hero vừa nâng nó lên vị trí nổi nhất trang và lặp lại đúng con số đó hai lần trên cùng một màn. Fix đề nghị: hoặc đổi đích sang /my-contracts cho ô "Đã quá hạn", hoặc để hộp thư nhận tham số lọc (/inbox?overdue=1) — nếu chưa làm ngay thì ghi vào backlog, đừng để trôi.

FLAG-5 [L] — cùng một request nhưng hai khoá cache, thay vì dùng lại khoá đã có

:367-370 tạo queryKey: ['pipeline-pe-inbox'] cho GET /purchase-evaluations/inbox. Nhưng fe-user/src/pages/InboxPage.tsx:77-78 đãqueryKey: ['pe-inbox'] gọi đúng endpoint đó, đúng bộ tham số đó (không tham số). Hai khoá khác nhau ⇒ hai bản cache độc lập cho cùng một tài nguyên: mỗi lần đi Trang chủ ↔ Hộp thư đều bắn thêm một request, và hai màn có thể hiển thị hai ảnh chụp khác thời điểm. Yêu cầu "queryKey mới không đè cache trang list" vẫn thoả (['pe-list', …] không bị đụng) — đây là chuyện tiết kiệm và nhất quán, không phải vi phạm spec. Fix đề nghị: dùng lại ['pe-inbox'] cho ô "Chờ tôi duyệt" của GĐ1 ⇒ số request thêm rơi từ +2 xuống +1 và hai màn luôn khớp nhau.

FLAG-6 [L] — báo lỗi hiện hai lần cho cùng một sự cố

:409-420 băng cảnh báo hasError gộp cả 5 query, trong đó có recentrecentPe. Khi đúng một danh sách hỏng, người dùng thấy đồng thời băng vàng ở hero ("Không tải được một vài số liệu") khối lỗi riêng của danh sách đó (:559-573 cho hợp đồng, phần tương ứng cho phiếu), mỗi chỗ một nút "Thử lại". Không sai chức năng — retryFailed chỉ gọi lại đúng query hỏng — nhưng là nhiễu. Tự đính chính trong lúc viết flag này: tôi định đề nghị "bỏ recent/recentPe khỏi hasError", nhưng đọc lại thì sairecent.data.total nuôi ô "Tổng hợp đồng" (:488) và recentPe.data.total nuôi ô "Tổng phiếu" (:434), tức hai query đó thực sự thuộc hero. Băng cảnh báo phủ chúng là ĐÚNG. Fix đề nghị (đã sửa theo đúng sự thật): giữ nguyên hasError, chỉ bỏ bớt một trong hai nút "Thử lại" trùng nhau — hoặc chấp nhận trùng vì nó vô hại. Mức L, không chặn commit.

FLAG-7 [L] — GĐ4 gắn nhãn "Sắp triển khai" trong khi phần khung của nó đã chạy thật (việc của owner quyết, không phải lỗi người làm)

:519-521 đánh GĐ4 "Ký cứng & phát hành (Bước 19 → 21)" là chưa triển khai. Trên thực tế module Hợp đồng đã có sẵn hai phase cuối DangDongDau(8) → DaPhatHanh(9) cùng chức năng đính kèm bản scan đã ký (POST /contracts/{id}/attachments). Người dùng đang dùng hai phase này mỗi ngày có thể ngơ ngác khi trang chủ bảo phần đó "sắp triển khai". Diff khớp đúng spec ở điểm này — owner tự chốt GĐ4 nằm ngoài hệ thống (run.md:10 và bảng run.md:26: "ngoài hệ thống — upload file đã ký (Contract attachments sẵn nền); thiết kế sau") — nên tôi ghi nhận là điểm cần owner biết, không tính là implementer làm sai. Fix đề nghị: nếu owner muốn tránh hiểu nhầm, đổi mô tả GĐ4 thành "Hiện làm ngoài hệ thống — đính kèm bản đã ký vào hồ sơ hợp đồng" thay vì chỉ badge "Sắp triển khai".


C — Quan sát KHÔNG tính FLAG (ghi để khỏi bị đào lại)

  • Mũi tên nối text-slate-400 (~2,6:1):165-166: KHÔNG vi phạm AA vì cả hai icon đều aria-hidden và thứ tự giai đoạn đã được mã hoá bằng badge chữ "Giai đoạn 1..4"; đây là đồ hoạ trang trí thuần tuý.
  • Nợ FD tự khai (chiều cao dải pipeline ở tablet 7681023px; số đỏ "quá hạn" nằm trong thẻ teal): tôi không phản bác được bằng cách soi tĩnh — cần mắt người trên prod. Ghi nhận là nợ đã khai minh bạch, không phải giấu.
  • Trùng số liệu hero ⟂ KPI: "Chờ tôi duyệt" và "Đã quá hạn" nay xuất hiện hai lần trên một màn. Owner đã yêu cầu giữ KPI cũ nên đây là hệ quả cố ý, không phải sơ suất.

D — Kết luận

Verdict: PASS_WITH_FLAGS — 7 FLAG (0 H / 2 M / 5 L). Không có gì chặn commit.

Bốn trục soi được lệnh giao, kết quả từng trục:

Trục Kết quả Căn cứ
Đúng-spec (4 nhãn + ranh bước) ĐẠT GĐ2 đúng nguyên văn "Đề xuất ký kết hợp đồng" (:464, khớp run.md:13); ranh 1→6 / 7→12 / 13→18 / 19→21 đúng từng chữ; tổng 21 bước cộng lại khớp. GĐ2/GĐ4 dùng badge xám nét đứt + biểu tượng đồng hồ cát, KHÔNG dùng đỏ/vàng ⇒ đọc ra "kế hoạch", không đọc nhầm thành lỗi.
Data đúng ĐẠT phase=7 đúng là Đã-duyệt; pendingMe được xử lý đúng bản chất (endpoint riêng trả mảng, không phải query-param); đọc total đúng tên field của Paged<>; không có phép chia nào nên không sinh NaN; skeleton chỉ nằm trong từng dòng số, khung 4 giai đoạn luôn hiện.
Không hồi quy KPI cũ ĐẠT Đủ 5 trường MyDashboardDto (:532, :539, :547, :555, :562) + đủ 2 danh sách gần đây, lại còn thêm nhánh báo lỗi mà bản cũ thiếu.
Vệ sinh query / TS / React ĐẠT Khoá cache mới không đụng khoá cũ; +2 request (trần 3); refetchOnWindowFocus:false nên không bão request; named export UserDashboardPage giữ nguyên; 0 import từ fe-admin; 0 import thừa (tôi đối chiếu từng icon với chỗ dùng).

Kiểm chứng độc lập tôi tự chạy (không tin lời khai của FD): npx tsc --noEmit -p tsconfig.app.json trong fe-userexit 0, output 0 byte. Đây là bản chạy riêng của tôi, không dùng lại kết quả build 506 ms mà lead đã chạy.

Giới hạn phải khai: không live-verify được (route /dashboard đòi đăng nhập, FE chưa deploy). Mọi kết luận về bố cục, tương phản thực tế và hành vi runtime là suy luận từ mã nguồn, chưa có một khung hình nào được nhìn bằng mắt.