Add Groups management and split Admin navigation

- New backend: Group + GroupMembership models, schemas, CRUD router at
  /api/admin/groups (list, create, get detail, update, delete, add/remove members)
- New Alembic migration: groups and group_memberships tables
- Frontend: Admin sidebar item is now an expandable accordion with
  Users and Groups sub-items; AdminPage redirects to /admin/users;
  new AdminUsersPage and AdminGroupsPage with inline member management panel
- API client: 7 new group functions + TypeScript types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-04-17 20:49:54 +02:00
parent 2bb1e03adf
commit 4e9ed97b05
15 changed files with 1035 additions and 191 deletions
+66 -9
View File
@@ -13,6 +13,8 @@ import {
UserCircle,
FileText,
Folder,
Users,
UsersRound,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import ThemeToggle from "@/components/ThemeToggle";
@@ -28,9 +30,11 @@ export default function Sidebar() {
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(() => {
@@ -41,6 +45,10 @@ export default function Sidebar() {
if (isDocsRoute) setDocsOpen(true);
}, [isDocsRoute]);
useEffect(() => {
if (isAdminRoute) setAdminOpen(true);
}, [isAdminRoute]);
const { data: categories = [] } = useQuery({
queryKey: ["categories"],
queryFn: listCategories,
@@ -200,17 +208,66 @@ export default function Sidebar() {
)}
</NavLink>
{/* Admin */}
{/* Admin — expandable */}
{user?.is_admin && (
<NavLink
to="/admin"
className={({ isActive }) => navItemClass(isActive)}
>
<ShieldCheck className="h-5 w-5 shrink-0" />
{sidebarExpanded && (
<span className="text-sm font-medium whitespace-nowrap">Admin</span>
<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>
)}
</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>
</div>
)}
</div>
)}
</nav>