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);
+}