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