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

118 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.tsx`**766 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|4` (`STAGE_INFO`) ⇒ link `/coming-soon?stage=N` là route THẬT, không đâm `*` |
| `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ệt`→**nửa** (linear-gradient 50% inline, viền tone-500) · `Cần chỉnh sửa lại`→**nửa amberx** · `Nháp`→**rỗ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ộ.
<!-- END sub-frontend-designer-dot4 · TOTAL=5 muc (muc 5 = lead on-behalf) -->