diff --git a/src/admin/components/Sidebar.tsx b/src/admin/components/Sidebar.tsx index 23217fa..86702c9 100644 --- a/src/admin/components/Sidebar.tsx +++ b/src/admin/components/Sidebar.tsx @@ -1,617 +1,7 @@ -import { type ReactNode } from "react"; import { NavLink, useLocation } from "react-router-dom"; import { useAuth } from "../context/AuthContext"; import { useTheme } from "../../context/ThemeContext"; - -interface MenuItem { - path: string; - label: string; - end?: boolean; - permission?: string | string[]; - matchPrefix?: string; - matchAlso?: string[]; - matchExclude?: string[]; - icon: ReactNode; -} - -interface MenuSection { - label: string; - items: MenuItem[]; -} - -const menuSections: MenuSection[] = [ - { - label: "Přehled", - items: [ - { - path: "/", - label: "Přehled", - end: true, - icon: ( - - - - - - - ), - }, - ], - }, - { - label: "Docházka", - items: [ - { - path: "/attendance", - label: "Záznam", - permission: "attendance.record", - end: true, - icon: ( - - - - - ), - }, - { - path: "/attendance/history", - label: "Moje historie", - permission: "attendance.history", - icon: ( - - - - - ), - }, - { - path: "/attendance/requests", - label: "Žádosti", - permission: "attendance.record", - icon: ( - - - - - - - ), - }, - { - path: "/attendance/approval", - label: "Schvalování", - permission: "attendance.approve", - icon: ( - - - - - ), - }, - { - path: "/attendance/admin", - label: "Správa", - permission: "attendance.manage", - matchPrefix: "/attendance/admin", - matchAlso: ["/attendance/create", "/attendance/location"], - icon: ( - - - - - - - - - - - - ), - }, - { - path: "/attendance/balances", - label: "Správa bilancí", - permission: "attendance.balances", - icon: ( - - - - - - ), - }, - { - path: "/plan-work", - label: "Plán prací", - permission: ["attendance.manage", "attendance.record"], - icon: ( - - - - - - - ), - }, - ], - }, - { - label: "Kniha jízd", - items: [ - { - path: "/trips", - label: "Záznam", - permission: "trips.record", - end: true, - icon: ( - - - - - - - ), - }, - { - path: "/trips/history", - label: "Moje historie", - permission: "trips.history", - icon: ( - - - - - ), - }, - { - path: "/trips/admin", - label: "Správa", - permission: "trips.manage", - icon: ( - - - - - - - - - - - - ), - }, - { - path: "/vehicles", - label: "Vozidla", - permission: "vehicles.manage", - icon: ( - - - - - - - ), - }, - ], - }, - { - label: "Administrativa", - items: [ - { - path: "/offers", - label: "Nabídky", - permission: "offers.view", - matchPrefix: "/offers", - matchExclude: ["/offers/customers", "/offers/templates"], - icon: ( - - - - - ), - }, - { - path: "/orders", - label: "Objednávky", - permission: "orders.view", - matchPrefix: "/orders", - icon: ( - - - - - - ), - }, - { - path: "/invoices", - label: "Faktury", - permission: "invoices.view", - matchPrefix: "/invoices", - icon: ( - - - - - ), - }, - { - path: "/projects", - label: "Projekty", - permission: "projects.view", - matchPrefix: "/projects", - icon: ( - - - - - - ), - }, - { - path: "/offers/customers", - label: "Zákazníci", - permission: "customers.view", - icon: ( - - - - - - - ), - }, - ], - }, - { - label: "Sklad", - items: [ - { - path: "/warehouse", - label: "Přehled", - permission: "warehouse.view", - end: true, - icon: ( - - - - - - ), - }, - { - path: "/warehouse/items", - label: "Položky", - permission: "warehouse.view", - matchPrefix: "/warehouse/items", - icon: ( - - - - - - ), - }, - { - path: "/warehouse/receipts", - label: "Příjmy", - permission: "warehouse.operate", - matchPrefix: "/warehouse/receipts", - icon: ( - - - - - - ), - }, - { - path: "/warehouse/issues", - label: "Výdeje", - permission: "warehouse.operate", - matchPrefix: "/warehouse/issues", - icon: ( - - - - - - ), - }, - { - path: "/warehouse/reservations", - label: "Rezervace", - permission: "warehouse.operate", - matchPrefix: "/warehouse/reservations", - icon: ( - - - - - - - ), - }, - { - path: "/warehouse/inventory", - label: "Inventura", - permission: "warehouse.inventory", - matchPrefix: "/warehouse/inventory", - icon: ( - - - - - ), - }, - { - path: "/warehouse/reports", - label: "Reporty", - permission: "warehouse.view", - end: true, - icon: ( - - - - - - ), - }, - { - path: "/warehouse/categories", - label: "Kategorie", - permission: "warehouse.manage", - matchPrefix: "/warehouse/categories", - icon: ( - - - - - - - - - - - - ), - }, - { - path: "/warehouse/locations", - label: "Lokace", - permission: "warehouse.manage", - matchPrefix: "/warehouse/locations", - icon: ( - - - - - - - ), - }, - { - path: "/warehouse/suppliers", - label: "Dodavatelé", - permission: "warehouse.manage", - matchPrefix: "/warehouse/suppliers", - icon: ( - - - - - - - ), - }, - ], - }, - { - label: "Systém", - items: [ - { - path: "/users", - label: "Uživatelé", - permission: "users.view", - icon: ( - - - - - - - ), - }, - { - path: "/settings", - label: "Nastavení", - permission: [ - "settings.company", - "settings.banking", - "settings.roles", - "settings.system", - "settings.templates", - ], - icon: ( - - - - - ), - }, - { - path: "/audit-log", - label: "Audit log", - permission: "settings.audit", - icon: ( - - - - - - - - ), - }, - ], - }, -]; +import { menuSections, isItemActive, hasItemPermission } from "../ui/navData"; interface SidebarProps { isOpen: boolean; @@ -624,36 +14,12 @@ export default function Sidebar({ isOpen, onClose, onLogout }: SidebarProps) { const { theme } = useTheme(); const location = useLocation(); - const isItemActive = (item: MenuItem) => { - if (item.matchPrefix) { - let active = location.pathname.startsWith(item.matchPrefix); - if (active && item.matchExclude) { - active = !item.matchExclude.some((ex) => - location.pathname.startsWith(ex), - ); - } - return active; - } - if (item.end) { - return location.pathname === item.path; - } - return location.pathname.startsWith(item.path); - }; - - const hasItemPermission = (item: MenuItem) => { - if (!item.permission) { - return true; - } - if (Array.isArray(item.permission)) { - return item.permission.some((p) => hasPermission(p)); - } - return hasPermission(item.permission); - }; - const visibleSections = menuSections .map((section) => ({ ...section, - items: section.items.filter(hasItemPermission), + items: section.items.filter((item) => + hasItemPermission(item, hasPermission), + ), })) .filter((section) => section.items.length > 0); @@ -709,15 +75,9 @@ export default function Sidebar({ isOpen, onClose, onLogout }: SidebarProps) { to={item.path} end={item.end} onClick={onClose} - className={() => { - let active = isItemActive(item); - if (!active && item.matchAlso) { - active = item.matchAlso.some((p) => - location.pathname.startsWith(p), - ); - } - return `admin-nav-item${active ? " active" : ""}`; - }} + className={() => + `admin-nav-item${isItemActive(item, location.pathname) ? " active" : ""}` + } > {item.icon} {item.label} diff --git a/src/admin/ui/navData.tsx b/src/admin/ui/navData.tsx new file mode 100644 index 0000000..55025b7 --- /dev/null +++ b/src/admin/ui/navData.tsx @@ -0,0 +1,642 @@ +import { type ReactNode } from "react"; + +export interface MenuItem { + path: string; + label: string; + end?: boolean; + permission?: string | string[]; + matchPrefix?: string; + matchAlso?: string[]; + matchExclude?: string[]; + icon: ReactNode; +} + +export interface MenuSection { + label: string; + items: MenuItem[]; +} + +export const menuSections: MenuSection[] = [ + { + label: "Přehled", + items: [ + { + path: "/", + label: "Přehled", + end: true, + icon: ( + + + + + + + ), + }, + ], + }, + { + label: "Docházka", + items: [ + { + path: "/attendance", + label: "Záznam", + permission: "attendance.record", + end: true, + icon: ( + + + + + ), + }, + { + path: "/attendance/history", + label: "Moje historie", + permission: "attendance.history", + icon: ( + + + + + ), + }, + { + path: "/attendance/requests", + label: "Žádosti", + permission: "attendance.record", + icon: ( + + + + + + + ), + }, + { + path: "/attendance/approval", + label: "Schvalování", + permission: "attendance.approve", + icon: ( + + + + + ), + }, + { + path: "/attendance/admin", + label: "Správa", + permission: "attendance.manage", + matchPrefix: "/attendance/admin", + matchAlso: ["/attendance/create", "/attendance/location"], + icon: ( + + + + + + + + + + + + ), + }, + { + path: "/attendance/balances", + label: "Správa bilancí", + permission: "attendance.balances", + icon: ( + + + + + + ), + }, + { + path: "/plan-work", + label: "Plán prací", + permission: ["attendance.manage", "attendance.record"], + icon: ( + + + + + + + ), + }, + ], + }, + { + label: "Kniha jízd", + items: [ + { + path: "/trips", + label: "Záznam", + permission: "trips.record", + end: true, + icon: ( + + + + + + + ), + }, + { + path: "/trips/history", + label: "Moje historie", + permission: "trips.history", + icon: ( + + + + + ), + }, + { + path: "/trips/admin", + label: "Správa", + permission: "trips.manage", + icon: ( + + + + + + + + + + + + ), + }, + { + path: "/vehicles", + label: "Vozidla", + permission: "vehicles.manage", + icon: ( + + + + + + + ), + }, + ], + }, + { + label: "Administrativa", + items: [ + { + path: "/offers", + label: "Nabídky", + permission: "offers.view", + matchPrefix: "/offers", + matchExclude: ["/offers/customers", "/offers/templates"], + icon: ( + + + + + ), + }, + { + path: "/orders", + label: "Objednávky", + permission: "orders.view", + matchPrefix: "/orders", + icon: ( + + + + + + ), + }, + { + path: "/invoices", + label: "Faktury", + permission: "invoices.view", + matchPrefix: "/invoices", + icon: ( + + + + + ), + }, + { + path: "/projects", + label: "Projekty", + permission: "projects.view", + matchPrefix: "/projects", + icon: ( + + + + + + ), + }, + { + path: "/offers/customers", + label: "Zákazníci", + permission: "customers.view", + icon: ( + + + + + + + ), + }, + ], + }, + { + label: "Sklad", + items: [ + { + path: "/warehouse", + label: "Přehled", + permission: "warehouse.view", + end: true, + icon: ( + + + + + + ), + }, + { + path: "/warehouse/items", + label: "Položky", + permission: "warehouse.view", + matchPrefix: "/warehouse/items", + icon: ( + + + + + + ), + }, + { + path: "/warehouse/receipts", + label: "Příjmy", + permission: "warehouse.operate", + matchPrefix: "/warehouse/receipts", + icon: ( + + + + + + ), + }, + { + path: "/warehouse/issues", + label: "Výdeje", + permission: "warehouse.operate", + matchPrefix: "/warehouse/issues", + icon: ( + + + + + + ), + }, + { + path: "/warehouse/reservations", + label: "Rezervace", + permission: "warehouse.operate", + matchPrefix: "/warehouse/reservations", + icon: ( + + + + + + + ), + }, + { + path: "/warehouse/inventory", + label: "Inventura", + permission: "warehouse.inventory", + matchPrefix: "/warehouse/inventory", + icon: ( + + + + + ), + }, + { + path: "/warehouse/reports", + label: "Reporty", + permission: "warehouse.view", + end: true, + icon: ( + + + + + + ), + }, + { + path: "/warehouse/categories", + label: "Kategorie", + permission: "warehouse.manage", + matchPrefix: "/warehouse/categories", + icon: ( + + + + + + + + + + + + ), + }, + { + path: "/warehouse/locations", + label: "Lokace", + permission: "warehouse.manage", + matchPrefix: "/warehouse/locations", + icon: ( + + + + + + + ), + }, + { + path: "/warehouse/suppliers", + label: "Dodavatelé", + permission: "warehouse.manage", + matchPrefix: "/warehouse/suppliers", + icon: ( + + + + + + + ), + }, + ], + }, + { + label: "Systém", + items: [ + { + path: "/users", + label: "Uživatelé", + permission: "users.view", + icon: ( + + + + + + + ), + }, + { + path: "/settings", + label: "Nastavení", + permission: [ + "settings.company", + "settings.banking", + "settings.roles", + "settings.system", + "settings.templates", + ], + icon: ( + + + + + ), + }, + { + path: "/audit-log", + label: "Audit log", + permission: "settings.audit", + icon: ( + + + + + + + + ), + }, + ], + }, +]; + +/** Active-state matching (mirrors the original Sidebar logic, incl. matchAlso). */ +export function isItemActive(item: MenuItem, pathname: string): boolean { + if (item.matchPrefix) { + let active = pathname.startsWith(item.matchPrefix); + if (active && item.matchExclude) { + active = !item.matchExclude.some((ex) => pathname.startsWith(ex)); + } + if (!active && item.matchAlso) { + active = item.matchAlso.some((p) => pathname.startsWith(p)); + } + return active; + } + if (item.matchAlso && item.matchAlso.some((p) => pathname.startsWith(p))) { + return true; + } + if (item.end) return pathname === item.path; + return pathname.startsWith(item.path); +} + +/** Permission check: true if no permission, or any listed permission is held. */ +export function hasItemPermission( + item: MenuItem, + hasPermission: (p: string) => boolean, +): boolean { + if (!item.permission) return true; + if (Array.isArray(item.permission)) { + return item.permission.some((p) => hasPermission(p)); + } + return hasPermission(item.permission); +}