14 KiB
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 buildPASS + 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").
PageHeadertitle "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" 5StatCard(541-582) +grid xl:grid-cols-2chứ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/inboxmảng phẳng, dùngselect: 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-chipKHÔ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).
- 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 JSXStageCard, chỉ đổititle/stepRangesang đọc từSTAGES⇒ echo tên là ĐÚNG do cấu trúc, không do cẩn thận. - Tab
role="tablist"+ 4role="tab"(roving tabindex, ←/→/Home/End,aria-selected,aria-controls), panelrole="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. - 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).
- 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ạibg-slate-200mờ +titletê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ọcrole="img"+ 1aria-labelkể trọn hành trình; từng chấmaria-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-9nói rõ). - 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ôngdiv role=row onClickcâm. - 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 metalg:hiddengộp Dự án · Ngày; cột Dự ánhidden lg:table-cell, Người soạnhidden xl:table-cell, Ngàyhidden md:table-cell, Hành trìnhhidden md:table-cell. Mobile còn 3 cột thật (Mã · Gói thầu+meta · Trạng thái). - 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*-totalpageSize=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 chungInboxPage). - Search debounce 300ms (
useEffect+setTimeout, 0 dep mới), đổi search/tab → resetpage=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ậnstage: StageMetathay 5 prop chuỗi rời ⇒ tab & hero KHÔNG THỂ lệch nhãn. Text hero giữ nguyên văn (soonDescché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ị x–y 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ả 2placeholderData: keepPreviousData(đổi trang không nháy trắng) +aria-busy+ mờ 60% khiisFetching. - 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 buildfe-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-slatesrc=2/css=1 (present). - Việc vai CHƯA kịp: tự-review rubric FD4 → chuyển cho
revieweradversarial (spawn kế tiếp) — không bịa hộ.