mitai-jinkendo/frontend/src/pages/AdminPanel.jsx

436 lines
19 KiB
React
Raw Normal View History

import { useState, useEffect } from 'react'
import { Plus, Trash2, Pencil, Check, X, Shield, Key, Settings } from 'lucide-react'
import { Link } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import { api } from '../utils/api'
const COLORS = ['#1D9E75','#378ADD','#D85A30','#EF9F27','#7F77DD','#D4537E','#639922','#888780']
function Avatar({ profile, size=36 }) {
const initials = profile.name.split(' ').map(n=>n[0]).join('').toUpperCase().slice(0,2)
return (
<div style={{width:size,height:size,borderRadius:'50%',background:profile.avatar_color||'#1D9E75',
display:'flex',alignItems:'center',justifyContent:'center',
fontSize:size*0.35,fontWeight:700,color:'white',flexShrink:0}}>
{initials}
</div>
)
}
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],
sex:'m', height:'', auth_type:'pin', session_days:30
})
const [error, setError] = useState(null)
const set = (k,v) => setForm(f=>({...f,[k]:v}))
const handleSave = async () => {
if (!form.name.trim()) return setError('Name eingeben')
if (form.pin.length < 4) return setError('PIN mind. 4 Zeichen')
try {
await onSave({...form, height:parseFloat(form.height)||178})
} catch(e) { setError(e.message) }
}
return (
<div style={{background:'var(--surface2)',borderRadius:10,padding:14,
border:'1.5px solid var(--accent)',marginBottom:12}}>
<div style={{fontWeight:600,fontSize:14,marginBottom:12,color:'var(--accent)'}}>Neues Profil</div>
<div className="form-row">
<label className="form-label">Name</label>
<input type="text" className="form-input" value={form.name} onChange={e=>set('name',e.target.value)} autoFocus/>
<span className="form-unit"/>
</div>
<div style={{marginBottom:10}}>
<div style={{fontSize:11,color:'var(--text3)',marginBottom:6}}>Farbe</div>
<div style={{display:'flex',gap:6}}>
{COLORS.map(c=>(
<div key={c} onClick={()=>set('avatar_color',c)}
style={{width:24,height:24,borderRadius:'50%',background:c,cursor:'pointer',
border:`3px solid ${form.avatar_color===c?'white':'transparent'}`,
boxShadow:form.avatar_color===c?`0 0 0 2px ${c}`:'none'}}/>
))}
</div>
</div>
<div className="form-row">
<label className="form-label">Geschlecht</label>
<select className="form-select" value={form.sex} onChange={e=>set('sex',e.target.value)}>
<option value="m">Männlich</option>
<option value="f">Weiblich</option>
</select>
</div>
<div className="form-row">
<label className="form-label">Größe</label>
<input type="number" className="form-input" placeholder="178" value={form.height} onChange={e=>set('height',e.target.value)}/>
<span className="form-unit">cm</span>
</div>
<div className="form-row">
<label className="form-label">E-Mail</label>
<input type="email" className="form-input" placeholder="optional, für Recovery"
value={form.email} onChange={e=>set('email',e.target.value)}/>
<span className="form-unit"/>
</div>
<div className="form-row">
<label className="form-label">Login</label>
<select className="form-select" value={form.auth_type} onChange={e=>set('auth_type',e.target.value)}>
<option value="pin">PIN</option>
<option value="password">Passwort</option>
</select>
</div>
<div className="form-row">
<label className="form-label">PIN/Passwort</label>
<input type="password" className="form-input" placeholder="Mind. 4 Zeichen"
value={form.pin} onChange={e=>set('pin',e.target.value)}/>
<span className="form-unit"/>
</div>
<div className="form-row">
<label className="form-label">Session</label>
<select className="form-select" value={form.session_days} onChange={e=>set('session_days',parseInt(e.target.value))}>
<option value={7}>7 Tage</option>
<option value={30}>30 Tage</option>
<option value={0}>Immer</option>
</select>
</div>
{error && <div style={{color:'#D85A30',fontSize:12,marginBottom:8}}>{error}</div>}
<div style={{display:'flex',gap:8}}>
<button className="btn btn-primary" style={{flex:1}} onClick={handleSave}><Check size={13}/> Erstellen</button>
<button className="btn btn-secondary" style={{flex:1}} onClick={onCancel}><X size={13}/> Abbrechen</button>
</div>
</div>
)
}
function EmailEditor({ profileId, currentEmail, onSaved }) {
const [email, setEmail] = useState(currentEmail||'')
const [msg, setMsg] = useState(null)
const save = async () => {
const token = localStorage.getItem('bodytrack_token')||''
await fetch(`/api/admin/profiles/${profileId}/email`, {
method:'PUT', headers:{'Content-Type':'application/json','X-Auth-Token':token},
body: JSON.stringify({email})
})
setMsg('✓ Gespeichert'); onSaved()
setTimeout(()=>setMsg(null),2000)
}
return (
<div style={{display:'flex',gap:8,alignItems:'center'}}>
<input type="email" className="form-input" placeholder="email@beispiel.de"
value={email} onChange={e=>setEmail(e.target.value)} style={{flex:1}}/>
<button className="btn btn-secondary" onClick={save}>Setzen</button>
{msg && <span style={{fontSize:11,color:'var(--accent)'}}>{msg}</span>}
</div>
)
}
function ProfileCard({ profile, currentId, onRefresh }) {
const [expanded, setExpanded] = useState(false)
const [perms, setPerms] = useState({
ai_enabled: profile.ai_enabled ?? 1,
ai_limit_day: profile.ai_limit_day || '',
export_enabled: profile.export_enabled ?? 1,
role: profile.role || 'user',
})
const [saving, setSaving] = useState(false)
const [newPin, setNewPin] = useState('')
const [pinMsg, setPinMsg] = useState(null)
const isSelf = profile.id === currentId
const savePerms = async () => {
setSaving(true)
try {
await api.adminSetPermissions(profile.id, {
ai_enabled: perms.ai_enabled,
ai_limit_day: perms.ai_limit_day ? parseInt(perms.ai_limit_day) : null,
export_enabled: perms.export_enabled,
role: perms.role,
})
await onRefresh()
} finally { setSaving(false) }
}
const savePin = async () => {
if (newPin.length < 4) return setPinMsg('Mind. 4 Zeichen')
try {
await fetch(`/api/admin/profiles/${profile.id}/pin`, {
method:'PUT', headers:{'Content-Type':'application/json',
'X-Auth-Token': localStorage.getItem('bodytrack_token')||''},
body: JSON.stringify({pin: newPin})
})
setNewPin(''); setPinMsg('✓ PIN geändert')
setTimeout(()=>setPinMsg(null),2000)
} catch(e) { setPinMsg('Fehler: '+e.message) }
}
const deleteProfile = async () => {
if (!confirm(`Profil "${profile.name}" und ALLE Daten löschen?`)) return
await api.adminDeleteProfile(profile.id)
await onRefresh()
}
return (
<div className="card section-gap">
<div style={{display:'flex',alignItems:'center',gap:10}}>
<Avatar profile={profile}/>
<div style={{flex:1}}>
<div style={{fontWeight:600,fontSize:14,display:'flex',alignItems:'center',gap:6}}>
{profile.name}
{profile.role==='admin' && <span style={{fontSize:10,color:'var(--accent)',background:'var(--accent-light)',padding:'1px 5px',borderRadius:4}}>👑 Admin</span>}
{isSelf && <span style={{fontSize:10,color:'var(--text3)'}}>Du</span>}
</div>
<div style={{fontSize:11,color:'var(--text3)'}}>
KI: {profile.ai_enabled?`✓${profile.ai_limit_day?` (max ${profile.ai_limit_day}/Tag)`:''}` : '✗'} ·
Export: {profile.export_enabled?'✓':'✗'} ·
Calls heute: {profile.ai_calls_today||0}
</div>
</div>
<div style={{display:'flex',gap:6}}>
<button className="btn btn-secondary" style={{padding:'5px 8px'}}
onClick={()=>setExpanded(e=>!e)}>
<Pencil size={12}/>
</button>
{!isSelf && (
<button className="btn btn-danger" style={{padding:'5px 8px'}}
onClick={deleteProfile}>
<Trash2 size={12}/>
</button>
)}
</div>
</div>
{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}}>
<div style={{fontSize:12,color:'var(--text3)',marginBottom:4}}>Rolle</div>
<div style={{display:'flex',gap:6}}>
{['user','admin'].map(r=>(
<button key={r} onClick={()=>setPerms(p=>({...p,role:r}))}
style={{flex:1,padding:'6px',borderRadius:8,border:`1.5px solid ${perms.role===r?'var(--accent)':'var(--border2)'}`,
background:perms.role===r?'var(--accent-light)':'var(--surface)',
color:perms.role===r?'var(--accent-dark)':'var(--text2)',
fontFamily:'var(--font)',fontSize:13,cursor:'pointer'}}>
{r==='admin'?'👑 Admin':'👤 Nutzer'}
</button>
))}
</div>
</div>
<Toggle value={!!perms.ai_enabled} onChange={v=>setPerms(p=>({...p,ai_enabled:v?1:0}))} label="KI-Analysen erlaubt"/>
{!!perms.ai_enabled && (
<div className="form-row" style={{paddingTop:6}}>
<label className="form-label" style={{fontSize:12}}>Max. KI-Calls/Tag</label>
<input type="number" className="form-input" style={{width:70}} min={1} max={100}
placeholder="∞" value={perms.ai_limit_day}
onChange={e=>setPerms(p=>({...p,ai_limit_day:e.target.value}))}/>
<span className="form-unit" style={{fontSize:11}}>/Tag</span>
</div>
)}
<Toggle value={!!perms.export_enabled} onChange={v=>setPerms(p=>({...p,export_enabled:v?1:0}))} label="Daten-Export erlaubt"/>
<button className="btn btn-primary btn-full" style={{marginTop:10}} onClick={savePerms} disabled={saving}>
{saving?'Speichern…':'Berechtigungen speichern'}
</button>
{/* 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
</div>
<div style={{display:'flex',gap:8}}>
<input type="password" className="form-input" placeholder="Neue PIN/Passwort"
value={newPin} onChange={e=>setNewPin(e.target.value)} style={{flex:1}}/>
<button className="btn btn-secondary" onClick={savePin}>Setzen</button>
</div>
{pinMsg && <div style={{fontSize:11,color:pinMsg.startsWith('✓')?'var(--accent)':'#D85A30',marginTop:4}}>{pinMsg}</div>}
</div>
</div>
)}
</div>
)
}
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() {
const { session } = useAuth()
const [profiles, setProfiles] = useState([])
const [creating, setCreating] = useState(false)
const [loading, setLoading] = useState(true)
const load = () => api.adminListProfiles().then(data=>{ setProfiles(data); setLoading(false) })
useEffect(()=>{ load() },[])
const handleCreate = async (form) => {
await api.adminCreateProfile(form)
setCreating(false)
await load()
}
if (loading) return <div className="empty-state"><div className="spinner"/></div>
return (
<div>
<div style={{display:'flex',alignItems:'center',gap:8,marginBottom:16}}>
<Shield size={18} color="var(--accent)"/>
<h2 style={{fontSize:17,fontWeight:700,margin:0}}>Benutzerverwaltung</h2>
</div>
<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.
</div>
{creating && (
<NewProfileForm onSave={handleCreate} onCancel={()=>setCreating(false)}/>
)}
{profiles.map(p=>(
<ProfileCard key={p.id} profile={p} currentId={session?.profile_id} onRefresh={load}/>
))}
{!creating && (
<button className="btn btn-secondary btn-full" style={{marginTop:12}}
onClick={()=>setCreating(true)}>
<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>
</div>
</div>
</div>
)
}