[CLAUDE] FE-User: sidebar accordion cho menu loại HĐ — chỉ 1 group expand cùng lúc
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m46s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m46s
User feedback: 7 group Ct_<Code> (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_<Code>` 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) <noreply@anthropic.com>
This commit is contained in:
@ -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<string, number> = {
|
||||
NguyenTacDv: 7,
|
||||
}
|
||||
|
||||
// Reverse lookup int → code (cho auto-expand từ URL ?type=)
|
||||
const INT_TO_TYPE_CODE: Record<number, string> = Object.fromEntries(
|
||||
Object.entries(TYPE_CODE_TO_INT).map(([k, v]) => [v, k]),
|
||||
)
|
||||
|
||||
// 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 {
|
||||
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_<Code> — 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<AccordionContextValue>({
|
||||
expandedCtCode: null,
|
||||
setExpandedCtCode: () => {},
|
||||
})
|
||||
|
||||
function MenuNodeRenderer({ node, depth = 0 }: { node: MenuNode; depth?: number }) {
|
||||
const hasChildren = node.children.length > 0
|
||||
if (hasChildren) return <MenuGroup node={node} depth={depth} />
|
||||
@ -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_<Code> 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 (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setOpen(o => !o)}
|
||||
onClick={toggle}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between rounded-md transition',
|
||||
isTopLevel
|
||||
? 'px-3 py-2 text-xs font-semibold uppercase tracking-wider text-slate-500 hover:bg-slate-100'
|
||||
: 'px-3 py-1.5 text-[13px] font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900',
|
||||
// Highlight Ct_ group đang active (accordion open) bằng tinted background
|
||||
isAccordion && open && 'bg-slate-50 text-slate-900',
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
@ -161,8 +204,27 @@ function StaticLeaf({ node }: { node: MenuNode }) {
|
||||
export function Layout() {
|
||||
const { menu } = useAuth()
|
||||
const filteredMenu = filterForUser(menu)
|
||||
const location = useLocation()
|
||||
|
||||
// Accordion state — chỉ 1 Ct_<Code> group expand. Auto-sync theo URL ?type=
|
||||
// (sidebar tự mở đúng loại HĐ user đang xem).
|
||||
const [expandedCtCode, setExpandedCtCode] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search)
|
||||
const typeParam = params.get('type')
|
||||
if (typeParam) {
|
||||
const code = INT_TO_TYPE_CODE[Number(typeParam)]
|
||||
if (code) setExpandedCtCode(code)
|
||||
}
|
||||
// KHÔNG reset null khi URL không có ?type — giữ context user-selected
|
||||
// (vd user đang xem dashboard nhưng đã click expand 1 group, giữ nguyên)
|
||||
}, [location.search])
|
||||
|
||||
const accordionValue = useMemo(() => ({ expandedCtCode, setExpandedCtCode }), [expandedCtCode])
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={accordionValue}>
|
||||
<div className="flex h-screen">
|
||||
<aside className="flex w-64 flex-col border-r border-slate-200 bg-white">
|
||||
<div className="flex h-16 items-center border-b border-slate-200 px-5">
|
||||
@ -185,5 +247,6 @@ export function Layout() {
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user