diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1f6227f..146477a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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() { }/> }/> }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> + }> + }> + } /> + } /> + } /> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + + }/> }/> diff --git a/frontend/src/app.css b/frontend/src/app.css index 5ebc86b..2b8a70f 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -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; } diff --git a/frontend/src/components/EmailSettings.jsx b/frontend/src/components/EmailSettings.jsx new file mode 100644 index 0000000..0f849ba --- /dev/null +++ b/frontend/src/components/EmailSettings.jsx @@ -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 ( +
+
+ 📧 E-Mail Konfiguration (SMTP) +
+ {!status ?
: ( + <> +
+ {status.configured + ? <>✓ Konfiguriert: {status.smtp_user} via {status.smtp_host} + : <>⚠️ Nicht konfiguriert. SMTP-Einstellungen in der .env Datei setzen.} +
+ {status.configured && ( + <> +
+ App-URL: {status.app_url}
+ Für korrekte Links in E-Mails (z.B. Recovery-Links). In .env als APP_URL setzen. +
+
+ setTestTo(e.target.value)} style={{flex:1}}/> + +
+ {testMsg &&
{testMsg}
} + + )} + {!status.configured && ( +
+ Füge folgende Zeilen zur .env Datei hinzu:
+ + SMTP_HOST=smtp.gmail.com
+ SMTP_PORT=587
+ SMTP_USER=deine@gmail.com
+ SMTP_PASS=dein_app_passwort
+ APP_URL=http://192.168.2.49:3002 +
+
+ )} + + )} +
+ ) +} diff --git a/frontend/src/config/adminNav.js b/frontend/src/config/adminNav.js new file mode 100644 index 0000000..49c76de --- /dev/null +++ b/frontend/src/config/adminNav.js @@ -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' }, + ], + }, +] diff --git a/frontend/src/config/appNav.js b/frontend/src/config/appNav.js index 6902e82..e3cd55c 100644 --- a/frontend/src/config/appNav.js +++ b/frontend/src/config/appNav.js @@ -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 } diff --git a/frontend/src/layouts/AdminShell.jsx b/frontend/src/layouts/AdminShell.jsx new file mode 100644 index 0000000..0570d17 --- /dev/null +++ b/frontend/src/layouts/AdminShell.jsx @@ -0,0 +1,40 @@ +import { Outlet, NavLink } from 'react-router-dom' +import { ADMIN_NAV_GROUPS } from '../config/adminNav' + +export default function AdminShell() { + return ( +
+
+ +
+
+ +
+
+
+
+ ) +} diff --git a/frontend/src/layouts/RequireAdmin.jsx b/frontend/src/layouts/RequireAdmin.jsx new file mode 100644 index 0000000..53effe2 --- /dev/null +++ b/frontend/src/layouts/RequireAdmin.jsx @@ -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 + } + return +} diff --git a/frontend/src/pages/AdminHomePage.jsx b/frontend/src/pages/AdminHomePage.jsx new file mode 100644 index 0000000..8e5a8e8 --- /dev/null +++ b/frontend/src/pages/AdminHomePage.jsx @@ -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 ( +
+
+ +

Adminbereich

+
+

+ Wähle links (Desktop) oder oben (Mobil) einen Bereich – oder springe direkt zu einer Gruppe: +

+
+ {LINK_GROUPS.map((group) => ( +
+
+ {group.label} +
+
+ {group.items.map((item) => ( + + {item.label} + + ))} +
+
+ ))} +
+
+ ) +} diff --git a/frontend/src/pages/AdminSystemPage.jsx b/frontend/src/pages/AdminSystemPage.jsx new file mode 100644 index 0000000..2b67929 --- /dev/null +++ b/frontend/src/pages/AdminSystemPage.jsx @@ -0,0 +1,65 @@ +import { Settings } from 'lucide-react' +import EmailSettings from '../components/EmailSettings' +import { api } from '../utils/api' + +export default function AdminSystemPage() { + return ( +
+
+ +

Basiseinstellungen

+
+

+ SMTP für System-E-Mails und Export der Placeholder-Metadaten für Dokumentation und Compliance. +

+ + + +
+
+ Placeholder-Metadaten (Export) +
+
+ Vollständige Metadaten aller registrierten Platzhalter (JSON/ZIP für Katalog und Reports). +
+
+ + +
+
+ JSON: Maschinenlesbare Metadaten ·{' '} + ZIP: Katalog, Gap Report, Export Spec +
+
+
+ ) +} diff --git a/frontend/src/pages/AdminPanel.jsx b/frontend/src/pages/AdminUsersPage.jsx similarity index 50% rename from frontend/src/pages/AdminPanel.jsx rename to frontend/src/pages/AdminUsersPage.jsx index b288158..88fe370 100644 --- a/frontend/src/pages/AdminPanel.jsx +++ b/frontend/src/pages/AdminUsersPage.jsx @@ -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 ( -
- {label} -
!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}}> -
-
-
- ) -} - 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 && (
- {/* Permissions */}
BERECHTIGUNGEN
@@ -231,7 +213,6 @@ function ProfileCard({ profile, currentId, onRefresh }) {
- {/* Feature-Overrides */}
Feature-Limits: Nutze die neue{' '} @@ -240,13 +221,11 @@ function ProfileCard({ profile, currentId, onRefresh }) { Seite um individuelle Limits zu setzen.
- {/* Email */}
E-MAIL (für Recovery & Zusammenfassungen)
- {/* PIN change */}
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 ( -
-
- 📧 E-Mail Konfiguration -
- {!status ?
: ( - <> -
- {status.configured - ? <>✓ Konfiguriert: {status.smtp_user} via {status.smtp_host} - : <>⚠️ Nicht konfiguriert. SMTP-Einstellungen in der .env Datei setzen.} -
- {status.configured && ( - <> -
- App-URL: {status.app_url}
- Für korrekte Links in E-Mails (z.B. Recovery-Links). In .env als APP_URL setzen. -
-
- setTestTo(e.target.value)} style={{flex:1}}/> - -
- {testMsg &&
{testMsg}
} - - )} - {!status.configured && ( -
- Füge folgende Zeilen zur .env Datei hinzu:
- - SMTP_HOST=smtp.gmail.com
- SMTP_PORT=587
- SMTP_USER=deine@gmail.com
- SMTP_PASS=dein_app_passwort
- APP_URL=http://192.168.2.49:3002 -
-
- )} - - )} -
- ) -} - -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() {
- 👑 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.
{creating && ( @@ -384,171 +286,6 @@ export default function AdminPanel() { Neues Profil anlegen )} - - {/* Email Settings */} - - - {/* v9c Subscription Management */} -
-
- Subscription-System (v9c) -
-
- Verwalte Tiers, Features und Limits für das neue Freemium-System. -
-
- - - - - - - - - - - - - - - -
-
- - {/* v9d Training Types Management */} -
-
- Trainingstypen (v9d) -
-
- Verwalte Trainingstypen, Kategorien und Activity-Mappings (lernendes System). -
-
- - - - - - - - - -
-
- - {/* KI-Prompts Section */} -
-
- KI-Prompts (v9f) -
-
- Verwalte AI-Prompts mit KI-Unterstützung: Generiere, optimiere und organisiere Prompts. -
-
- - - -
-
- - {/* Goal Types Section */} -
-
- Ziel-Typen (v9e) -
-
- Verwalte Goal-Type-Definitionen: Erstelle custom goal types mit oder ohne automatische Datenquelle. -
-
- - - -
-
- - {/* Focus Areas Section */} -
-
- Focus Areas (v9g) -
-
- Verwalte Focus Area Definitionen: Dynamisches, erweiterbares System mit 26+ Bereichen über 7 Kategorien. -
-
- - - -
-
- - {/* Placeholder Metadata Export Section */} -
-
- Placeholder Metadata Export (v1.0) -
-
- Exportiere vollständige Metadaten aller 116 Placeholders. Normative Compliance v1.0.0. -
-
- - -
-
- JSON: Maschinenlesbare Metadaten aller Placeholders
- ZIP: Katalog (JSON + MD), Gap Report, Export Spec (4 Dateien) -
-
) } diff --git a/frontend/src/pages/SettingsPage.jsx b/frontend/src/pages/SettingsPage.jsx index 70cebfd..4def0fc 100644 --- a/frontend/src/pages/SettingsPage.jsx +++ b/frontend/src/pages/SettingsPage.jsx @@ -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() {
- {/* Admin Panel */} - {isAdmin && ( -
-
-
- Admin -
- -
- {adminOpen &&
} -
- )} - {/* Export */}
Daten exportieren