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

14 KiB
Raw Blame History

sub-reviewer-diff-dot4 — soi đối kháng diff đợt-4 "Danh sách phiếu toàn trình" (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 — 1321 dòng (984+/337), 1 file. Tác giả diff: frontend-designer (sub-file sub-frontend-designer-dot4.md). Vai đó CHƯA tự-review ⇒ đây là lớp review DUY NHẤT của đợt-4. Tiền lệ: đợt-1 đã soi cùng trang (sub-reviewer-diff-tongquan.md, 7 FLAG 0H/2M/5L) — mục §A của đợt-1 CÒN GIÁ TRỊ, không lặp lại; đợt này thêm trục REGRESSION so với đợt-1. 🔴 Ghi đĩa TỪNG FLAG ngay khi tìm ra (chống #53). KHÔNG batch. 🔴 Giới hạn khai trước: KHÔNG live-verify (chưa deploy, /dashboard cần login) ⇒ toàn bộ là soi TĨNH trên diff + đọc mã nguồn BE/FE/route. Mọi phát biểu về runtime là suy luận từ mã, đánh dấu rõ chỗ chưa chứng được.


0 — Tiến trình (append-only, ghi trong lúc làm)

  • [t0] Đã chạm đĩa: git diff --stat → đúng 1 file fe-user/src/pages/UserDashboardPage.tsx 984+/337. Các file M khác (docs/STATUS.md, docs/HANDOFF.md, scripts/*, .claude/commands/*, .claude/governance/*) KHÔNG thuộc vật soi (của lead). 0 file fe-admin/**. ràng buộc "fe-user ONLY" THOẢ.

  • [t1] Trục 1 (BỎ) đo xong · Trục 7 (route) đo xong · FLAG-1 chốt (xem dưới).


FLAG — ghi từng cái, ngay khi chốt

FLAG-1 [M] — bấm dòng hợp đồng trên ĐIỆN THOẠI dẫn tới ngõ cụt (mở ra danh sách, không mở ra hợp đồng)

fe-user/src/pages/UserDashboardPage.tsx:1395onOpen={id => navigate(/my-contracts?id=${id})}.

Trang đích fe-user/src/pages/contracts/MyContractsPage.tsx chỉ render nội dung hợp đồng ở Panel 2/3, mà cả hai panel đều hidden … lg:block (MyContractsPage.tsx:172:189). Dưới 1024px người dùng chỉ thấy Panel 1 là danh sách — hợp đồng vừa bấm KHÔNG mở ra. Chính trang đó đã tự biết điều này: selectContract() (MyContractsPage.tsx:59-68) có nhánh window.matchMedia('(min-width: 1024px)'), hễ hẹp hơn thì navigate('/contracts/:id') fullpage. Bảng mới trên trang chủ không có nhánh đó, nên nó phá đúng quy ước mà trang đích đặt ra.

Mức phơi nhiễm tăng thật chứ không chỉ trên lý thuyết: bảng GĐ3 giữ 3 cột (Mã HĐ · Tên hợp đồng · Trạng thái) ở mọi bề rộng (:860-866, các cột còn lại mới hidden md/lg/xl), tức dòng bấm được trên điện thoại; và nó thay cho danh sách "HĐ gần đây" 5 dòng cũ bằng danh sách phân trang đầy đủ.

Khai trung thực về mức lỗi: đây là tật kế thừa, KHÔNG phải hồi quy — bản cũ cũng navigate(/my-contracts?id=${c.id}) (dòng bị xoá trong diff). Tôi vẫn để mức M vì đợt-4 nhân diện tích chạm của nó lên nhiều lần và biến nó thành đường mở hợp đồng CHÍNH của trang chủ.

Fix đề nghị: dùng lại đúng nhánh của trang đích — nếu window.matchMedia('(min-width: 1024px)').matches thì /my-contracts?id=<id>, ngược lại /contracts/<id> (route có thật, App.tsx:60). Hoặc đơn giản hơn: luôn /contracts/<id>.

FLAG-2 [M] — cột "Hành trình" nói CHẮC một điều nó không biết: "các giai đoạn còn lại chưa tới"

fe-user/src/pages/UserDashboardPage.tsx:337-344 khai DOT_TEXT.none = 'chưa tới', rồi :378 ghép thành nhãn đọc-được của cả cụm: "Hành trình toàn trình: giai đoạn N … ; các giai đoạn còn lại chưa tới.", và :389 gắn tooltip từng chấm "Giai đoạn 1 · Duyệt NCC — chưa tới".

Hai chỗ câu đó SAI:

  1. Dòng hợp đồng (bảng GĐ3). Một hợp đồng đang ở giai đoạn 3 thì giai đoạn 1 và 2 là các giai đoạn phía trước nó, không thể "chưa tới". Người dùng rê chuột lên chấm đầu sẽ đọc "Giai đoạn 1 · Duyệt NCC — chưa tới" cho một hợp đồng đã đi qua chặng đó (hoặc ít nhất không ai biết là chưa).
  2. Dòng phiếu đã sinh hợp đồng (bảng GĐ1). PeListItem.contractId (fe-user/src/types/purchaseEvaluation.ts:126) có thật trong payload đang tải — backend chiếu thẳng x.e.ContractId vào DTO (src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs, dòng projection x.e.SelectedSupplierId, …, x.e.ContractId, …). Phiếu nào contractId != null là phiếu đã sinh hợp đồng, tức GĐ3 đã tới; vậy mà chấm GĐ3 vẫn báo "chưa tới". Không tốn thêm một request nào để biết điều này.

Tác giả có ghi lý do trong mã (:331-332): không suy diễn "có HĐ ⇒ GĐ1/GĐ2 xong" vì chưa có cầu nối per-phiếu. Lập luận đó ĐÚNG cho chiều "đã xong", nhưng câu chữ được chọn lại khẳng định chiều ngược lại — "chưa tới" cũng là một khẳng định, và là khẳng định sai. Không biết thì phải nói là không biết.

Đây đúng lớp lỗi đợt-1 đã ghi (FLAG-3 "nói quá điều dữ liệu chứng minh được"), tái phát ở chỗ mới.

Fix đề nghị: (a) đổi DOT_TEXT.none thành câu trung tính, ví dụ "chưa có thông tin ở giai đoạn này", và bỏ mệnh đề "các giai đoạn còn lại chưa tới" khỏi aria-label; (b) nếu muốn đúng hơn nữa mà vẫn 0 request: phiếu có contractId != null thì cho chấm GĐ3 mang trạng thái riêng (ví dụ "đã sinh hợp đồng").

FLAG-2 · bổ sung (ca thứ ba, cùng một lớp lỗi): hợp đồng ở phase DangDongDau(8) hoặc DaPhatHanh(9) — tức đã đóng dấu / đã phát hành, đúng phần việc mà chính trang này gọi là giai đoạn 4 "Hợp đồng cứng" (bước 19 → 21) — vẫn được contractDot (:410-416) chấm vào giai đoạn 3, còn chấm giai đoạn 4 báo "chưa tới". Bản hợp đồng cứng đã phát hành rồi mà hành trình bảo giai đoạn hợp đồng cứng chưa tới.

FLAG-3 [L] — nút phân trang tính từ DỮ LIỆU TRẢ VỀ chứ không từ trang đang chọn ⇒ bấm nhanh 2 lần chỉ đi được 1 trang

fe-user/src/pages/UserDashboardPage.tsx:611:618onClick={() => onPage(data.page - 1)} / onPage(data.page + 1), trong đó data là dữ liệu đã tải xong, không phải state page. Hai bảng đều bật placeholderData: keepPreviousData (:1151, :1163), nghĩa là trong lúc trang mới đang bay thì data vẫn là trang cũ.

Hệ quả đo được từ mã: đang ở trang 1, bấm "Sau" → setPage(2), request bay, data.page vẫn = 1. Bấm "Sau" lần nữa trước khi request về → tính ra 1 + 1 = 2setPage(2) trên state đã là 2 ⇒ không có gì xảy ra. Người dùng bấm hai lần nhưng chỉ tiến một trang, và nút "Sau"/"Trước" trong khoảng đó bật/tắt theo hasNext/hasPrev của trang cũ.

Kèm theo, cùng chỗ này có một ca hiển thị vô nghĩa: nếu tổng số bản ghi giảm trong lúc đang xem trang cuối (người khác xoá phiếu, rồi query refetch), from = (page-1)*pageSize+1 có thể lớn hơn data.total, dòng :607-608 in ra dạng "Hiển thị 4130 trong 30 phiếu" và bảng rơi vào empty-state "Chưa có phiếu Duyệt NCC nào" trong khi thực tế vẫn còn 30 phiếu.

Fix đề nghị: tính từ state — truyền page xuống Pager và dùng onPage(page + 1) / onPage(page - 1); thêm chốt if (data.page > data.totalPages && data.totalPages > 0) setPage(1) (hoặc kẹp from = Math.min(from, data.total)).


TRỤC 2 — param BE có thật không, và tab không chọn có bắn request không

Đã đọc thẳng controller, không tin lời khai:

Điều được khai Đo ở đâu Kết quả
/purchase-evaluations nhận page·pageSize·search·sortDesc·type·phase·projectId·approvalWorkflowId src/Backend/SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs:18-28 ĐÚNG cả 8
/contracts nhận page·pageSize·search·sortDesc·phase·supplierId·projectId ContractsController.cs:16-24 ĐÚNG cả 7
search PE lọc MaPhieu TenGoiThau Project.Name Project.Code PurchaseEvaluationFeatures.cs (khối if (!string.IsNullOrWhiteSpace(request.Search))) ĐÚNG — placeholder :1352 "Tìm mã phiếu, gói thầu, dự án…" mô tả đúng chừng đó, không hứa quá
search HĐ lọc MaHopDong TenHopDong Supplier.Name Project.Name ContractFeatures.cs:309-316 ĐÚNG — placeholder :1380 khớp
pendingMe KHÔNG phải param BE PurchaseEvaluationsController.cs:30-35/inbox là endpoint riêng, trả List<> phẳng ĐÚNG, và mã ghi rõ điều này ở đầu file (:33-35)
hasNext/hasPrev/totalPages do BE tính src/Backend/SolutionErp.Application/Common/Models/PagedResult.cs:9-11 CÓ THẬT (HasNext => Page*PageSize < Total, HasPrev => Page > 1) ⇒ nút biên bind thẳng là đúng

Lazy per-tab: enabled: stage === 1 (:1150) và enabled: stage === 3 (:1162). Đứng ở tab GĐ2 hoặc GĐ4 ⇒ 0 request danh sách. Đứng ở GĐ1 ⇒ chỉ dash-stage-pe bắn, dash-stage-contracts im, và ngược lại. đúng như khai.

Tổng request lúc mở trang (tab GĐ1): 5 query nền (my-dashboard, pipeline-pe-total, pipeline-contract-total, pipeline-pe-approved, pe-inbox) + 1 query danh sách = 6. Bản HEAD là 5. Tức +1 request, nhưng 2 query *-recentpageSize=5 đã rút xuống pageSize=1 nên tải về NHẸ hơn. Không phải bão request: main.tsx:7-14 đặt refetchOnWindowFocus:false + retry:1.

TRỤC 5 — hồi quy so với vòng-1 (đo HEAD ⟂ cây làm việc, không đo bằng trí nhớ)

Nền so sánh: HEAD = 766 dòng và ĐÃ chứa hero + các bản vá vòng-1 (đợt 1-3 đã commit) ⇒ diff 984+/337 này đúng là đợt-4 đứng một mình.

Thứ phải còn sống Phép đo HEAD Cây làm việc Kết luận
Bản vá FLAG-1 vòng-1 (aria-label mang tên giai đoạn) đếm group=aria-label={`${group} 6 / 1 6 / 1 CÒN NGUYÊN
Nhãn 4 GĐ chốt so từng chuỗi với components/pe/PePipelineStrip.tsx:15-19 (nguồn chuẩn) Duyệt NCC · Kế hoạch ký kết HĐ · Duyệt hợp đồng · Hợp đồng cứng KHỚP TỪNG CHỮ; nay còn gom về một mảng STAGES (:131-148) nên hero và tab không thể lệch nhau
['pe-inbox'] dùng chung + select đếm so key ⟂ hàm tải với pages/InboxPage.tsx:76-79 1 1 CÒN NGUYÊN — cùng key, cùng hàm tải trả MẢNG, chỉ select: d => d.length ở phía quan sát (:1070-1074) nên không phá cache của Hộp thư
Bản vá FLAG-2 vòng-1 (chữ "Pipeline" / "Upload") đếm "Toàn trình 4 giai đoạn" và "Tải lên bản hợp đồng" 1 / 1 1 / 1 CÒN NGUYÊN
Tiếng Việt 100% ở phần MỚI rút mọi chuỗi hiển thị mới rồi soi từng chuỗi KHÔNG lọt chữ Anh nào. (Sắp, Sắp triển khai, Trước, Sau, Trang, Hiển thị, Xoá ô tìm kiếm, Mở màn Duyệt NCC, Hành trình, (chưa cấp mã), (chưa đặt tên)… đều thuần Việt)
0 dependency mới git diff --stat -- fe-user/package.json fe-admin/package.json rỗng 0 thay đổi
Named export grep "export function UserDashboardPage" 1 1 CÒN NGUYÊN
Không đụng fe-admin git diff --name-only | grep fe-admin 0 file THOẢ
Đích điều hướng của hero liệt kê mọi navigate(...) ở HEAD ⟂ cây làm việc 8 dạng 10 dạng 8 dạng cũ giữ y nguyên, 2 dạng mới (/purchase-evaluations/new, /coming-soon?stage=N) đều là route CÓ THẬT (App.tsx:52,65)

Ghi chú về một con số thoạt nhìn giống hồi quy: chuỗi "Duyệt hợp đồng" giảm 5 → 2. Truy ra: HEAD có 1 lần ở chú thích + 1 lần title= + 3 lần group= (truyền tay cho từng dòng số liệu); nay còn 1 chú thích + 1 lần khai trong STAGES, còn group đọc gián tiếp qua STAGES[2].title. Không mất nhãn nào.

FLAG-4 [L] — người dùng trình đọc màn hình không được báo khi kết quả tìm kiếm đổi

fe-user/src/pages/UserDashboardPage.tsx:606 đặt aria-live="polite" lên dòng "Hiển thị xy trong N", nhưng chính khối chứa nó bị gỡ khỏi cây khi không có kết quả (:601if (!data || data.total === 0) return null). Nghĩa là đúng lúc cần thông báo nhất — gõ một từ khoá không khớp gì — vùng thông báo biến mất, và khối trạng thái rỗng thay thế nó (:704-731) thì không nằm trong bất kỳ vùng aria-live nào. Người dùng bàn phím/trình đọc gõ xong sẽ không nghe gì cả và không biết bảng vừa đổi.

Fix đề nghị: giữ vùng aria-live="polite" luôn tồn tại (khi rỗng thì thông báo "Không tìm thấy phiếu nào khớp"), hoặc gắn role="status" cho khối trạng thái rỗng.