mitai-jinkendo/frontend/src/pages/SleepPage.jsx
Lars 1644b34d5c
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
fix: manual sleep entry creation + import overwrite protection
Critical fixes:
1. Added "+ Schlaf erfassen" button back (was missing!)
   - Opens NewEntryForm component inline
   - Default: 450 min (7h 30min), quality 3
   - Collapsible detail view
   - Live plausibility check

2. Fixed import overwriting manual entries
   - Problem: ON CONFLICT WHERE clause didn't prevent updates
   - Solution: Explicit if/else logic
     - If manual entry exists → skip (don't touch)
     - If non-manual entry exists → UPDATE
     - If no entry exists → INSERT
   - Properly counts imported vs skipped

Test results:
 CSV import with drag & drop
 Inline editing
 Segment timeline view with colors
 Source badges (Manual/Apple Health)
 Plausibility check (backend + frontend)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 13:43:02 +01:00

927 lines
33 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, useRef } from 'react'
import { Moon, Plus, Edit2, Trash2, X, Save, TrendingUp, Upload, ChevronDown, ChevronUp, AlertCircle } from 'lucide-react'
import { api } from '../utils/api'
/**
* SleepPage - Sleep tracking with CSV import (v9d Phase 2c)
*
* Features:
* - Drag & Drop CSV import
* - Inline editing (no scroll to top)
* - Source badges (manual/Apple Health/Garmin)
* - Expandable segment view (JSONB sleep_segments)
* - Plausibility check (phases sum = duration)
* - Toast notifications (no alerts)
*/
export default function SleepPage() {
const [sleep, setSleep] = useState([])
const [stats, setStats] = useState(null)
const [loading, setLoading] = useState(true)
const [importing, setImporting] = useState(false)
const [dragging, setDragging] = useState(false)
const [toast, setToast] = useState(null)
const [expandedId, setExpandedId] = useState(null)
const [editingId, setEditingId] = useState(null)
const fileInputRef = useRef(null)
useEffect(() => {
load()
}, [])
const load = () => {
setLoading(true)
Promise.all([
api.listSleep(30),
api.getSleepStats(7).catch(() => null)
]).then(([sleepData, statsData]) => {
setSleep(sleepData)
setStats(statsData)
setLoading(false)
}).catch(err => {
console.error('Failed to load sleep data:', err)
setLoading(false)
})
}
const showToast = (message, type = 'success') => {
setToast({ message, type })
setTimeout(() => setToast(null), 4000)
}
const handleImport = async (file) => {
if (!file) return
if (!file.name.endsWith('.csv')) {
showToast('Bitte eine CSV-Datei auswählen', 'error')
return
}
setImporting(true)
try {
const result = await api.importAppleHealthSleep(file)
await load()
showToast(`${result.imported} Nächte importiert, ${result.skipped} übersprungen`)
} catch (err) {
showToast('Import fehlgeschlagen: ' + err.message, 'error')
} finally {
setImporting(false)
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
}
const handleDrop = async (e) => {
e.preventDefault()
setDragging(false)
const file = e.dataTransfer.files[0]
if (file) await handleImport(file)
}
const handleFileSelect = async (e) => {
const file = e.target.files[0]
if (file) await handleImport(file)
}
const handleDelete = async (id, date) => {
if (!confirm(`Schlaf-Eintrag vom ${date} wirklich löschen?`)) return
try {
await api.deleteSleep(id)
await load()
showToast('Eintrag gelöscht')
} catch (err) {
showToast('Löschen fehlgeschlagen: ' + err.message, 'error')
}
}
const formatDuration = (minutes) => {
const h = Math.floor(minutes / 60)
const m = minutes % 60
return `${h}h ${m}min`
}
const getSourceBadge = (source) => {
const colors = {
manual: { bg: 'var(--surface2)', color: 'var(--text2)', label: 'Manuell' },
apple_health: { bg: '#1D9E75', color: 'white', label: 'Apple Health' },
garmin: { bg: '#007DB7', color: 'white', label: 'Garmin' }
}
const s = colors[source] || colors.manual
return (
<span style={{
fontSize: 10,
padding: '2px 6px',
borderRadius: 4,
background: s.bg,
color: s.color,
fontWeight: 600
}}>
{s.label}
</span>
)
}
if (loading) {
return (
<div style={{ padding: 20, textAlign: 'center' }}>
<div className="spinner" style={{ width: 32, height: 32, margin: '0 auto' }} />
</div>
)
}
return (
<div style={{ padding: '16px 16px 80px' }}>
{/* Toast Notification */}
{toast && (
<div style={{
position: 'fixed',
top: 16,
left: '50%',
transform: 'translateX(-50%)',
zIndex: 9999,
background: toast.type === 'error' ? '#D85A30' : 'var(--accent)',
color: 'white',
padding: '12px 20px',
borderRadius: 8,
fontSize: 14,
fontWeight: 600,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
animation: 'slideDown 0.3s ease'
}}>
{toast.message}
</div>
)}
{/* Header */}
<div style={{ marginBottom: 20, display: 'flex', alignItems: 'center', gap: 12 }}>
<Moon size={24} color="var(--accent)" />
<h1 style={{ margin: 0, fontSize: 20, fontWeight: 700 }}>Schlaf</h1>
</div>
{/* Stats Card */}
{stats && stats.total_nights > 0 && (
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 12, display: 'flex', alignItems: 'center', gap: 6 }}>
<TrendingUp size={16} color="var(--accent)" />
<span>Übersicht (letzte 7 Tage)</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div style={{ textAlign: 'center', padding: 12, background: 'var(--surface)', borderRadius: 8 }}>
<div style={{ fontSize: 18, fontWeight: 700, color: 'var(--accent)' }}>
{formatDuration(Math.round(stats.avg_duration_minutes))}
</div>
<div style={{ fontSize: 11, color: 'var(--text3)' }}>Ø Schlafdauer</div>
</div>
<div style={{ textAlign: 'center', padding: 12, background: 'var(--surface)', borderRadius: 8 }}>
<div style={{ fontSize: 18, fontWeight: 700 }}>
{stats.avg_quality ? stats.avg_quality.toFixed(1) : '—'}/5
</div>
<div style={{ fontSize: 11, color: 'var(--text3)' }}>Ø Qualität</div>
</div>
</div>
{stats.nights_below_goal > 0 && (
<div style={{ marginTop: 12, fontSize: 12, color: '#D85A30', textAlign: 'center' }}>
{stats.nights_below_goal} Nächte unter Ziel ({formatDuration(stats.sleep_goal_minutes)})
</div>
)}
</div>
)}
{/* CSV Import Drag & Drop */}
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 12 }}>CSV Import</div>
<div
onDragOver={e => { e.preventDefault(); setDragging(true) }}
onDragLeave={() => setDragging(false)}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
style={{
border: `2px dashed ${dragging ? 'var(--accent)' : 'var(--border2)'}`,
borderRadius: 10,
padding: '24px 16px',
textAlign: 'center',
background: dragging ? 'var(--accent-light)' : 'var(--surface2)',
cursor: importing ? 'not-allowed' : 'pointer',
transition: 'all 0.15s',
opacity: importing ? 0.6 : 1
}}>
{importing ? (
<>
<div className="spinner" style={{ width: 24, height: 24, margin: '0 auto 8px' }} />
<div style={{ fontSize: 14, fontWeight: 500, color: 'var(--text2)' }}>
Importiere Schlaf-Daten...
</div>
</>
) : (
<>
<Upload size={28} style={{ color: dragging ? 'var(--accent)' : 'var(--text3)', marginBottom: 8 }} />
<div style={{ fontSize: 14, fontWeight: 500, color: dragging ? 'var(--accent-dark)' : 'var(--text2)' }}>
{dragging ? 'CSV loslassen...' : 'CSV hierher ziehen oder tippen'}
</div>
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 4 }}>
Apple Health Export (.csv)
</div>
</>
)}
</div>
<input
ref={fileInputRef}
type="file"
accept=".csv"
onChange={handleFileSelect}
style={{ display: 'none' }}
/>
</div>
{/* Manual Entry Button */}
<button
onClick={() => setEditingId('new')}
className="btn btn-primary btn-full"
style={{ marginBottom: 16 }}
>
<Plus size={16} /> Schlaf erfassen
</button>
{/* New Entry Form (if creating) */}
{editingId === 'new' && (
<NewEntryForm
onSave={async (data) => {
try {
await api.createSleep(data)
await load()
setEditingId(null)
showToast('Gespeichert')
} catch (err) {
showToast(err.message, 'error')
}
}}
onCancel={() => setEditingId(null)}
formatDuration={formatDuration}
/>
)}
{/* Sleep List */}
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 12 }}>
Letzte 30 Nächte ({sleep.length})
</div>
{sleep.length === 0 ? (
<div className="card" style={{ padding: 40, textAlign: 'center', color: 'var(--text3)' }}>
Noch keine Einträge erfasst
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{sleep.map(entry => (
<SleepEntry
key={entry.id}
entry={entry}
expanded={expandedId === entry.id}
editing={editingId === entry.id}
onToggleExpand={() => setExpandedId(expandedId === entry.id ? null : entry.id)}
onEdit={() => setEditingId(entry.id)}
onCancelEdit={() => setEditingId(null)}
onSave={async (data) => {
try {
await api.updateSleep(entry.id, data)
await load()
setEditingId(null)
showToast('Gespeichert')
} catch (err) {
showToast(err.message, 'error')
}
}}
onDelete={() => handleDelete(entry.id, entry.date)}
formatDuration={formatDuration}
getSourceBadge={getSourceBadge}
/>
))}
</div>
)}
</div>
)
}
// ── Sleep Entry Component (Inline Editing) ────────────────────────────────────
function SleepEntry({ entry, expanded, editing, onToggleExpand, onEdit, onCancelEdit, onSave, onDelete, formatDuration, getSourceBadge }) {
const [formData, setFormData] = useState({
date: entry.date,
duration_minutes: entry.duration_minutes,
quality: entry.quality,
bedtime: entry.bedtime || '',
wake_time: entry.wake_time || '',
wake_count: entry.wake_count || 0,
deep_minutes: entry.deep_minutes || '',
rem_minutes: entry.rem_minutes || '',
light_minutes: entry.light_minutes || '',
awake_minutes: entry.awake_minutes || '',
note: entry.note || '',
source: entry.source || 'manual'
})
const [saving, setSaving] = useState(false)
const [plausibilityError, setPlausibilityError] = useState(null)
// Live plausibility check
useEffect(() => {
if (!editing) return
const phases = [formData.deep_minutes, formData.rem_minutes, formData.light_minutes, formData.awake_minutes]
if (phases.some(p => p !== '' && p !== null)) {
const sum = phases.reduce((a, b) => a + (parseInt(b) || 0), 0)
const diff = Math.abs(formData.duration_minutes - sum)
if (diff > 5) {
setPlausibilityError(`Phasen-Summe (${sum} min) weicht um ${diff} min ab (Toleranz: 5 min)`)
} else {
setPlausibilityError(null)
}
} else {
setPlausibilityError(null)
}
}, [editing, formData])
const handleSave = async () => {
if (plausibilityError) {
alert(plausibilityError)
return
}
setSaving(true)
try {
await onSave(formData)
} finally {
setSaving(false)
}
}
if (editing) {
// ── Edit Mode ────────────────────────────────────────────────────────────────
return (
<div className="card" style={{ padding: 12, border: '2px solid var(--accent)' }}>
<div style={{ fontWeight: 600, marginBottom: 12, fontSize: 13 }}> Bearbeiten</div>
{/* Basic Fields */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<div>
<div className="form-label">Datum</div>
<input
type="date"
className="form-input"
value={formData.date}
onChange={e => setFormData({ ...formData, date: e.target.value })}
style={{ width: '100%' }}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 10 }}>
<div>
<div className="form-label">Schlafdauer (Minuten)</div>
<input
type="number"
className="form-input"
value={formData.duration_minutes}
onChange={e => setFormData({ ...formData, duration_minutes: parseInt(e.target.value) || 0 })}
style={{ width: '100%' }}
min="1"
/>
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}>
= {formatDuration(formData.duration_minutes)}
</div>
</div>
<div>
<div className="form-label">Qualität</div>
<select
className="form-input"
value={formData.quality || ''}
onChange={e => setFormData({ ...formData, quality: parseInt(e.target.value) || null })}
style={{ width: '100%' }}
>
<option value=""></option>
<option value="1"> 1</option>
<option value="2"> 2</option>
<option value="3"> 3</option>
<option value="4"> 4</option>
<option value="5"> 5</option>
</select>
</div>
</div>
{/* Detail Fields */}
<div style={{ paddingTop: 10, borderTop: '1px solid var(--border)' }}>
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 8 }}>Details (optional)</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 10 }}>
<div>
<div className="form-label">Eingeschlafen</div>
<input
type="time"
className="form-input"
value={formData.bedtime}
onChange={e => setFormData({ ...formData, bedtime: e.target.value })}
style={{ width: '100%' }}
/>
</div>
<div>
<div className="form-label">Aufgewacht</div>
<input
type="time"
className="form-input"
value={formData.wake_time}
onChange={e => setFormData({ ...formData, wake_time: e.target.value })}
style={{ width: '100%' }}
/>
</div>
</div>
<div style={{ marginBottom: 10 }}>
<div className="form-label">Aufwachungen</div>
<input
type="number"
className="form-input"
value={formData.wake_count}
onChange={e => setFormData({ ...formData, wake_count: parseInt(e.target.value) || 0 })}
style={{ width: '100%' }}
min="0"
/>
</div>
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 8 }}>Schlafphasen (Minuten)</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
<div>
<div className="form-label">Tiefschlaf</div>
<input
type="number"
className="form-input"
value={formData.deep_minutes}
onChange={e => setFormData({ ...formData, deep_minutes: parseInt(e.target.value) || '' })}
style={{ width: '100%' }}
min="0"
/>
</div>
<div>
<div className="form-label">REM-Schlaf</div>
<input
type="number"
className="form-input"
value={formData.rem_minutes}
onChange={e => setFormData({ ...formData, rem_minutes: parseInt(e.target.value) || '' })}
style={{ width: '100%' }}
min="0"
/>
</div>
<div>
<div className="form-label">Leichtschlaf</div>
<input
type="number"
className="form-input"
value={formData.light_minutes}
onChange={e => setFormData({ ...formData, light_minutes: parseInt(e.target.value) || '' })}
style={{ width: '100%' }}
min="0"
/>
</div>
<div>
<div className="form-label">Wach im Bett</div>
<input
type="number"
className="form-input"
value={formData.awake_minutes}
onChange={e => setFormData({ ...formData, awake_minutes: parseInt(e.target.value) || '' })}
style={{ width: '100%' }}
min="0"
/>
</div>
</div>
{plausibilityError && (
<div style={{ marginTop: 8, padding: 8, background: '#D85A30', color: 'white', borderRadius: 6, fontSize: 11, display: 'flex', alignItems: 'center', gap: 6 }}>
<AlertCircle size={14} />
{plausibilityError}
</div>
)}
</div>
<div>
<div className="form-label">Notiz</div>
<textarea
className="form-input"
value={formData.note}
onChange={e => setFormData({ ...formData, note: e.target.value })}
rows={2}
style={{ width: '100%', resize: 'vertical' }}
placeholder="z.B. 'Gut durchgeschlafen', 'Stress', ..."
/>
</div>
{/* Action Buttons */}
<div style={{ display: 'flex', gap: 8 }}>
<button
onClick={handleSave}
disabled={saving || !!plausibilityError}
className="btn btn-primary"
style={{ flex: 1 }}
>
{saving ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'center' }}>
<div className="spinner" style={{ width: 14, height: 14 }} />
Speichere...
</div>
) : (
<>
<Save size={16} /> Speichern
</>
)}
</button>
<button
onClick={onCancelEdit}
disabled={saving}
className="btn btn-secondary"
style={{ flex: 1 }}
>
<X size={16} /> Abbrechen
</button>
</div>
</div>
</div>
)
}
// ── View Mode ──────────────────────────────────────────────────────────────────
return (
<div className="card" style={{ padding: 12 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
{/* Main Info */}
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<div style={{ fontSize: 13, fontWeight: 600 }}>
{new Date(entry.date).toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit' })}
</div>
{getSourceBadge(entry.source)}
</div>
<div style={{ fontSize: 12, color: 'var(--text2)' }}>
{entry.duration_formatted}
{entry.quality && ` · ${'★'.repeat(entry.quality)}${'☆'.repeat(5 - entry.quality)}`}
{entry.wake_count > 0 && ` · ${entry.wake_count}x aufgewacht`}
</div>
{entry.bedtime && entry.wake_time && (
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}>
{entry.bedtime} {entry.wake_time}
</div>
)}
{entry.note && (
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 4, fontStyle: 'italic' }}>
"{entry.note}"
</div>
)}
</div>
{/* Action Buttons */}
<div style={{ display: 'flex', gap: 6 }}>
{entry.sleep_segments && (
<button
onClick={onToggleExpand}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 6,
color: 'var(--accent)'
}}
title={expanded ? 'Details ausblenden' : 'Details anzeigen'}
>
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</button>
)}
<button
onClick={onEdit}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 6,
color: 'var(--accent)'
}}
title="Bearbeiten"
>
<Edit2 size={16} />
</button>
<button
onClick={onDelete}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 6,
color: '#D85A30'
}}
title="Löschen"
>
<Trash2 size={16} />
</button>
</div>
</div>
{/* Expanded: Sleep Segments Timeline */}
{expanded && entry.sleep_segments && (
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--border)' }}>
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 8 }}>Schlafphasen-Timeline</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{entry.sleep_segments.map((seg, i) => {
const phaseColors = {
deep: '#1D9E75',
rem: '#7B68EE',
light: '#87CEEB',
awake: '#D85A30'
}
const phaseLabels = {
deep: 'Tiefschlaf',
rem: 'REM',
light: 'Leichtschlaf',
awake: 'Wach'
}
return (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11 }}>
<div style={{
width: 10,
height: 10,
borderRadius: '50%',
background: phaseColors[seg.phase] || 'var(--border)'
}} />
<div style={{ flex: 1, color: 'var(--text2)' }}>
{new Date(seg.start).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
{' '}
{new Date(seg.end).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
</div>
<div style={{ fontWeight: 600, minWidth: 80 }}>
{phaseLabels[seg.phase]}
</div>
<div style={{ color: 'var(--text3)', minWidth: 50, textAlign: 'right' }}>
{seg.duration_min} min
</div>
</div>
)
})}
</div>
</div>
)}
</div>
)
}
// ── New Entry Form Component ──────────────────────────────────────────────────
function NewEntryForm({ onSave, onCancel, formatDuration }) {
const [formData, setFormData] = useState({
date: new Date().toISOString().split('T')[0],
duration_minutes: 450, // 7h 30min default
quality: 3,
bedtime: '',
wake_time: '',
wake_count: 0,
deep_minutes: '',
rem_minutes: '',
light_minutes: '',
awake_minutes: '',
note: '',
source: 'manual'
})
const [saving, setSaving] = useState(false)
const [plausibilityError, setPlausibilityError] = useState(null)
const [showDetail, setShowDetail] = useState(false)
// Live plausibility check
useEffect(() => {
const phases = [formData.deep_minutes, formData.rem_minutes, formData.light_minutes, formData.awake_minutes]
if (phases.some(p => p !== '' && p !== null)) {
const sum = phases.reduce((a, b) => a + (parseInt(b) || 0), 0)
const diff = Math.abs(formData.duration_minutes - sum)
if (diff > 5) {
setPlausibilityError(`Phasen-Summe (${sum} min) weicht um ${diff} min ab (Toleranz: 5 min)`)
} else {
setPlausibilityError(null)
}
} else {
setPlausibilityError(null)
}
}, [formData])
const handleSave = async () => {
if (plausibilityError) {
alert(plausibilityError)
return
}
setSaving(true)
try {
await onSave(formData)
} finally {
setSaving(false)
}
}
return (
<div className="card" style={{ padding: 16, marginBottom: 16, border: '2px solid var(--accent)' }}>
<div style={{ fontWeight: 600, marginBottom: 12 }}> Neuer Eintrag</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div>
<div className="form-label">Datum</div>
<input
type="date"
className="form-input"
value={formData.date}
onChange={e => setFormData({ ...formData, date: e.target.value })}
style={{ width: '100%' }}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 12 }}>
<div>
<div className="form-label">Schlafdauer (Minuten)</div>
<input
type="number"
className="form-input"
value={formData.duration_minutes}
onChange={e => setFormData({ ...formData, duration_minutes: parseInt(e.target.value) || 0 })}
style={{ width: '100%' }}
min="1"
/>
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 4 }}>
= {formatDuration(formData.duration_minutes)}
</div>
</div>
<div>
<div className="form-label">Qualität</div>
<select
className="form-input"
value={formData.quality || ''}
onChange={e => setFormData({ ...formData, quality: parseInt(e.target.value) || null })}
style={{ width: '100%' }}
>
<option value=""></option>
<option value="1"> 1</option>
<option value="2"> 2</option>
<option value="3"> 3</option>
<option value="4"> 4</option>
<option value="5"> 5</option>
</select>
</div>
</div>
<div>
<div className="form-label">Notiz (optional)</div>
<textarea
className="form-input"
value={formData.note}
onChange={e => setFormData({ ...formData, note: e.target.value })}
placeholder="z.B. 'Gut durchgeschlafen', 'Stress', ..."
rows={2}
style={{ width: '100%', resize: 'vertical' }}
/>
</div>
{/* Toggle Detail View */}
<button
onClick={() => setShowDetail(!showDetail)}
className="btn btn-secondary"
style={{ fontSize: 12 }}
>
{showDetail ? ' Detailansicht ausblenden' : '+ Detailansicht anzeigen'}
</button>
{/* Detail Fields */}
{showDetail && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, paddingTop: 12, borderTop: '1px solid var(--border)' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div>
<div className="form-label">Eingeschlafen (HH:MM)</div>
<input
type="time"
className="form-input"
value={formData.bedtime}
onChange={e => setFormData({ ...formData, bedtime: e.target.value })}
style={{ width: '100%' }}
/>
</div>
<div>
<div className="form-label">Aufgewacht (HH:MM)</div>
<input
type="time"
className="form-input"
value={formData.wake_time}
onChange={e => setFormData({ ...formData, wake_time: e.target.value })}
style={{ width: '100%' }}
/>
</div>
</div>
<div>
<div className="form-label">Aufwachungen</div>
<input
type="number"
className="form-input"
value={formData.wake_count || 0}
onChange={e => setFormData({ ...formData, wake_count: parseInt(e.target.value) || 0 })}
style={{ width: '100%' }}
min="0"
/>
</div>
<div style={{ fontWeight: 600, fontSize: 13, marginTop: 8 }}>Schlafphasen (Minuten)</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div>
<div className="form-label">Tiefschlaf</div>
<input
type="number"
className="form-input"
value={formData.deep_minutes}
onChange={e => setFormData({ ...formData, deep_minutes: parseInt(e.target.value) || '' })}
placeholder="—"
style={{ width: '100%' }}
min="0"
/>
</div>
<div>
<div className="form-label">REM-Schlaf</div>
<input
type="number"
className="form-input"
value={formData.rem_minutes}
onChange={e => setFormData({ ...formData, rem_minutes: parseInt(e.target.value) || '' })}
placeholder="—"
style={{ width: '100%' }}
min="0"
/>
</div>
<div>
<div className="form-label">Leichtschlaf</div>
<input
type="number"
className="form-input"
value={formData.light_minutes}
onChange={e => setFormData({ ...formData, light_minutes: parseInt(e.target.value) || '' })}
placeholder="—"
style={{ width: '100%' }}
min="0"
/>
</div>
<div>
<div className="form-label">Wach im Bett</div>
<input
type="number"
className="form-input"
value={formData.awake_minutes}
onChange={e => setFormData({ ...formData, awake_minutes: parseInt(e.target.value) || '' })}
placeholder="—"
style={{ width: '100%' }}
min="0"
/>
</div>
</div>
{plausibilityError && (
<div style={{ marginTop: 8, padding: 8, background: '#D85A30', color: 'white', borderRadius: 6, fontSize: 11, display: 'flex', alignItems: 'center', gap: 6 }}>
<AlertCircle size={14} />
{plausibilityError}
</div>
)}
</div>
)}
{/* Action Buttons */}
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
<button
onClick={handleSave}
disabled={saving || !!plausibilityError}
className="btn btn-primary"
style={{ flex: 1 }}
>
{saving ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'center' }}>
<div className="spinner" style={{ width: 14, height: 14 }} />
Speichere...
</div>
) : (
<>
<Save size={16} /> Speichern
</>
)}
</button>
<button
onClick={onCancel}
disabled={saving}
className="btn btn-secondary"
style={{ flex: 1 }}
>
<X size={16} /> Abbrechen
</button>
</div>
</div>
</div>
)
}