import { type ReactNode } from "react"; import OdinMark from "../components/odin/OdinMark"; /** Palette key driving a section's tile wash + glyph color (see theme.ts). */ export type NavHue = | "primary" | "info" | "teal" | "warning" | "violet" | "slate"; export interface MenuItem { path: string; label: string; end?: boolean; permission?: string | string[]; matchPrefix?: string; matchAlso?: string[]; matchExclude?: string[]; icon: ReactNode; /** Icon ships its own container (OdinMark) — SidebarNav skips the tile. */ rawIcon?: boolean; } export interface MenuSection { label: string; hue: NavHue; items: MenuItem[]; } /** * One stroke glyph per item, every glyph unique within the set (pinned by * navdata-icons.test.ts). 24-viewBox, stroke 2, round caps — rendered at * 16px inside SidebarNav's 26px section-hue tile. */ function glyph(children: ReactNode) { return ( {children} ); } export const menuSections: MenuSection[] = [ { label: "Přehled", hue: "primary", items: [ { path: "/", label: "Přehled", end: true, // tachometr — dashboard icon: glyph( <> , ), }, { path: "/odin", label: "Odin", permission: "ai.use", end: true, icon: , rawIcon: true, }, ], }, { label: "Docházka", hue: "info", items: [ { path: "/attendance", label: "Záznam", permission: "attendance.record", end: true, // píchačky — hodiny icon: glyph( <> , ), }, { path: "/attendance/history", label: "Moje historie", permission: "attendance.history", // výkaz s hodinami — timesheet icon: glyph( <> , ), }, { path: "/attendance/requests", label: "Žádosti", permission: "attendance.record", // odeslaná žádost — vlaštovka icon: glyph( <> , ), }, { path: "/attendance/approval", label: "Schvalování", permission: "attendance.approve", // razítko icon: glyph( <> , ), }, { path: "/attendance/admin", label: "Správa", permission: "attendance.manage", matchPrefix: "/attendance/admin", matchAlso: ["/attendance/create", "/attendance/location"], // osoba + fajfka icon: glyph( <> , ), }, { path: "/attendance/balances", label: "Správa bilancí", permission: "attendance.balances", // kniha plus/mínus — ruční korekce bilancí icon: glyph( <> , ), }, { path: "/plan-work", label: "Plán prací", permission: ["attendance.manage", "attendance.record"], // kalendář s plánovacím blokem icon: glyph( <> , ), }, ], }, { label: "Kniha jízd", hue: "teal", items: [ { path: "/trips", label: "Záznam", permission: "trips.record", end: true, // trasa A→B icon: glyph( <> , ), }, { path: "/trips/history", label: "Moje historie", permission: "trips.history", // navštívená místa — pin icon: glyph( <> , ), }, { path: "/trips/admin", label: "Správa", permission: "trips.manage", // schránka s fajfkou — kniha jízd icon: glyph( <> , ), }, { path: "/vehicles", label: "Vozidla", permission: "vehicles.manage", // auto icon: glyph( <> , ), }, ], }, { label: "Administrativa", hue: "warning", items: [ { path: "/offers", label: "Nabídky", permission: "offers.view", matchPrefix: "/offers", matchExclude: ["/offers/customers", "/offers/templates"], // cenovka icon: glyph( <> , ), }, { path: "/orders", label: "Objednávky", permission: "orders.view", matchPrefix: "/orders", // košík icon: glyph( <> , ), }, { path: "/invoices", label: "Faktury", permission: "invoices.view", matchPrefix: "/invoices", // účtenka icon: glyph( <> , ), }, { path: "/projects", label: "Projekty", permission: "projects.view", matchPrefix: "/projects", // složka (NAS projektové složky) icon: glyph( , ), }, { path: "/offers/customers", label: "Zákazníci", permission: "customers.view", // vizitka icon: glyph( <> , ), }, ], }, { label: "Sklad", hue: "violet", items: [ { path: "/warehouse", label: "Přehled", permission: "warehouse.view", end: true, // skladová hala icon: glyph( <> , ), }, { path: "/warehouse/items", label: "Položky", permission: "warehouse.view", matchPrefix: "/warehouse/items", // krabice icon: glyph( <> , ), }, { path: "/warehouse/receipts", label: "Příjmy", permission: "warehouse.operate", matchPrefix: "/warehouse/receipts", // šipka do krabice icon: glyph( <> , ), }, { path: "/warehouse/issues", label: "Výdeje", permission: "warehouse.operate", matchPrefix: "/warehouse/issues", // šipka z krabice ven icon: glyph( <> , ), }, { path: "/warehouse/reservations", label: "Rezervace", permission: "warehouse.operate", matchPrefix: "/warehouse/reservations", // krabice se záložkou icon: glyph( <> , ), }, { path: "/warehouse/inventory", label: "Inventura", permission: "warehouse.inventory", matchPrefix: "/warehouse/inventory", // čárový kód — fyzický sken zásob icon: glyph( <> , ), }, { path: "/warehouse/reports", label: "Reporty", permission: "warehouse.view", end: true, // graf v rámu icon: glyph( <> , ), }, { path: "/warehouse/categories", label: "Kategorie", permission: "warehouse.manage", matchPrefix: "/warehouse/categories", // tvary — třídění do kategorií icon: glyph( <> , ), }, { path: "/warehouse/locations", label: "Lokace", permission: "warehouse.manage", matchPrefix: "/warehouse/locations", // regál s krabicemi icon: glyph( <> , ), }, { path: "/warehouse/suppliers", label: "Dodavatelé", permission: "warehouse.manage", matchPrefix: "/warehouse/suppliers", // kamion icon: glyph( <> , ), }, ], }, { label: "Systém", hue: "slate", items: [ { path: "/users", label: "Uživatelé", permission: "users.view", // lidé — uživatelské účty icon: glyph( <> , ), }, { path: "/settings", label: "Nastavení", permission: [ "settings.company", "settings.banking", "settings.roles", "settings.system", "settings.templates", ], // ozubené kolo icon: glyph( <> , ), }, { path: "/audit-log", label: "Audit log", permission: "settings.audit", // lupa nad záznamy icon: glyph( <> , ), }, ], }, ]; /** 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; // Boundary-safe prefix match: exact, or a child route under item.path (so a // sibling like "/x-foo" can't spuriously activate "/x"). Avoids a double "/" // when item.path is the root. return ( pathname === item.path || pathname.startsWith(item.path === "/" ? "/" : `${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); }