From 09ebd20eda5d195f3782002d48be4aef7af9f6f6 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 12 Aug 2026 19:07:38 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-User:=20YC-029=20W1=20=E2=80=94?= =?UTF-8?q?=20danh-t=C3=ADnh=20menu=207=20lo=E1=BA=A1i=20H=C4=90=20(accord?= =?UTF-8?q?ion=20ct=20+=20view=3Dapproved=20+=20WfView=20slice=20HD-*)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- fe-user/src/components/Layout.tsx | 50 ++++++++++++++++--- .../src/pages/pe/WorkflowMatrixViewPage.tsx | 43 ++++++++++++++-- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/fe-user/src/components/Layout.tsx b/fe-user/src/components/Layout.tsx index 45c211a1..da5b5374 100644 --- a/fe-user/src/components/Layout.tsx +++ b/fe-user/src/components/Layout.tsx @@ -28,6 +28,14 @@ const INT_TO_TYPE_CODE: Record = Object.fromEntries( Object.entries(TYPE_CODE_TO_INT).map(([k, v]) => [v, k]), ) +// [YC-029 W1 S190] ALLOWLIST pathname được phép auto-expand nhóm Ct_ từ `?type=`. +// 🔴 EXACT match, KHÔNG `startsWith('/contracts')`: `/contracts/workflow-matrix?type=3` +// mang `type` = ApplicableType (=3 cho CẢ 7 loại HĐ), KHÔNG phải ContractType ⇒ +// startsWith sẽ tái sinh bug A (mở nhầm nhóm "Nhà cung cấp" cho mọi leaf Luồng duyệt). +// Cùng lý do, `/hard-copies?type=1..7` (`Hdc_*` :70-76) nằm NGOÀI allowlist: nó có +// nhóm menu riêng, để lọt vào đây là cross-talk mở nhóm HĐ khi user đang ở Bản cứng. +const CT_TYPE_PARAM_PATHS = ['/my-contracts', '/contracts/new', '/inbox'] as const + // Detect Ct_ group key (no suffix, distinguish from leaf Ct__List/Create/Pending) const CT_GROUP_PATTERN = /^Ct_([^_]+)$/ function getCtGroupCode(key: string): string | null { @@ -138,8 +146,14 @@ function resolvePath(key: string): string | null { } if (staticMap[key]) return staticMap[key] - // [S159-đợt5 owner "y chang Duyệt NCC"] +WfView/Approved/Deleted. WfView cả 7 loại - // đều ?type=3 (HĐ chung 1 bộ workflow V2) — thêm &ct=N để active-state riêng từng leaf. + // [S159-đợt5 owner "y chang Duyệt NCC"] +WfView/Approved/Deleted. + // 🔴 [YC-029 W1 S190 — vá comment stale] Từ S188 HĐ có **7 bộ quy trình duyệt per-loại** + // (`HD-TP`/`HD-GK`/`HD-NCC`/`HD-DV`/`HD-MB`/`HD-NT-NCC`/`HD-NT-DV` — nhà mã BE + // `Domain/Contracts/ContractApprovalWorkflowFamily.cs:33-42`), KHÔNG còn "chung một bộ". + // `type=3` ở URL WfView là **ApplicableType.Contract** (hằng cho cả họ), còn `ct=N` là + // **danh-tính leaf** (ContractType 1..7): vừa cho active-state riêng từng leaf, vừa là + // tham số trang WfView dùng để slice đúng bộ `HD-*` của loại đó. + // ⚠️ `ct` (và `view` dưới đây) KHÔNG được cho vào `TRANSIENT_QUERY_KEYS` — xem chú ở đó. const match = key.match(/^Ct_([^_]+)_(List|Create|Pending|WfView|Approved|Deleted)$/) if (match) { const [, code, action] = match @@ -149,7 +163,10 @@ function resolvePath(key: string): string | null { if (action === 'Create') return `/contracts/new?type=${typeInt}` if (action === 'Pending') return `/inbox?type=${typeInt}` if (action === 'WfView') return `/contracts/workflow-matrix?type=3&ct=${typeInt}` - if (action === 'Approved') return `/my-contracts?type=${typeInt}&phase=9` + // [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 + // trang Danh sách ⇒ 2 leaf cùng sáng (bug B). Trang bỏ qua param lạ, chỉ `phase=9` lọc thật. + if (action === 'Approved') return `/my-contracts?type=${typeInt}&phase=9&view=approved` if (action === 'Deleted') return `/my-contracts?type=${typeInt}&deleted=1` } @@ -347,6 +364,11 @@ function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) { // compare để menu giữ highlight khi user select row / search / filter. // Ví dụ leaf "Danh sách" `?type=1` vẫn highlight khi user click phiếu → // `?type=1&id=abc`. Trước đó exact-set match → mất highlight (bug UAT 2026-05-08). +// +// 🔴 [YC-029 W1 S190] CẤM thêm vào set dưới đây 3 param DANH-TÍNH của leaf HĐ: khoá-định-danh +// mục "Đã duyệt" (khuôn PE :185), khoá loại HĐ của leaf "Luồng duyệt", khoá "đến lượt tôi" của +// leaf "Đang duyệt" (W3). Cho vào set = bị strip khi so sánh ⇒ URL 2 leaf bằng nhau ⇒ menu +// sáng cùng lúc (đúng bug B anh báo). Chúng là DANH-TÍNH điều hướng, KHÔNG phải bộ lọc. const TRANSIENT_QUERY_KEYS = new Set(['id', 'q', 'editHeader', 'page', 'phase', 'awId']) // So sánh 2 query string dạng key-value set (thứ tự param không quan trọng, @@ -469,6 +491,20 @@ export function Layout() { useEffect(() => { const params = new URLSearchParams(location.search) + + // 🔴 [YC-029 W1 S190 — bug A] `ct` ĐI TRƯỚC `type`: leaf "Luồng duyệt" của 7 nhóm HĐ + // đi URL `/contracts/workflow-matrix?type=3&ct=N`, trong đó `type=3` là ApplicableType + // (hằng cho cả họ) chứ KHÔNG phải ContractType ⇒ đọc `type` ở đây sẽ mở nhầm nhóm 03 + // "Nhà cung cấp" cho cả 7 leaf. `ct` mới là danh-tính loại HĐ của leaf. + // Clamp `/^[1-7]$/` bằng REGEX CHUỖI, không `Number()`: `Number('03')===3` lọt lưới. + // `ct` rác/thiếu ⇒ rơi xuống nhánh `type` cũ (không tự chế hành vi mới). + const rawCt = params.get('ct') + if (rawCt !== null && /^[1-7]$/.test(rawCt)) { + const ctCode = INT_TO_TYPE_CODE[Number(rawCt)] + if (ctCode) setExpandedCtCode(ctCode) + return + } + const typeParam = params.get('type') if (!typeParam) return @@ -476,9 +512,11 @@ export function Layout() { if (location.pathname.startsWith('/purchase-evaluations')) { const code = INT_TO_PE_CODE[n] if (code) setExpandedPeCode(code) - } else { - // /contracts, /my-contracts, /inbox, /contracts/new... — all use - // ContractType enum + } else if ((CT_TYPE_PARAM_PATHS as readonly string[]).includes(location.pathname)) { + // [YC-029 W1 S190] CHỈ 3 pathname trong allowlist EXACT (:32-38) mới được suy nhóm + // HĐ từ `?type=`. Trước đây `else` trần ⇒ MỌI trang có `?type=` đều đá accordion + // (workflow-matrix, hard-copies…). Trang nào cần auto-expand thì THÊM TÊN vào + // allowlist — đừng nới lại thành startsWith. const code = INT_TO_TYPE_CODE[n] if (code) setExpandedCtCode(code) } diff --git a/fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx b/fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx index 1538fdeb..770a015f 100644 --- a/fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx +++ b/fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx @@ -49,14 +49,34 @@ const LEVEL_PALETTE = [ 'bg-rose-100 text-rose-800 ring-rose-300', ] as const +// [YC-029 W1 S190] ContractType (1..7) → MÃ quy trình duyệt HĐ của loại đó. +// 🔴 NGUỒN CHÉP: `src/Backend/SolutionErp.Domain/Contracts/ContractApprovalWorkflowFamily.cs:33-42` +// (nhà DUY NHẤT của họ mã ở production). Đây là BẢN SAO THỨ 3 của họ này ở FE +// (bản 2 = `fe-admin/src/lib/contractWorkflowFamily.ts`, tự khai fe-admin-ONLY nên KHÔNG +// import chéo app được) ⇒ chép TỪNG KÝ TỰ, chú ý token gạch-nối `HD-NT-NCC` / `HD-NT-DV`. +// Drift-check = ô C-9 (đếm 7 mã distinct trong chính file này). +const INT_TO_HD_CODE: Record = { + 1: 'HD-TP', // HopDongThauPhu + 2: 'HD-GK', // HopDongGiaoKhoan + 3: 'HD-NCC', // HopDongNhaCungCap + 4: 'HD-DV', // HopDongDichVu + 5: 'HD-MB', // HopDongMuaBan + 6: 'HD-NT-NCC', // HopDongNguyenTacNCC + 7: 'HD-NT-DV', // HopDongNguyenTacDichVu +} + export function WorkflowMatrixViewPage() { const [searchParams] = useSearchParams() // Trang này gắn ở 3 route (/purchase-evaluations|/contracts|/khkk + /workflow-matrix) // ⇒ link "xem tất cả" phải bám đường dẫn ĐANG đứng, không hardcode một cái. const { pathname } = useLocation() const rawType = Number(searchParams.get('type')) - // [S159-đợt5] +3 = Contract (ApplicableType.Contract — HĐ dùng CHUNG 1 bộ workflow V2 - // cho mọi ContractType ⇒ 7 leaf 'Luồng duyệt' của 7 nhóm HĐ đều trỏ về matrix này). + // [S159-đợt5] +3 = Contract (ApplicableType.Contract) — 7 leaf 'Luồng duyệt' của 7 nhóm HĐ + // đều trỏ về matrix này vì `ApplicableType` là HẰNG của cả họ, chỉ có MỘT giá trị cho HĐ. + // 🔴 [YC-029 W1 S190 — vá comment stale] Từ S188 mỗi loại HĐ có QUY TRÌNH RIÊNG: 7 bộ + // per-loại `HD-TP`/`HD-GK`/`HD-NCC`/`HD-DV`/`HD-MB`/`HD-NT-NCC`/`HD-NT-DV` + // (`ContractApprovalWorkflowFamily.cs:33-42`) ⇒ 7 leaf KHÔNG còn hiện y hệt nhau: leaf nào + // cũng mang thêm `&ct=N` (ContractType) và trang slice đúng bộ của loại đó (nhánh `ct` bên dưới). // [W2 KHKK S161] +10 (ContractSigningPlan) — leaf Khkk_WfView đi alias /khkk/workflow-matrix?type=10. const typeInt = rawType === 1 || rawType === 2 || rawType === 3 || rawType === 10 ? rawType : 1 @@ -93,10 +113,27 @@ export function WorkflowMatrixViewPage() { // `KHKK-N{n}` (`DbInitializer.cs:609`) và cũng chính `code` này là thứ BE dùng để // gác phiếu-pin-đúng-nhóm (`ContractSigningPlanFeatures.cs:420` regex `^KHKK-N([1-8])$`). // Một quy trình có thể có nhiều VERSION cùng `code` ⇒ lọc giữ đủ các version. + // ── [YC-029 W1 S190] Lọc theo LOẠI HĐ (7 bộ quy trình per-loại từ S188) ──────────── + // Cùng bài với `group` ở trên: 7 leaf "Luồng duyệt" của 7 nhóm HĐ đều trỏ về CÙNG trang + // này (`type=3` là ApplicableType — hằng của cả họ), chỉ khác `&ct=N`. Không lọc thì 7 leaf + // hiện y hệt nhau — mục menu nói dối tên nó. + // 🔴 Gate ĐỦ 3 VẾ: chỉ áp khi `typeInt === 3` (HĐ). Thiếu vế này thì `?type=1&ct=5` sẽ đem + // mã `HD-MB` đi lọc bộ Duyệt-NCC ⇒ TRANG TRẮNG. `ct` rác/thiếu ⇒ BỎ QUA, giữ hành vi cũ. + // Clamp bằng REGEX CHUỖI chứ không `Number()`: `Number('03')===3` lọt lưới. + const rawCt = searchParams.get('ct') + const hdCode = + typeInt === 3 && rawCt !== null && /^[1-7]$/.test(rawCt) ? INT_TO_HD_CODE[Number(rawCt)] : null + + // 🔴 LỌC CLIENT-SIDE y khuôn `group`: query đã tải TRỌN bộ quy trình của `applicableType=3`, + // `ct` chỉ là lát cắt của đúng payload đó ⇒ KHÔNG thêm `ct` vào `queryKey` (thêm = 7 lần gọi + // mạng cho cùng một payload + 7 ô cache trùng nội dung). Khớp bằng `code` (BE seed + // `ContractApprovalWorkflowFamily.CodeFor`), giữ đủ mọi VERSION cùng `code`. const workflows = khkkGroup !== null ? allWorkflows.filter(wf => wf.code === khkkGroupWorkflowCode(khkkGroup)) - : allWorkflows + : hdCode !== null + ? allWorkflows.filter(wf => wf.code === hdCode) + : allWorkflows return ( // [Plan AA hotfix S24 t1] Bro UAT request: dịch hết content sang trái.