Files
solution-erp/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/sub-frontend-designer-tongquan.md
2026-07-29 11:27:32 +07:00

15 KiB
Raw Blame History

sub-frontend-designer-tongquan — T1 FE "Tổng quan tất cả luồng duyệt" (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 — môi trường spawn này KHÔNG có dev-server/Playwright (FD2 visual loop KHÔNG chạy được). Thay bằng: (i) npm run build fe-user PASS + tail output · (ii) tự-review tĩnh theo rubric FD4. KHÔNG bịa screenshot.


1 — Khảo sát file đích (ĐÃ ĐỌC, không suy đoán)

fe-user/src/pages/UserDashboardPage.tsx — 309 dòng, named export UserDashboardPage. Trạng thái TRƯỚC khi sửa:

  • Header: <PageHeader title={"Xin chào, " + fullName} description="Tổng quan công việc HĐ của bạn — click vào card để xem chi tiết." />
  • 3 query TanStack (đúng như lệnh mô tả):
    • ['my-dashboard']GET /reports/my-dashboardMyDashboard {draftsInProgress, pendingMyApproval, dueSoon, overdue, draftsTotalValue} (type khai LOCAL trong file, dòng 25-31).
    • ['my-contracts-recent']GET /contracts params {page:1,pageSize:5}Paged<ContractListItem>.
    • ['my-pe-recent']GET /purchase-evaluations params {page:1,pageSize:5}Paged<PeListItem>.
  • Section hiện có: (a) "Của tôi" = grid 5 StatCard (local component, card-accent + icon-chip + stat-value, tone map STAT_TONE 6 tone dùng CSS var --color-*); (b) "HĐ gần đây" list 5; (c) "Phiếu Duyệt NCC gần đây" list 5 (có PeFinalizeChip + PE display-status pill).
  • Helper local: fmtMoney (tỷ/tr/locale). Import sẵn: PhaseBadge, SlaTimer, EmptyState, Button, cn, ContractTypeLabel, PE display-status helpers.

⇒ Kế hoạch: GIỮ toàn bộ 3 query + 5 KPI + 2 list (owner: "đừng vứt"), THÊM hero pipeline 4 GĐ ở trên cùng, đổi PageHeader title.

2 — Data thật cho pipeline (đọc trước khi dùng)

  • PeListItem (fe-user/src/types/purchaseEvaluation.ts) có phase; PurchaseEvaluationPhase.DaDuyet = 7, ChoDuyet = 10, TraLai = 98, TuChoi = 99 — khớp lệnh "phase=7 = Đã duyệt".

🔴 2 ĐÍNH CHÍNH so với brief của lead (đã đọc đĩa, KHÔNG tin liệt kê)

  1. Paged<T> KHÔNG có totalCount — field tên là total. fe-user/src/types/master.ts:1-9{items, total, page, pageSize, totalPages, hasNext, hasPrev}; khớp BE PagedResult.cs:3-7 (Total).
  2. pendingMe KHÔNG phải query-param của GET /purchase-evaluations — nó là URL-param của trang list, và trang list map nó sang endpoint KHÁC: GET /purchase-evaluations/inbox trả mảng phẳng PeListItem[] (không paging) — PurchaseEvaluationsListPage.tsx:81-89 + PurchaseEvaluationsController.cs:30-35. ⇒ Đếm "chờ tôi duyệt" = inbox.length, KHÔNG dùng được pageSize=1. phase THÌ ĐÚNG là query-param BE (PurchaseEvaluationsController.cs:23 PurchaseEvaluationPhase? phase), phase=7 = Đã duyệt.

Nguồn số cho 4 GĐ (chốt) — chỉ +2 request, dưới trần ~3

Ô số Nguồn Request thêm?
GĐ1 · Tổng phiếu total của query đã có ['my-pe-recent'] (/purchase-evaluations?page=1&pageSize=5) 0 (tái dùng)
GĐ1 · Đã duyệt GET /purchase-evaluations?page=1&pageSize=1&phase=7.total +1
GĐ1 · Chờ tôi duyệt GET /purchase-evaluations/inbox.length +1
GĐ2 (sắp) không có số riêng — mượn ô "Đã duyệt" GĐ1 làm câu "N phiếu sẵn sàng nối tiếp" 0
GĐ3 · Tổng HĐ total của query đã có ['my-contracts-recent'] 0
GĐ3 · Chờ tôi duyệt / Quá hạn ['my-dashboard'] pendingMyApproval / overdueđã xác minh handler CHỈ đếm bảng Contracts (GetMyDashboardQuery.cs:51-75, db.Contracts) ⇒ gán vào GĐ3 là ĐÚNG NGHĨA, không phải mượn số 0
GĐ4 (sắp) KHÔNG gán số (owner tuyên "sắp triển khai" — không bịa data từ ContractPhase.DaPhatHanh) 0

Token/DS đã nạp (FD1 — dùng, không reinvent)

fe-user/src/index.css @theme: brand-50..900 (brand-600 = #1f7dc1), accent-500/600 đỏ, teal / amberx / violet / greenx chỉ có stop 50/100/500/600/700 (KHÔNG -800 — gotcha cũ). Utility sẵn: .card-accent (rail --accent, hover lift, có prefers-reduced-motion), .icon-chip (--chip-bg/--chip-fg), .stat-value, .label-eyebrow, .app-gradient-brand. Icon: đã node -e verify 18 export lucide dùng tới (Workflow, ClipboardCheck, FileSignature, Stamp, Hourglass, ChevronRight/Down, RefreshCw, ShieldCheck…) — tránh bẫy alias-dodge. Route đã verify tồn tại trong App.tsx: /purchase-evaluations, /my-contracts, /inbox, /contracts/new, /purchase-evaluations/:id — mọi link đều trỏ route THẬT (không đâm * fallback).

3 — Quyết định design (trước khi code)

  • Header: title → "Tổng quan tất cả luồng duyệt"; lời chào Xin chào, {tên} KHÔNG vứt — dời xuống description cùng câu định vị 4 GĐ.
  • Hero = dải pipeline 4 GĐ: grid lg:grid-cols-[1fr_auto_1fr_auto_1fr_auto_1fr] — 3 ô auto là mũi tên nối (ChevronRight ở lg, ChevronDown khi dọc <lg), aria-hidden. Mobile = 1 cột dọc, mũi tên xuống.
  • LIVE (GĐ1 brand / GĐ3 teal): nền trắng + .card-accent rail màu + .icon-chip tinted + 3 MiniStat bấm được. SOON (GĐ2/GĐ4): border-dashed border-slate-300 bg-slate-50 + badge Hourglass "Sắp triển khai" xám-trung tính (KHÔNG đỏ/vàng cảnh báo → không trông như lỗi) + 1 dòng mô tả owner đưa nguyên văn.
  • a11y: card LIVE là <div>, phần bấm là <button> con (MiniStat + "Xem tất cả") ⇒ KHÔNG lồng button-trong-button; mọi button có focus-visible:ring-2 ring-brand-500. Nhãn phụ đổi text-slate-400text-slate-500 (slate-400 = 2.5:1 FAIL AA; slate-500 = 4.76:1 PASS).
  • Bug spacing sẵn có được vá luôn: 2 section list ("HĐ gần đây" + "Phiếu Duyệt NCC gần đây") trước đây dính sát nhau (section thứ 2 không có mt/mb) → gói vào grid gap-4 xl:grid-cols-2.
  • States: StatNum = loading → thanh animate-pulse; error → + title giải thích; có banner lỗi + nút Thử lại (refetch) khi query pipeline hỏng. List giữ skeleton/EmptyState cũ.

4 — File đã sửa (chỉ 1 file, fe-user ONLY)

D:\Dropbox\CONG_VIEC\SOLUTION\SOLUTION_ERP\fe-user\src\pages\UserDashboardPage.tsx — redesign TẠI CHỖ, named export UserDashboardPage GIỮ NGUYÊN (route /dashboard + nhãn sidebar "Tổng quan" không phải đụng). git status xác nhận KHÔNG file fe-admin/** nào bị chạm (các file khác đang M là của lead: WAL/STATUS/HANDOFF/scripts…).

Cấu trúc mới (trên → dưới):

  1. PageHeader — title "Tổng quan tất cả luồng duyệt", description có lời chào + câu định vị.
  2. HERO pipeline 4 GĐ (<section> bo góc, viền, shadow) — header có icon Workflow + eyebrow + "Pipeline 4 giai đoạn · 21 bước"; banner lỗi + nút Thử lại (chỉ hiện khi có query fail); dải 4 StageCard + 3 StageArrow.
  3. "Việc của tôi" — 5 StatCard cũ nguyên vẹn (thêm ghi chú "Số liệu hợp đồng của riêng bạn (giai đoạn 3)" cho khớp sự thật BE).
  4. 2 danh sách gần đây trong grid gap-4 xl:grid-cols-2 (HĐ gần đây | Phiếu Duyệt NCC gần đây) — thêm nhánh isError + nút Thử lại cho cả 2 (trước đây lỗi API = list rỗng câm).

Component mới (local, không đẻ file mới, không dep mới): StageCard · StageArrow · StageStatRow · StageLink · StageSoon · SectionTitle + map STAGE_TONE. Nội dung 4 GĐ đúng nguyên văn owner: GĐ1 "Duyệt NCC" (Bước 1 → 6 · CEO chốt) · GĐ2 "Đề xuất ký kết hợp đồng" (Bước 7 → 12) — "Phiếu nối tiếp từ phiếu Duyệt NCC đã duyệt — chốt giá trị ký kết." · GĐ3 "Duyệt Hợp đồng" (Bước 13 → 18) · GĐ4 "Ký cứng & phát hành" (Bước 19 → 21) — "Upload bản hợp đồng đã ký cứng."

5 — Build (bằng chứng thay screenshot #1)

cd fe-user && npm run build (script = tsc -b && vite build) — PASS. Tail lần chạy cuối:

dist/index.html                     0.87 kB │ gzip:   0.46 kB
dist/assets/index-BIJWA-lW.css     88.95 kB │ gzip:  14.45 kB
dist/assets/index-BBc0xk0r.js   1,550.37 kB │ gzip: 393.40 kB
(!) Some chunks are larger than 500 kB after minification…      ← pre-existing
[INEFFECTIVE_DYNAMIC_IMPORT] src/lib/realtime.ts …              ← pre-existing
✓ built in 561ms

Chống bẫy "xanh vì cache": chạy thêm npx tsc -b --force (full type-check, bỏ tsbuildinfo) → exit 0, 0 lỗi. 3 warning (thứ tự @import font, chunk >500 kB, INEFFECTIVE_DYNAMIC_IMPORT) đều có sẵn từ trước, không sinh từ file này.

6 — 🔴 NO-SCREENSHOT (khai theo lệnh) + verify tĩnh thay thế

KHAI: NO-SCREENSHOT. Môi trường spawn này không có dev-server/Playwright ⇒ KHÔNG chạy được FD2 visual loop, KHÔNG có ảnh, KHÔNG bịa ảnh. Thay bằng 2 lớp kiểm tra chạy được:

(a) Soi CSS đã build — Tailwind v4 chỉ sinh class nó quét thấy; class không sinh = "im lặng mất màu" (bẫy cũ của repo với stop -800 của teal/violet). Grep file dist/assets/*.css vừa build:

class có trong dist?
text-brand-800 / text-brand-800! ✓ (kèm !important)
text-teal-700 · ring-teal-100 · ring-brand-200
text-accent-600! color:var(--color-accent-600)!important
text-slate-600! !important
bg-amberx-50 · text-amberx-700 · border-amberx-500
bg-brand-50 · bg-teal-50 · bg-violet-50 · text-violet-700
border-dashed · ring-inset
lưới pipeline lg:grid-cols-[1fr_auto_1fr_auto_1fr_auto_1fr] ✓ → grid-template-columns:1fr auto 1fr auto 1fr auto 1fr (1 hit, trong media query lg)

Chỉ dùng stop màu CÓ THẬT trong @theme (teal/violet/amberx/greenx: 50/100/500/600/700 — KHÔNG -800) và mọi màu chữ đè lên rule unlayered (h1..h4, .stat-value) đều có dấu !, đã xác nhận !important nằm trong CSS.

(b) Verify icon trước khi buildnode -e liệt 18 export lucide-react (Workflow/ClipboardCheck/FileSignature/Stamp/Hourglass/ChevronRight/ChevronDown/RefreshCw/ArrowRight…) → tất cả object, không dính bẫy alias.

7 — Tự-phê tĩnh theo rubric (FD4) — thẳng, không "trông ổn"

Tiêu chí Verdict Căn cứ / điểm còn nợ
Hierarchy PASS 1 focal point = dải pipeline (khối duy nhất có viền + shadow + nền trắng nổi trên nền #f8fafc); KPI cá nhân xuống hạng 2; list xuống hạng 3. LIVE ⟂ SOON phân biệt bằng 3 tín hiệu cùng chiều (nền trắng vs xám, rail màu vs nét đứt, có số vs badge).
Spacing/rhythm PASS Toàn bộ giá trị nằm trên thang Tailwind (p-3.5/p-4, mt-1/2/3, gap-1.5/2/3/4) — 0 magic number. Vá lỗi cũ: 2 section list trước đây dính sát nhau (section thứ 2 không có margin) → nay grid gap-4.
Typography PASS Không thêm size lạ: 10px badge / 11px nhãn-phụ / 13-14px thân / 15px tiêu-đề-GĐ / text-3xl KPI. Nhãn có leading-tight để tiếng Việt 2 dòng không dính.
Color/contrast PASS (1 nợ ghi rõ) brand-600 #1F7DC1 là accent chính, KHÔNG default-blue. Đo: text-slate-600 7.0:1 · text-slate-500 4.8:1 · text-brand-700 5.6:1 · text-brand-800 ~8:1 · text-teal-700 ~5.6:1 · text-accent-600 ~7.4:1 — đều ≥ AA. Sửa 2 chỗ hụt AA: hint của StatCard slate-400(2.5:1) → slate-500; dấu "—" lỗi slate-400slate-500. Nợ (KHÔNG tự sửa): class dùng chung .label-eyebrow (brand-600 trên trắng ≈ 4.42:1, hụt 4.5) — đây là token toàn app ở index.css, sửa = ảnh hưởng nhiều trang + đụng file agent khác ⇒ trang này , dùng text-brand-700 cục bộ. Đề xuất lead cân nhắc đổi .label-eyebrow → brand-700 ở một đợt riêng.
Polish/states PASS Mỗi ô số có đủ 3 trạng thái: loading = thanh animate-pulse, error = "—" + title, có số = số thật. Có banner lỗi cấp trang + Thử lại chỉ refetch query nào fail. 2 list: skeleton / error + Thử lại (mới) / empty / data. Hover + focus-visible:ring-2 trên MỌI phần bấm được. Motion: chỉ dùng .card-accent (đã có prefers-reduced-motion trong index.css) + transition màu — không thêm animation nào cần chặn.
Responsive PASS (theo suy luận tĩnh, chưa soi mắt) <lg = 1 cột dọc, mũi tên ChevronDown chen giữa (đúng yêu cầu owner). lg = lưới 7 cột 1fr auto ×3. Ở 1280px (trừ sidebar ~240 + p-6) mỗi thẻ ≈ 216px: hàng badge+chip ≈ 122px → vừa; nhãn "Chờ tôi duyệt" 11px ≈ 72px + số ≈ 20px trong 190px → vừa 1 dòng, và nếu xuống dòng vẫn an toàn nhờ leading-tight + min-w-0. h-full + items-stretch giữ 4 thẻ bằng chiều cao dù tiêu đề GĐ2 dài 2 dòng. Không có overflow-x (không đặt min-width cứng).
Detail PASS tabular-nums cho mọi số → cột phải thẳng hàng. Không lồng <button> trong <button> (thẻ GĐ là <div>, phần bấm là button con) — HTML hợp lệ, không "nuốt" focus. Ring của row trong list dùng ring-inset<section>overflow-hidden (ring outset sẽ bị cắt).
Anti-slop (FD3) PASS 0 emoji trong UI (dùng lucide) · 0 gradient loè (chỉ giữ gradient brand nhạt sẵn có ở header list + rail) · GĐ chưa làm KHÔNG dùng đỏ/vàng cảnh báo (xám nét đứt = "kế hoạch", không phải "lỗi") · KHÔNG bịa số cho GĐ2/GĐ4.

Điểm KHÔNG dám khai: chưa có mắt người/ảnh nào nhìn trang này. Hai thứ tôi muốn soi khi có prod: (i) chiều cao dải pipeline ở tablet 7681023px (4 thẻ xếp dọc ≈ 630px, có thể muốn thu gọn), (ii) khi Đã quá hạn > 0 số đỏ trong thẻ teal có chỏi không.

8 — Bàn giao / lưu ý cho lead

  • KHÔNG commit (đúng luật: lead commit, scope FE-User). KHÔNG chạm BE, không thêm dependency, không sửa index.css (file đang có agent khác đụng).
  • 2 request thêm đúng như thiết kế: ['pipeline-pe-approved']['pipeline-pe-inbox'] — queryKey mới, KHÔNG đụng cache của trang list (['pe-list', …]).
  • Nếu sau này GĐ2/GĐ4 có module thật: chỉ cần truyền tone + đổi <StageSoon> thành mấy <StageStatRow> — khung đã chừa sẵn, không phải đập lại.
  • Khi deploy prod: đây là trang duy nhất cần soi mắt lại (route /dashboard, login user thường). Rig dev authed vẫn vướng gotcha #3 (API HTTPS :5443 ⟂ vite proxy HTTP) — verify bằng prod là đường ngắn nhất.