Files
Business-Management/frontend/src/components/Sidebar.tsx
T
curo1305 003fbee20f Move plugin settings access from sidebar to app card
Remove the "Extensions" section from the sidebar nav. Instead, each app
card on the Apps page shows an "Extension" button when the current user
has access to that app's plugin (matched by service ID). The button links
to /settings/plugins/:id alongside the existing admin Settings button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 02:31:12 +02:00

329 lines
11 KiB
TypeScript

import { useState, useEffect } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import {
Home,
Grid2X2,
Settings,
ShieldCheck,
ChevronRight,
ChevronLeft,
ChevronDown,
LogOut,
UserCircle,
FileText,
Folder,
Users,
UsersRound,
Palette,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import ThemeToggle from "@/components/ThemeToggle";
import { useAuth } from "@/hooks/useAuth";
import { getMe, listCategories } from "@/api/client";
import { cn } from "@/lib/utils";
export default function Sidebar() {
const [sidebarExpanded, setSidebarExpanded] = useState(true);
const { logout } = useAuth();
const location = useLocation();
const { data: user } = useQuery({ queryKey: ["me"], queryFn: getMe });
const isAppsRoute = location.pathname.startsWith("/apps");
const isDocsRoute = location.pathname.startsWith("/apps/documents");
const isAdminRoute = location.pathname.startsWith("/admin");
const [appsOpen, setAppsOpen] = useState(isAppsRoute);
const [docsOpen, setDocsOpen] = useState(isDocsRoute);
const [adminOpen, setAdminOpen] = useState(isAdminRoute);
// Auto-open sections when navigating to their routes
useEffect(() => {
if (isAppsRoute) setAppsOpen(true);
}, [isAppsRoute]);
useEffect(() => {
if (isDocsRoute) setDocsOpen(true);
}, [isDocsRoute]);
useEffect(() => {
if (isAdminRoute) setAdminOpen(true);
}, [isAdminRoute]);
const { data: categories = [] } = useQuery({
queryKey: ["categories"],
queryFn: listCategories,
enabled: appsOpen && docsOpen && !!user,
});
const navItemClass = (isActive: boolean) =>
cn(
"flex items-center rounded-lg transition-colors",
sidebarExpanded ? "px-3 py-2 gap-3" : "justify-center py-3",
isActive
? "bg-primary/10 text-primary"
: "text-muted hover:bg-muted/20 hover:text-foreground"
);
const subItemClass = (isActive: boolean) =>
cn(
"flex items-center rounded-lg transition-colors text-sm",
"pl-8 pr-3 py-1.5 gap-2",
isActive
? "bg-primary/10 text-primary"
: "text-muted hover:bg-muted/20 hover:text-foreground"
);
const subSubItemClass = (isActive: boolean) =>
cn(
"flex items-center rounded-lg transition-colors text-sm",
"pl-12 pr-3 py-1 gap-2",
isActive
? "bg-primary/10 text-primary"
: "text-muted hover:bg-muted/20 hover:text-foreground"
);
return (
<aside
className={cn(
"flex flex-col h-screen bg-surface border-r border-border transition-all duration-200 shrink-0",
sidebarExpanded ? "w-56" : "w-16"
)}
>
<nav className="flex-1 py-4 px-2 space-y-1 overflow-y-auto overflow-x-hidden">
{/* Home */}
<NavLink
to="/"
end
className={({ isActive }) => navItemClass(isActive)}
>
<Home className="h-5 w-5 shrink-0" />
{sidebarExpanded && (
<span className="text-sm font-medium whitespace-nowrap">Home</span>
)}
</NavLink>
{/* Apps — expandable */}
<div>
{sidebarExpanded ? (
<div
className={cn(
"flex items-center rounded-lg transition-colors",
isAppsRoute
? "bg-primary/10 text-primary"
: "text-muted hover:bg-muted/20 hover:text-foreground"
)}
>
<NavLink
to="/apps"
end
className="flex items-center gap-3 px-3 py-2 flex-1 min-w-0"
>
<Grid2X2 className="h-5 w-5 shrink-0" />
<span className="text-sm font-medium whitespace-nowrap">Apps</span>
</NavLink>
<button
onClick={() => setAppsOpen((o) => !o)}
className="px-2 py-2 rounded-r-lg"
aria-label={appsOpen ? "Collapse apps" : "Expand apps"}
>
{appsOpen ? (
<ChevronDown className="h-4 w-4 shrink-0" />
) : (
<ChevronRight className="h-4 w-4 shrink-0" />
)}
</button>
</div>
) : (
<NavLink
to="/apps"
end
className={({ isActive }) => navItemClass(isActive)}
>
<Grid2X2 className="h-5 w-5 shrink-0" />
</NavLink>
)}
{/* Apps sub-items — only when sidebar is expanded and appsOpen */}
{sidebarExpanded && appsOpen && (
<div className="mt-0.5 space-y-0.5">
{/* Documents service */}
<div>
<div
className={cn(
"flex items-center rounded-lg transition-colors text-sm",
isDocsRoute
? "bg-primary/10 text-primary"
: "text-muted hover:bg-muted/20 hover:text-foreground"
)}
>
<NavLink
to="/apps/documents"
end
className="flex items-center gap-2 pl-8 pr-2 py-1.5 flex-1 min-w-0"
>
<FileText className="h-4 w-4 shrink-0" />
<span className="whitespace-nowrap">Documents</span>
</NavLink>
<button
onClick={() => setDocsOpen((o) => !o)}
className="px-2 py-1.5 rounded-r-lg"
aria-label={docsOpen ? "Collapse documents" : "Expand documents"}
>
{docsOpen ? (
<ChevronDown className="h-3.5 w-3.5 shrink-0" />
) : (
<ChevronRight className="h-3.5 w-3.5 shrink-0" />
)}
</button>
</div>
{/* Categories */}
{docsOpen && (
<div className="mt-0.5 space-y-0.5">
{categories.map((cat) => (
<NavLink
key={cat.id}
to={`/apps/documents?category_id=${cat.id}`}
className={({ isActive }) => subSubItemClass(isActive)}
>
<Folder className="h-3.5 w-3.5 shrink-0" />
<span className="whitespace-nowrap truncate">{cat.name}</span>
</NavLink>
))}
</div>
)}
</div>
</div>
)}
</div>
{/* Settings */}
<NavLink
to="/settings"
className={({ isActive }) => navItemClass(isActive)}
>
<Settings className="h-5 w-5 shrink-0" />
{sidebarExpanded && (
<span className="text-sm font-medium whitespace-nowrap">Settings</span>
)}
</NavLink>
{/* Admin — expandable */}
{user?.is_admin && (
<div>
{sidebarExpanded ? (
<div
className={cn(
"flex items-center rounded-lg transition-colors",
isAdminRoute
? "bg-primary/10 text-primary"
: "text-muted hover:bg-muted/20 hover:text-foreground"
)}
>
<NavLink
to="/admin/users"
className="flex items-center gap-3 px-3 py-2 flex-1 min-w-0"
>
<ShieldCheck className="h-5 w-5 shrink-0" />
<span className="text-sm font-medium whitespace-nowrap">Admin</span>
</NavLink>
<button
onClick={() => setAdminOpen((o) => !o)}
className="px-2 py-2 rounded-r-lg"
aria-label={adminOpen ? "Collapse admin" : "Expand admin"}
>
{adminOpen ? (
<ChevronDown className="h-4 w-4 shrink-0" />
) : (
<ChevronRight className="h-4 w-4 shrink-0" />
)}
</button>
</div>
) : (
<NavLink
to="/admin/users"
className={({ isActive }) => navItemClass(isActive)}
>
<ShieldCheck className="h-5 w-5 shrink-0" />
</NavLink>
)}
{/* Admin sub-items */}
{sidebarExpanded && adminOpen && (
<div className="mt-0.5 space-y-0.5">
<NavLink
to="/admin/users"
className={({ isActive }) => subItemClass(isActive)}
>
<Users className="h-4 w-4 shrink-0" />
<span className="whitespace-nowrap">Users</span>
</NavLink>
<NavLink
to="/admin/groups"
className={({ isActive }) => subItemClass(isActive)}
>
<UsersRound className="h-4 w-4 shrink-0" />
<span className="whitespace-nowrap">Groups</span>
</NavLink>
<NavLink
to="/admin/appearance"
className={({ isActive }) => subItemClass(isActive)}
>
<Palette className="h-4 w-4 shrink-0" />
<span className="whitespace-nowrap">Appearance</span>
</NavLink>
</div>
)}
</div>
)}
</nav>
{/* Bottom section */}
<div className="py-4 px-2 space-y-1 border-t border-border">
<div
className={cn(
"flex items-center rounded-lg py-2 text-muted",
sidebarExpanded ? "px-3 gap-3" : "justify-center"
)}
>
<UserCircle className="h-5 w-5 shrink-0" />
{sidebarExpanded && user && (
<span className="text-sm font-medium truncate">{user.email}</span>
)}
</div>
<div className={cn("flex", sidebarExpanded ? "px-1" : "justify-center")}>
<ThemeToggle />
</div>
<Button
variant="ghost"
className={cn(
"w-full text-muted hover:text-foreground",
sidebarExpanded ? "justify-start px-3 gap-3" : "justify-center px-0"
)}
onClick={logout}
>
<LogOut className="h-5 w-5 shrink-0" />
{sidebarExpanded && <span className="text-sm font-medium">Logout</span>}
</Button>
<Button
variant="ghost"
size="icon"
className={cn("w-full text-muted hover:text-foreground", !sidebarExpanded && "px-0")}
onClick={() => setSidebarExpanded((e) => !e)}
aria-label={sidebarExpanded ? "Collapse sidebar" : "Expand sidebar"}
>
{sidebarExpanded ? (
<ChevronLeft className="h-5 w-5" />
) : (
<ChevronRight className="h-5 w-5" />
)}
</Button>
</div>
</aside>
);
}