feat: Add Admin Group Hub page and update navigation structure for improved admin management
This commit is contained in:
parent
bbc59457ac
commit
b7f2e2adbe
|
|
@ -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/>}/>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
57
frontend/src/pages/AdminGroupHubPage.jsx
Normal file
57
frontend/src/pages/AdminGroupHubPage.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user