[CLAUDE] FE-User: YC-029 W1 — danh-tính menu 7 loại HĐ (accordion ct + view=approved + WfView slice HD-*)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-08-12 19:07:38 +07:00
parent 3addd373e2
commit 09ebd20eda
2 changed files with 84 additions and 9 deletions

View File

@ -28,6 +28,14 @@ const INT_TO_TYPE_CODE: Record<number, string> = 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_<Code> group key (no suffix, distinguish from leaf Ct_<Code>_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)
}

View File

@ -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<number, string> = {
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,9 +113,26 @@ 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))
: hdCode !== null
? allWorkflows.filter(wf => wf.code === hdCode)
: allWorkflows
return (