From 7ea3957acc1da5ef0a3726eb9c4918969eb111a5 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 23 Apr 2026 09:33:56 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-User:=20sidebar=20accordion=20cho?= =?UTF-8?q?=20menu=20lo=E1=BA=A1i=20H=C4=90=20=E2=80=94=20ch=E1=BB=89=201?= =?UTF-8?q?=20group=20expand=20c=C3=B9ng=20l=C3=BAc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback: 7 group Ct_ (HĐ Thầu phụ / Giao khoán / NCC / Dịch vụ / Mua bán / Nguyên tắc NCC / Nguyên tắc DV) trước đây expand tự do → sidebar dài lê thê khi user mở nhiều. Mỗi group nên độc lập (accordion): chỉ 1 group expand cùng lúc. ## Cách làm ### AccordionContext lifted to Layout - Layout maintain `expandedCtCode: string | null` state - React Context expose getter + setter cho MenuGroup - MenuGroup detect key `Ct_` qua regex `/^Ct_([^_]+)$/`: - Match → controlled mode: open = (expandedCtCode === code) - Toggle = setExpandedCtCode(open ? null : code) - Group khác (Hợp đồng top-level, Quy trình admin, ...) giữ behavior cũ (independent local useState) ### Auto-expand theo URL ?type= useEffect watch location.search: - `/my-contracts?type=5` → INT_TO_TYPE_CODE[5] = "MuaBan" → expand HĐ Mua bán - `/contracts/new?type=2` → expand HĐ Giao khoán - `/inbox?type=3` → expand HĐ Nhà cung cấp - URL không có ?type= → KHÔNG reset (giữ user-selected context) ### Visual: highlight active group Ct_ group đang accordion-open: `bg-slate-50 text-slate-900` (subtle tint để user biết group nào đang active trong 7 type). ## Build fe-user: tsc -b + vite build pass (1888 modules, 1.08MB JS, 380ms) Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-user/src/components/Layout.tsx | 111 +++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 24 deletions(-) diff --git a/fe-user/src/components/Layout.tsx b/fe-user/src/components/Layout.tsx index 0a51bcd..cd8e30e 100644 --- a/fe-user/src/components/Layout.tsx +++ b/fe-user/src/components/Layout.tsx @@ -1,7 +1,7 @@ -import { Link, NavLink, Outlet } from 'react-router-dom' +import { createContext, useContext, useEffect, useMemo, useState } from 'react' +import { Link, NavLink, Outlet, useLocation } from 'react-router-dom' import { ChevronDown, Circle, type LucideIcon } from 'lucide-react' import * as Icons from 'lucide-react' -import { useState } from 'react' import { useAuth } from '@/contexts/AuthContext' import { TopBar } from '@/components/TopBar' import type { MenuNode } from '@/types/menu' @@ -23,6 +23,18 @@ const TYPE_CODE_TO_INT: Record = { NguyenTacDv: 7, } +// Reverse lookup int → code (cho auto-expand từ URL ?type=) +const INT_TO_TYPE_CODE: Record = Object.fromEntries( + Object.entries(TYPE_CODE_TO_INT).map(([k, v]) => [v, k]), +) + +// Detect Ct_ group key (no suffix, distinguish from leaf Ct__List/Create/Pending) +const CT_GROUP_PATTERN = /^Ct_([^_]+)$/ +function getCtGroupCode(key: string): string | null { + const m = key.match(CT_GROUP_PATTERN) + return m ? m[1] : null +} + // 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). function resolvePath(key: string): string | null { @@ -58,6 +70,18 @@ function filterForUser(nodes: MenuNode[]): MenuNode[] { .map(n => ({ ...n, children: filterForUser(n.children) })) } +// Accordion state cho 7 group Ct_ — chỉ 1 expand cùng lúc. Auto-expand +// theo URL `?type=X` để khi navigate giữa các loại HĐ menu tự sync. Group +// non-Ct_ giữ behavior cũ (independent local useState). +type AccordionContextValue = { + expandedCtCode: string | null + setExpandedCtCode: (code: string | null) => void +} +const AccordionContext = createContext({ + expandedCtCode: null, + setExpandedCtCode: () => {}, +}) + function MenuNodeRenderer({ node, depth = 0 }: { node: MenuNode; depth?: number }) { const hasChildren = node.children.length > 0 if (hasChildren) return @@ -65,19 +89,38 @@ function MenuNodeRenderer({ node, depth = 0 }: { node: MenuNode; depth?: number } function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) { - const [open, setOpen] = useState(depth === 0) + const ctCode = getCtGroupCode(node.key) + const accordion = useContext(AccordionContext) + + // Local state cho group thường (top-level "Hợp đồng" mặc định mở) + const [localOpen, setLocalOpen] = useState(depth === 0) + + // Ct_ group: state controlled bởi accordion context + const isAccordion = ctCode != null + const open = isAccordion ? accordion.expandedCtCode === ctCode : localOpen + + function toggle() { + if (isAccordion) { + accordion.setExpandedCtCode(open ? null : ctCode) + } else { + setLocalOpen(o => !o) + } + } + const Icon = getIcon(node.icon) const isTopLevel = depth === 0 return (