import { useState, useEffect } from 'react' import { Save, Download, Upload, Check, LogOut, Key, BarChart3, Target, LayoutGrid, LayoutDashboard } from 'lucide-react' import { Link } from 'react-router-dom' import { useProfile } from '../context/ProfileContext' import { useAuth } from '../context/AuthContext' import { Avatar } from './ProfileSelect' import { api } from '../utils/api' import FeatureUsageOverview from '../components/FeatureUsageOverview' import UsageBadge from '../components/UsageBadge' const COLORS = ['#1D9E75','#378ADD','#D85A30','#EF9F27','#7F77DD','#D4537E','#639922','#888780'] function dobInputValue(dob) { if (!dob) return '' const s = String(dob) return s.length >= 10 ? s.slice(0, 10) : s } export default function SettingsPage() { const { profiles, activeProfile, setActiveProfile, refreshProfiles } = useProfile() const { logout, canExport, isAdmin } = useAuth() 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') } } const [form, setForm] = useState({ name: '', email: '', sex: 'm', dob: '', height: '', goal_weight: '', goal_bf_pct: '', avatar_color: COLORS[0], }) const setF = (k, v) => setForm((f) => ({ ...f, [k]: v })) const [profileErr, setProfileErr] = 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) } } useEffect(() => { if (!activeProfile) return const sexRaw = activeProfile.sex || 'm' setForm({ name: activeProfile.name || '', email: activeProfile.email || '', sex: sexRaw === 'f' ? 'w' : sexRaw, dob: dobInputValue(activeProfile.dob), height: activeProfile.height != null ? String(activeProfile.height) : '', goal_weight: activeProfile.goal_weight != null ? String(activeProfile.goal_weight) : '', goal_bf_pct: activeProfile.goal_bf_pct != null ? String(activeProfile.goal_bf_pct) : '', avatar_color: activeProfile.avatar_color || COLORS[0], }) setProfileErr(null) }, [activeProfile?.id]) const handleQualityFilterChange = async (level) => { await api.updateActiveProfile({ quality_filter_level: level }) await refreshProfiles() const updated = profiles.find(p => p.id === activeProfile?.id) if (updated) setActiveProfile({ ...updated, quality_filter_level: level }) setSaved(true) setTimeout(() => setSaved(false), 2000) } const handleSaveMyProfile = async () => { if (!activeProfile) return const name = form.name.trim() if (!name) { setProfileErr('Bitte einen Namen eingeben.') return } const h = parseFloat(form.height) if (!form.height || Number.isNaN(h) || h < 100 || h > 250) { setProfileErr('Bitte eine gültige Größe (100–250 cm) eingeben.') return } let goal_weight = null if (form.goal_weight !== '') { goal_weight = parseFloat(form.goal_weight) if (Number.isNaN(goal_weight)) { setProfileErr('Zielgewicht: bitte eine gültige Zahl eingeben oder leer lassen.') return } } let goal_bf_pct = null if (form.goal_bf_pct !== '') { goal_bf_pct = parseFloat(form.goal_bf_pct) if (Number.isNaN(goal_bf_pct)) { setProfileErr('Ziel-KF%: bitte eine gültige Zahl eingeben oder leer lassen.') return } } setProfileErr(null) try { const payload = { name, sex: form.sex, dob: form.dob ? form.dob : null, height: h, avatar_color: form.avatar_color, goal_weight, goal_bf_pct, email: form.email.trim() === '' ? null : form.email.trim(), } await api.updateActiveProfile(payload) await refreshProfiles() setSaved(true) setTimeout(() => setSaved(false), 2000) } catch (e) { setProfileErr(e.message || 'Speichern fehlgeschlagen') } } const handleExportPlaceholders = async () => { try { const data = await api.exportPlaceholderValues() const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `placeholders-${activeProfile?.name || 'profile'}-${new Date().toISOString().split('T')[0]}.json` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } catch (e) { alert('Fehler beim Export: ' + e.message) } } return (
Hier bearbeitest du nur das aktive Profil. Zum Anlegen weiterer Profile oder zum Verwalten anderer Nutzer nutzt du den Admin-Bereich (Zugriff nur als Administrator).
{isAdmin && (Diese Felder bleiben vorerst erhalten; strategische Ziele verwaltest du unter{' '} Analyse → Ziele.
Kacheln wählen und sortieren. Es wird nur dein persönliches Layout gespeichert – der App-Standard für neue Nutzer wird dadurch nicht überschrieben.
Übersicht anpassenKonkrete Ziele, Focus Areas und Fortschritt – eigener Bereich{' '} Ziele in der Navigation (nicht in der KI-Analyse).
Zu den ZielenZiel-Übersicht-Pilot: Schnelleingabe, KPIs, Körper-Chart, Aktivität. Die reguläre Übersicht konfigurierst du unter Übersicht anpassen oben.
Übersicht über deine Feature-Nutzung und verfügbare Kontingente.
Exportiert alle Daten von {activeProfile?.name}: Gewicht, Umfänge, Caliper, Ernährung, Aktivität und KI-Auswertungen.
Der ZIP-Export enthält separate Dateien für Excel und eine lesbare KI-Auswertungsdatei.
Importiere einen ZIP-Export zurück in {activeProfile?.name}. Vorhandene Einträge werden nicht überschrieben.
Der Import erkennt automatisch das Format und importiert nur neue Einträge.
Qualitätsfilter wirkt auf alle Ansichten: Dashboard, Charts, Statistiken und KI-Analysen.