import { useState, useEffect, useRef } from 'react'
import { Upload, Pencil, Trash2, Check, X, CheckCircle } from 'lucide-react'
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts'
import { api } from '../utils/api'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
dayjs.locale('de')
const ACTIVITY_TYPES = [
'Traditionelles Krafttraining','Matrial Arts','Outdoor Spaziergang',
'Innenräume Spaziergang','Laufen','Radfahren','Schwimmen',
'Cardio Dance','Geist & Körper','Sonstiges'
]
function empty() {
return {
date: dayjs().format('YYYY-MM-DD'),
activity_type: 'Traditionelles Krafttraining',
duration_min: '', kcal_active: '',
hr_avg: '', hr_max: '', rpe: '', notes: ''
}
}
// ── Import Panel ──────────────────────────────────────────────────────────────
function ImportPanel({ onImported }) {
const fileRef = useRef()
const [status, setStatus] = useState(null)
const [error, setError] = useState(null)
const [dragging, setDragging] = useState(false)
const runImport = async (file) => {
setStatus('loading'); setError(null)
try {
const result = await api.importActivityCsv(file)
setStatus(result); onImported()
} catch(err) {
setError('Import fehlgeschlagen: ' + err.message); setStatus(null)
}
}
return (
📥 Apple Health Import
Health Auto Export App → Workouts exportieren → CSV → hier hochladen.
Nur die Workouts-…csv Datei wird benötigt (nicht die Detaildateien).
{ const f=e.target.files[0]; if(f) runImport(f); e.target.value='' }}/>
{e.preventDefault();setDragging(true)}}
onDragLeave={()=>setDragging(false)}
onDrop={e=>{e.preventDefault();setDragging(false);const f=e.dataTransfer.files[0];if(f)runImport(f)}}
onClick={()=>fileRef.current.click()}
style={{border:`2px dashed ${dragging?'var(--accent)':'var(--border2)'}`,borderRadius:10,
padding:'20px 16px',textAlign:'center',background:dragging?'var(--accent-light)':'var(--surface2)',
cursor:'pointer',transition:'all 0.15s'}}>
{dragging?'Datei loslassen…':'CSV hierher ziehen oder tippen'}
{status==='loading' && (
Importiere…
)}
{error &&
{error}
}
{status && status!=='loading' && (
Import erfolgreich
{status.inserted} Trainings importiert · {status.skipped} übersprungen
)}
)
}
// ── Manual Entry ──────────────────────────────────────────────────────────────
function EntryForm({ form, setForm, onSave, onCancel, saveLabel='Speichern' }) {
const set = (k,v) => setForm(f=>({...f,[k]:v}))
return (
)
}
// ── Main Page ─────────────────────────────────────────────────────────────────
export default function ActivityPage() {
const [entries, setEntries] = useState([])
const [stats, setStats] = useState(null)
const [tab, setTab] = useState('list')
const [form, setForm] = useState(empty())
const [editing, setEditing] = useState(null)
const [saved, setSaved] = useState(false)
const load = async () => {
const [e, s] = await Promise.all([api.listActivity(), api.activityStats()])
setEntries(e); setStats(s)
}
useEffect(()=>{ load() },[])
const handleSave = async () => {
const payload = {...form}
if(payload.duration_min) payload.duration_min = parseFloat(payload.duration_min)
if(payload.kcal_active) payload.kcal_active = parseFloat(payload.kcal_active)
if(payload.hr_avg) payload.hr_avg = parseFloat(payload.hr_avg)
if(payload.hr_max) payload.hr_max = parseFloat(payload.hr_max)
if(payload.rpe) payload.rpe = parseInt(payload.rpe)
payload.source = 'manual'
await api.createActivity(payload)
setSaved(true); await load()
setTimeout(()=>{ setSaved(false); setForm(empty()) }, 1500)
}
const handleUpdate = async () => {
const payload = {...editing}
await api.updateActivity(editing.id, payload)
setEditing(null); await load()
}
const handleDelete = async (id) => {
if(!confirm('Training löschen?')) return
await api.deleteActivity(id); await load()
}
// Chart data: kcal per day (last 30 days)
const chartData = (() => {
const byDate = {}
entries.forEach(e=>{
byDate[e.date] = (byDate[e.date]||0) + (e.kcal_active||0)
})
return Object.entries(byDate).sort((a,b)=>a[0].localeCompare(b[0])).slice(-30).map(([date,kcal])=>({
date: dayjs(date).format('DD.MM'), kcal: Math.round(kcal)
}))
})()
const TYPE_COLORS = {
'Traditionelles Krafttraining':'#1D9E75','Matrial Arts':'#D85A30',
'Outdoor Spaziergang':'#378ADD','Innenräume Spaziergang':'#7F77DD',
'Laufen':'#EF9F27','Radfahren':'#D4537E','Sonstiges':'#888780'
}
return (
Aktivität
{/* Übersicht */}
{stats && stats.count>0 && (
{[['Trainings',stats.count,'var(--text1)'],
['Kcal gesamt',Math.round(stats.total_kcal),'#EF9F27'],
['Stunden',Math.round(stats.total_min/60*10)/10,'#378ADD']].map(([l,v,c])=>(
))}
)}
{tab==='import' &&
}
{tab==='add' && (
)}
{tab==='stats' && stats && (
{chartData.length>=2 && (
Aktive Kalorien pro Tag
[`${v} kcal`,'Aktiv']}/>
)}
Nach Trainingsart
{Object.entries(stats.by_type).sort((a,b)=>b[1].kcal-a[1].kcal).map(([type,data])=>(
{type}
{data.count}× · {Math.round(data.min)} Min · {Math.round(data.kcal)} kcal
))}
)}
{tab==='list' && (
{entries.length===0 && (
Keine Trainings
Importiere deine Apple Health Daten oder trage manuell ein.
)}
{entries.map(e=>{
const isEd = editing?.id===e.id
const color = TYPE_COLORS[e.activity_type]||'#888'
return (
{isEd ? (
setEditing(null)} saveLabel="Speichern"/>
) : (
{e.activity_type}
{dayjs(e.date).format('dd, DD. MMMM YYYY')}
{e.start_time && e.start_time.length>10 && ` · ${e.start_time.slice(11,16)}`}
{e.duration_min && ⏱ {Math.round(e.duration_min)} Min}
{e.kcal_active && 🔥 {Math.round(e.kcal_active)} kcal}
{e.hr_avg && ❤️ Ø{Math.round(e.hr_avg)} bpm}
{e.hr_max && ↑{Math.round(e.hr_max)} bpm}
{e.distance_km && e.distance_km>0 && 📍 {Math.round(e.distance_km*10)/10} km}
{e.rpe && RPE {e.rpe}/10}
{e.source==='apple_health' && Apple Health}
{e.notes &&
"{e.notes}"
}
)}
)
})}
)}
)
}