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
)}
)
}