diff --git a/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/sub-frontend-designer-dot4.md b/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/sub-frontend-designer-dot4.md new file mode 100644 index 0000000..473ff2b --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/sub-frontend-designer-dot4.md @@ -0,0 +1,117 @@ +# 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Ỏ: `
` "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 +[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 +[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` (`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=` (`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**: `` (chuột bấm đâu cũng được) + trong ô Mã có `