diff --git a/fe-user/src/components/Layout.tsx b/fe-user/src/components/Layout.tsx index da5b5374..90048145 100644 --- a/fe-user/src/components/Layout.tsx +++ b/fe-user/src/components/Layout.tsx @@ -59,7 +59,11 @@ const INT_TO_PE_CODE: Record = Object.fromEntries( ) // User-side menu key → route. Differs from admin: Danh sách points to -// /my-contracts (user's own drafts), Duyệt to /inbox (pending THEIR approval). +// /my-contracts (user's own drafts). +// 🔴 [YC-029 W3 S190 — vá comment stale] Câu cũ "Duyệt to /inbox" HẾT ĐÚNG cho họ `Ct_*`: +// leaf "Đang duyệt" nay Ở LẠI `/my-contracts?type=N&pendingMe=1` (xem nhánh `Pending` :164). +// `/inbox` + `InboxPage` GIỮ NGUYÊN, đổi vai: hộp thư TỔNG mọi loại, vào bằng mục cố định +// "Hộp thư" (`USER_FIXED_TOP` / `staticResolvePath('__inbox')`), không còn là đích của leaf HĐ. function resolvePath(key: string): string | null { const staticMap: Record = { // [W2 KHKK S161] GĐ2 hết skeleton — 6 leaf re-point trang thật (gotcha #50: @@ -161,7 +165,13 @@ function resolvePath(key: string): string | null { if (!typeInt) return null if (action === 'List') return `/my-contracts?type=${typeInt}` if (action === 'Create') return `/contracts/new?type=${typeInt}` - if (action === 'Pending') return `/inbox?type=${typeInt}` + // 🔴 [YC-029 W3 S190] "Đang duyệt" Ở LẠI trang danh sách thay vì nhảy sang `/inbox` + // (anh: bấm mục con của loại HĐ mà bị đá sang hộp thư tổng = mất ngữ cảnh loại). + // `pendingMe=1` là DANH-TÍNH leaf (KHÔNG vào `TRANSIENT_QUERY_KEYS` — xem chú ở đó), + // trang tự ĐỔI ENDPOINT sang `GET /contracts/inbox` khi thấy nó. + // GIỮ NGUYÊN route `/inbox` + `InboxPage`: đó là hộp thư TỔNG (mục cố định "Hộp thư"), + // không xoá — đường vào khác, dữ liệu cùng nguồn. + if (action === 'Pending') return `/my-contracts?type=${typeInt}&pendingMe=1` if (action === 'WfView') return `/contracts/workflow-matrix?type=3&ct=${typeInt}` // [YC-029 W1 S190] `view=approved` = KHOÁ ĐỊNH DANH (khuôn PE :185, H-4): `phase` nằm // trong TRANSIENT (là bộ lọc) nên URL "Đã duyệt" trùng hệt URL user tự lọc phase=9 trên diff --git a/fe-user/src/pages/contracts/MyContractsPage.tsx b/fe-user/src/pages/contracts/MyContractsPage.tsx index 9b5048d5..9b1afe47 100644 --- a/fe-user/src/pages/contracts/MyContractsPage.tsx +++ b/fe-user/src/pages/contracts/MyContractsPage.tsx @@ -12,6 +12,11 @@ // ở Panel 2 chứ không bỏ. Delta này plan W2 KHÔNG nêu (plan chỉ nói "panel-2 giữ detail") — // khai ở đây để người sau khỏi tưởng ai đó lỡ tay đánh rơi. // +// [S190 · W3 YC-029] Panel 3 có HAI NGUỒN, chọn bằng `?pendingMe=1` của URL: +// • mặc định → `GET /contracts` (phân trang, lọc `type`/`phase`/`search` phía server) +// • pendingMe → `GET /contracts/inbox` (0 tham số, trọn gói) — leaf "Đang duyệt" nay ở lại +// trang này thay vì nhảy sang `/inbox`. Xem khối `queryFn` để biết vì sao KHÔNG gửi param. +// // Mobile (< lg): GIỮ NGUYÊN 100% hiện trạng — chỉ danh-sách hiện, click row → fullpage // /contracts/:id. Cây là khối DESKTOP-ONLY y như TRƯỚC hoán-đổi (nó vốn nằm trong panel // `hidden lg:block`), nên đợt này không đổi một pixel nào ở < lg. @@ -52,6 +57,10 @@ export function MyContractsPage() { // 'Đã xóa' (?deleted=1 — endpoint /contracts/deleted mới, chỉ-xem). const phaseFilter = searchParams.get('phase') ? Number(searchParams.get('phase')) : null const showDeleted = searchParams.get('deleted') === '1' + // [S190 W3 YC-029] leaf "Đang duyệt" nay Ở LẠI trang này (`Layout.tsx` nhánh `Pending`) + // thay vì nhảy sang `/inbox`. `pendingMe` là DANH-TÍNH leaf ⇒ CẤM đưa vào + // `TRANSIENT_QUERY_KEYS` (strip = 2 leaf "Danh sách"/"Đang duyệt" cùng sáng). + const pendingMe = searchParams.get('pendingMe') === '1' // [S190 W2 · Q2 nhánh-đã-chọn] Select loại HĐ ở đầu panel CÂY. Mặc định = `?type=N` của URL // (menu loại HĐ đang đứng). 🔴 Đổi Select CHỈ đổi bộ lọc CÂY — KHÔNG navigate, KHÔNG đụng @@ -73,9 +82,28 @@ export function MyContractsPage() { // cache cũ (payload chưa lọc) tự hết hiệu lực vì mỗi `type` là 1 ô key riêng sẵn rồi. // [S190 W2] +trục `search`: ô tìm nay đi SERVER-SIDE (xem `params.search`) ⇒ mỗi từ khoá // là một payload khác nhau, thiếu trục này là ăn cache của từ khoá trước. - queryKey: ['my-contracts', typeFilter, phaseFilter, showDeleted, search], - queryFn: async () => - ( + // [S190 W3] +trục `pendingMe`: 2 chế độ đọc 2 NGUỒN khác nhau (`/contracts` phân trang + // ⟂ `/contracts/inbox` trọn gói) ⇒ dùng chung ô cache là trả nhầm danh sách. + queryKey: ['my-contracts', typeFilter, phaseFilter, showDeleted, search, pendingMe], + queryFn: async () => { + // [S190 W3 — khuôn PE `PurchaseEvaluationsListPage.tsx:77-99`] Nhánh "Đã xóa" đứng + // TRƯỚC `pendingMe`: 2 view LOẠI TRỪ nhau, và HĐ đã xóa chỉ thấy được qua endpoint + // riêng `/contracts/deleted`. Đảo thứ tự = URL `?deleted=1&pendingMe=1` rơi vào inbox + // (inbox không bao giờ trả HĐ đã xóa) ⇒ màn "Đã xóa" rỗng THẦM LẶNG. + if (pendingMe && !showDeleted) { + // 🔴 ĐỔI ENDPOINT, KHÔNG gửi thêm param: `GET /contracts` KHÔNG có param `pendingMe` + // (`ContractsController.cs:17-26` — 8 param, không có nó) và ASP.NET NUỐT param lạ IM + // LẶNG ⇒ gửi lên sẽ ra Y HỆT danh sách thường, hỏng câm chứ không đỏ. + // `GET /contracts/inbox` (`ContractsController.cs:41-43`) = 0 THAM SỐ, trả + // `List` (không phân trang) → lọc loại ở CLIENT + ép shape. + // [carry:w7b-inbox-param] LỆCH-W7 CHẤP NHẬN ĐỢT NÀY: W7 (S187) đã chuyển lọc `type` + // lên server cho 2 nhánh kia; nhánh này vẫn lọc client vì BE chưa có param — + // `GetMyInboxQuery` là record 0 tham số và còn `Take(100)` cứng. Nâng BE = việc đợt sau. + const res = await api.get('/contracts/inbox') + const items = typeFilter != null ? res.data.filter(c => c.type === typeFilter) : res.data + return { items, total: items.length, page: 1, pageSize: items.length } + } + return ( await api.get>(showDeleted ? '/contracts/deleted' : '/contracts', { params: { page: 1, @@ -97,7 +125,8 @@ export function MyContractsPage() { ...(!showDeleted && phaseFilter != null ? { phase: phaseFilter } : {}), }, }) - ).data, + ).data + }, }) const detail = useQuery({ @@ -143,6 +172,19 @@ export function MyContractsPage() { const typeLabel = typeFilter != null ? ContractTypeLabel[typeFilter] : null + // [S190 W3] "Rỗng vì LỌC" ⟂ "rỗng vì CHƯA CÓ GÌ" phải khác câu: dùng chung một câu thì + // người đang ở mục "Đang duyệt" đọc thành "loại HĐ này chưa có dữ liệu" — sai nghĩa hẳn. + const emptyTitle = pendingMe && !showDeleted ? 'Không có HĐ chờ bạn duyệt' : 'Không có HĐ' + const emptyDescription = showDeleted + ? 'Chưa có HĐ nào bị xóa.' + : pendingMe + ? typeLabel + ? `Không HĐ "${typeLabel}" nào đang đến lượt bạn duyệt.` + : 'Không có HĐ nào đang đến lượt bạn duyệt.' + : typeFilter != null + ? 'Loại HĐ này chưa có dữ liệu.' + : 'Tạo HĐ mới để bắt đầu.' + return (
{/* Compact page header — chiếm ít chiều cao để 3 panel có max chỗ */} @@ -166,6 +208,13 @@ export function MyContractsPage() { Đã phát hành )} + {/* [S190 W3] Chế độ phải NHÌN THẤY ĐƯỢC: cùng URL `/my-contracts`, cùng tiêu đề, + nhưng danh-sách đọc từ hộp thư đến ⇒ không có nhãn thì user tưởng HĐ biến mất. */} + {!showDeleted && pendingMe && ( + + Đang duyệt — đến lượt tôi + + )} {!showDeleted && (
{list.isLoading && ( @@ -265,8 +324,8 @@ export function MyContractsPage() {
)}