[CLAUDE] FE: accordion mutex Pe_* + sidebar width w-72 + label nowrap
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m56s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m56s
2 fix user session 3:
1. Accordion mutex cho Pe_ groups (2 phieu Duyet NCC + Duyet NCC va
Giai phap) — truoc chi Ct_ (7 HD) co mutex, Pe_ dung localOpen rieng
→ user click 1 Pe_ group xong click Pe_ khac → ca 2 expanded cung luc.
Fix: extend AccordionContextValue + expandedPeCode + INT_TO_PE_CODE
map. MenuGroup branch: isCtAccordion || isPeAccordion (families doc
lap — user co the mo 1 Ct_ + 1 Pe_ cung luc). useEffect URL sync:
/purchase-evaluations?type → Pe; /contracts* ?type → Ct.
2. Sidebar width w-64 (256px) → w-72 (288px) de label QUY TRINH CHON
THAU PHU - NCC (30 char uppercase tracking-wide) fit single-line
thay vi wrap 2 dong. Top-level group class:
text-xs tracking-wider → text-[11px] tracking-wide + whitespace-nowrap
Dong bo ca fe-admin + fe-user.
This commit is contained in:
@ -35,6 +35,21 @@ function getCtGroupCode(key: string): string | null {
|
||||
return m ? m[1] : null
|
||||
}
|
||||
|
||||
// Pe_<Code> group key (Duyệt NCC / Duyệt NCC và Giải pháp). Riêng accordion
|
||||
// để Pe_* mutex với nhau (không mix với Ct_*) — user có thể expand 1 Ct_ HĐ
|
||||
// + 1 Pe_ phiếu cùng lúc vì 2 module khác nhau.
|
||||
const PE_GROUP_PATTERN = /^Pe_([^_]+)$/
|
||||
function getPeGroupCode(key: string): string | null {
|
||||
const m = key.match(PE_GROUP_PATTERN)
|
||||
return m ? m[1] : null
|
||||
}
|
||||
|
||||
// PE type code → int (mirror BE PurchaseEvaluationType enum)
|
||||
const PE_CODE_TO_INT: Record<string, number> = { DuyetNcc: 1, DuyetNccPhuongAn: 2 }
|
||||
const INT_TO_PE_CODE: Record<number, string> = Object.fromEntries(
|
||||
Object.entries(PE_CODE_TO_INT).map(([k, v]) => [v, k]),
|
||||
)
|
||||
|
||||
// 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 {
|
||||
@ -59,7 +74,6 @@ function resolvePath(key: string): string | null {
|
||||
const peMatch = key.match(/^Pe_([^_]+)_(List|Create|Pending)$/)
|
||||
if (peMatch) {
|
||||
const [, code, action] = peMatch
|
||||
const PE_CODE_TO_INT: Record<string, number> = { DuyetNcc: 1, DuyetNccPhuongAn: 2 }
|
||||
const typeInt = PE_CODE_TO_INT[code]
|
||||
if (!typeInt) return null
|
||||
if (action === 'List') return `/purchase-evaluations?type=${typeInt}`
|
||||
@ -83,16 +97,21 @@ 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).
|
||||
// Accordion state cho groups Ct_<Code> (7 HĐ) + Pe_<Code> (2 phiếu) — mỗi
|
||||
// family mutex độc lập. Auto-expand theo URL `?type=X` (Ct_ dùng route
|
||||
// /contracts|/my-contracts|/inbox, Pe_ dùng /purchase-evaluations). Group
|
||||
// không match prefix giữ behavior cũ (independent local useState).
|
||||
type AccordionContextValue = {
|
||||
expandedCtCode: string | null
|
||||
setExpandedCtCode: (code: string | null) => void
|
||||
expandedPeCode: string | null
|
||||
setExpandedPeCode: (code: string | null) => void
|
||||
}
|
||||
const AccordionContext = createContext<AccordionContextValue>({
|
||||
expandedCtCode: null,
|
||||
setExpandedCtCode: () => {},
|
||||
expandedPeCode: null,
|
||||
setExpandedPeCode: () => {},
|
||||
})
|
||||
|
||||
function MenuNodeRenderer({ node, depth = 0 }: { node: MenuNode; depth?: number }) {
|
||||
@ -103,18 +122,28 @@ function MenuNodeRenderer({ node, depth = 0 }: { node: MenuNode; depth?: number
|
||||
|
||||
function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) {
|
||||
const ctCode = getCtGroupCode(node.key)
|
||||
const peCode = getPeGroupCode(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
|
||||
// Ct_<Code> / Pe_<Code> group: state controlled bởi accordion context,
|
||||
// mỗi family (Ct / Pe) mutex độc lập.
|
||||
const isCtAccordion = ctCode != null
|
||||
const isPeAccordion = peCode != null
|
||||
const isAccordion = isCtAccordion || isPeAccordion
|
||||
const open = isCtAccordion
|
||||
? accordion.expandedCtCode === ctCode
|
||||
: isPeAccordion
|
||||
? accordion.expandedPeCode === peCode
|
||||
: localOpen
|
||||
|
||||
function toggle() {
|
||||
if (isAccordion) {
|
||||
if (isCtAccordion) {
|
||||
accordion.setExpandedCtCode(open ? null : ctCode)
|
||||
} else if (isPeAccordion) {
|
||||
accordion.setExpandedPeCode(open ? null : peCode)
|
||||
} else {
|
||||
setLocalOpen(o => !o)
|
||||
}
|
||||
@ -130,7 +159,7 @@ function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) {
|
||||
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-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500 hover:bg-slate-100 whitespace-nowrap'
|
||||
: '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',
|
||||
@ -219,27 +248,38 @@ export function Layout() {
|
||||
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).
|
||||
// Accordion state — 1 Ct_ HĐ + 1 Pe_ phiếu expand tối đa. Auto-sync theo
|
||||
// URL: /contracts|/my-contracts|/inbox?type=N → Ct, /purchase-evaluations
|
||||
// ?type=N → Pe. Khác pathname gốc → skip (không reset, giữ user choice).
|
||||
const [expandedCtCode, setExpandedCtCode] = useState<string | null>(null)
|
||||
const [expandedPeCode, setExpandedPeCode] = 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 (!typeParam) return
|
||||
|
||||
const n = Number(typeParam)
|
||||
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
|
||||
const code = INT_TO_TYPE_CODE[n]
|
||||
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])
|
||||
}, [location.pathname, location.search])
|
||||
|
||||
const accordionValue = useMemo(() => ({ expandedCtCode, setExpandedCtCode }), [expandedCtCode])
|
||||
const accordionValue = useMemo(
|
||||
() => ({ expandedCtCode, setExpandedCtCode, expandedPeCode, setExpandedPeCode }),
|
||||
[expandedCtCode, expandedPeCode],
|
||||
)
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={accordionValue}>
|
||||
<div className="flex h-screen">
|
||||
<aside className="flex w-64 flex-col border-r border-slate-200 bg-white">
|
||||
<aside className="flex w-72 flex-col border-r border-slate-200 bg-white">
|
||||
<div className="flex h-16 items-center border-b border-slate-200 px-5">
|
||||
<Link to="/dashboard" className="flex items-center gap-2.5">
|
||||
<img src="/logo.png" alt="Solutions" className="h-8 w-auto" />
|
||||
|
||||
Reference in New Issue
Block a user