Files
solution-erp/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/sub-frontend-designer-dot4.md
2026-07-29 13:25:28 +07:00

14 KiB
Raw Blame History

sub-frontend-designer-dot4 — Đợt-4 redesign UserDashboardPage.tsx (fe-user ONLY)

Run: 2026-07-29-S159-tong-quan-pipeline-menu · vai: frontend-designer · file đích: fe-user/src/pages/UserDashboardPage.tsx 🔴 GHI ĐĨA TRONG LÚC LÀM (chống #53). Mỗi mục = 1 bước ĐÃ XONG, ghi ngay khi xong. 🔴 NO-SCREENSHOT — spawn này không có dev-server/Playwright + trang sau login (ProtectedRoute) ⇒ FD2 visual-loop KHÔNG chạy được. Thay bằng (i) npm run build PASS + tail, (ii) tự-review tĩnh theo rubric FD4. KHÔNG bịa screenshot.


1 — Khảo sát file HIỆN HÀNH (đọc đĩa, KHÔNG dùng bản nhớ)

fe-user/src/pages/UserDashboardPage.tsx766 dòng (không phải 309 như đợt-1; đã qua review + 3 đợt sửa nhãn). Trạng thái TRƯỚC đợt-4:

  • Comment đầu file đã ghi nhãn 4 GĐ bản sửa mới nhất: GĐ1 "Duyệt NCC" (bước 1→6, CEO chốt) · GĐ2 "Kế hoạch ký kết HĐ" (7→12) · GĐ3 "Duyệt hợp đồng" (13→18) · GĐ4 "Hợp đồng cứng" (19→21). ⇒ Tab đợt-4 phải echo ĐÚNG 4 tên này (không lấy tên đợt-1 "Đề xuất ký kết hợp đồng"/"Ký cứng & phát hành").
  • PageHeader title "Tổng quan tất cả luồng duyệt".
  • HERO pipeline 4 GĐ (dòng 403-538) — GIỮ (owner không khoanh).
  • Khối BỊ KHOANH BỎ: <section> "Việc của tôi" 5 StatCard (541-582) + grid xl:grid-cols-2 chứa "HĐ gần đây" (587-673) + "Phiếu Duyệt NCC gần đây" (678-762).
  • 5 query đang có: ['my-dashboard'] · ['my-contracts-recent'] (/contracts?page=1&pageSize=5) · ['my-pe-recent'] (/purchase-evaluations?page=1&pageSize=5) · ['pipeline-pe-approved'] (phase=7, pageSize=1 lấy .total) · ['pe-inbox'] (endpoint /inbox mảng phẳng, dùng select: d => d.length để KHÔNG vỡ cache dùng chung với InboxPage).
  • Component local tái dùng được: STAGE_TONE (brand/teal), StageCard, StageArrow, StageStatRow, StageLink, StageSoon, SectionTitle, fmtMoney, StatCard+STAT_TONE.
  • Gotcha CSS đã ghi sẵn trong file: h1..h4, .stat-value, .icon-chip KHÔNG nằm trong @layer ⇒ thắng utility Tailwind v4, muốn đổi màu heading phải dùng !.

2 — Khảo sát BE (xác nhận PARAM THẬT, không chế)

src/Backend/SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs:18-35

[HttpGet] List(page=1, pageSize=20, search=null, sortDesc=true, type=null, phase=null,
                projectId=null, approvalWorkflowId=null) -> PagedResult<PurchaseEvaluationListItemDto>
[HttpGet("inbox")] Inbox(type=null, approvalWorkflowId=null) -> List<...>   // MẢNG PHẲNG, không paging

src/Backend/SolutionErp.Api/Controllers/ContractsController.cs:16-28

[HttpGet] List(page=1, pageSize=20, search=null, sortDesc=true, phase=null,
                supplierId=null, projectId=null) -> PagedResult<ContractListItemDto>
[HttpGet("inbox")] Inbox() -> List<...>

CẢ paging LẪN search đều có thật ở cả 2 endpoint ⇒ yêu cầu 3 (phân trang thật + ô tìm kiếm) làm được ĐẦY ĐỦ, KHÔNG phải cắt.

Phạm vi search (đọc handler, để đặt placeholder trung thực):

  • PE — PurchaseEvaluationFeatures.cs:624-632: MaPhieu TenGoiThau Project.Name Project.Code.
  • HĐ — ContractFeatures.cs:309-316: MaHopDong TenHopDong Supplier.Name Project.Name. ⇒ placeholder PE = "Tìm mã phiếu, gói thầu, dự án…" · HĐ = "Tìm mã HĐ, tên HĐ, nhà cung cấp, dự án…" (đúng cái BE thật lọc, không hứa quá).

Paged<T> (fe-user/src/types/master.ts) = {items,total,page,pageSize,totalPages,hasNext,hasPrev} — có hasNext/hasPrev ⇒ nút Trước/Sau bind thẳng, không tự tính.

⚠️ Nhắc lại đính chính đợt-1 (vẫn đúng): KHÔNG có query-param pendingMe ở BE — đó là URL-param của trang list, map sang endpoint /inbox.

2b — Tài sản có sẵn (FD6 chống reinvent — đã đọc, KHÔNG suy đoán)

Thứ Đường dẫn Dùng vào
PePipelineStrip components/pe/PePipelineStrip.tsx NGUỒN TÊN 4 GĐ CANONICAL (Duyệt NCC / Kế hoạch ký kết HĐ / Duyệt hợp đồng / Hợp đồng cứng) — tab đợt-4 lấy tên khớp file này, KHÔNG chép tay
PeFinalizeChip components/pe/PeFinalizeChip.tsx cột Trạng thái GĐ1 (tái dùng như list cũ)
PeUrgentChips components/pe/PeUrgentChips.tsx cờ GẤP PRO/CCM cạnh mã phiếu (có sẵn trên PeListItem, 0 request thêm)
PhaseBadge · EmptyState · Button · Input components/… bảng GĐ3 · empty/error · nút · ô tìm
getPeDisplayStatus + PeDisplayStatus{Label,Color} types/purchaseEvaluation.ts chip trạng thái GĐ1
ComingSoonPage pages/ComingSoonPage.tsx nhận `?stage=2
Paged.hasNext/hasPrev/totalPages types/master.ts nút Trước/Sau bind thẳng, KHÔNG tự tính
keepPreviousData @tanstack/react-query v5.99.2 (node-verify: typeof === function) phân trang không nháy trắng

Route đích đã verify trong App.tsx: /purchase-evaluations/:id (mở phiếu — đúng cách list "gần đây" cũ navigate) · /my-contracts?id=<guid> (MyContractsPage.tsx:30 đọc searchParams.get('id')) · /contracts/new · /coming-soon. Icon lucide đã node-verify 26 export (Search, ChevronLeft/Right, ListChecks, SearchX, Plus, X… ) — tránh bẫy alias-dodge.

3 — Quyết định design (chốt TRƯỚC khi code)

Xoá: section "Việc của tôi" (5 StatCard) + grid 2 list "HĐ gần đây"/"Phiếu Duyệt NCC gần đây" — đúng khối owner khoanh. Kéo theo: StatCard/STAT_TONE/fmtMoney?/SlaTimer/Pencil,Inbox,Clock,Coins không còn dùng ⇒ phải gỡ import (TS noUnusedLocals sẽ chặn build nếu để lại). fmtMoney GIỮ (bảng GĐ3 hiện cột Giá trị).

Thêm: 1 section Danh sách phiếu toàn trình = tabs (tầng liên kết 1) → bảng (tầng liên kết 2 = cột Hành trình).

  1. Single source STAGES — 1 mảng const cấp module (n / title / stepRange / icon / tone / soon) cho CẢ hero LẪN tab. Lý do: nhãn 4 GĐ đã drift qua 3 đợt sửa; hero + tab chép tay = drift lần 4. Hero giữ nguyên JSX StageCard, chỉ đổi title/stepRange sang đọc từ STAGES ⇒ echo tên là ĐÚNG do cấu trúc, không do cẩn thận.
  2. Tab role="tablist" + 4 role="tab" (roving tabindex, ←/→/Home/End, aria-selected, aria-controls), panel role="tabpanel" + aria-labelledby. KHÔNG chèn mũi tên giữa các tab (con của tablist phải là tab) — mạch "toàn trình" đã do hero ngay trên gánh + mỗi tab mang eyebrow "Giai đoạn N" khớp badge hero.
  3. Màu tab active = tone của GĐ đó (GĐ1 brand, GĐ3 teal, GĐ2/4 slate) ⇒ bảng bên dưới ăn cùng accent → mắt nối được "ô hero ↔ tab ↔ bảng". Contrast (tự tính, không phỏng):
    • bg-brand-700 #1b6aa3 + chữ trắng = 5.77:1 ✓ (KHÔNG dùng brand-600 = 4.41:1 — chính gotcha đã ghi trong file, dòng 409-410)
    • bg-teal-700 #0a7170 + trắng = 5.82:1 ✓ · bg-slate-600 + trắng ≈ 7:1 ✓
    • Badge count trên tab active = pill trắng chữ text-brand-800 (7.75:1) thay vì bg-white/20 (3.85:1 ✗ — đã loại).
  4. Cột Hành trình = JourneyDots (4 chấm + gạch nối): chấm của CHÍNH GĐ dòng đó mang trạng thái, 3 chấm còn lại bg-slate-200 mờ + title tên GĐ. Map trạng thái (mở rộng đúng tinh thần owner, không bịa nghĩa): Đã duyệtđầy (tone-600) · Đã gửi duyệtnửa (linear-gradient 50% inline, viền tone-500) · Cần chỉnh sửa lạinửa amberx · Nháprỗng (viền slate-300) · Từ chốiđầy accent-500 (đỏ) + tooltip "dừng tại GĐ này". a11y: bọc role="img" + 1 aria-label kể trọn hành trình; từng chấm aria-hidden + title (tooltip chuột). Không bắt screen-reader đọc 4 chấm rời. GĐ3 dùng CÙNG component (chấm 3 sáng, 1/2/4 mờ) — KHÔNG suy diễn "HĐ tồn tại ⇒ GĐ1/2 xong" (chưa có cầu nối per-phiếu; PePipelineStrip.tsx:8-9 nói rõ).
  5. Row click: <tr onClick> (chuột bấm đâu cũng được) + trong ô Mã có <button> THẬT (stopPropagation) làm điểm focus bàn phím ⇒ WCAG 2.1.1 đủ, KHÔNG lồng button-trong-row-button, không div role=row onClick câm.
  6. Responsive (không tràn ngang 375): ô "Gói thầu"/"Tên HĐ" dùng w-full max-w-0 truncate (ăn hết chỗ thừa rồi cắt) + dòng meta lg:hidden gộp Dự án · Ngày; cột Dự án hidden lg:table-cell, Người soạn hidden xl:table-cell, Ngày hidden md:table-cell, Hành trình hidden md:table-cell. Mobile còn 3 cột thật (Mã · Gói thầu+meta · Trạng thái).
  7. Request: list lazy theo tab (enabled: stage===1 / stage===3) ⇒ GĐ2/GĐ4 bắn 0 request. 2 query *-recent (pageSize 5) đổi thành *-total pageSize=1 (hero chỉ cần .total) ⇒ tổng vẫn 5 query nền + 1 list = 6, payload NHỎ HƠN bản cũ. ['pe-inbox'] GIỮ NGUYÊN key+queryFn+select (ràng buộc owner + cache chung InboxPage).
  8. Search debounce 300ms (useEffect+setTimeout, 0 dep mới), đổi search/tab → reset page=1. Placeholder ghi ĐÚNG cái BE lọc.

4 — File đã sửa (CHỈ 1 file, fe-user ONLY, 0 dep mới)

D:\Dropbox\CONG_VIEC\SOLUTION\SOLUTION_ERP\fe-user\src\pages\UserDashboardPage.tsx — sửa TẠI CHỖ (1 atomic Write cả file, chống truncate #53). Named export UserDashboardPage GIỮ ⇒ route /dashboard + sidebar không phải đụng. 766 → 1.130 dòng.

GỠ (đúng khối owner khoanh đỏ)

Gỡ Kéo theo phải gỡ (TS noUnusedLocals sẽ chặn build nếu để lại)
<section> "Việc của tôi" — 5 StatCard (HĐ đang soạn / Chờ tôi duyệt / Sắp quá hạn / Đã quá hạn / Tổng giá trị nháp) StatCard, STAT_TONE, icon Pencil Inbox Clock Coins, CheckCircle2
<section> "HĐ gần đây" (list 5) SlaTimer import
<section> "Phiếu Duyệt NCC gần đây" (list 5) SectionTitle
query ['my-contracts-recent'] pageSize 5 → đổi thành ['pipeline-contract-total'] pageSize 1 (hero chỉ cần .total)
query ['my-pe-recent'] pageSize 5 → đổi thành ['pipeline-pe-total'] pageSize 1

THÊM

  • STAGES: StageMeta[]single source 4 giai đoạn (n/title/stepRange/icon/tone/soonDesc). Hero (StageCard) đổi signature nhận stage: StageMeta thay 5 prop chuỗi rời ⇒ tab & hero KHÔNG THỂ lệch nhãn. Text hero giữ nguyên văn (soonDesc chép đúng câu đang chạy) — owner đã duyệt hero, không đổi chữ.
  • JourneyDots + DOT_TEXT + dotVisual + peDot() + contractDot() — cột Hành trình.
  • TAB_TONE + StageTab — thanh tab 4 GĐ.
  • ListToolbar (ô tìm + nút "Mở màn …") · TableSkeleton (colSpan, không lệch cột ẩn) · Pager (Trước/Sau + "Hiển thị xy trong N", aria-live=polite).
  • PeTable (7 cột: Mã phiếu · Gói thầu · Dự án · Người soạn · Ngày · Trạng thái · Hành trình) · ContractTable (7 cột: Mã HĐ · Tên HĐ · Loại · Giá trị · Ngày · Trạng thái · Hành trình) · SoonPanel (GĐ2/GĐ4).
  • 2 query list LAZY: ['dash-stage-pe',{page,pageSize,search}] (enabled: stage===1) · ['dash-stage-contracts',{…}] (enabled: stage===3), cả 2 placeholderData: keepPreviousData (đổi trang không nháy trắng) + aria-busy + mờ 60% khi isFetching.
  • State: stage · page · searchInput/search (debounce 300ms) · tabRefs (roving focus).

GIỮ NGUYÊN 100% (không đụng)

PageHeader title/description · toàn bộ HERO (4 StageCard + StageArrow + StageStatRow + StageLink + StageSoon + STAGE_TONE + banner lỗi + retryFailed) · query ['my-dashboard'] · ['pipeline-pe-approved'] · ['pe-inbox'] (key + queryFn + select: d => d.length y nguyên — cache dùng chung InboxPage) · fmtMoney · mọi target navigate() của hero.

Route đích của bảng (đều là route THẬT trong App.tsx)

onOpen GĐ1 → /purchase-evaluations/:id (đúng cách list "gần đây" cũ navigate) · onOpen GĐ3 → /my-contracts?id=<guid> (đúng cách cũ) · onCreate/purchase-evaluations/new · /contracts/new (CTA "Tạo HĐ mới" giữ ở empty-state GĐ3 như owner dặn) · SoonPanel/coming-soon?stage=2|4.

5 — [LEAD GHI ON-BEHALF @13:2x — vai chết #53 đúng lúc verify CSS, đĩa giữ trọn §1-§4]

  • npm run build fe-user: PASS 419ms (lead chạy độc lập SAU khi vai chết — luật #68).
  • 🔴 Nỗi lo CSS-token của vai (Tailwind v4 drop class không quét được): VERIFY SẠCH — grep pattern class-ghép-động (`...-${...}` trong className) = 0 hit; mọi biến thể màu đi qua lookup-map TĨNH (STAGE_TONE:158 · DOT_TEXT:337 · TAB_TONE:422) = literal đầy đủ trong source ⇒ Tailwind quét được hết. divide-slate src=2/css=1 (present).
  • Việc vai CHƯA kịp: tự-review rubric FD4 → chuyển cho reviewer adversarial (spawn kế tiếp) — không bịa hộ.