Files
bellsystems-cp/frontend/src/layout/Sidebar.jsx

187 lines
6.2 KiB
JavaScript

import { useState } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { useAuth } from "../auth/AuthContext";
const navItems = [
{ to: "/", label: "Dashboard", permission: null },
{
label: "Melodies",
permission: "melodies",
children: [
{ to: "/melodies", label: "Main Editor" },
{ to: "/melodies/archetypes", label: "Archetypes" },
{ to: "/melodies/settings", label: "Settings" },
{ to: "/melodies/composer", label: "Composer" },
],
},
{ to: "/devices", label: "Devices", permission: "devices" },
{ to: "/users", label: "App Users", permission: "app_users" },
{
label: "MQTT",
permission: "mqtt",
children: [
{ to: "/mqtt", label: "Dashboard" },
{ to: "/mqtt/commands", label: "Commands" },
{ to: "/mqtt/logs", label: "Logs" },
],
},
{ to: "/equipment/notes", label: "Issues and Notes", permission: "equipment" },
{
label: "Manufacturing",
permission: "manufacturing",
children: [
{ to: "/manufacturing", label: "Device Inventory" },
{ to: "/manufacturing/batch/new", label: "New Batch" },
{ to: "/firmware", label: "Firmware" },
],
},
];
const linkClass = (isActive, locked) =>
`block px-3 py-2 rounded-md text-sm transition-colors ${
locked
? "opacity-40 cursor-not-allowed"
: isActive
? "bg-[var(--accent)] text-[var(--bg-primary)] font-medium"
: "text-[var(--text-secondary)] hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-heading)]"
}`;
export default function Sidebar() {
const { hasPermission, hasRole } = useAuth();
const location = useLocation();
const canViewSection = (permission) => {
if (!permission) return true;
return hasPermission(permission, "view");
};
// Settings visible only to sysadmin and admin
const canManageStaff = hasRole("sysadmin", "admin");
return (
<aside className="w-56 h-screen flex-shrink-0 p-4 border-r flex flex-col overflow-y-auto" style={{ backgroundColor: "var(--bg-sidebar)", borderColor: "var(--border-primary)" }}>
<div className="mb-8 px-2">
<img
src="/logo-dark.png"
alt="BellSystems"
className="h-10 w-auto"
/>
</div>
<nav className="space-y-1 flex-1">
{navItems.map((item) => {
const hasAccess = canViewSection(item.permission);
return item.children ? (
<CollapsibleGroup
key={item.label}
label={item.label}
children={item.children}
currentPath={location.pathname}
locked={!hasAccess}
/>
) : (
<NavLink
key={item.to}
to={hasAccess ? item.to : "#"}
end={item.to === "/"}
className={({ isActive }) => linkClass(isActive && hasAccess, !hasAccess)}
onClick={(e) => !hasAccess && e.preventDefault()}
>
<span className="flex items-center gap-2">
{item.label}
{!hasAccess && (
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
)}
</span>
</NavLink>
);
})}
</nav>
{/* Settings section at the bottom */}
{canManageStaff && (
<div className="mt-4 pt-4 border-t" style={{ borderColor: "var(--border-primary)" }}>
<nav className="space-y-1">
<CollapsibleGroup
label="Settings"
children={[
{ to: "/settings/staff", label: "Staff" },
]}
currentPath={location.pathname}
/>
</nav>
</div>
)}
</aside>
);
}
function CollapsibleGroup({ label, children, currentPath, locked = false }) {
const isChildActive = children.some(
(child) =>
currentPath === child.to ||
(child.to !== "/" && currentPath.startsWith(child.to + "/"))
);
const [open, setOpen] = useState(isChildActive);
const shouldBeOpen = open || isChildActive;
return (
<div>
<button
type="button"
onClick={() => !locked && setOpen(!shouldBeOpen)}
className={`w-full flex items-center justify-between px-3 py-2 rounded-md text-sm transition-colors ${
locked
? "opacity-40 cursor-not-allowed"
: isChildActive
? "text-[var(--text-heading)]"
: "text-[var(--text-secondary)] hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-heading)]"
}`}
>
<span className="flex items-center gap-2">
{label}
{locked && (
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
)}
</span>
{!locked && (
<svg
className={`w-4 h-4 transition-transform ${shouldBeOpen ? "rotate-90" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
)}
</button>
{!locked && shouldBeOpen && (
<div className="ml-3 mt-1 space-y-1">
{children.map((child) => (
<NavLink
key={child.to}
to={child.to}
end
className={({ isActive }) =>
`block pl-4 pr-3 py-1.5 rounded-md text-sm transition-colors ${
isActive
? "bg-[var(--accent)] text-[var(--bg-primary)] font-medium"
: "text-[var(--text-muted)] hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-heading)]"
}`
}
>
{child.label}
</NavLink>
))}
</div>
)}
</div>
);
}