import { useState, useEffect } from 'react' import { Save, Download, Upload, Trash2, Plus, Check, Pencil, X, LogOut, Shield, 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' const COLORS = ['#1D9E75','#378ADD','#D85A30','#EF9F27','#7F77DD','#D4537E','#639922','#888780'] function ProfileForm({ profile, onSave, onCancel, title }) { const [form, setForm] = useState({ name: profile?.name || '', sex: profile?.sex || 'm', dob: profile?.dob || '', height: profile?.height || '', goal_weight: profile?.goal_weight || '', goal_bf_pct: profile?.goal_bf_pct || '', avatar_color: profile?.avatar_color || COLORS[0], }) const set = (k,v) => setForm(f=>({...f,[k]:v})) return (
{title &&
{title}
}
set('name',e.target.value)} autoFocus/>
Avatar-Farbe
{COLORS.map(c=>(
set('avatar_color',c)} style={{width:26,height:26,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'}}/> ))}
set('dob',e.target.value)}/>
set('height',e.target.value)}/> cm
Ziele (optional)
set('goal_weight',e.target.value)} placeholder="–"/> kg
set('goal_bf_pct',e.target.value)} placeholder="–"/> %
) } export default function SettingsPage() { const { profiles, activeProfile, setActiveProfile, refreshProfiles } = useProfile() const { logout, isAdmin, canExport } = useAuth() const [adminOpen, setAdminOpen] = useState(false) const [pinOpen, setPinOpen] = useState(false) const [newPin, setNewPin] = useState('') const [pinMsg, setPinMsg] = useState(null) const [exportUsage, setExportUsage] = useState(null) // Phase 3: Usage badge // Load feature usage for export badges useEffect(() => { api.getFeatureUsage().then(features => { const exportFeature = features.find(f => f.feature_id === 'data_export') setExportUsage(exportFeature) }).catch(err => console.error('Failed to load usage:', err)) }, []) const handleLogout = async () => { if (!confirm('Ausloggen?')) return await logout() } const handlePinChange = async () => { if (newPin.length < 4) return setPinMsg('Mind. 4 Zeichen') try { const token = localStorage.getItem('bodytrack_token')||'' const pid = localStorage.getItem('bodytrack_active_profile')||'' const r = await fetch('/api/auth/pin', { method:'PUT', headers:{'Content-Type':'application/json','X-Auth-Token':token,'X-Profile-Id':pid}, body: JSON.stringify({pin: newPin}) }) if (!r.ok) throw new Error('Fehler') setNewPin(''); setPinMsg('✓ PIN geändert') setTimeout(()=>setPinMsg(null), 2000) } catch(e) { setPinMsg('Fehler beim Speichern') } } // editingId: string ID of profile being edited, or 'new' for new profile, or null const [editingId, setEditingId] = useState(null) const [saved, setSaved] = useState(false) const [importing, setImporting] = useState(false) const [importMsg, setImportMsg] = useState(null) const handleImport = async (e) => { const file = e.target.files?.[0] if (!file) return if (!confirm(`Backup "${file.name}" importieren? Vorhandene Einträge werden nicht überschrieben.`)) { e.target.value = '' // Reset file input return } setImporting(true) setImportMsg(null) try { const formData = new FormData() formData.append('file', file) const token = localStorage.getItem('bodytrack_token')||'' const pid = localStorage.getItem('bodytrack_active_profile')||'' const res = await fetch('/api/import/zip', { method: 'POST', headers: { 'X-Auth-Token': token, 'X-Profile-Id': pid }, body: formData }) const data = await res.json() if (!res.ok) { throw new Error(data.detail || 'Import fehlgeschlagen') } // Show success message with stats const stats = data.stats const lines = [] if (stats.weight > 0) lines.push(`${stats.weight} Gewicht`) if (stats.circumferences > 0) lines.push(`${stats.circumferences} Umfänge`) if (stats.caliper > 0) lines.push(`${stats.caliper} Caliper`) if (stats.nutrition > 0) lines.push(`${stats.nutrition} Ernährung`) if (stats.activity > 0) lines.push(`${stats.activity} Aktivität`) if (stats.photos > 0) lines.push(`${stats.photos} Fotos`) if (stats.insights > 0) lines.push(`${stats.insights} KI-Analysen`) setImportMsg({ type: 'success', text: `✓ Import erfolgreich: ${lines.join(', ')}` }) // Refresh data (in case new entries were added) await refreshProfiles() } catch (err) { setImportMsg({ type: 'error', text: `✗ ${err.message}` }) } finally { setImporting(false) e.target.value = '' // Reset file input setTimeout(() => setImportMsg(null), 5000) } } const handleSave = async (form, profileId) => { const data = {} if (form.name) data.name = form.name if (form.sex) data.sex = form.sex if (form.dob) data.dob = form.dob if (form.height) data.height = parseFloat(form.height) if (form.avatar_color) data.avatar_color = form.avatar_color if (form.goal_weight) data.goal_weight = parseFloat(form.goal_weight) if (form.goal_bf_pct) data.goal_bf_pct = parseFloat(form.goal_bf_pct) if (profileId === 'new') { const p = await api.createProfile({ ...data, name: form.name || 'Neues Profil' }) await refreshProfiles() // Don't auto-switch – just close the form } else { await api.updateProfile(profileId, data) await refreshProfiles() // If editing active profile, update it if (profileId === activeProfile?.id) { const updated = profiles.find(p => p.id === profileId) if (updated) setActiveProfile({...updated, ...data}) } } setEditingId(null) setSaved(true) setTimeout(() => setSaved(false), 2000) } const handleDelete = async (id) => { if (!confirm('Profil und ALLE zugehörigen Daten unwiderruflich löschen?')) return await api.deleteProfile(id) await refreshProfiles() if (activeProfile?.id === id) { const remaining = profiles.filter(p => p.id !== id) if (remaining.length) setActiveProfile(remaining[0]) } setEditingId(null) } return (

Einstellungen

{/* Profile list */}
Profile ({profiles.length})
{profiles.map(p => (
{p.name}
{p.sex==='m'?'Männlich':'Weiblich'} {p.height ? ` · ${p.height} cm` : ''} {p.goal_weight ? ` · Ziel: ${p.goal_weight} kg` : ''}
{activeProfile?.id === p.id ? Aktiv : } {profiles.length > 1 && ( )}
{/* Edit form – only shown for THIS profile */} {editingId === p.id && ( handleSave(form, p.id)} onCancel={() => setEditingId(null)} /> )}
))} {/* New profile */} {editingId === 'new' ? ( handleSave(form, 'new')} onCancel={() => setEditingId(null)} /> ) : ( )}
{/* Auth actions */}
🔐 Konto
{pinOpen && (
setNewPin(e.target.value)} style={{flex:1}}/>
{pinMsg &&
{pinMsg}
}
)}
{/* Feature Usage Overview (Phase 3) */}
Kontingente

Übersicht über deine Feature-Nutzung und verfügbare Kontingente.

{/* Admin Panel */} {isAdmin && (
Admin
{adminOpen &&
}
)} {/* Export */}
Daten exportieren

Exportiert alle Daten von {activeProfile?.name}: Gewicht, Umfänge, Caliper, Ernährung, Aktivität und KI-Auswertungen.

{!canExport && (
🔒 Export ist für dein Profil nicht freigeschaltet. Bitte den Admin kontaktieren.
)} {canExport && <> }

Der ZIP-Export enthält separate Dateien für Excel und eine lesbare KI-Auswertungsdatei.

{/* Import */}
Backup importieren

Importiere einen ZIP-Export zurück in {activeProfile?.name}. Vorhandene Einträge werden nicht überschrieben.

{!canExport && (
🔒 Import ist für dein Profil nicht freigeschaltet. Bitte den Admin kontaktieren.
)} {canExport && ( <> {importMsg && (
{importMsg.text}
)} )}

Der Import erkennt automatisch das Format und importiert nur neue Einträge.

{saved && (
Gespeichert
)}
) }