mitai-jinkendo/frontend/src/pages/RestDaysPage.jsx
Lars a639d08037
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
feat: Update capture page layout for improved responsiveness and organization across multiple screens
2026-04-05 10:14:07 +02:00

508 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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