diff --git a/fe-admin/src/components/Layout.tsx b/fe-admin/src/components/Layout.tsx index dbd36e6..144b46c 100644 --- a/fe-admin/src/components/Layout.tsx +++ b/fe-admin/src/components/Layout.tsx @@ -127,7 +127,11 @@ function resolvePath(key: string): string | null { // [S155 đợt 2] 2 mục MỚI — CẢ HAI tái dùng `PurchaseEvaluationsListPage`, 0 route mới, // 0 page mới. "Đã duyệt" chỉ là filter phase sẵn có (`DaDuyet = 7`, đã thông 3 tầng); // "Đã xóa" là view riêng trong cùng page, gọi endpoint `/purchase-evaluations/deleted`. - if (action === 'Approved') return `/purchase-evaluations?type=${typeInt}&phase=7` + // 🔴 `view=approved` = KHOÁ ĐỊNH DANH (trang bỏ qua, chỉ `phase=7` lọc thật). URL mục + // "Đã duyệt" trùng hệt URL user tự lọc trạng thái trên Danh sách ⇒ menu sáng nhầm + // (bug UAT 2026-07-27). `phase` thuộc TRANSIENT nên không làm danh tính được. + // Mirror `fe-user/src/components/Layout.tsx` — cùng bản vá. + if (action === 'Approved') return `/purchase-evaluations?type=${typeInt}&phase=7&view=approved` if (action === 'Deleted') return `/purchase-evaluations?type=${typeInt}&deleted=1` } // PE workflow admin leaf: PeWf_ → /system/pe-workflows/ @@ -225,8 +229,17 @@ const TRANSIENT_QUERY_KEYS = new Set(['id', 'q', 'editHeader', 'page', 'phase', function queryMatches(current: string, target: string): boolean { const a = new URLSearchParams(current) const b = new URLSearchParams(target) - const aKeys = [...a.keys()].filter(k => !TRANSIENT_QUERY_KEYS.has(k)).sort() - const bKeys = [...b.keys()].filter(k => !TRANSIENT_QUERY_KEYS.has(k)).sort() + // 🔴 [S155 đợt 2] Transient chỉ được bỏ qua khi MENU ĐÍCH KHÔNG tự ghim key đó. + // Bug UAT 2026-07-27: click "Danh sách" mà menu sáng "Đã duyệt". `phase` vừa là + // BỘ LỌC trạng thái (nên nằm trong TRANSIENT — fix 2026-05-08) vừa vừa-mới thành + // DANH TÍNH điều hướng của "Đã duyệt" (`?type=N&phase=7`). Strip cả 2 vế ⇒ 2 mục + // KHÔNG PHÂN BIỆT ĐƯỢC. Quyền quyết thuộc về ĐÍCH: ghim ⇒ so nghiêm, không ghim ⇒ + // giữ hành vi cũ. (Mirror `fe-user/src/components/Layout.tsx` — cùng bản vá.) + const pinnedByTarget = new Set(b.keys()) + const ignorable = (k: string) => TRANSIENT_QUERY_KEYS.has(k) && !pinnedByTarget.has(k) + + const aKeys = [...a.keys()].filter(k => !ignorable(k)).sort() + const bKeys = [...b.keys()].filter(k => !ignorable(k)).sort() if (aKeys.length !== bKeys.length) return false return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k)) } diff --git a/fe-user/src/components/Layout.tsx b/fe-user/src/components/Layout.tsx index 41fe922..43669e6 100644 --- a/fe-user/src/components/Layout.tsx +++ b/fe-user/src/components/Layout.tsx @@ -140,7 +140,12 @@ function resolvePath(key: string): string | null { // [S155 đợt 2] 2 mục MỚI — CẢ HAI tái dùng `PurchaseEvaluationsListPage`, 0 route mới, // 0 page mới. "Đã duyệt" chỉ là filter phase sẵn có (`DaDuyet = 7`, đã thông 3 tầng); // "Đã xóa" là view riêng trong cùng page, gọi endpoint `/purchase-evaluations/deleted`. - if (action === 'Approved') return `/purchase-evaluations?type=${typeInt}&phase=7` + // 🔴 `view=approved` là KHOÁ ĐỊNH DANH, KHÔNG phải bộ lọc — trang bỏ qua nó, chỉ + // `phase=7` mới thật sự lọc. Vì sao phải có: URL của mục "Đã duyệt" TRÙNG HỆT URL + // mà user tự lọc trạng thái "Đã duyệt" trên trang Danh sách ⇒ không phân biệt được + // bằng URL ⇒ menu sáng nhầm (bug UAT anh báo 2026-07-27). `phase` nằm trong + // TRANSIENT (là bộ lọc) nên KHÔNG dùng làm danh tính được; `view` thì không. + if (action === 'Approved') return `/purchase-evaluations?type=${typeInt}&phase=7&view=approved` if (action === 'Deleted') return `/purchase-evaluations?type=${typeInt}&deleted=1` } return null @@ -277,8 +282,22 @@ const TRANSIENT_QUERY_KEYS = new Set(['id', 'q', 'editHeader', 'page', 'phase', function queryMatches(current: string, target: string): boolean { const a = new URLSearchParams(current) const b = new URLSearchParams(target) - const aKeys = [...a.keys()].filter(k => !TRANSIENT_QUERY_KEYS.has(k)).sort() - const bKeys = [...b.keys()].filter(k => !TRANSIENT_QUERY_KEYS.has(k)).sort() + + // 🔴 [S155 đợt 2] Transient chỉ được bỏ qua khi MENU ĐÍCH KHÔNG tự ghim key đó. + // Bug UAT 2026-07-27 (anh báo): click "Danh sách" mà menu sáng "Đã duyệt". + // Nguyên nhân: `phase` vừa là BỘ LỌC trạng thái trên trang danh sách (nên nằm + // trong TRANSIENT — fix 2026-05-08), vừa vừa-mới trở thành DANH TÍNH điều hướng + // của mục "Đã duyệt" (`?type=N&phase=7`, đợt 2). Strip cả 2 vế ⇒ "Danh sách" + // (`[type]`) và "Đã duyệt" (`[type]` sau khi mất `phase`) thành KHÔNG PHÂN BIỆT + // ĐƯỢC ⇒ URL `?type=1` khớp cả hai, mục render sau thắng. + // Cùng một tham số không thể vừa là thứ-bị-bỏ-qua vừa là thứ-định-danh; nên + // quyền quyết định thuộc về ĐÍCH: đích ghim `phase` ⇒ so khớp NGHIÊM; đích không + // ghim ⇒ giữ nguyên hành vi cũ (lọc/chọn dòng không làm mất highlight). + const pinnedByTarget = new Set(b.keys()) + const ignorable = (k: string) => TRANSIENT_QUERY_KEYS.has(k) && !pinnedByTarget.has(k) + + const aKeys = [...a.keys()].filter(k => !ignorable(k)).sort() + const bKeys = [...b.keys()].filter(k => !ignorable(k)).sort() if (aKeys.length !== bKeys.length) return false return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k)) }