# 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-dashboard` → `MyDashboard {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-400`→`text-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 build** — `node -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-400` → `slate-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 **né**, 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` vì `<section>` có `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 768–1023px (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']` và `['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.

<!-- END sub-frontend-designer-tongquan · TOTAL=8 muc -->
