feat: Add Admin Group Hub page and update navigation structure for improved admin management
All checks were successful
Deploy Development / deploy (push) Successful in 53s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

This commit is contained in:
Lars 2026-04-05 11:05:45 +02:00
parent bbc59457ac
commit b7f2e2adbe
7 changed files with 247 additions and 191 deletions

View File

@ -37,6 +37,7 @@ import AdminFocusAreasPage from './pages/AdminFocusAreasPage'
import AdminHomePage from './pages/AdminHomePage'
import AdminUsersPage from './pages/AdminUsersPage'
import AdminSystemPage from './pages/AdminSystemPage'
import AdminGroupHubPage from './pages/AdminGroupHubPage'
import RequireAdmin from './layouts/RequireAdmin'
import AdminShell from './layouts/AdminShell'
import SubscriptionPage from './pages/SubscriptionPage'
@ -227,6 +228,7 @@ function AppShell() {
<Route element={<RequireAdmin />}>
<Route path="admin" element={<AdminShell />}>
<Route index element={<AdminHomePage />} />
<Route path="g/:groupId" element={<AdminGroupHubPage />} />
<Route path="users" element={<AdminUsersPage />} />
<Route path="system" element={<AdminSystemPage />} />
<Route path="tier-limits" element={<AdminTierLimitsPage/>}/>

View File

@ -304,6 +304,11 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
opacity: 1;
}
a.analysis-split__nav-item {
text-decoration: none;
box-sizing: border-box;
}
.analysis-split__main {
min-width: 0;
}
@ -474,90 +479,11 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
}
}
/* Admin: gruppierte Sub-Navigation (Mobil = Chips pro Gruppe, Desktop = linke Spalte) */
/* Admin: Split-Layout wie .analysis-split (nur Gruppen in der Nav) */
.admin-shell {
width: 100%;
}
.admin-shell__layout {
display: flex;
flex-direction: column;
gap: 16px;
}
.admin-shell__nav-groups {
display: flex;
flex-direction: column;
gap: 14px;
}
.admin-shell__nav-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.admin-shell__group-title {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text3);
padding: 0 2px;
}
.admin-shell__group-chips {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 6px;
overflow-x: auto;
padding-bottom: 4px;
-ms-overflow-style: none;
scrollbar-width: none;
}
.admin-shell__group-chips::-webkit-scrollbar {
display: none;
}
.admin-shell__nav-item {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 7px 12px;
border-radius: 20px;
border: 1.5px solid var(--border2);
background: var(--surface);
color: var(--text2);
font-family: var(--font);
font-size: 13px;
font-weight: 500;
text-decoration: none;
white-space: nowrap;
box-sizing: border-box;
}
.admin-shell__nav-item:hover {
border-color: var(--accent);
color: var(--text1);
}
.admin-shell__nav-item--active {
border-color: var(--accent);
background: var(--accent);
color: white;
}
.admin-shell__nav-item--active:hover {
color: white;
}
.admin-shell__main {
min-width: 0;
}
.admin-page {
width: 100%;
}
@ -568,40 +494,6 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
margin-left: auto;
margin-right: auto;
}
.admin-shell__layout {
flex-direction: row;
align-items: flex-start;
gap: 24px;
}
.admin-shell__nav-wrap {
flex: 0 0 260px;
max-width: 280px;
position: sticky;
top: 16px;
align-self: flex-start;
}
.admin-shell__group-chips {
flex-direction: column;
overflow-x: visible;
overflow-y: visible;
padding-bottom: 0;
gap: 6px;
}
.admin-shell__nav-item {
width: 100%;
justify-content: flex-start;
border-radius: 10px;
white-space: normal;
padding: 9px 12px;
}
.admin-shell__main {
flex: 1;
}
}
.muted { color: var(--text3); font-size: 13px; }

View File

@ -1,61 +1,154 @@
/**
* Gruppierte Admin-Navigation (Shell: Chips mobil, Seitenleiste Desktop).
* @typedef {{ to: string, label: string, end?: boolean }} AdminNavItem
* @typedef {{ id: string, label: string, items: AdminNavItem[] }} AdminNavGroup
* Admin: Nur Gruppen in der Shell-Navigation; konkrete Seiten wählt man auf der Hub-Seite (/admin/g/:id).
* @typedef {{ to: string, label: string, description?: string }} AdminGroupItem
* @typedef {{ id: string, label: string, description: string, items: AdminGroupItem[] }} AdminGroup
*/
/** @type {AdminNavGroup[]} */
export const ADMIN_NAV_GROUPS = [
{
id: 'overview',
label: 'Übersicht',
items: [{ to: '/admin', label: 'Start', end: true }],
},
/** @type {AdminGroup[]} */
export const ADMIN_GROUPS = [
{
id: 'users',
label: 'Benutzerverwaltung',
items: [{ to: '/admin/users', label: 'Profile & Rollen' }],
description: 'Profile anlegen, Rollen setzen und Recovery-E-Mails pflegen.',
items: [
{
to: '/admin/users',
label: 'Profile & Rollen',
description: 'Neue Profile, Admin-Rolle, PIN und E-Mail pro Nutzer.',
},
],
},
{
id: 'features',
label: 'Features',
description: 'Feature-Registry, Kontingente und individuelle Overrides.',
items: [
{ to: '/admin/features', label: 'Feature-Registry' },
{ to: '/admin/tier-limits', label: 'Tier-Limits' },
{ to: '/admin/user-restrictions', label: 'User-Overrides' },
{
to: '/admin/features',
label: 'Feature-Registry',
description: 'Features definieren und Kategorien zuordnen.',
},
{
to: '/admin/tier-limits',
label: 'Tier-Limits',
description: 'Limit-Matrix pro Tier bearbeiten.',
},
{
to: '/admin/user-restrictions',
label: 'User-Overrides',
description: 'Individuelle Feature-Limits setzen.',
},
],
},
{
id: 'subscription',
label: 'Subscription',
description: 'Tiers und Gutscheine für das Freemium-System.',
items: [
{ to: '/admin/tiers', label: 'Tiers' },
{ to: '/admin/coupons', label: 'Coupons' },
{ to: '/admin/tiers', label: 'Tiers', description: 'Abo-Stufen und Zuordnung.' },
{ to: '/admin/coupons', label: 'Coupons', description: 'Gutscheincodes verwalten.' },
],
},
{
id: 'training',
label: 'Trainingstypen',
description: 'Trainingstypen, Mappings und Trainings-Profile.',
items: [
{ to: '/admin/training-types', label: 'Trainingstypen' },
{ to: '/admin/activity-mappings', label: 'Activity-Mappings' },
{ to: '/admin/training-profiles', label: 'Trainings-Profile' },
{
to: '/admin/training-types',
label: 'Trainingstypen',
description: 'Kategorien und Typen verwalten.',
},
{
to: '/admin/activity-mappings',
label: 'Activity-Mappings',
description: 'Lernendes Zuordnungssystem (Sprache / Apple Health).',
},
{
to: '/admin/training-profiles',
label: 'Trainings-Profile',
description: 'Training-Type-Profile (#15).',
},
],
},
{
id: 'goals',
label: 'Ziele & Fokus',
description: 'Ziel-Typen und Focus Areas.',
items: [
{ to: '/admin/goal-types', label: 'Ziel-Typen' },
{ to: '/admin/focus-areas', label: 'Focus Areas' },
{
to: '/admin/goal-types',
label: 'Ziel-Typen',
description: 'Custom Goal Types mit oder ohne Datenquelle.',
},
{
to: '/admin/focus-areas',
label: 'Focus Areas',
description: 'Dynamische Fokusbereiche und Kategorien.',
},
],
},
{
id: 'prompts',
label: 'KI-Prompts',
description: 'Pipeline- und Basis-Prompts für die KI-Analyse.',
items: [
{
to: '/admin/prompts',
label: 'KI-Prompts verwalten',
description: 'Prompts bearbeiten, Stages, Test & Export.',
},
],
},
{
id: 'system',
label: 'Basiseinstellungen',
description: 'System-E-Mail und Platzhalter-Metadaten.',
items: [
{ to: '/admin/prompts', label: 'KI-Prompts' },
{ to: '/admin/system', label: 'SMTP & Metadaten-Export' },
{
to: '/admin/system',
label: 'SMTP & Metadaten-Export',
description: 'SMTP-Status, Test-Mail und Placeholder-Katalog (JSON/ZIP).',
},
],
},
]
export function adminGroupHubPath(groupId) {
return `/admin/g/${groupId}`
}
/**
* Shell-Navigation: Übersicht + eine Zeile/Spalte pro Gruppe (ohne Einzelseiten).
* @typedef {{ id: string, label: string, to: string, end?: boolean }} AdminShellNavEntry
* @returns {AdminShellNavEntry[]}
*/
export function getAdminShellNavEntries() {
return [
{ id: 'overview', label: 'Übersicht', to: '/admin', end: true },
...ADMIN_GROUPS.map((g) => ({
id: g.id,
label: g.label,
to: adminGroupHubPath(g.id),
end: false,
})),
]
}
/** Aktiver Shell-Eintrag inkl. Leaf-Routen der Gruppe (z. B. /admin/features → Gruppe „Features“). */
export function adminShellEntryIsActive(pathname, entry) {
if (entry.id === 'overview') {
return pathname === '/admin'
}
const group = ADMIN_GROUPS.find((x) => x.id === entry.id)
if (!group) return false
if (pathname === adminGroupHubPath(group.id)) return true
return group.items.some((it) => pathname === it.to)
}
/** Anzahl Unterseiten für Chip-Badge (wie KI-Analyse). */
export function adminShellEntryItemCount(entry) {
if (entry.id === 'overview') return 0
const g = ADMIN_GROUPS.find((x) => x.id === entry.id)
return g ? g.items.length : 0
}

View File

@ -1,35 +1,46 @@
import { Outlet, NavLink } from 'react-router-dom'
import { ADMIN_NAV_GROUPS } from '../config/adminNav'
import { Outlet, NavLink, useLocation } from 'react-router-dom'
import {
getAdminShellNavEntries,
adminShellEntryIsActive,
adminShellEntryItemCount,
} from '../config/adminNav'
/**
* Wie KI-Analyse: nur Gruppen-Chips (mobil) bzw. Seitenleiste (desktop);
* konkrete Admin-Seiten über Hub unter /admin/g/:groupId.
*/
export default function AdminShell() {
const loc = useLocation()
const entries = getAdminShellNavEntries()
return (
<div className="admin-shell">
<div className="admin-shell__layout">
<nav className="admin-shell__nav-wrap" aria-label="Adminbereich">
<div className="admin-shell__nav-groups">
{ADMIN_NAV_GROUPS.map((group) => (
<div key={group.id} className="admin-shell__nav-group">
<div className="admin-shell__group-title">{group.label}</div>
<div className="admin-shell__group-chips">
{group.items.map((item) => (
<div className="analysis-split">
<div className="analysis-split__nav-wrap">
<nav className="analysis-split__nav" aria-label="Adminbereich">
{entries.map((entry) => {
const active = adminShellEntryIsActive(loc.pathname, entry)
const count = adminShellEntryItemCount(entry)
return (
<NavLink
key={`${group.id}-${item.to}`}
to={item.to}
end={!!item.end}
className={({ isActive }) =>
'admin-shell__nav-item' +
(isActive ? ' admin-shell__nav-item--active' : '')
key={entry.id}
to={entry.to}
end={!!entry.end}
className={() =>
'analysis-split__nav-item' +
(active ? ' analysis-split__nav-item--active' : '')
}
>
{item.label}
{entry.label}
{count > 0 && (
<span className="analysis-split__nav-cat-count">({count})</span>
)}
</NavLink>
))}
</div>
</div>
))}
</div>
)
})}
</nav>
<div className="admin-shell__main">
</div>
<div className="analysis-split__main">
<div className="admin-page">
<Outlet />
</div>

View File

@ -0,0 +1,57 @@
import { useParams, Navigate, Link } from 'react-router-dom'
import { ADMIN_GROUPS } from '../config/adminNav'
export default function AdminGroupHubPage() {
const { groupId } = useParams()
const group = ADMIN_GROUPS.find((g) => g.id === groupId)
if (!group) {
return <Navigate to="/admin" replace />
}
return (
<div>
<h2 className="page-title" style={{ margin: '0 0 8px', fontSize: 18 }}>
{group.label}
</h2>
<p style={{ fontSize: 13, color: 'var(--text2)', marginBottom: 18, lineHeight: 1.6 }}>
{group.description}
</p>
<p style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 12 }}>
Bereich wählen · {group.items.length}{' '}
{group.items.length === 1 ? 'Seite' : 'Seiten'}
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{group.items.map((item) => (
<Link
key={item.to}
to={item.to}
className="card section-gap"
style={{
margin: 0,
textDecoration: 'none',
color: 'inherit',
borderColor: 'var(--accent)',
borderWidth: 2,
display: 'block',
}}
>
<div style={{ fontWeight: 700, fontSize: 15, color: 'var(--accent)', marginBottom: 4 }}>
{item.label}
</div>
{item.description && (
<div style={{ fontSize: 12, color: 'var(--text2)', lineHeight: 1.5 }}>
{item.description}
</div>
)}
</Link>
))}
</div>
<div style={{ marginTop: 20 }}>
<Link to="/admin" className="btn btn-secondary" style={{ fontSize: 13 }}>
Zur Übersicht
</Link>
</div>
</div>
)
}

View File

@ -1,40 +1,41 @@
import { Link } from 'react-router-dom'
import { Shield } from 'lucide-react'
import { ADMIN_NAV_GROUPS } from '../config/adminNav'
const LINK_GROUPS = ADMIN_NAV_GROUPS.filter((g) => g.id !== 'overview')
import { ADMIN_GROUPS, adminGroupHubPath } from '../config/adminNav'
export default function AdminHomePage() {
return (
<div>
<div style={{display:'flex',alignItems:'center',gap:8,marginBottom:12}}>
<Shield size={20} color="var(--accent)"/>
<h1 style={{fontSize:18,fontWeight:700,margin:0}}>Adminbereich</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Shield size={20} color="var(--accent)" />
<h1 className="page-title" style={{ margin: 0 }}>Adminbereich</h1>
</div>
<p style={{fontSize:13,color:'var(--text2)',marginBottom:20,lineHeight:1.6}}>
Wähle links (Desktop) oder oben (Mobil) einen Bereich oder springe direkt zu einer Gruppe:
<p style={{ fontSize: 13, color: 'var(--text2)', marginBottom: 18, lineHeight: 1.6 }}>
Wähle links oder oben eine <strong>Gruppe</strong>. Die einzelnen Verwaltungsseiten erreichst du auf der
folgenden Gruppenseite wie bei der KI-Analyse (Kategorie Detail).
</p>
<div style={{display:'flex',flexDirection:'column',gap:16}}>
{LINK_GROUPS.map((group) => (
<div key={group.id} className="card section-gap" style={{margin:0}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:10,color:'var(--text1)'}}>
{group.label}
</div>
<div style={{display:'flex',flexDirection:'column',gap:8}}>
{group.items.map((item) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{ADMIN_GROUPS.map((group) => (
<Link
key={item.to}
to={item.to}
className="btn btn-secondary btn-full"
style={{ textAlign: 'center', textDecoration: 'none', boxSizing: 'border-box' }}
key={group.id}
to={adminGroupHubPath(group.id)}
className="card section-gap"
style={{
margin: 0,
textDecoration: 'none',
color: 'inherit',
display: 'block',
}}
>
{item.label}
<div style={{ fontWeight: 700, fontSize: 15, marginBottom: 6 }}>{group.label}</div>
<div style={{ fontSize: 12, color: 'var(--text2)', lineHeight: 1.5 }}>
{group.description}
</div>
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 8 }}>
{group.items.length} {group.items.length === 1 ? 'Bereich' : 'Bereiche'}
</div>
</Link>
))}
</div>
</div>
))}
</div>
</div>
)
}

View File

@ -632,7 +632,7 @@ export default function Analysis() {
<div className="empty-state">
<p>Keine aktiven Pipeline-Prompts verfügbar.</p>
<p style={{fontSize:12,color:'var(--text3)',marginTop:8}}>
Erstelle Pipeline-Prompts im Admin-Bereich (Einstellungen Admin KI-Prompts).
Erstelle Pipeline-Prompts im Admin-Bereich unter Admin KI-Prompts.
</p>
</div>
)}