import { useEffect, useMemo, useState, type ReactNode } from 'react' import { Link, NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom' import { ApiHttpError } from '../api/http' import { fetchNavMenu } from '../api/session' import type { NavItem } from '../api/types' import { useAuth } from '../auth/AuthProvider' import { isExternalNavUrl, spaPathFromCiUrl, } from '../lib/ciSpaPaths' const navLabelCollator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true, }) function compareNavItemsByLabel(a: NavItem, b: NavItem): number { const labelResult = navLabelCollator.compare(a.label?.trim() || '', b.label?.trim() || '') return labelResult || a.id - b.id } function sortNavTree(items: NavItem[]): NavItem[] { return [...items] .map((item) => ({ ...item, children: sortNavTree(item.children ?? []), })) .sort(compareNavItemsByLabel) } function normalizeNavItems(payload: unknown): NavItem[] { const rawItems = Array.isArray(payload) ? (payload as NavItem[]) : payload && typeof payload === 'object' && 'data' in payload && Array.isArray((payload as { data: unknown }).data) ? ((payload as { data: NavItem[] }).data ?? []) : [] if (rawItems.length === 0) return [] if (rawItems.some((item) => Array.isArray(item.children) && item.children.length > 0)) { return sortNavTree(rawItems) } const byId = new Map() for (const item of rawItems) { byId.set(item.id, { ...item, children: [] }) } const tree: NavItem[] = [] for (const item of byId.values()) { const parentId = item.menu_parent_id ?? null if (parentId != null && byId.has(parentId)) { byId.get(parentId)?.children.push(item) } else { tree.push(item) } } return sortNavTree(tree) } function branchContainsPath(item: NavItem, pathname: string): boolean { const spa = item.url ? spaPathFromCiUrl(item.url) : null if (spa && pathname.startsWith(spa)) return true return (item.children ?? []).some((child) => branchContainsPath(child, pathname)) } function NavTree({ items, pathname }: { items: NavItem[]; pathname: string }) { return ( ) } function NavBranch({ item, depth, pathname }: { item: NavItem; depth: number; pathname: string }) { const enabled = item.is_enabled !== 0 const label = item.label?.trim() || 'Menu' const url = item.url?.trim() || '' const external = isExternalNavUrl(url) const spa = spaPathFromCiUrl(url) const pad = 8 + depth * 10 const icon = item.icon_class ? ( ) : null const children = sortNavTree((item.children ?? []).filter((c) => c.is_enabled !== 0)) const hasChildren = children.length > 0 const [open, setOpen] = useState(() => hasChildren && branchContainsPath(item, pathname)) useEffect(() => { if (hasChildren && branchContainsPath(item, pathname)) { setOpen(true) } }, [hasChildren, item, pathname]) if (!enabled) return null const linkInner = ( <> {icon} {label} {hasChildren ? ( ) : null} ) const linkClass = ({ isActive }: { isActive: boolean }) => `nav-link rounded py-2 px-2 ${isActive ? 'active fw-semibold' : ''}` let main: ReactNode const handleBranchToggle = () => { if (hasChildren) { setOpen((value) => !value) } } if (external) { main = ( { event.preventDefault() handleBranchToggle() } : undefined} > {linkInner} ) } else if (spa && !hasChildren) { main = ( {linkInner} ) } else if (spa && hasChildren) { main = ( ) } else { main = ( ) } return (
  • {main} {children.length > 0 && open ? (
      {children.map((ch) => ( ))}
    ) : null}
  • ) } export function ManagementLayout() { const { user, logout } = useAuth() const navigate = useNavigate() const location = useLocation() const [items, setItems] = useState([]) const [menuError, setMenuError] = useState(null) const [sidebarOpen, setSidebarOpen] = useState(false) useEffect(() => { let cancelled = false ;(async () => { try { const res = await fetchNavMenu() const list = res.data?.items if (!cancelled && list != null) { setItems(normalizeNavItems(list)) setMenuError(null) } } catch (e) { if (!cancelled) { if (e instanceof ApiHttpError && e.status === 401) { logout() navigate('/login', { replace: true, state: { from: `${location.pathname}${location.search}${location.hash}`, message: 'Your session expired. Please sign in again.', }, }) return } setItems([]) setMenuError(e instanceof Error ? e.message : 'Unable to load menu.') } } })() return () => { cancelled = true } }, [location.hash, location.pathname, location.search, logout, navigate]) const navTree = useMemo(() => normalizeNavItems(items), [items]) return (
    Al Rahma Sunday School
    {user?.name}
    {sidebarOpen ? (
    ) }