508 lines
16 KiB
JavaScript
508 lines
16 KiB
JavaScript
import { useState, useEffect } from 'react'
|
||
import { Pencil, Trash2, Check, X } from 'lucide-react'
|
||
import { api } from '../utils/api'
|
||
import dayjs from 'dayjs'
|
||
import 'dayjs/locale/de'
|
||
dayjs.locale('de')
|
||
|
||
// Quick Mode Presets
|
||
const PRESETS = [
|
||
{
|
||
id: 'muscle_recovery',
|
||
icon: '💪',
|
||
label: 'Kraft-Ruhetag',
|
||
description: 'Muskelregeneration – Kraft & HIIT pausieren, Cardio erlaubt',
|
||
color: '#D85A30',
|
||
config: {
|
||
focus: 'muscle_recovery',
|
||
rest_from: ['strength', 'hiit'],
|
||
allows: ['cardio_low', 'meditation', 'mobility', 'walk'],
|
||
intensity_max: 60,
|
||
}
|
||
},
|
||
{
|
||
id: 'cardio_recovery',
|
||
icon: '🏃',
|
||
label: 'Cardio-Ruhetag',
|
||
description: 'Ausdauererholung – Cardio pausieren, Kraft & Mobility erlaubt',
|
||
color: '#378ADD',
|
||
config: {
|
||
focus: 'cardio_recovery',
|
||
rest_from: ['cardio', 'cardio_low', 'cardio_high'],
|
||
allows: ['strength', 'mobility', 'meditation'],
|
||
intensity_max: 70,
|
||
}
|
||
},
|
||
{
|
||
id: 'mental_rest',
|
||
icon: '🧘',
|
||
label: 'Entspannungstag',
|
||
description: 'Mentale Erholung – Nur Meditation & Spaziergang',
|
||
color: '#7B68EE',
|
||
config: {
|
||
focus: 'mental_rest',
|
||
rest_from: ['strength', 'cardio', 'hiit', 'power'],
|
||
allows: ['meditation', 'walk'],
|
||
intensity_max: 40,
|
||
}
|
||
},
|
||
{
|
||
id: 'deload',
|
||
icon: '📉',
|
||
label: 'Deload',
|
||
description: 'Reduzierte Intensität – Alles erlaubt, max 70% Last',
|
||
color: '#E67E22',
|
||
config: {
|
||
focus: 'deload',
|
||
rest_from: [],
|
||
allows: ['strength', 'cardio', 'mobility', 'meditation'],
|
||
intensity_max: 70,
|
||
}
|
||
},
|
||
]
|
||
|
||
const FOCUS_LABELS = {
|
||
muscle_recovery: 'Muskelregeneration',
|
||
cardio_recovery: 'Cardio-Erholung',
|
||
mental_rest: 'Mentale Erholung',
|
||
deload: 'Deload',
|
||
injury: 'Verletzungspause',
|
||
}
|
||
|
||
export default function RestDaysPage() {
|
||
const [restDays, setRestDays] = useState([])
|
||
const [showForm, setShowForm] = useState(false)
|
||
const [formData, setFormData] = useState({
|
||
date: dayjs().format('YYYY-MM-DD'),
|
||
preset: null,
|
||
note: '',
|
||
})
|
||
const [editing, setEditing] = useState(null)
|
||
const [saving, setSaving] = useState(false)
|
||
const [error, setError] = useState(null)
|
||
const [toast, setToast] = useState(null)
|
||
|
||
useEffect(() => {
|
||
loadRestDays()
|
||
}, [])
|
||
|
||
const loadRestDays = async () => {
|
||
try {
|
||
const data = await api.listRestDays(90)
|
||
setRestDays(data)
|
||
} catch (err) {
|
||
console.error('Failed to load rest days:', err)
|
||
setError('Fehler beim Laden der Ruhetage')
|
||
}
|
||
}
|
||
|
||
const showToast = (message, duration = 2000) => {
|
||
setToast(message)
|
||
setTimeout(() => setToast(null), duration)
|
||
}
|
||
|
||
const handlePresetSelect = (preset) => {
|
||
setFormData(f => ({ ...f, preset: preset.id }))
|
||
}
|
||
|
||
const handleSave = async () => {
|
||
if (!formData.preset) {
|
||
setError('Bitte wähle einen Ruhetag-Typ')
|
||
return
|
||
}
|
||
|
||
setSaving(true)
|
||
setError(null)
|
||
|
||
try {
|
||
const preset = PRESETS.find(p => p.id === formData.preset)
|
||
await api.createRestDay({
|
||
date: formData.date,
|
||
rest_config: preset.config,
|
||
note: formData.note,
|
||
})
|
||
|
||
showToast('✓ Ruhetag gespeichert')
|
||
await loadRestDays()
|
||
setShowForm(false)
|
||
setFormData({
|
||
date: dayjs().format('YYYY-MM-DD'),
|
||
preset: null,
|
||
note: '',
|
||
})
|
||
} catch (err) {
|
||
console.error('Save failed:', err)
|
||
setError(err.message || 'Fehler beim Speichern')
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
const handleDelete = async (id) => {
|
||
if (!confirm('Ruhetag löschen?')) return
|
||
|
||
try {
|
||
await api.deleteRestDay(id)
|
||
showToast('✓ Gelöscht')
|
||
await loadRestDays()
|
||
} catch (err) {
|
||
console.error('Delete failed:', err)
|
||
setError(err.message || 'Fehler beim Löschen')
|
||
}
|
||
}
|
||
|
||
const startEdit = (day) => {
|
||
setEditing({
|
||
...day,
|
||
note: day.note || '',
|
||
})
|
||
}
|
||
|
||
const cancelEdit = () => {
|
||
setEditing(null)
|
||
}
|
||
|
||
const handleUpdate = async () => {
|
||
try {
|
||
await api.updateRestDay(editing.id, {
|
||
note: editing.note,
|
||
})
|
||
showToast('✓ Aktualisiert')
|
||
setEditing(null)
|
||
await loadRestDays()
|
||
} catch (err) {
|
||
console.error('Update failed:', err)
|
||
setError(err.message || 'Fehler beim Aktualisieren')
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="capture-page">
|
||
<h1 className="page-title">Ruhetage</h1>
|
||
|
||
{/* Toast Notification */}
|
||
{toast && (
|
||
<div style={{
|
||
position: 'fixed',
|
||
top: 70,
|
||
left: '50%',
|
||
transform: 'translateX(-50%)',
|
||
background: 'var(--accent)',
|
||
color: 'white',
|
||
padding: '10px 20px',
|
||
borderRadius: 8,
|
||
fontSize: 14,
|
||
fontWeight: 600,
|
||
zIndex: 1000,
|
||
animation: 'slideDown 0.3s ease',
|
||
}}>
|
||
{toast}
|
||
</div>
|
||
)}
|
||
|
||
{/* New Entry Form */}
|
||
{!showForm ? (
|
||
<button
|
||
className="btn btn-primary btn-full"
|
||
style={{ marginBottom: 16 }}
|
||
onClick={() => setShowForm(true)}
|
||
>
|
||
+ Ruhetag erfassen
|
||
</button>
|
||
) : (
|
||
<div className="card" style={{ marginBottom: 16 }}>
|
||
<div className="card-title">Neuer Ruhetag</div>
|
||
|
||
{/* Date */}
|
||
<div className="form-row">
|
||
<label className="form-label">Datum</label>
|
||
<input
|
||
type="date"
|
||
className="form-input"
|
||
style={{ width: 140 }}
|
||
value={formData.date}
|
||
onChange={e => setFormData(f => ({ ...f, date: e.target.value }))}
|
||
/>
|
||
<span className="form-unit" />
|
||
</div>
|
||
|
||
{/* Presets */}
|
||
<div style={{ marginTop: 16 }}>
|
||
<label className="form-label" style={{ marginBottom: 8 }}>Typ wählen:</label>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
{PRESETS.map(preset => (
|
||
<button
|
||
key={preset.id}
|
||
onClick={() => handlePresetSelect(preset)}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
padding: '12px 14px',
|
||
borderRadius: 10,
|
||
border: formData.preset === preset.id
|
||
? `2px solid ${preset.color}`
|
||
: '1.5px solid var(--border)',
|
||
background: formData.preset === preset.id
|
||
? `${preset.color}14`
|
||
: 'var(--surface)',
|
||
cursor: 'pointer',
|
||
textAlign: 'left',
|
||
transition: 'all 0.15s',
|
||
}}
|
||
>
|
||
<div style={{
|
||
fontSize: 24,
|
||
width: 40,
|
||
height: 40,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
borderRadius: 8,
|
||
background: `${preset.color}22`,
|
||
flexShrink: 0,
|
||
}}>
|
||
{preset.icon}
|
||
</div>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{
|
||
fontSize: 14,
|
||
fontWeight: 600,
|
||
color: formData.preset === preset.id ? preset.color : 'var(--text1)',
|
||
marginBottom: 2,
|
||
}}>
|
||
{preset.label}
|
||
</div>
|
||
<div style={{ fontSize: 11, color: 'var(--text3)' }}>
|
||
{preset.description}
|
||
</div>
|
||
</div>
|
||
{formData.preset === preset.id && (
|
||
<Check size={18} style={{ color: preset.color, flexShrink: 0 }} />
|
||
)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Note */}
|
||
<div className="form-row" style={{ marginTop: 16 }}>
|
||
<label className="form-label">Notiz</label>
|
||
<input
|
||
type="text"
|
||
className="form-input"
|
||
placeholder="optional"
|
||
value={formData.note}
|
||
onChange={e => setFormData(f => ({ ...f, note: e.target.value }))}
|
||
/>
|
||
<span className="form-unit" />
|
||
</div>
|
||
|
||
{/* Error */}
|
||
{error && (
|
||
<div style={{
|
||
padding: '10px',
|
||
background: 'var(--danger-bg)',
|
||
border: '1px solid var(--danger)',
|
||
borderRadius: 8,
|
||
fontSize: 13,
|
||
color: 'var(--danger)',
|
||
marginTop: 8,
|
||
}}>
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{/* Actions */}
|
||
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
|
||
<button
|
||
className="btn btn-primary"
|
||
style={{ flex: 1 }}
|
||
onClick={handleSave}
|
||
disabled={saving}
|
||
>
|
||
{saving ? '...' : 'Speichern'}
|
||
</button>
|
||
<button
|
||
className="btn btn-secondary"
|
||
style={{ flex: 1 }}
|
||
onClick={() => {
|
||
setShowForm(false)
|
||
setFormData({
|
||
date: dayjs().format('YYYY-MM-DD'),
|
||
preset: null,
|
||
note: '',
|
||
})
|
||
setError(null)
|
||
}}
|
||
>
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* List */}
|
||
<div className="section-gap">
|
||
<div className="card-title" style={{ marginBottom: 8 }}>
|
||
Verlauf ({restDays.length})
|
||
</div>
|
||
|
||
{restDays.length === 0 && (
|
||
<p className="muted">Noch keine Ruhetage erfasst.</p>
|
||
)}
|
||
|
||
{restDays.map(day => {
|
||
const isEditing = editing?.id === day.id
|
||
const focus = day.rest_config?.focus || 'mental_rest'
|
||
const preset = PRESETS.find(p => p.id === focus)
|
||
const isToday = day.date === dayjs().format('YYYY-MM-DD')
|
||
|
||
return (
|
||
<div key={day.id} className="card" style={{
|
||
marginBottom: 8,
|
||
border: isToday ? '2px solid var(--accent)' : undefined,
|
||
background: isToday ? 'var(--accent)08' : undefined,
|
||
}}>
|
||
{isEditing ? (
|
||
<div>
|
||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>
|
||
{dayjs(day.date).format('DD. MMMM YYYY')}
|
||
</div>
|
||
<div className="form-row">
|
||
<label className="form-label">Notiz</label>
|
||
<input
|
||
type="text"
|
||
className="form-input"
|
||
value={editing.note}
|
||
onChange={e => setEditing(d => ({ ...d, note: e.target.value }))}
|
||
/>
|
||
<span className="form-unit" />
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
|
||
<button
|
||
className="btn btn-primary"
|
||
style={{ flex: 1 }}
|
||
onClick={handleUpdate}
|
||
>
|
||
<Check size={13} /> Speichern
|
||
</button>
|
||
<button
|
||
className="btn btn-secondary"
|
||
style={{ flex: 1 }}
|
||
onClick={cancelEdit}
|
||
>
|
||
<X size={13} /> Abbrechen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginBottom: 8,
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<div style={{ fontWeight: 600, fontSize: 14 }}>
|
||
{dayjs(day.date).format('DD. MMMM YYYY')}
|
||
</div>
|
||
{isToday && (
|
||
<span style={{
|
||
padding: '2px 8px',
|
||
borderRadius: 4,
|
||
background: 'var(--accent)',
|
||
color: 'white',
|
||
fontSize: 11,
|
||
fontWeight: 600,
|
||
}}>
|
||
HEUTE
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 6 }}>
|
||
<button
|
||
className="btn btn-secondary"
|
||
style={{ padding: '5px 8px' }}
|
||
onClick={() => startEdit(day)}
|
||
>
|
||
<Pencil size={13} />
|
||
</button>
|
||
<button
|
||
className="btn btn-danger"
|
||
style={{ padding: '5px 8px' }}
|
||
onClick={() => handleDelete(day.id)}
|
||
>
|
||
<Trash2 size={13} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Preset Badge */}
|
||
<div style={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
padding: '4px 10px',
|
||
borderRadius: 6,
|
||
background: `${preset?.color || '#888'}22`,
|
||
border: `1px solid ${preset?.color || '#888'}`,
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
color: preset?.color || '#888',
|
||
marginBottom: day.note ? 8 : 0,
|
||
}}>
|
||
<span style={{ fontSize: 14 }}>{preset?.icon || '📅'}</span>
|
||
{FOCUS_LABELS[focus] || focus}
|
||
</div>
|
||
|
||
{/* Note */}
|
||
{day.note && (
|
||
<p style={{
|
||
fontSize: 12,
|
||
color: 'var(--text2)',
|
||
fontStyle: 'italic',
|
||
marginTop: 6,
|
||
}}>
|
||
"{day.note}"
|
||
</p>
|
||
)}
|
||
|
||
{/* Details (collapsible, optional for later) */}
|
||
{day.rest_config && (
|
||
<div style={{
|
||
marginTop: 8,
|
||
padding: 8,
|
||
background: 'var(--bg)',
|
||
borderRadius: 6,
|
||
fontSize: 11,
|
||
color: 'var(--text3)',
|
||
}}>
|
||
{day.rest_config.rest_from?.length > 0 && (
|
||
<div>
|
||
<strong>Pausiert:</strong> {day.rest_config.rest_from.join(', ')}
|
||
</div>
|
||
)}
|
||
{day.rest_config.allows?.length > 0 && (
|
||
<div>
|
||
<strong>Erlaubt:</strong> {day.rest_config.allows.join(', ')}
|
||
</div>
|
||
)}
|
||
{day.rest_config.intensity_max && (
|
||
<div>
|
||
<strong>Max Intensität:</strong> {day.rest_config.intensity_max}% HFmax
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|