Sidebar (user-approved design, docs/superpowers/specs/2026-06-12-sidebar- icons-design.md): all 31 menu items get unique contextual stroke glyphs in rounded-square tiles tinted per section (Docházka blue, Kniha jízd teal, Administrativa amber, Sklad violet, Systém slate; new teal/violet/slate palette entries with channel tokens in both schemes). Replaces the old set where 13 items shared a glyph (3x people, 3x sliders, ...) and Faktury was a dollar sign. Odin stays the only solid + animated tile. Uniqueness pinned by navdata-icons.test.ts (tsconfig.test.json gains jsx for the .tsx import). Headers: ProjectDetail and OrderDetail now match the Offer/Invoice shell — number rendered in secondary color at regular weight next to the bold entity word, and flexWrap on both left header boxes so the title drops below Zpět on phones instead of overflowing the viewport (the reported mobile bug on received orders). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
556 lines
16 KiB
TypeScript
556 lines
16 KiB
TypeScript
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 (
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
{children}
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export const menuSections: MenuSection[] = [
|
|
{
|
|
label: "Přehled",
|
|
hue: "primary",
|
|
items: [
|
|
{
|
|
path: "/",
|
|
label: "Přehled",
|
|
end: true,
|
|
// tachometr — dashboard
|
|
icon: glyph(
|
|
<>
|
|
<circle cx="12" cy="13" r="8.5" />
|
|
<path d="M12 13l4-4.5" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/odin",
|
|
label: "Odin",
|
|
permission: "ai.use",
|
|
end: true,
|
|
icon: <OdinMark size={26} />,
|
|
rawIcon: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: "Docházka",
|
|
hue: "info",
|
|
items: [
|
|
{
|
|
path: "/attendance",
|
|
label: "Záznam",
|
|
permission: "attendance.record",
|
|
end: true,
|
|
// píchačky — hodiny
|
|
icon: glyph(
|
|
<>
|
|
<circle cx="12" cy="12" r="9" />
|
|
<polyline points="12 7 12 12 15 15" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/attendance/history",
|
|
label: "Moje historie",
|
|
permission: "attendance.history",
|
|
// výkaz s hodinami — timesheet
|
|
icon: glyph(
|
|
<>
|
|
<path d="M14 3H5a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h6" />
|
|
<line x1="7" y1="8" x2="13" y2="8" />
|
|
<line x1="7" y1="12" x2="11" y2="12" />
|
|
<circle cx="16.5" cy="16" r="4.5" />
|
|
<polyline points="16.5 14 16.5 16 18 17" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/attendance/requests",
|
|
label: "Žádosti",
|
|
permission: "attendance.record",
|
|
// odeslaná žádost — vlaštovka
|
|
icon: glyph(
|
|
<>
|
|
<path d="M22 2L11 13" />
|
|
<path d="M22 2l-7 20-4-9-9-4z" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/attendance/approval",
|
|
label: "Schvalování",
|
|
permission: "attendance.approve",
|
|
// razítko
|
|
icon: glyph(
|
|
<>
|
|
<path d="M9.5 12V9.8C9.5 8.4 8 7.8 8 6a4 4 0 0 1 8 0c0 1.8-1.5 2.4-1.5 3.8V12" />
|
|
<path d="M5 16a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v2H5z" />
|
|
<line x1="5" y1="21" x2="19" y2="21" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/attendance/admin",
|
|
label: "Správa",
|
|
permission: "attendance.manage",
|
|
matchPrefix: "/attendance/admin",
|
|
matchAlso: ["/attendance/create", "/attendance/location"],
|
|
// osoba + fajfka
|
|
icon: glyph(
|
|
<>
|
|
<circle cx="9" cy="8" r="3.5" />
|
|
<path d="M3 20a6 6 0 0 1 12 0" />
|
|
<path d="M15.5 11.5l2 2 4-4" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/attendance/balances",
|
|
label: "Správa bilancí",
|
|
permission: "attendance.balances",
|
|
// kniha plus/mínus — ruční korekce bilancí
|
|
icon: glyph(
|
|
<>
|
|
<rect x="4" y="3" width="16" height="18" rx="2" />
|
|
<line x1="9" y1="8.5" x2="15" y2="8.5" />
|
|
<line x1="12" y1="5.5" x2="12" y2="11.5" />
|
|
<line x1="9" y1="16" x2="15" y2="16" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/plan-work",
|
|
label: "Plán prací",
|
|
permission: ["attendance.manage", "attendance.record"],
|
|
// kalendář s plánovacím blokem
|
|
icon: glyph(
|
|
<>
|
|
<rect x="3" y="4" width="18" height="17" rx="2" />
|
|
<line x1="16" y1="2" x2="16" y2="6" />
|
|
<line x1="8" y1="2" x2="8" y2="6" />
|
|
<line x1="3" y1="9.5" x2="21" y2="9.5" />
|
|
<path d="M8 14h3v3H8z" />
|
|
</>,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: "Kniha jízd",
|
|
hue: "teal",
|
|
items: [
|
|
{
|
|
path: "/trips",
|
|
label: "Záznam",
|
|
permission: "trips.record",
|
|
end: true,
|
|
// trasa A→B
|
|
icon: glyph(
|
|
<>
|
|
<circle cx="5" cy="19" r="2" />
|
|
<circle cx="19" cy="5" r="2" />
|
|
<path d="M5 17C5 11 19 13 19 7" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/trips/history",
|
|
label: "Moje historie",
|
|
permission: "trips.history",
|
|
// navštívená místa — pin
|
|
icon: glyph(
|
|
<>
|
|
<path d="M12 21s-6-5.2-6-10a6 6 0 0 1 12 0c0 4.8-6 10-6 10z" />
|
|
<circle cx="12" cy="11" r="2.5" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/trips/admin",
|
|
label: "Správa",
|
|
permission: "trips.manage",
|
|
// schránka s fajfkou — kniha jízd
|
|
icon: glyph(
|
|
<>
|
|
<rect x="5" y="4" width="14" height="17" rx="2" />
|
|
<path d="M9 2h6v4H9z" />
|
|
<path d="M9 13l2 2 4-4" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/vehicles",
|
|
label: "Vozidla",
|
|
permission: "vehicles.manage",
|
|
// auto
|
|
icon: glyph(
|
|
<>
|
|
<path d="M5 17H3v-4l2-5h11l3 5h2v4h-2" />
|
|
<circle cx="7.5" cy="17" r="2" />
|
|
<circle cx="16.5" cy="17" r="2" />
|
|
<line x1="9.5" y1="17" x2="14.5" y2="17" />
|
|
</>,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: "Administrativa",
|
|
hue: "warning",
|
|
items: [
|
|
{
|
|
path: "/offers",
|
|
label: "Nabídky",
|
|
permission: "offers.view",
|
|
matchPrefix: "/offers",
|
|
matchExclude: ["/offers/customers", "/offers/templates"],
|
|
// cenovka
|
|
icon: glyph(
|
|
<>
|
|
<path d="M20.6 13.4L11 3H4v7l9.6 10.4a2 2 0 0 0 2.8 0l4.2-4.2a2 2 0 0 0 0-2.8z" />
|
|
<circle cx="8" cy="8" r="1.5" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/orders",
|
|
label: "Objednávky",
|
|
permission: "orders.view",
|
|
matchPrefix: "/orders",
|
|
// košík
|
|
icon: glyph(
|
|
<>
|
|
<circle cx="9" cy="20" r="1.6" />
|
|
<circle cx="17" cy="20" r="1.6" />
|
|
<path d="M2 3h3l2.7 12h10.6l2.7-9H6" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/invoices",
|
|
label: "Faktury",
|
|
permission: "invoices.view",
|
|
matchPrefix: "/invoices",
|
|
// účtenka
|
|
icon: glyph(
|
|
<>
|
|
<path d="M6 2h12v20l-3-2-3 2-3-2-3 2z" />
|
|
<line x1="9" y1="7" x2="15" y2="7" />
|
|
<line x1="9" y1="11" x2="15" y2="11" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/projects",
|
|
label: "Projekty",
|
|
permission: "projects.view",
|
|
matchPrefix: "/projects",
|
|
// složka (NAS projektové složky)
|
|
icon: glyph(
|
|
<path d="M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />,
|
|
),
|
|
},
|
|
{
|
|
path: "/offers/customers",
|
|
label: "Zákazníci",
|
|
permission: "customers.view",
|
|
// vizitka
|
|
icon: glyph(
|
|
<>
|
|
<rect x="2" y="4" width="20" height="16" rx="2" />
|
|
<circle cx="8.5" cy="10.5" r="2.2" />
|
|
<path d="M5 16.5a3.5 3.5 0 0 1 7 0" />
|
|
<line x1="14.5" y1="9.5" x2="19" y2="9.5" />
|
|
<line x1="14.5" y1="13.5" x2="19" y2="13.5" />
|
|
</>,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: "Sklad",
|
|
hue: "violet",
|
|
items: [
|
|
{
|
|
path: "/warehouse",
|
|
label: "Přehled",
|
|
permission: "warehouse.view",
|
|
end: true,
|
|
// skladová hala
|
|
icon: glyph(
|
|
<>
|
|
<path d="M3 21V9l9-5 9 5v12" />
|
|
<path d="M7 21v-7h10v7" />
|
|
<line x1="7" y1="17.5" x2="17" y2="17.5" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/warehouse/items",
|
|
label: "Položky",
|
|
permission: "warehouse.view",
|
|
matchPrefix: "/warehouse/items",
|
|
// krabice
|
|
icon: glyph(
|
|
<>
|
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
|
|
<line x1="12" y1="22.08" x2="12" y2="12" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/warehouse/receipts",
|
|
label: "Příjmy",
|
|
permission: "warehouse.operate",
|
|
matchPrefix: "/warehouse/receipts",
|
|
// šipka do krabice
|
|
icon: glyph(
|
|
<>
|
|
<rect x="4" y="12" width="16" height="9" rx="1" />
|
|
<polyline points="8 6 12 10 16 6" />
|
|
<line x1="12" y1="2" x2="12" y2="10" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/warehouse/issues",
|
|
label: "Výdeje",
|
|
permission: "warehouse.operate",
|
|
matchPrefix: "/warehouse/issues",
|
|
// šipka z krabice ven
|
|
icon: glyph(
|
|
<>
|
|
<rect x="4" y="13" width="16" height="8" rx="1" />
|
|
<polyline points="8 6 12 2 16 6" />
|
|
<line x1="12" y1="2" x2="12" y2="11" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/warehouse/reservations",
|
|
label: "Rezervace",
|
|
permission: "warehouse.operate",
|
|
matchPrefix: "/warehouse/reservations",
|
|
// krabice se záložkou
|
|
icon: glyph(
|
|
<>
|
|
<rect x="3" y="8" width="13" height="12" rx="1" />
|
|
<path d="M16 3h5v9l-2.5-2L16 12z" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/warehouse/inventory",
|
|
label: "Inventura",
|
|
permission: "warehouse.inventory",
|
|
matchPrefix: "/warehouse/inventory",
|
|
// čárový kód — fyzický sken zásob
|
|
icon: glyph(
|
|
<>
|
|
<line x1="4" y1="6" x2="4" y2="18" />
|
|
<line x1="8" y1="6" x2="8" y2="18" />
|
|
<line x1="12" y1="6" x2="12" y2="18" />
|
|
<line x1="17" y1="6" x2="17" y2="18" />
|
|
<line x1="20" y1="6" x2="20" y2="18" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/warehouse/reports",
|
|
label: "Reporty",
|
|
permission: "warehouse.view",
|
|
end: true,
|
|
// graf v rámu
|
|
icon: glyph(
|
|
<>
|
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
<polyline points="7 14 11 10 14 13 17 8" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/warehouse/categories",
|
|
label: "Kategorie",
|
|
permission: "warehouse.manage",
|
|
matchPrefix: "/warehouse/categories",
|
|
// tvary — třídění do kategorií
|
|
icon: glyph(
|
|
<>
|
|
<circle cx="7.5" cy="7.5" r="3.5" />
|
|
<rect x="13.5" y="4" width="7" height="7" rx="1" />
|
|
<path d="M7.5 13.5l3.5 7h-7z" />
|
|
<rect x="13.5" y="13.5" width="7" height="7" rx="3.5" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/warehouse/locations",
|
|
label: "Lokace",
|
|
permission: "warehouse.manage",
|
|
matchPrefix: "/warehouse/locations",
|
|
// regál s krabicemi
|
|
icon: glyph(
|
|
<>
|
|
<path d="M3 21V4" />
|
|
<path d="M21 21V4" />
|
|
<line x1="3" y1="12" x2="21" y2="12" />
|
|
<line x1="3" y1="21" x2="21" y2="21" />
|
|
<rect x="7" y="8" width="4" height="4" />
|
|
<rect x="13" y="17" width="4" height="4" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/warehouse/suppliers",
|
|
label: "Dodavatelé",
|
|
permission: "warehouse.manage",
|
|
matchPrefix: "/warehouse/suppliers",
|
|
// kamion
|
|
icon: glyph(
|
|
<>
|
|
<rect x="1" y="6" width="13" height="10" rx="1" />
|
|
<path d="M14 9h4l3 4v3h-7" />
|
|
<circle cx="6" cy="18.5" r="2" />
|
|
<circle cx="17.5" cy="18.5" r="2" />
|
|
</>,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: "Systém",
|
|
hue: "slate",
|
|
items: [
|
|
{
|
|
path: "/users",
|
|
label: "Uživatelé",
|
|
permission: "users.view",
|
|
// lidé — uživatelské účty
|
|
icon: glyph(
|
|
<>
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
|
<circle cx="9" cy="7" r="4" />
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/settings",
|
|
label: "Nastavení",
|
|
permission: [
|
|
"settings.company",
|
|
"settings.banking",
|
|
"settings.roles",
|
|
"settings.system",
|
|
"settings.templates",
|
|
],
|
|
// ozubené kolo
|
|
icon: glyph(
|
|
<>
|
|
<circle cx="12" cy="12" r="3" />
|
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
|
</>,
|
|
),
|
|
},
|
|
{
|
|
path: "/audit-log",
|
|
label: "Audit log",
|
|
permission: "settings.audit",
|
|
// lupa nad záznamy
|
|
icon: glyph(
|
|
<>
|
|
<path d="M4 5h12" />
|
|
<path d="M4 10h6" />
|
|
<path d="M4 15h5" />
|
|
<circle cx="15" cy="14" r="4.5" />
|
|
<line x1="18.2" y1="17.2" x2="21.5" y2="20.5" />
|
|
</>,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
/** 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);
|
|
}
|