feat: Refactor admin settings and user management
All checks were successful
Deploy Development / deploy (push) Successful in 53s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 14s

- Removed Admin Panel from SettingsPage and adjusted related logic.
- Added EmailSettings component for SMTP configuration and testing.
- Created admin navigation structure in adminNav.js for better organization.
- Implemented AdminShell layout for consistent admin UI.
- Added RequireAdmin component to protect admin routes.
- Developed AdminHomePage for admin dashboard with navigation links.
- Created AdminSystemPage for SMTP settings and placeholder metadata export.
- Implemented AdminUsersPage for user management, including profile creation and editing.
This commit is contained in:
Lars 2026-04-05 10:32:43 +02:00
parent 190c0dd7fa
commit bbc59457ac
11 changed files with 454 additions and 298 deletions

View File

@ -34,6 +34,11 @@ import AdminTrainingProfiles from './pages/AdminTrainingProfiles'
import AdminPromptsPage from './pages/AdminPromptsPage'
import AdminGoalTypesPage from './pages/AdminGoalTypesPage'
import AdminFocusAreasPage from './pages/AdminFocusAreasPage'
import AdminHomePage from './pages/AdminHomePage'
import AdminUsersPage from './pages/AdminUsersPage'
import AdminSystemPage from './pages/AdminSystemPage'
import RequireAdmin from './layouts/RequireAdmin'
import AdminShell from './layouts/AdminShell'
import SubscriptionPage from './pages/SubscriptionPage'
import SleepPage from './pages/SleepPage'
import RestDaysPage from './pages/RestDaysPage'
@ -219,17 +224,24 @@ function AppShell() {
<Route path="/goals" element={<GoalsPage/>}/>
<Route path="/analysis" element={<Analysis/>}/>
<Route path="/settings" element={<SettingsPage/>}/>
<Route path="/admin/tier-limits" element={<AdminTierLimitsPage/>}/>
<Route path="/admin/features" element={<AdminFeaturesPage/>}/>
<Route path="/admin/tiers" element={<AdminTiersPage/>}/>
<Route path="/admin/coupons" element={<AdminCouponsPage/>}/>
<Route path="/admin/user-restrictions" element={<AdminUserRestrictionsPage/>}/>
<Route path="/admin/training-types" element={<AdminTrainingTypesPage/>}/>
<Route path="/admin/activity-mappings" element={<AdminActivityMappingsPage/>}/>
<Route path="/admin/training-profiles" element={<AdminTrainingProfiles/>}/>
<Route path="/admin/prompts" element={<AdminPromptsPage/>}/>
<Route path="/admin/goal-types" element={<AdminGoalTypesPage/>}/>
<Route path="/admin/focus-areas" element={<AdminFocusAreasPage/>}/>
<Route element={<RequireAdmin />}>
<Route path="admin" element={<AdminShell />}>
<Route index element={<AdminHomePage />} />
<Route path="users" element={<AdminUsersPage />} />
<Route path="system" element={<AdminSystemPage />} />
<Route path="tier-limits" element={<AdminTierLimitsPage/>}/>
<Route path="features" element={<AdminFeaturesPage/>}/>
<Route path="tiers" element={<AdminTiersPage/>}/>
<Route path="coupons" element={<AdminCouponsPage/>}/>
<Route path="user-restrictions" element={<AdminUserRestrictionsPage/>}/>
<Route path="training-types" element={<AdminTrainingTypesPage/>}/>
<Route path="activity-mappings" element={<AdminActivityMappingsPage/>}/>
<Route path="training-profiles" element={<AdminTrainingProfiles/>}/>
<Route path="prompts" element={<AdminPromptsPage/>}/>
<Route path="goal-types" element={<AdminGoalTypesPage/>}/>
<Route path="focus-areas" element={<AdminFocusAreasPage/>}/>
</Route>
</Route>
<Route path="/workflow-editor/:id" element={<WorkflowEditorPage/>}/>
<Route path="/subscription" element={<SubscriptionPage/>}/>
</Routes>

View File

@ -474,6 +474,136 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
}
}
/* Admin: gruppierte Sub-Navigation (Mobil = Chips pro Gruppe, Desktop = linke Spalte) */
.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%;
}
@media (min-width: 1024px) {
.admin-page {
max-width: var(--capture-content-max);
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; }
.empty-state { text-align: center; padding: 48px 16px; color: var(--text3); }
.empty-state h3 { font-size: 16px; color: var(--text2); margin-bottom: 6px; }

View File

@ -0,0 +1,78 @@
import { useState, useEffect } from 'react'
export default function EmailSettings() {
const [status, setStatus] = useState(null)
const [testTo, setTestTo] = useState('')
const [testing, setTesting] = useState(false)
const [testMsg, setTestMsg] = useState(null)
useEffect(()=>{
const token = localStorage.getItem('bodytrack_token')||''
fetch('/api/admin/email/status',{headers:{'X-Auth-Token':token}})
.then(r=>r.json()).then(setStatus)
},[])
const sendTest = async () => {
if (!testTo) return
setTesting(true); setTestMsg(null)
try {
const token = localStorage.getItem('bodytrack_token')||''
const r = await fetch('/api/admin/email/test',{
method:'POST',headers:{'Content-Type':'application/json','X-Auth-Token':token},
body:JSON.stringify({to:testTo})
})
if(!r.ok) throw new Error((await r.json()).detail)
setTestMsg('✓ Test-E-Mail gesendet!')
} catch(e){ setTestMsg('✗ Fehler: '+e.message) }
finally{ setTesting(false) }
}
return (
<div className="card section-gap" style={{marginTop:0}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:10,display:'flex',alignItems:'center',gap:6}}>
📧 E-Mail Konfiguration (SMTP)
</div>
{!status ? <div className="spinner" style={{width:16,height:16}}/> : (
<>
<div style={{padding:'8px 12px',borderRadius:8,marginBottom:12,
background:status.configured?'var(--accent-light)':'var(--warn-bg)',
fontSize:12,color:status.configured?'var(--accent-dark)':'var(--warn-text)'}}>
{status.configured
? <> Konfiguriert: <strong>{status.smtp_user}</strong> via {status.smtp_host}</>
: <> Nicht konfiguriert. SMTP-Einstellungen in der <code>.env</code> Datei setzen.</>}
</div>
{status.configured && (
<>
<div style={{fontSize:11,color:'var(--text3)',marginBottom:10,lineHeight:1.5}}>
<strong>App-URL:</strong> {status.app_url}<br/>
<span style={{fontSize:10}}>Für korrekte Links in E-Mails (z.B. Recovery-Links). In .env als APP_URL setzen.</span>
</div>
<div style={{display:'flex',gap:8}}>
<input type="email" className="form-input" placeholder="test@beispiel.de"
value={testTo} onChange={e=>setTestTo(e.target.value)} style={{flex:1}}/>
<button className="btn btn-secondary" onClick={sendTest} disabled={testing}>
{testing?'…':'Test'}
</button>
</div>
{testMsg && <div style={{fontSize:12,marginTop:6,
color:testMsg.startsWith('✓')?'var(--accent)':'#D85A30'}}>{testMsg}</div>}
</>
)}
{!status.configured && (
<div style={{fontSize:11,color:'var(--text3)',lineHeight:1.6}}>
Füge folgende Zeilen zur <code>.env</code> Datei hinzu:<br/>
<code style={{background:'var(--surface2)',padding:'6px 8px',borderRadius:4,
display:'block',marginTop:6,fontSize:11}}>
SMTP_HOST=smtp.gmail.com<br/>
SMTP_PORT=587<br/>
SMTP_USER=deine@gmail.com<br/>
SMTP_PASS=dein_app_passwort<br/>
APP_URL=http://192.168.2.49:3002
</code>
</div>
)}
</>
)}
</div>
)
}

View File

@ -0,0 +1,61 @@
/**
* Gruppierte Admin-Navigation (Shell: Chips mobil, Seitenleiste Desktop).
* @typedef {{ to: string, label: string, end?: boolean }} AdminNavItem
* @typedef {{ id: string, label: string, items: AdminNavItem[] }} AdminNavGroup
*/
/** @type {AdminNavGroup[]} */
export const ADMIN_NAV_GROUPS = [
{
id: 'overview',
label: 'Übersicht',
items: [{ to: '/admin', label: 'Start', end: true }],
},
{
id: 'users',
label: 'Benutzerverwaltung',
items: [{ to: '/admin/users', label: 'Profile & Rollen' }],
},
{
id: 'features',
label: 'Features',
items: [
{ to: '/admin/features', label: 'Feature-Registry' },
{ to: '/admin/tier-limits', label: 'Tier-Limits' },
{ to: '/admin/user-restrictions', label: 'User-Overrides' },
],
},
{
id: 'subscription',
label: 'Subscription',
items: [
{ to: '/admin/tiers', label: 'Tiers' },
{ to: '/admin/coupons', label: 'Coupons' },
],
},
{
id: 'training',
label: 'Trainingstypen',
items: [
{ to: '/admin/training-types', label: 'Trainingstypen' },
{ to: '/admin/activity-mappings', label: 'Activity-Mappings' },
{ to: '/admin/training-profiles', label: 'Trainings-Profile' },
],
},
{
id: 'goals',
label: 'Ziele & Fokus',
items: [
{ to: '/admin/goal-types', label: 'Ziel-Typen' },
{ to: '/admin/focus-areas', label: 'Focus Areas' },
],
},
{
id: 'system',
label: 'Basiseinstellungen',
items: [
{ to: '/admin/prompts', label: 'KI-Prompts' },
{ to: '/admin/system', label: 'SMTP & Metadaten-Export' },
],
},
]

View File

@ -37,7 +37,7 @@ export function getMainNavItems(isAdmin) {
Icon: icons[i]
}))
if (isAdmin) {
raw.push({ to: '/admin/features', label: 'Admin', Icon: Shield })
raw.push({ to: '/admin', label: 'Admin', end: false, Icon: Shield })
}
return raw
}

View File

@ -0,0 +1,40 @@
import { Outlet, NavLink } from 'react-router-dom'
import { ADMIN_NAV_GROUPS } from '../config/adminNav'
export default function AdminShell() {
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) => (
<NavLink
key={`${group.id}-${item.to}`}
to={item.to}
end={!!item.end}
className={({ isActive }) =>
'admin-shell__nav-item' +
(isActive ? ' admin-shell__nav-item--active' : '')
}
>
{item.label}
</NavLink>
))}
</div>
</div>
))}
</div>
</nav>
<div className="admin-shell__main">
<div className="admin-page">
<Outlet />
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,11 @@
import { Navigate, Outlet, useLocation } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
export default function RequireAdmin() {
const { isAdmin } = useAuth()
const loc = useLocation()
if (!isAdmin) {
return <Navigate to="/" replace state={{ from: loc.pathname, adminDenied: true }} />
}
return <Outlet />
}

View File

@ -0,0 +1,40 @@
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')
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>
<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>
<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) => (
<Link
key={item.to}
to={item.to}
className="btn btn-secondary btn-full"
style={{ textAlign: 'center', textDecoration: 'none', boxSizing: 'border-box' }}
>
{item.label}
</Link>
))}
</div>
</div>
))}
</div>
</div>
)
}

View File

@ -0,0 +1,65 @@
import { Settings } from 'lucide-react'
import EmailSettings from '../components/EmailSettings'
import { api } from '../utils/api'
export default function AdminSystemPage() {
return (
<div>
<div style={{display:'flex',alignItems:'center',gap:8,marginBottom:16}}>
<Settings size={18} color="var(--accent)"/>
<h2 style={{fontSize:17,fontWeight:700,margin:0}}>Basiseinstellungen</h2>
</div>
<p style={{fontSize:13,color:'var(--text2)',marginBottom:16,lineHeight:1.6}}>
SMTP für System-E-Mails und Export der Placeholder-Metadaten für Dokumentation und Compliance.
</p>
<EmailSettings />
<div className="card section-gap" style={{marginTop:16}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:12,display:'flex',alignItems:'center',gap:6}}>
<Settings size={16} color="var(--accent)"/> Placeholder-Metadaten (Export)
</div>
<div style={{fontSize:12,color:'var(--text3)',marginBottom:12,lineHeight:1.5}}>
Vollständige Metadaten aller registrierten Platzhalter (JSON/ZIP für Katalog und Reports).
</div>
<div style={{display:'grid',gap:8}}>
<button type="button" className="btn btn-secondary btn-full"
onClick={async()=>{
try {
const data = await api.exportPlaceholdersExtendedJson()
const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'})
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `placeholder-metadata-extended-${new Date().toISOString().split('T')[0]}.json`
a.click()
window.URL.revokeObjectURL(url)
} catch(e) {
alert('Fehler beim Export: '+e.message)
}
}}>
📄 Complete JSON exportieren
</button>
<button type="button" className="btn btn-secondary btn-full"
onClick={()=>{
try {
const token = localStorage.getItem('bodytrack_token')
const a = document.createElement('a')
a.href = `/api/prompts/placeholders/export-catalog-zip?token=${token}`
a.download = `placeholder-catalog-${new Date().toISOString().split('T')[0]}.zip`
a.click()
} catch(e) {
alert('Fehler beim Export: '+e.message)
}
}}>
📦 Complete ZIP (JSON + Markdown + Reports)
</button>
</div>
<div style={{fontSize:11,color:'var(--text3)',marginTop:8,lineHeight:1.5}}>
<strong>JSON:</strong> Maschinenlesbare Metadaten ·{' '}
<strong>ZIP:</strong> Katalog, Gap Report, Export Spec
</div>
</div>
</div>
)
}

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { Plus, Trash2, Pencil, Check, X, Shield, Key, Settings } from 'lucide-react'
import { Plus, Trash2, Pencil, Check, X, Shield, Key } from 'lucide-react'
import { Link } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import { api } from '../utils/api'
@ -17,23 +17,6 @@ function Avatar({ profile, size=36 }) {
)
}
function Toggle({ value, onChange, label, disabled=false }) {
return (
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',
padding:'8px 0',borderBottom:'1px solid var(--border)'}}>
<span style={{fontSize:13,color:disabled?'var(--text3)':'var(--text1)'}}>{label}</span>
<div onClick={()=>!disabled&&onChange(!value)}
style={{width:40,height:22,borderRadius:11,background:value?'var(--accent)':'var(--border)',
position:'relative',cursor:disabled?'not-allowed':'pointer',transition:'background 0.2s',
opacity:disabled?0.5:1}}>
<div style={{position:'absolute',top:2,left:value?18:2,width:18,height:18,
borderRadius:'50%',background:'white',transition:'left 0.2s',
boxShadow:'0 1px 3px rgba(0,0,0,0.2)'}}/>
</div>
</div>
)
}
function NewProfileForm({ onSave, onCancel }) {
const [form, setForm] = useState({
name:'', pin:'', email:'', avatar_color:COLORS[0],
@ -210,7 +193,6 @@ function ProfileCard({ profile, currentId, onRefresh }) {
{expanded && (
<div style={{marginTop:12,paddingTop:12,borderTop:'1px solid var(--border)'}}>
{/* Permissions */}
<div style={{fontSize:12,fontWeight:600,color:'var(--text3)',marginBottom:8}}>BERECHTIGUNGEN</div>
<div style={{marginBottom:8}}>
@ -231,7 +213,6 @@ function ProfileCard({ profile, currentId, onRefresh }) {
</button>
</div>
{/* Feature-Overrides */}
<div style={{marginBottom:12,padding:10,background:'var(--accent-light)',borderRadius:6,fontSize:12}}>
<strong>Feature-Limits:</strong> Nutze die neue{' '}
<Link to="/admin/user-restrictions" style={{color:'var(--accent-dark)',fontWeight:600}}>
@ -240,13 +221,11 @@ function ProfileCard({ profile, currentId, onRefresh }) {
Seite um individuelle Limits zu setzen.
</div>
{/* Email */}
<div style={{marginTop:12,paddingTop:12,borderTop:'1px solid var(--border)'}}>
<div style={{fontSize:12,fontWeight:600,color:'var(--text3)',marginBottom:6}}>E-MAIL (für Recovery & Zusammenfassungen)</div>
<EmailEditor profileId={profile.id} currentEmail={profile.email} onSaved={onRefresh}/>
</div>
{/* PIN change */}
<div style={{marginTop:14,paddingTop:12,borderTop:'1px solid var(--border)'}}>
<div style={{fontSize:12,fontWeight:600,color:'var(--text3)',marginBottom:8,display:'flex',alignItems:'center',gap:4}}>
<Key size={12}/> PIN / PASSWORT ÄNDERN
@ -264,84 +243,7 @@ function ProfileCard({ profile, currentId, onRefresh }) {
)
}
function EmailSettings() {
const [status, setStatus] = useState(null)
const [testTo, setTestTo] = useState('')
const [testing, setTesting] = useState(false)
const [testMsg, setTestMsg] = useState(null)
useEffect(()=>{
const token = localStorage.getItem('bodytrack_token')||''
fetch('/api/admin/email/status',{headers:{'X-Auth-Token':token}})
.then(r=>r.json()).then(setStatus)
},[])
const sendTest = async () => {
if (!testTo) return
setTesting(true); setTestMsg(null)
try {
const token = localStorage.getItem('bodytrack_token')||''
const r = await fetch('/api/admin/email/test',{
method:'POST',headers:{'Content-Type':'application/json','X-Auth-Token':token},
body:JSON.stringify({to:testTo})
})
if(!r.ok) throw new Error((await r.json()).detail)
setTestMsg('✓ Test-E-Mail gesendet!')
} catch(e){ setTestMsg('✗ Fehler: '+e.message) }
finally{ setTesting(false) }
}
return (
<div className="card section-gap" style={{marginTop:16}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:10,display:'flex',alignItems:'center',gap:6}}>
📧 E-Mail Konfiguration
</div>
{!status ? <div className="spinner" style={{width:16,height:16}}/> : (
<>
<div style={{padding:'8px 12px',borderRadius:8,marginBottom:12,
background:status.configured?'var(--accent-light)':'var(--warn-bg)',
fontSize:12,color:status.configured?'var(--accent-dark)':'var(--warn-text)'}}>
{status.configured
? <> Konfiguriert: <strong>{status.smtp_user}</strong> via {status.smtp_host}</>
: <> Nicht konfiguriert. SMTP-Einstellungen in der <code>.env</code> Datei setzen.</>}
</div>
{status.configured && (
<>
<div style={{fontSize:11,color:'var(--text3)',marginBottom:10,lineHeight:1.5}}>
<strong>App-URL:</strong> {status.app_url}<br/>
<span style={{fontSize:10}}>Für korrekte Links in E-Mails (z.B. Recovery-Links). In .env als APP_URL setzen.</span>
</div>
<div style={{display:'flex',gap:8}}>
<input type="email" className="form-input" placeholder="test@beispiel.de"
value={testTo} onChange={e=>setTestTo(e.target.value)} style={{flex:1}}/>
<button className="btn btn-secondary" onClick={sendTest} disabled={testing}>
{testing?'…':'Test'}
</button>
</div>
{testMsg && <div style={{fontSize:12,marginTop:6,
color:testMsg.startsWith('✓')?'var(--accent)':'#D85A30'}}>{testMsg}</div>}
</>
)}
{!status.configured && (
<div style={{fontSize:11,color:'var(--text3)',lineHeight:1.6}}>
Füge folgende Zeilen zur <code>.env</code> Datei hinzu:<br/>
<code style={{background:'var(--surface2)',padding:'6px 8px',borderRadius:4,
display:'block',marginTop:6,fontSize:11}}>
SMTP_HOST=smtp.gmail.com<br/>
SMTP_PORT=587<br/>
SMTP_USER=deine@gmail.com<br/>
SMTP_PASS=dein_app_passwort<br/>
APP_URL=http://192.168.2.49:3002
</code>
</div>
)}
</>
)}
</div>
)
}
export default function AdminPanel() {
export default function AdminUsersPage() {
const { session } = useAuth()
const [profiles, setProfiles] = useState([])
const [creating, setCreating] = useState(false)
@ -367,7 +269,7 @@ export default function AdminPanel() {
<div style={{padding:'10px 12px',background:'var(--accent-light)',borderRadius:8,
fontSize:12,color:'var(--accent-dark)',marginBottom:16,lineHeight:1.5}}>
👑 Du bist Admin. Hier kannst du Profile verwalten, Berechtigungen setzen und KI-Limits konfigurieren.
👑 Profile anlegen, Rollen setzen und Recovery-E-Mail pro Nutzer pflegen. Feature-Limits über User-Overrides in der Seitenleiste.
</div>
{creating && (
@ -384,171 +286,6 @@ export default function AdminPanel() {
<Plus size={14}/> Neues Profil anlegen
</button>
)}
{/* Email Settings */}
<EmailSettings/>
{/* v9c Subscription Management */}
<div className="card section-gap" style={{marginTop:16}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:12,display:'flex',alignItems:'center',gap:6}}>
<Settings size={16} color="var(--accent)"/> Subscription-System (v9c)
</div>
<div style={{fontSize:12,color:'var(--text3)',marginBottom:12,lineHeight:1.5}}>
Verwalte Tiers, Features und Limits für das neue Freemium-System.
</div>
<div style={{display:'grid',gap:8}}>
<Link to="/admin/tiers">
<button className="btn btn-secondary btn-full">
🎯 Tiers verwalten
</button>
</Link>
<Link to="/admin/features">
<button className="btn btn-secondary btn-full">
🔧 Feature-Registry verwalten
</button>
</Link>
<Link to="/admin/tier-limits">
<button className="btn btn-secondary btn-full">
📊 Tier Limits Matrix bearbeiten
</button>
</Link>
<Link to="/admin/coupons">
<button className="btn btn-secondary btn-full">
🎟 Coupons verwalten
</button>
</Link>
<Link to="/admin/user-restrictions">
<button className="btn btn-secondary btn-full">
👤 User Feature-Overrides
</button>
</Link>
</div>
</div>
{/* v9d Training Types Management */}
<div className="card section-gap" style={{marginTop:16}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:12,display:'flex',alignItems:'center',gap:6}}>
<Settings size={16} color="var(--accent)"/> Trainingstypen (v9d)
</div>
<div style={{fontSize:12,color:'var(--text3)',marginBottom:12,lineHeight:1.5}}>
Verwalte Trainingstypen, Kategorien und Activity-Mappings (lernendes System).
</div>
<div style={{display:'grid',gap:8}}>
<Link to="/admin/training-types">
<button className="btn btn-secondary btn-full">
🏋 Trainingstypen verwalten
</button>
</Link>
<Link to="/admin/activity-mappings">
<button className="btn btn-secondary btn-full">
🔗 Activity-Mappings (lernendes System)
</button>
</Link>
<Link to="/admin/training-profiles">
<button className="btn btn-secondary btn-full">
Training Type Profiles (#15)
</button>
</Link>
</div>
</div>
{/* KI-Prompts Section */}
<div className="card section-gap" style={{marginTop:16}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:12,display:'flex',alignItems:'center',gap:6}}>
<Settings size={16} color="var(--accent)"/> KI-Prompts (v9f)
</div>
<div style={{fontSize:12,color:'var(--text3)',marginBottom:12,lineHeight:1.5}}>
Verwalte AI-Prompts mit KI-Unterstützung: Generiere, optimiere und organisiere Prompts.
</div>
<div style={{display:'grid',gap:8}}>
<Link to="/admin/prompts">
<button className="btn btn-secondary btn-full">
🤖 KI-Prompts verwalten
</button>
</Link>
</div>
</div>
{/* Goal Types Section */}
<div className="card section-gap" style={{marginTop:16}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:12,display:'flex',alignItems:'center',gap:6}}>
<Settings size={16} color="var(--accent)"/> Ziel-Typen (v9e)
</div>
<div style={{fontSize:12,color:'var(--text3)',marginBottom:12,lineHeight:1.5}}>
Verwalte Goal-Type-Definitionen: Erstelle custom goal types mit oder ohne automatische Datenquelle.
</div>
<div style={{display:'grid',gap:8}}>
<Link to="/admin/goal-types">
<button className="btn btn-secondary btn-full">
🎯 Ziel-Typen verwalten
</button>
</Link>
</div>
</div>
{/* Focus Areas Section */}
<div className="card section-gap" style={{marginTop:16}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:12,display:'flex',alignItems:'center',gap:6}}>
<Settings size={16} color="var(--accent)"/> Focus Areas (v9g)
</div>
<div style={{fontSize:12,color:'var(--text3)',marginBottom:12,lineHeight:1.5}}>
Verwalte Focus Area Definitionen: Dynamisches, erweiterbares System mit 26+ Bereichen über 7 Kategorien.
</div>
<div style={{display:'grid',gap:8}}>
<Link to="/admin/focus-areas">
<button className="btn btn-secondary btn-full">
🎯 Focus Areas verwalten
</button>
</Link>
</div>
</div>
{/* Placeholder Metadata Export Section */}
<div className="card section-gap" style={{marginTop:16}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:12,display:'flex',alignItems:'center',gap:6}}>
<Settings size={16} color="var(--accent)"/> Placeholder Metadata Export (v1.0)
</div>
<div style={{fontSize:12,color:'var(--text3)',marginBottom:12,lineHeight:1.5}}>
Exportiere vollständige Metadaten aller 116 Placeholders. Normative Compliance v1.0.0.
</div>
<div style={{display:'grid',gap:8}}>
<button className="btn btn-secondary btn-full"
onClick={async()=>{
try {
const data = await api.exportPlaceholdersExtendedJson()
const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'})
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `placeholder-metadata-extended-${new Date().toISOString().split('T')[0]}.json`
a.click()
window.URL.revokeObjectURL(url)
} catch(e) {
alert('Fehler beim Export: '+e.message)
}
}}>
📄 Complete JSON exportieren
</button>
<button className="btn btn-secondary btn-full"
onClick={async()=>{
try {
const token = localStorage.getItem('bodytrack_token')
const a = document.createElement('a')
a.href = `/api/prompts/placeholders/export-catalog-zip?token=${token}`
a.download = `placeholder-catalog-${new Date().toISOString().split('T')[0]}.zip`
a.click()
} catch(e) {
alert('Fehler beim Export: '+e.message)
}
}}>
📦 Complete ZIP (JSON + Markdown + Reports)
</button>
</div>
<div style={{fontSize:11,color:'var(--text3)',marginTop:8,lineHeight:1.5}}>
<strong>JSON:</strong> Maschinenlesbare Metadaten aller Placeholders<br/>
<strong>ZIP:</strong> Katalog (JSON + MD), Gap Report, Export Spec (4 Dateien)
</div>
</div>
</div>
)
}

View File

@ -1,10 +1,9 @@
import { useState, useEffect } from 'react'
import { Save, Download, Upload, Trash2, Plus, Check, Pencil, X, LogOut, Shield, Key, BarChart3 } from 'lucide-react'
import { Save, Download, Upload, Trash2, Plus, Check, Pencil, X, LogOut, Key, BarChart3 } from 'lucide-react'
import { useProfile } from '../context/ProfileContext'
import { useAuth } from '../context/AuthContext'
import { Avatar } from './ProfileSelect'
import { api } from '../utils/api'
import AdminPanel from './AdminPanel'
import FeatureUsageOverview from '../components/FeatureUsageOverview'
import UsageBadge from '../components/UsageBadge'
@ -96,8 +95,7 @@ function ProfileForm({ profile, onSave, onCancel, title }) {
export default function SettingsPage() {
const { profiles, activeProfile, setActiveProfile, refreshProfiles } = useProfile()
const { logout, isAdmin, canExport } = useAuth()
const [adminOpen, setAdminOpen] = useState(false)
const { logout, canExport } = useAuth()
const [pinOpen, setPinOpen] = useState(false)
const [newPin, setNewPin] = useState('')
const [pinMsg, setPinMsg] = useState(null)
@ -375,22 +373,6 @@ export default function SettingsPage() {
<FeatureUsageOverview />
</div>
{/* Admin Panel */}
{isAdmin && (
<div className="card section-gap">
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between'}}>
<div className="card-title" style={{margin:0,display:'flex',alignItems:'center',gap:6}}>
<Shield size={15} color="var(--accent)"/> Admin
</div>
<button className="btn btn-secondary" style={{fontSize:12}}
onClick={()=>setAdminOpen(o=>!o)}>
{adminOpen?'Schließen':'Öffnen'}
</button>
</div>
{adminOpen && <div style={{marginTop:12}}><AdminPanel/></div>}
</div>
)}
{/* Export */}
<div className="card section-gap">
<div className="card-title">Daten exportieren</div>