2026-03-22 16:33:32 +01:00
|
|
|
|
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 = {
|
feat: multi-dimensional rest days + development routes architecture (v9d → v9e)
## Changes:
**Frontend:**
- Fix double icon in rest day list (removed icons from FOCUS_LABELS)
- Icon now shows once with proper styling
**Migration 011:**
- Remove UNIQUE constraint (profile_id, date) from rest_days
- Allow multiple rest day types per date
- Use case: Muscle recovery + Mental rest same day
**Architecture: Development Routes**
New document: `.claude/docs/functional/DEVELOPMENT_ROUTES.md`
6 Independent Development Routes:
- 💪 Kraft (Strength): Muscle, power, HIIT
- 🏃 Kondition (Conditioning): Cardio, endurance, VO2max
- 🧘 Mental: Stress, focus, competition readiness
- 🤸 Koordination (Coordination): Balance, agility, technique
- 🧘♂️ Mobilität (Mobility): Flexibility, ROM, fascia
- 🎯 Technik (Technique): Sport-specific skills
Each route has:
- Independent rest requirements
- Independent training plans
- Independent progress tracking
- Independent goals & habits
**Future (v9e):**
- Route-based weekly planning
- Multi-route conflict validation
- Auto-rest on poor recovery
- Route balance analysis (KI)
**Future (v9g):**
- Habits per route (route_habits table)
- Streak tracking per route
- Dashboard route-habits widget
**Backlog Updated:**
- v9d: Rest days ✅ (in testing)
- v9e: Development Routes & Weekly Planning (new)
- v9g: Habits per Route (extended)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 16:51:09 +01:00
|
|
|
|
muscle_recovery: 'Muskelregeneration',
|
|
|
|
|
|
cardio_recovery: 'Cardio-Erholung',
|
|
|
|
|
|
mental_rest: 'Mentale Erholung',
|
|
|
|
|
|
deload: 'Deload',
|
|
|
|
|
|
injury: 'Verletzungspause',
|
2026-03-22 16:33:32 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 (
|
2026-04-05 10:14:07 +02:00
|
|
|
|
<div className="capture-page">
|
2026-03-22 16:33:32 +01:00
|
|
|
|
<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)
|
2026-03-23 08:38:57 +01:00
|
|
|
|
const isToday = day.date === dayjs().format('YYYY-MM-DD')
|
2026-03-22 16:33:32 +01:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-23 08:38:57 +01:00
|
|
|
|
<div key={day.id} className="card" style={{
|
|
|
|
|
|
marginBottom: 8,
|
|
|
|
|
|
border: isToday ? '2px solid var(--accent)' : undefined,
|
|
|
|
|
|
background: isToday ? 'var(--accent)08' : undefined,
|
|
|
|
|
|
}}>
|
2026-03-22 16:33:32 +01:00
|
|
|
|
{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,
|
|
|
|
|
|
}}>
|
2026-03-23 08:38:57 +01:00
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
2026-03-22 16:33:32 +01:00
|
|
|
|
</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,
|
|
|
|
|
|
}}>
|
feat: multi-dimensional rest days + development routes architecture (v9d → v9e)
## Changes:
**Frontend:**
- Fix double icon in rest day list (removed icons from FOCUS_LABELS)
- Icon now shows once with proper styling
**Migration 011:**
- Remove UNIQUE constraint (profile_id, date) from rest_days
- Allow multiple rest day types per date
- Use case: Muscle recovery + Mental rest same day
**Architecture: Development Routes**
New document: `.claude/docs/functional/DEVELOPMENT_ROUTES.md`
6 Independent Development Routes:
- 💪 Kraft (Strength): Muscle, power, HIIT
- 🏃 Kondition (Conditioning): Cardio, endurance, VO2max
- 🧘 Mental: Stress, focus, competition readiness
- 🤸 Koordination (Coordination): Balance, agility, technique
- 🧘♂️ Mobilität (Mobility): Flexibility, ROM, fascia
- 🎯 Technik (Technique): Sport-specific skills
Each route has:
- Independent rest requirements
- Independent training plans
- Independent progress tracking
- Independent goals & habits
**Future (v9e):**
- Route-based weekly planning
- Multi-route conflict validation
- Auto-rest on poor recovery
- Route balance analysis (KI)
**Future (v9g):**
- Habits per route (route_habits table)
- Streak tracking per route
- Dashboard route-habits widget
**Backlog Updated:**
- v9d: Rest days ✅ (in testing)
- v9e: Development Routes & Weekly Planning (new)
- v9g: Habits per Route (extended)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 16:51:09 +01:00
|
|
|
|
<span style={{ fontSize: 14 }}>{preset?.icon || '📅'}</span>
|
|
|
|
|
|
{FOCUS_LABELS[focus] || focus}
|
2026-03-22 16:33:32 +01:00
|
|
|
|
</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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|