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>
192 lines
6.3 KiB
TypeScript
192 lines
6.3 KiB
TypeScript
import { NavLink, useLocation } from "react-router-dom";
|
|
import Box from "@mui/material/Box";
|
|
import List from "@mui/material/List";
|
|
import ListItemButton from "@mui/material/ListItemButton";
|
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
|
import ListItemText from "@mui/material/ListItemText";
|
|
import Typography from "@mui/material/Typography";
|
|
import Avatar from "@mui/material/Avatar";
|
|
import Button from "@mui/material/Button";
|
|
import { useAuth } from "../context/AuthContext";
|
|
import { useTheme as useAppTheme } from "../../context/ThemeContext";
|
|
import { menuSections, isItemActive, hasItemPermission } from "./navData";
|
|
|
|
interface SidebarNavProps {
|
|
/** Closes the mobile drawer on item click (optional — desktop omits it). */
|
|
onNavigate?: () => void;
|
|
/** Logout handler, owned by the parent. */
|
|
onLogout: () => void;
|
|
}
|
|
|
|
export default function SidebarNav({ onNavigate, onLogout }: SidebarNavProps) {
|
|
const { user, hasPermission } = useAuth();
|
|
const { theme } = useAppTheme();
|
|
const location = useLocation();
|
|
|
|
const visibleSections = menuSections
|
|
.map((section) => ({
|
|
...section,
|
|
items: section.items.filter((item) =>
|
|
hasItemPermission(item, hasPermission),
|
|
),
|
|
}))
|
|
.filter((section) => section.items.length > 0);
|
|
|
|
return (
|
|
<Box
|
|
sx={{ display: "flex", flexDirection: "column", height: "100%", p: 1.5 }}
|
|
>
|
|
{/* Logo header */}
|
|
<Box sx={{ px: 1, py: 1 }}>
|
|
<Box
|
|
component="img"
|
|
src={`/api/admin/company-settings/logo?variant=${theme === "dark" ? "dark" : "light"}`}
|
|
alt="Logo"
|
|
onError={(e) => {
|
|
e.currentTarget.src =
|
|
theme === "dark"
|
|
? "/images/logo-dark.png"
|
|
: "/images/logo-light.png";
|
|
}}
|
|
sx={{
|
|
maxHeight: 40,
|
|
maxWidth: "100%",
|
|
objectFit: "contain",
|
|
display: "block",
|
|
}}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Nav */}
|
|
<Box sx={{ flex: 1, overflowY: "auto", mt: 1 }}>
|
|
{visibleSections.map((section) => (
|
|
<Box key={section.label}>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{
|
|
display: "block",
|
|
px: 1.5,
|
|
py: 0.5,
|
|
textTransform: "uppercase",
|
|
letterSpacing: ".06em",
|
|
fontWeight: 700,
|
|
color: "text.secondary",
|
|
fontSize: ".64rem",
|
|
}}
|
|
>
|
|
{section.label}
|
|
</Typography>
|
|
<List disablePadding>
|
|
{section.items.map((item) => (
|
|
<ListItemButton
|
|
key={item.path}
|
|
component={NavLink}
|
|
to={item.path}
|
|
end={item.end}
|
|
onClick={onNavigate}
|
|
selected={isItemActive(item, location.pathname)}
|
|
sx={{
|
|
borderRadius: 2,
|
|
mb: 0.25,
|
|
py: 0.45,
|
|
color: "text.secondary",
|
|
"&.Mui-selected, &.Mui-selected:hover": {
|
|
bgcolor: "background.paper",
|
|
color: "primary.main",
|
|
fontWeight: 700,
|
|
boxShadow: "0 2px 8px rgba(20,20,40,0.07)",
|
|
},
|
|
}}
|
|
>
|
|
<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}
|
|
slotProps={{
|
|
primary: { fontSize: ".82rem", fontWeight: "inherit" },
|
|
}}
|
|
/>
|
|
</ListItemButton>
|
|
))}
|
|
</List>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
|
|
{/* Footer */}
|
|
<Box sx={{ pt: 1, mt: 1, borderTop: 1, borderColor: "divider" }}>
|
|
<Box
|
|
sx={{ display: "flex", alignItems: "center", gap: 1, px: 0.5, mb: 1 }}
|
|
>
|
|
<Avatar
|
|
sx={{
|
|
width: 34,
|
|
height: 34,
|
|
bgcolor: "primary.main",
|
|
fontSize: ".8rem",
|
|
fontWeight: 700,
|
|
fontFamily: "'Urbanist', sans-serif",
|
|
}}
|
|
>
|
|
{user?.fullName?.charAt(0) || user?.username?.charAt(0) || "U"}
|
|
</Avatar>
|
|
<Box sx={{ minWidth: 0 }}>
|
|
<Typography noWrap sx={{ fontSize: ".82rem", fontWeight: 600 }}>
|
|
{user?.fullName || user?.username}
|
|
</Typography>
|
|
<Typography
|
|
noWrap
|
|
sx={{ fontSize: ".7rem", color: "text.secondary" }}
|
|
>
|
|
{user?.roleDisplay}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
<Button
|
|
fullWidth
|
|
color="inherit"
|
|
onClick={onLogout}
|
|
startIcon={
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="18"
|
|
height="18"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
|
<polyline points="16 17 21 12 16 7" />
|
|
<line x1="21" y1="12" x2="9" y2="12" />
|
|
</svg>
|
|
}
|
|
>
|
|
Odhlásit se
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|