Compare commits

..

1 Commits

Author SHA1 Message Date
BOHA
fd2e613aa5 feat(ui): sidebar icon refresh + unified detail-page headers
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>
2026-06-12 23:37:44 +02:00
10 changed files with 489 additions and 358 deletions

View File

@@ -0,0 +1,52 @@
# Sidebar icon refresh — design
**Date:** 2026-06-12 · **Status:** approved (visual companion session, 3 screens)
## Problem
All 31 sidebar items have icons, but 13 share a glyph with another item
(3× "people", 3× "sliders", 2× grid/bar-chart/box/history-clock) and Faktury
uses a dollar sign in an app that bills in Kč/EUR. Duplicated glyphs defeat
the purpose of icons as unique visual anchors.
## Decisions (user-approved)
1. **Full refresh** of all items (option B over targeted fix).
2. **Tile language (option B of 3 rendered directions):** every item's glyph
sits in a rounded-square tile (border-radius 32%, echoing OdinMark), as a
**soft color wash — one muted hue per menu section**:
Přehled `primary` (red) · Docházka `info` (blue) · Kniha jízd `teal` ·
Administrativa `warning` (amber) · Sklad `violet` · Systém `slate`.
3. **Odin stays unique:** the only **solid** (gradient) tile and the only
animated icon. No animation anywhere else. Red stays reserved for
Odin/primary.
4. **Glyph set** approved on the full-set screen: receipt for Faktury,
business card for Zákazníci, truck for Dodavatelé, stamp for Schvalování,
ledger ± for Správa bilancí, route/pin/car for Kniha jízd, barcode for
Inventura, shelf rack for Lokace, arrows into/out of a box for
Příjmy/Výdeje, warehouse hall for Sklad přehled, tag/cart/folder for
Nabídky/Objednávky/Projekty, gauge for Přehled, paper plane for Žádosti,
timesheet for Moje historie (docházka), shapes for Kategorie, chart frame
for Reporty, magnifier-over-list for Audit log. Kept concepts (now unique
in the set): clock (Záznam docházky), calendar (Plán prací), people
(Uživatelé), gear (Nastavení), box (Položky).
## Implementation
- `theme.ts`: add `teal`, `violet`, `slate` palette entries (light + dark
mains, full main/light/dark/contrastText so cssVariables emits `*Channel`
tokens) + TS module augmentation.
- `navData.tsx`: `MenuSection.hue` (palette key), new glyph SVGs
(stroke, 24-viewBox, width 2, round caps), `MenuItem.rawIcon` flag for
Odin (its icon ships its own tile).
- `SidebarNav.tsx`: renders the tile (26 px, radius 32%, `bgcolor:
rgba(<hue>.mainChannel / 0.12)`, glyph color `<hue>.main`, svg 16 px)
around every non-`rawIcon` item. No hardcoded hex; washes are
channel-alpha so both schemes work.
- Pinning test `navdata-icons.test.ts`: every non-raw icon renders to unique
static markup (no two items may ever share a glyph again).
## Out of scope
Animation for non-Odin icons (explicitly rejected), page-level icons,
mobile drawer changes (it reuses SidebarNav).

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "app-ts",
"version": "2.4.31",
"version": "2.4.32",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "app-ts",
"version": "2.4.31",
"version": "2.4.32",
"license": "ISC",
"dependencies": {
"@anthropic-ai/sdk": "^0.102.0",

View File

@@ -1,6 +1,6 @@
{
"name": "app-ts",
"version": "2.4.31",
"version": "2.4.32",
"description": "",
"main": "dist/server.js",
"scripts": {

View File

@@ -0,0 +1,51 @@
import { describe, it, expect } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { isValidElement, type ReactElement } from "react";
import { menuSections, type MenuItem } from "../admin/ui/navData";
/**
* Pins the sidebar icon-refresh contract (2026-06-12 design): every menu
* item has a glyph, and no two items share one — duplicated glyphs were the
* whole problem the refresh fixed (3× "people", 3× "sliders", …). Odin's
* rawIcon (OdinMark, MUI-styled + animated) is exempt from rendering here
* but still must exist and stay the ONLY rawIcon.
*/
const allItems: Array<MenuItem & { section: string }> = menuSections.flatMap(
(s) => s.items.map((item) => ({ section: s.label, ...item })),
);
describe("sidebar nav icons", () => {
it("every item has an icon and a section hue", () => {
for (const section of menuSections) {
expect(section.hue, `section ${section.label}`).toBeTruthy();
}
for (const item of allItems) {
expect(isValidElement(item.icon), `${item.section}/${item.label}`).toBe(
true,
);
}
});
it("Odin is the only rawIcon (its own tile + animation stay unique)", () => {
const raw = allItems.filter((i) => i.rawIcon);
expect(raw.map((i) => i.label)).toEqual(["Odin"]);
});
it("no two items share a glyph", () => {
const rendered = allItems
.filter((i) => !i.rawIcon)
.map((item) => ({
key: `${item.section}/${item.label}`,
markup: renderToStaticMarkup(item.icon as ReactElement),
}));
const seen = new Map<string, string>();
for (const { key, markup } of rendered) {
const dupe = seen.get(markup);
expect(dupe, `${key} shares its glyph with ${dupe}`).toBeUndefined();
seen.set(markup, key);
}
expect(seen.size).toBe(rendered.length);
});
});

View File

@@ -389,7 +389,17 @@ export default function OrderDetail() {
mb: 3,
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
{/* flexWrap: the title must drop below the Zpět button on phones
instead of overflowing the viewport (same as Offer/Invoice
detail headers). */}
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 2,
flexWrap: "wrap",
}}
>
<Button
component={RouterLink}
to="/orders"
@@ -399,9 +409,22 @@ export default function OrderDetail() {
>
Zpět
</Button>
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1.5,
flexWrap: "wrap",
}}
>
<Typography variant="h4">
Objednávka {order.order_number}
Objednávka
<Box
component="span"
sx={{ color: "text.secondary", ml: 1, fontWeight: 400 }}
>
{order.order_number}
</Box>
</Typography>
<StatusChip
label={statusLabel(ORDER_STATUS, order.status)}

View File

@@ -298,7 +298,17 @@ export default function ProjectDetail() {
mb: 3,
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
{/* flexWrap: the title must drop below the Zpět button on phones
instead of being squeezed beside it (mirrors Offer/Invoice
detail headers). */}
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 2,
flexWrap: "wrap",
}}
>
<Button
component={RouterLink}
to="/projects"
@@ -308,9 +318,22 @@ export default function ProjectDetail() {
>
Zpět
</Button>
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1.5,
flexWrap: "wrap",
}}
>
<Typography variant="h4">
Projekt {project.project_number}
Projekt
<Box
component="span"
sx={{ color: "text.secondary", ml: 1, fontWeight: 400 }}
>
{project.project_number}
</Box>
</Typography>
<StatusChip
label={STATUS_LABELS[project.status] || project.status}

View File

@@ -1,4 +1,9 @@
import { createTheme, type Theme } from "@mui/material/styles";
import {
createTheme,
type Theme,
type PaletteColor,
type PaletteColorChannel,
} from "@mui/material/styles";
const FONT_BODY = "'Plus Jakarta Sans', system-ui, sans-serif";
const FONT_HEADING = "'Urbanist', sans-serif";
@@ -33,6 +38,26 @@ export const theme = createTheme({
warning: { main: "#b45309", contrastText: "#fff" },
error: { main: "#b91c1c", contrastText: "#fff" },
info: { main: "#1d4ed8", contrastText: "#fff" },
// Sidebar section hues (full objects so cssVariables emits *Channel
// tokens — the nav tiles use channel-alpha washes).
teal: {
main: "#0e8a7c",
light: "#3aa99c",
dark: "#0a675d",
contrastText: "#fff",
},
violet: {
main: "#6a4cb4",
light: "#8a6fd0",
dark: "#54399a",
contrastText: "#fff",
},
slate: {
main: "#5c6470",
light: "#7d8694",
dark: "#454c56",
contrastText: "#fff",
},
background: { default: "#f4f3f1", paper: "#ffffff" },
text: { primary: "#1a1a1a", secondary: "#555555" },
divider: "rgba(0,0,0,0.1)",
@@ -46,6 +71,25 @@ export const theme = createTheme({
warning: { main: "#f59e0b", contrastText: "#1a1a1a" },
error: { main: "#ef4444", contrastText: "#1a1a1a" },
info: { main: "#3b82f6", contrastText: "#1a1a1a" },
// Sidebar section hues — brighter mains so the glyphs read on dark.
teal: {
main: "#2dd4bf",
light: "#5eead4",
dark: "#14b8a6",
contrastText: "#1a1a1a",
},
violet: {
main: "#a78bfa",
light: "#c4b5fd",
dark: "#8b5cf6",
contrastText: "#1a1a1a",
},
slate: {
main: "#94a3b8",
light: "#cbd5e1",
dark: "#64748b",
contrastText: "#1a1a1a",
},
background: { default: "#0f0f0f", paper: "#1a1a1a" },
text: { primary: "#ffffff", secondary: "#a0a0a0" },
divider: "rgba(255,255,255,0.08)",
@@ -239,6 +283,24 @@ export const fonts = {
mono: FONT_MONO,
};
// Custom sidebar-section palette entries (teal/violet/slate above). The
// channel intersection matters: theme.vars.palette.<hue>.mainChannel is how
// the nav tiles build their alpha washes.
type NavPaletteColor = PaletteColor & PaletteColorChannel;
declare module "@mui/material/styles" {
interface Palette {
teal: NavPaletteColor;
violet: NavPaletteColor;
slate: NavPaletteColor;
}
interface PaletteOptions {
teal?: PaletteOptions["primary"];
violet?: PaletteOptions["primary"];
slate?: PaletteOptions["primary"];
}
}
export type IconBadgeColor =
| "default"
| "primary"

View File

@@ -88,9 +88,8 @@ export default function SidebarNav({ onNavigate, onLogout }: SidebarNavProps) {
sx={{
borderRadius: 2,
mb: 0.25,
py: 0.6,
py: 0.45,
color: "text.secondary",
"& svg": { width: 18, height: 18 },
"&.Mui-selected, &.Mui-selected:hover": {
bgcolor: "background.paper",
color: "primary.main",
@@ -99,8 +98,29 @@ export default function SidebarNav({ onNavigate, onLogout }: SidebarNavProps) {
},
}}
>
<ListItemIcon sx={{ minWidth: 32, color: "inherit" }}>
<ListItemIcon sx={{ minWidth: 36, color: "inherit" }}>
{item.rawIcon ? (
item.icon
) : (
<Box
sx={(t) => ({
width: 26,
height: 26,
// Echoes OdinMark's rounded-square tile; only Odin
// is solid + animated, these are quiet washes.
borderRadius: "32%",
flexShrink: 0,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
bgcolor: `rgba(${t.vars!.palette[section.hue].mainChannel} / 0.12)`,
color: t.vars!.palette[section.hue].main,
"& svg": { width: 16, height: 16 },
})}
>
{item.icon}
</Box>
)}
</ListItemIcon>
<ListItemText
primary={item.label}

View File

@@ -1,6 +1,15 @@
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;
@@ -10,33 +19,51 @@ export interface MenuItem {
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[];
}
export const menuSections: MenuSection[] = [
{
label: "Přehled",
items: [
{
path: "/",
label: "Přehled",
end: true,
icon: (
/**
* 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"
>
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="14" y="14" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
{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" />
</>,
),
},
{
@@ -44,78 +71,66 @@ export const menuSections: MenuSection[] = [
label: "Odin",
permission: "ai.use",
end: true,
icon: <OdinMark size={22} />,
icon: <OdinMark size={26} />,
rawIcon: true,
},
],
},
{
label: "Docházka",
hue: "info",
items: [
{
path: "/attendance",
label: "Záznam",
permission: "attendance.record",
end: true,
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
// píchačky — hodiny
icon: glyph(
<>
<circle cx="12" cy="12" r="9" />
<polyline points="12 7 12 12 15 15" />
</svg>
</>,
),
},
{
path: "/attendance/history",
label: "Moje historie",
permission: "attendance.history",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="12 8 12 12 14 14" />
<path d="M3.05 11a9 9 0 1 1 .5 4m-.5 5v-5h5" />
</svg>
// 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",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="12" y1="18" x2="12" y2="12" />
<line x1="9" y1="15" x2="15" y2="15" />
</svg>
// 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",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M9 12l2 2 4-4" />
<circle cx="12" cy="12" r="10" />
</svg>
// 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" />
</>,
),
},
{
@@ -124,147 +139,108 @@ export const menuSections: MenuSection[] = [
permission: "attendance.manage",
matchPrefix: "/attendance/admin",
matchAlso: ["/attendance/create", "/attendance/location"],
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="4" y1="21" x2="4" y2="14" />
<line x1="4" y1="10" x2="4" y2="3" />
<line x1="12" y1="21" x2="12" y2="12" />
<line x1="12" y1="8" x2="12" y2="3" />
<line x1="20" y1="21" x2="20" y2="16" />
<line x1="20" y1="12" x2="20" y2="3" />
<line x1="1" y1="14" x2="7" y2="14" />
<line x1="9" y1="8" x2="15" y2="8" />
<line x1="17" y1="16" x2="23" y2="16" />
</svg>
// 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",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="18" y1="20" x2="18" y2="10" />
<line x1="12" y1="20" x2="12" y2="4" />
<line x1="6" y1="20" x2="6" y2="14" />
</svg>
// 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"],
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="4" width="18" height="18" rx="2" />
// 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="10" x2="21" y2="10" />
</svg>
<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,
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="5" cy="18" r="3" />
<circle cx="19" cy="18" r="3" />
<path d="M5 18V12L8 5h8l3 7v6" />
<path d="M10 18h4" />
</svg>
// 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",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="12 8 12 12 14 14" />
<path d="M3.05 11a9 9 0 1 1 .5 4m-.5 5v-5h5" />
</svg>
// 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",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="4" y1="21" x2="4" y2="14" />
<line x1="4" y1="10" x2="4" y2="3" />
<line x1="12" y1="21" x2="12" y2="12" />
<line x1="12" y1="8" x2="12" y2="3" />
<line x1="20" y1="21" x2="20" y2="16" />
<line x1="20" y1="12" x2="20" y2="3" />
<line x1="1" y1="14" x2="7" y2="14" />
<line x1="9" y1="8" x2="15" y2="8" />
<line x1="17" y1="16" x2="23" y2="16" />
</svg>
// 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",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<rect x="1" y="3" width="15" height="13" rx="2" />
<path d="M16 8h4l3 3v5h-7V8z" />
<circle cx="5.5" cy="18.5" r="2.5" />
<circle cx="18.5" cy="18.5" r="2.5" />
</svg>
// 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",
@@ -272,16 +248,12 @@ export const menuSections: MenuSection[] = [
permission: "offers.view",
matchPrefix: "/offers",
matchExclude: ["/offers/customers", "/offers/templates"],
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
// 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" />
</>,
),
},
{
@@ -289,17 +261,13 @@ export const menuSections: MenuSection[] = [
label: "Objednávky",
permission: "orders.view",
matchPrefix: "/orders",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
<line x1="3" y1="6" x2="21" y2="6" />
<path d="M16 10a4 4 0 0 1-8 0" />
</svg>
// 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" />
</>,
),
},
{
@@ -307,16 +275,13 @@ export const menuSections: MenuSection[] = [
label: "Faktury",
permission: "invoices.view",
matchPrefix: "/invoices",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="12" y1="1" x2="12" y2="23" />
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
</svg>
// úč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" />
</>,
),
},
{
@@ -324,60 +289,44 @@ export const menuSections: MenuSection[] = [
label: "Projekty",
permission: "projects.view",
matchPrefix: "/projects",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<rect x="2" y="3" width="20" height="14" rx="2" />
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
// 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",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<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" />
</svg>
// 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,
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<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" />
</svg>
// 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" />
</>,
),
},
{
@@ -385,17 +334,13 @@ export const menuSections: MenuSection[] = [
label: "Položky",
permission: "warehouse.view",
matchPrefix: "/warehouse/items",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
// 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" />
</svg>
</>,
),
},
{
@@ -403,17 +348,13 @@ export const menuSections: MenuSection[] = [
label: "Příjmy",
permission: "warehouse.operate",
matchPrefix: "/warehouse/receipts",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="17 11 12 6 7 11" />
<line x1="12" y1="6" x2="12" y2="18" />
<path d="M5 19h14" />
</svg>
// š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" />
</>,
),
},
{
@@ -421,17 +362,13 @@ export const menuSections: MenuSection[] = [
label: "Výdeje",
permission: "warehouse.operate",
matchPrefix: "/warehouse/issues",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="7 13 12 18 17 13" />
<line x1="12" y1="18" x2="12" y2="6" />
<path d="M5 5h14" />
</svg>
// š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" />
</>,
),
},
{
@@ -439,18 +376,12 @@ export const menuSections: MenuSection[] = [
label: "Rezervace",
permission: "warehouse.operate",
matchPrefix: "/warehouse/reservations",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<line x1="19" y1="8" x2="19" y2="14" />
<line x1="22" y1="11" x2="16" y2="11" />
</svg>
// 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" />
</>,
),
},
{
@@ -458,16 +389,15 @@ export const menuSections: MenuSection[] = [
label: "Inventura",
permission: "warehouse.inventory",
matchPrefix: "/warehouse/inventory",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M9 11l3 3L22 4" />
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
</svg>
// čá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" />
</>,
),
},
{
@@ -475,17 +405,12 @@ export const menuSections: MenuSection[] = [
label: "Reporty",
permission: "warehouse.view",
end: true,
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="18" y1="20" x2="18" y2="10" />
<line x1="12" y1="20" x2="12" y2="4" />
<line x1="6" y1="20" x2="6" y2="14" />
</svg>
// 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" />
</>,
),
},
{
@@ -493,23 +418,14 @@ export const menuSections: MenuSection[] = [
label: "Kategorie",
permission: "warehouse.manage",
matchPrefix: "/warehouse/categories",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="4" y1="21" x2="4" y2="14" />
<line x1="4" y1="10" x2="4" y2="3" />
<line x1="12" y1="21" x2="12" y2="12" />
<line x1="12" y1="8" x2="12" y2="3" />
<line x1="20" y1="21" x2="20" y2="16" />
<line x1="20" y1="12" x2="20" y2="3" />
<line x1="1" y1="14" x2="7" y2="14" />
<line x1="9" y1="8" x2="15" y2="8" />
<line x1="17" y1="16" x2="23" y2="16" />
</svg>
// 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" />
</>,
),
},
{
@@ -517,18 +433,16 @@ export const menuSections: MenuSection[] = [
label: "Lokace",
permission: "warehouse.manage",
matchPrefix: "/warehouse/locations",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
<rect x="14" y="14" width="7" height="7" rx="1" />
</svg>
// 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" />
</>,
),
},
{
@@ -536,41 +450,34 @@ export const menuSections: MenuSection[] = [
label: "Dodavatelé",
permission: "warehouse.manage",
matchPrefix: "/warehouse/suppliers",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<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" />
</svg>
// 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",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
// 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" />
</svg>
</>,
),
},
{
@@ -583,35 +490,27 @@ export const menuSections: MenuSection[] = [
"settings.system",
"settings.templates",
],
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
// 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" />
</svg>
</>,
),
},
{
path: "/audit-log",
label: "Audit log",
permission: "settings.audit",
icon: (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
// 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" />
</>,
),
},
],

View File

@@ -4,6 +4,7 @@
"module": "commonjs",
"lib": ["ES2022"],
"types": ["node", "vitest/globals"],
"jsx": "react-jsx",
"moduleResolution": "node",
"noEmit": true,
"strict": true,