Files
solution-erp/fe-admin/src/components/Layout.tsx

402 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'
import { cn } from '@/lib/cn'
import { findContractWfByTypeCode } from '@/lib/contractWorkflowFamily'
function getIcon(name: string | null): LucideIcon {
if (!name) return Circle
const candidate = (Icons as unknown as Record<string, LucideIcon>)[name]
return candidate ?? Circle
}
// Map contract type code → ContractType int enum value (mirrors
// Domain/Contracts/ContractType.cs for URL filter).
const TYPE_CODE_TO_INT: Record<string, number> = {
ThauPhu: 1,
GiaoKhoan: 2,
NhaCungCap: 3,
DichVu: 4,
MuaBan: 5,
NguyenTacNcc: 6,
NguyenTacDv: 7,
}
// Resolve menu key → route. Static map for top-level items, pattern for
// Ct_<Type>_<Action> sub-menu entries.
function resolvePath(key: string): string | null {
const staticMap: Record<string, string> = {
// [W2 KHKK S161] GĐ2 hết skeleton — re-point trang thật (gotcha #50: thiếu route =
// sidebar drop SILENT). WfView phía admin trỏ Designer type-10 (fe-admin CỐ Ý không có
// matrix page — chốt 2026-07-27; admin "Luồng duyệt" = dựng/sửa quy trình).
// [K4b S166 — spec K4b-7] 5 leaf trỏ `/khkk/*` mang `group=1` tường minh, mirror
// fe-user (trang coi "vắng group" = xem tất cả, nên thiếu ở đây là nhóm 1 nói dối).
// 🔴 `Khkk_WfView` CỐ Ý KHÔNG mang `group`: bên admin nó trỏ Designer quy trình,
// không phải trang danh sách — Designer không đọc tham số này (fe-user mới đi
// `/khkk/workflow-matrix?type=10&group=1`). Lệch có chủ đích, không mirror mù.
Khkk_WfView: '/system/approval-workflows-v2/ContractSigningPlan',
Khkk_List: '/khkk/list?group=1',
Khkk_Create: '/khkk/create?group=1',
Khkk_Pending: '/khkk/list?group=1&filter=ChoDuyet',
Khkk_Approved: '/khkk/list?group=1&filter=DaDuyet',
Khkk_Deleted: '/khkk/list?group=1&view=deleted', // [S159-đợt5 FLAG-1] thiếu là leaf drop SILENT (gotcha #50)
// [W7 KHKK S161] GĐ4 hết skeleton — 7 leaf `Hdc_*` re-point trang thật
// `/hard-copies?type=N` (N = ContractType int, mirror TYPE_CODE_TO_INT ở trên).
// gotcha #50: thiếu/để sai route ở đây = leaf DROP IM LẶNG khỏi sidebar.
Hdc_ThauPhu: '/hard-copies?type=1',
Hdc_GiaoKhoan: '/hard-copies?type=2',
Hdc_NhaCungCap: '/hard-copies?type=3',
Hdc_DichVu: '/hard-copies?type=4',
Hdc_MuaBan: '/hard-copies?type=5',
Hdc_NguyenTacNcc: '/hard-copies?type=6',
Hdc_NguyenTacDv: '/hard-copies?type=7',
Dashboard: '/dashboard',
Suppliers: '/master/suppliers',
Projects: '/master/projects',
Departments: '/master/departments',
Contracts: '/contracts',
Forms: '/forms',
Reports: '/reports',
Users: '/system/users',
Roles: '/system/roles',
Permissions: '/system/permissions',
MenuVisibility: '/system/menu-visibility',
Workflows: '/system/workflows',
CatalogUnits: '/master/catalogs/units',
CatalogMaterials: '/master/catalogs/materials',
CatalogServices: '/master/catalogs/services',
CatalogWorkItems: '/master/catalogs/work-items',
// [K1 — S164, Mig 70] Danh mục Hợp đồng SP-002 — "chỗ thứ 4" Pattern 16-bis:
// BE seed leaf `ContractCatalog` dưới group Catalogs, thiếu dòng này thì
// resolvePath trả null → MenuLeaf DROP IM LẶNG khỏi sidebar (gotcha #50).
ContractCatalog: '/master/catalogs/contract-catalog',
PurchaseEvaluations: '/purchase-evaluations',
PeWorkflows: '/system/pe-workflows',
// [Phase 10.1 G-H1 S33 2026-05-26] Module Hồ sơ Nhân sự (Mig 34). LESSON
// Plan CA Hotfix 1 gotcha #50: PHẢI mirror staticMap khi thêm page mới
// — nếu thiếu, MenuLeaf line ~198 `if (!path) return null` → sidebar drop silent.
Hrm_HoSo: '/employees',
// [Phase 10.2 G-H2 S35 2026-05-28] Cấu hình HRM 4 catalog leaf (Mig 35).
// Pattern 16-bis 4-place mirror (staticMap = 4th place, dễ miss nhất).
Hrm_Config_LeaveTypes: '/hrm/configs/leave-types',
Hrm_Config_Holidays: '/hrm/configs/holidays',
Hrm_Config_Shifts: '/hrm/configs/shifts',
Hrm_Config_OtPolicies: '/hrm/configs/ot-policies',
// [P11-C S51] danh mục xe công + tài xế — cùng page :kind-driven.
Hrm_Config_Vehicles: '/hrm/configs/vehicles',
Hrm_Config_Drivers: '/hrm/configs/drivers',
// [Phase 10.2 G-O1 S34 2026-05-27] Module Văn phòng số — Danh bạ nội bộ.
// 4-place mirror Pattern 16-bis: types/ + pages/ + App.tsx + menuKeys + staticMap.
Off_Dashboard: '/office/dashboard',
Off_DanhBa: '/directory',
// [Phase 10.2 G-O2 S36 2026-05-28] Phòng họp Booking + Catalog (Mig 36).
// Pattern 16-bis 4-place mirror 7× cumulative — staticMap = 4th place dễ miss.
// View leaf + Book leaf đều trỏ calendar (user perspective); Manage trỏ catalog.
Off_PhongHop_View: '/meeting-calendar',
Off_PhongHop_Book: '/meeting-calendar',
Off_PhongHop_Manage: '/meeting-rooms',
Off_DeXuat_List: '/proposals',
Off_DeXuat_Create: '/proposals/new',
Off_DeXuat_Inbox: '/proposals?status=2&inboxOnly=true',
// Phase 10.3-10.4 S38 — Workflow Apps skeleton (8 entries)
Off_DonTu_Leave: '/workflow-apps/leave',
Off_DonTu_Ot: '/workflow-apps/ot',
Off_DonTu_Travel: '/workflow-apps/travel',
Off_DatXe: '/workflow-apps/vehicle',
Off_ItTicket: '/it-tickets',
Off_ChamCong: '/attendance',
// [P11-E S52] Báo cáo chấm công — admin-only leaf (route từ App.tsx S52).
Off_AttendanceReport: '/attendance/report',
Hrm_Dashboard: '/hr/dashboard',
}
if (staticMap[key]) return staticMap[key]
const match = key.match(/^Ct_([^_]+)_(List|Create|Pending)$/)
if (match) {
const [, code, action] = match
const typeInt = TYPE_CODE_TO_INT[code]
if (!typeInt) return null
if (action === 'List') return `/contracts?type=${typeInt}`
if (action === 'Create') return `/contracts/new?type=${typeInt}`
if (action === 'Pending') return `/contracts?type=${typeInt}&pendingMe=1`
}
// Workflow admin per ContractType: Wf_<Code> → /system/workflows/<code>
const wfMatch = key.match(/^Wf_(.+)$/)
if (wfMatch) {
const code = wfMatch[1]
if (TYPE_CODE_TO_INT[code]) return `/system/workflows/${code}`
}
// Pe_<Code>_<Action> cho module Duyệt NCC
// [S155 đợt 2 — owner restructure menu 2026-07-27] +`Approved` +`Deleted`. Đây là
// "chỗ thứ 4" của Pattern 16-bis (staticMap/resolvePath) — key seed ở BE mà quên nới
// regex thì `resolvePath` trả null ⇒ MenuLeaf DROP IM LẶNG khỏi sidebar (gotcha #50).
// 🔴 CỐ Ý KHÔNG có `WfView` ở đây (khác fe-user): fe-admin không có
// `pages/pe/WorkflowMatrixViewPage.tsx` lẫn route `/purchase-evaluations/workflow-matrix`
// ⇒ thêm vào sẽ đẻ link chết rơi vào catch-all "Trang này chưa được build". Admin đã có
// Designer (`/system/approval-workflows-v2/...`) sửa được nên bản matrix chỉ-xem là thừa.
// Lead chốt 2026-07-27 = GIỮ NGUYÊN lệch này; đừng "vá" ở lượt sau.
const peMatch = key.match(/^Pe_([^_]+)_(List|Create|Pending|Approved|Deleted)$/)
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}`
// "Thao tác" leaf → workspace 2-panel (Q4 2026-05-07): pick + create + sửa
// tables inline. Header-only `/new` page giữ tồn tại cho deep-link cũ
// (PeDetailTabs "Sửa header" button vẫn navigate sang đó).
if (action === 'Create') return `/purchase-evaluations/workspace?type=${typeInt}`
if (action === 'Pending') return `/purchase-evaluations?type=${typeInt}&pendingMe=1`
// [S155 đợt 2] 2 mục MỚI — CẢ HAI tái dùng `PurchaseEvaluationsListPage`, 0 route mới,
// 0 page mới. "Đã duyệt" chỉ là filter phase sẵn có (`DaDuyet = 7`, đã thông 3 tầng);
// "Đã xóa" là view riêng trong cùng page, gọi endpoint `/purchase-evaluations/deleted`.
// 🔴 `view=approved` = KHOÁ ĐỊNH DANH (trang bỏ qua, chỉ `phase=7` lọc thật). URL mục
// "Đã duyệt" trùng hệt URL user tự lọc trạng thái trên Danh sách ⇒ menu sáng nhầm
// (bug UAT 2026-07-27). `phase` thuộc TRANSIENT nên không làm danh tính được.
// Mirror `fe-user/src/components/Layout.tsx` — cùng bản vá.
if (action === 'Approved') return `/purchase-evaluations?type=${typeInt}&phase=7&view=approved`
if (action === 'Deleted') return `/purchase-evaluations?type=${typeInt}&deleted=1`
}
// PE workflow admin leaf: PeWf_<Code> → /system/pe-workflows/<code>
const peWfMatch = key.match(/^PeWf_(.+)$/)
if (peWfMatch) {
const code = peWfMatch[1]
if (code === 'DuyetNcc' || code === 'DuyetNccPhuongAn') return `/system/pe-workflows/${code}`
}
// Quy trình duyệt MỚI (Mig 22 — Session 17): root = group bowed, leaf =
// type-specific designer. Sau UAT thay thế PeWorkflows + Workflows cũ.
if (key === 'ApprovalWorkflowsV2') return '/system/approval-workflows-v2'
const awV2Match = key.match(/^AwV2_(.+)$/)
if (awV2Match) {
const code = awV2Match[1]
if (code === 'DuyetNcc' || code === 'DuyetNccPhuongAn' || code === 'Contract') {
return `/system/approval-workflows-v2/${code}`
}
// [K3 S164/S165] Họ MỚI `AwV2_KhkkN1..N8` — 8 quy trình duyệt theo NHÓM của
// Kế hoạch ký kết HĐ (mỗi nhóm 1 đội hình 3 trạm PRO → CCM → CEO). Đây là "chỗ
// thứ 4" Pattern 16-bis: BE seed leaf mà thiếu nhánh này thì `resolvePath` trả
// null ⇒ MenuLeaf DROP IM LẶNG khỏi sidebar (gotcha #50).
// 🔴 CHỈ nới họ `AwV2_*` (Designer, fe-admin CÓ trang). KHÔNG nới họ `Khkk_*`
// — fe-admin cố ý KHÔNG có `WorkflowMatrixViewPage` (xem ghi chú khối `Pe_*`
// bên trên); nới ở đó = đẻ leaf chết rơi vào catch-all "chưa được build".
if (/^KhkkN[1-8]$/.test(code)) return `/system/approval-workflows-v2/${code}`
// [W4 S190 · YC-029] Họ MỚI `AwV2_Hd*` — 7 quy trình duyệt HĐ per-LOẠI (seed @S188,
// sidebar Order 11..17). Vẫn là "chỗ thứ 4" Pattern 16-bis: BE seed leaf mà thiếu nhánh
// này thì `resolvePath` trả null ⇒ MenuLeaf DROP IM LẶNG khỏi sidebar (gotcha #50).
// 🔴 Tra bằng BẢNG (`lib/contractWorkflowFamily.ts`, mirror BE `ContractApprovalWorkflowFamily.cs`),
// KHÔNG regex đoán ngược: `HdNTNCC` chẻ ra `NT-NCC` hay `N-TNCC` là hai mã khác nhau.
if (findContractWfByTypeCode(code)) return `/system/approval-workflows-v2/${code}`
}
return null
}
// ============================================================================
// [K6 — S166 2026-08-01, QĐ7] ADMIN THU VỀ VAI "NƠI-CONFIG"
// ============================================================================
// Sidebar fe-admin chỉ còn: Tổng quan · Hệ thống (Người dùng / Vai trò / Phân
// quyền / Menu eOffice, cùng nhánh con "Quy trình duyệt (Mới)" + toàn bộ Designer).
// Mọi module NGHIỆP VỤ (4 giai đoạn HĐ, Duyệt NCC, Danh mục, HRM, Văn phòng…)
// làm việc ở fe-user; admin không phải là chỗ chạy nghiệp vụ nữa.
//
// 🔴 LỊCH SỬ ĐÃ ĐẢO 2 LẦN — đọc kỹ trước khi "sửa cho đúng":
// · S29 : ẩn "Danh mục" khỏi admin.
// · S57 : BỎ ẩn — gom master data về 1 chỗ cho cả admin lẫn nhân viên, admin
// quản master trực tiếp trên fe-admin. (Đảo lần 1.)
// · S164 : QĐ7 ẩn LẠI, lần này ẩn rộng hơn hẳn — admin thu về vai NƠI-CONFIG.
// (Đảo lần 2, chính là luật đang chạy dưới đây.)
// ⇒ Comment cũ của S57 nay KHÔNG còn hiệu lực. Ai đọc lịch sử git thấy hai
// lời ngược nhau thì đây là chỗ giải thích: cả hai đều từng đúng, mốc sau
// thắng mốc trước. Đừng "khôi phục" S57 vì tưởng gặp bug.
//
// 🔴 ĐÂY LÀ LỚP HIỂN THỊ, KHÔNG PHẢI LỚP QUYỀN (2 tầng độc lập — gotcha #82):
// Route fe-admin còn nguyên, quyền của tài khoản admin còn nguyên ⇒ deep-link
// thẳng `/purchase-evaluations`, `/khkk/list`… VẪN vào được và API vẫn trả 200.
// Thu gọn ở đây = bớt lối đi trên menu, KHÔNG phải thu hồi quyền. Muốn CHẶN
// thật thì phải làm ở BE authz (`[Authorize(Policy=…)]`), không phải ở file này.
//
// 🔴 CƠ CHẾ: `filterForAdmin` lọc ĐỆ QUY mọi tầng ⇒ ẩn 1 root là cả cây con chết
// theo. Vì vậy tập dưới chỉ liệt ROOT (+2 nhánh con của `System`), KHÔNG liệt
// từng leaf — liệt leaf vừa thừa vừa phải bảo trì lại mỗi lần BE thêm key.
// Hệ quả phụ (tốt): các leaf `Khkk_G{2..8}_*` không còn được render bên admin
// nên chuyện `resolvePath` thiếu nhánh `Khkk_G*` (khai ở K4a) hết là vấn đề —
// nay chúng bị cắt từ tầng cha chứ không "rơi im lặng" ở tầng leaf nữa.
// ⚠️ Nếu sau này gỡ `KeHoachKyKet` khỏi tập này thì PHẢI nới regex `Khkk_G*`
// trong `resolvePath` TRƯỚC, kèm quyết định `WfView` admin trỏ đâu (admin
// không có `WorkflowMatrixViewPage`; Designer không nhận `?group=`).
const ADMIN_HIDDEN_KEYS = new Set<string>([
'PurchaseEvaluations', // GĐ1 root — Pe_* chết theo
'KeHoachKyKet', // GĐ2 root — toàn bộ nhánh Khkk_G* chết theo
'Contracts', // GĐ3 root — Ct_* vốn đã có luật prefix, nay ẩn cả root
'HopDongCung', // GĐ4 root — Hdc_* chết theo
'Forms',
'Reports',
'Master', // Danh mục root — Suppliers/Projects/Departments/Catalogs + Cấu hình HRM chết theo
'Hrm',
'Off',
'Personal',
'Workflows', // CON của System ⇒ phải liệt riêng (Wf_* chết theo)
'PeWorkflows', // CON của System ⇒ phải liệt riêng (PeWf_* chết theo)
])
function isAdminHidden(key: string): boolean {
// `Ct_*` giữ luật prefix cũ: leaf hợp đồng theo loại là chuyện của app người
// dùng. Thừa so với việc ẩn root `Contracts` ở trên, nhưng giữ để một ngày
// nào đó mở lại root thì đám leaf `Ct_*` không ùa về theo. (KHÔNG ghi số lá
// ở đây — số-đếm trong comment thối ngay khi BE thêm key [bài #84]; cần số
// thì grep `Ct_` trong MenuKeys.cs phía BE.)
return ADMIN_HIDDEN_KEYS.has(key) || key.startsWith('Ct_')
}
function filterForAdmin(nodes: MenuNode[]): MenuNode[] {
return nodes
.filter(n => !isAdminHidden(n.key))
.map(n => ({ ...n, children: filterForAdmin(n.children) }))
}
function MenuNodeRenderer({ node, depth = 0 }: { node: MenuNode; depth?: number }) {
const hasChildren = node.children.length > 0
if (hasChildren) return <MenuGroup node={node} depth={depth} />
return <MenuLeaf node={node} depth={depth} />
}
function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) {
// Top-level groups expanded by default, nested ones collapsed to reduce noise
const [open, setOpen] = useState(depth === 0)
const Icon = getIcon(node.icon)
const isTopLevel = depth === 0
return (
<div>
<button
onClick={() => setOpen(o => !o)}
className={cn(
'relative block w-full rounded-md text-left transition',
isTopLevel
? 'px-3 py-2 pr-7 text-[11px] font-semibold uppercase tracking-wide text-slate-500 hover:bg-slate-100 whitespace-nowrap'
: 'px-3 py-1.5 pr-7 text-[12px] font-medium leading-snug text-slate-600 hover:bg-slate-100 hover:text-slate-900',
)}
>
{/* [Plan AA S24 t2 wrap fix] inline-block icon + inline text — label
dài wrap về đầu hàng (under icon, KHÔNG indent sau icon). Mirror
fe-user rule §3.9. ChevronDown absolute right. */}
<Icon className="mr-1.5 -mt-0.5 inline-block h-4 w-4 align-middle" />
<span className="align-middle" title={node.label}>{node.label}</span>
<ChevronDown className={cn(
'absolute right-2 top-2 h-3.5 w-3.5 text-slate-400 transition',
!open && '-rotate-90',
)} />
</button>
{open && (
<div className={cn(
'mt-0.5 space-y-0.5',
depth === 0 ? 'pl-2' : 'ml-3 mt-1 border-l border-slate-100 pl-3',
)}>
{node.children.map(c => (
<MenuNodeRenderer key={c.key} node={c} depth={depth + 1} />
))}
</div>
)}
</div>
)
}
// Transient query keys — không phải "navigation identity", strip trước khi
// 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).
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,
// transient keys ignored). Fix bug: /contracts?type=1 và ?type=1&pendingMe=1
// cùng highlight vì NavLink built-in `end` prop chỉ match pathname.
function queryMatches(current: string, target: string): boolean {
const a = new URLSearchParams(current)
const b = new URLSearchParams(target)
// 🔴 [S155 đợt 2] Transient chỉ được bỏ qua khi MENU ĐÍCH KHÔNG tự ghim key đó.
// Bug UAT 2026-07-27: click "Danh sách" mà menu sáng "Đã duyệt". `phase` vừa là
// BỘ LỌC trạng thái (nên nằm trong TRANSIENT — fix 2026-05-08) vừa vừa-mới thành
// DANH TÍNH điều hướng của "Đã duyệt" (`?type=N&phase=7`). Strip cả 2 vế ⇒ 2 mục
// KHÔNG PHÂN BIỆT ĐƯỢC. Quyền quyết thuộc về ĐÍCH: ghim ⇒ so nghiêm, không ghim ⇒
// giữ hành vi cũ. (Mirror `fe-user/src/components/Layout.tsx` — cùng bản vá.)
const pinnedByTarget = new Set(b.keys())
const ignorable = (k: string) => TRANSIENT_QUERY_KEYS.has(k) && !pinnedByTarget.has(k)
const aKeys = [...a.keys()].filter(k => !ignorable(k)).sort()
const bKeys = [...b.keys()].filter(k => !ignorable(k)).sort()
if (aKeys.length !== bKeys.length) return false
return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k))
}
function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) {
const Icon = getIcon(node.icon)
const path = resolvePath(node.key)
const location = useLocation()
if (!path) return null
const isDeep = depth >= 2
const [targetPath, targetQuery = ''] = path.split('?')
const isActive = location.pathname === targetPath
&& queryMatches(location.search.replace(/^\?/, ''), targetQuery)
return (
<NavLink
to={path}
title={node.label}
className={cn(
// Density-first: active leaf gets a brand left-rail + tint (crisp
// selected affordance, NAMGROUP). Inactive stays quiet slate.
'relative block rounded-md leading-snug transition-colors',
isDeep ? 'px-3 py-1 text-[11px]' : 'px-3 py-1.5 text-[12px] font-medium',
isActive
? 'bg-brand-50 font-semibold text-brand-700 before:absolute before:inset-y-1 before:left-0 before:w-0.5 before:rounded-full before:bg-brand-600'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
)}
>
{/* [Plan AA S24 t2 wrap fix] inline-block icon + inline text → mirror fe-user. */}
<Icon className={cn('mr-2 -mt-0.5 inline-block align-middle', isDeep ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
<span className="align-middle">{node.label}</span>
</NavLink>
)
}
export function Layout() {
const { menu } = useAuth()
return (
// Brand stripe — dải nhận diện #1F7DC1 đỉnh app (mirror fe-user S58,
// anh yêu cầu "trang trí lên 1 tý"; guide accent sparing).
<div className="flex h-screen flex-col">
<div className="h-1 shrink-0 bg-gradient-to-r from-brand-700 via-brand-500 to-brand-600" />
<div className="flex min-h-0 flex-1">
<aside className="flex w-72 flex-col border-r border-slate-200 bg-white xl:w-80">
<div className="flex h-16 items-center border-b border-brand-100 bg-gradient-to-b from-brand-50/70 to-transparent px-5">
<Link to="/dashboard" className="flex items-center gap-2.5">
<img src="/logo.png" alt="Solutions" className="h-8 w-auto" />
<span className="text-[11px] font-semibold uppercase tracking-wider text-brand-600/80">Admin</span>
</Link>
</div>
<nav className="flex-1 space-y-1 overflow-y-auto p-3">
{filterForAdmin(menu).map(n => (
<MenuNodeRenderer key={n.key} node={n} depth={0} />
))}
</nav>
</aside>
<div className="flex flex-1 flex-col overflow-hidden">
<TopBar />
<main className="flex-1 overflow-auto">
<Outlet />
</main>
</div>
</div>
</div>
)
}