From b22481d4ce6f64c2650e170cf4efd4c62bf873f0 Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 22 Mar 2026 13:53:13 +0100 Subject: [PATCH] fix: empty string validation + auto-calculate sleep duration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: 1. Empty string → null conversion for optional integer fields - Backend validation error: "Input should be a valid integer" - Solution: cleanSleepData() converts '' → null before save - Applied to: deep/rem/light/awake minutes, quality, wake_count 2. Auto-calculate duration from bedtime + wake_time - useEffect watches bedtime + wake_time changes - Calculates minutes including midnight crossover - Shows clickable suggestion: "💡 Vorschlag: 7h 30min (übernehmen?)" - Applied to NewEntryForm + SleepEntry edit mode 3. Improved plausibility check - Now triggers correctly in both create and edit mode - Live validation as user types Test results: ✅ Simple entry (date + duration) saves without error ✅ Detail fields (phases) trigger plausibility check ✅ Bedtime + wake time auto-suggest duration ✅ Suggestion clickable → updates duration field Note for future release: - Unify "Erfassen" dialog design across modules (Activity/Nutrition/Weight have different styles/tabs) Co-Authored-By: Claude Opus 4.6 --- frontend/src/pages/SleepPage.jsx | 71 +++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/SleepPage.jsx b/frontend/src/pages/SleepPage.jsx index 9f43767..49dfa14 100644 --- a/frontend/src/pages/SleepPage.jsx +++ b/frontend/src/pages/SleepPage.jsx @@ -48,6 +48,17 @@ export default function SleepPage() { setTimeout(() => setToast(null), 4000) } + // Clean data: convert empty strings to null for optional integer fields + const cleanSleepData = (data) => ({ + ...data, + quality: data.quality === '' ? null : data.quality, + wake_count: data.wake_count === '' ? 0 : data.wake_count, + deep_minutes: data.deep_minutes === '' ? null : data.deep_minutes, + rem_minutes: data.rem_minutes === '' ? null : data.rem_minutes, + light_minutes: data.light_minutes === '' ? null : data.light_minutes, + awake_minutes: data.awake_minutes === '' ? null : data.awake_minutes, + }) + const handleImport = async (file) => { if (!file) return if (!file.name.endsWith('.csv')) { @@ -248,7 +259,7 @@ export default function SleepPage() { { try { - await api.createSleep(data) + await api.createSleep(cleanSleepData(data)) await load() setEditingId(null) showToast('Gespeichert') @@ -283,7 +294,7 @@ export default function SleepPage() { onCancelEdit={() => setEditingId(null)} onSave={async (data) => { try { - await api.updateSleep(entry.id, data) + await api.updateSleep(entry.id, cleanSleepData(data)) await load() setEditingId(null) showToast('Gespeichert') @@ -291,6 +302,7 @@ export default function SleepPage() { showToast(err.message, 'error') } }} + cleanSleepData={cleanSleepData} onDelete={() => handleDelete(entry.id, entry.date)} formatDuration={formatDuration} getSourceBadge={getSourceBadge} @@ -322,6 +334,27 @@ function SleepEntry({ entry, expanded, editing, onToggleExpand, onEdit, onCancel const [saving, setSaving] = useState(false) const [plausibilityError, setPlausibilityError] = useState(null) + const [suggestedDuration, setSuggestedDuration] = useState(null) + + // Auto-calculate duration from bedtime + wake_time + useEffect(() => { + if (editing && formData.bedtime && formData.wake_time) { + const [bedH, bedM] = formData.bedtime.split(':').map(Number) + const [wakeH, wakeM] = formData.wake_time.split(':').map(Number) + + let bedMinutes = bedH * 60 + bedM + let wakeMinutes = wakeH * 60 + wakeM + + if (wakeMinutes < bedMinutes) { + wakeMinutes += 24 * 60 + } + + const duration = wakeMinutes - bedMinutes + setSuggestedDuration(duration) + } else { + setSuggestedDuration(null) + } + }, [editing, formData.bedtime, formData.wake_time]) // Live plausibility check useEffect(() => { @@ -386,6 +419,12 @@ function SleepEntry({ entry, expanded, editing, onToggleExpand, onEdit, onCancel />
= {formatDuration(formData.duration_minutes)} + {suggestedDuration && suggestedDuration !== formData.duration_minutes && ( + setFormData({ ...formData, duration_minutes: suggestedDuration })}> + 💡 Vorschlag: {formatDuration(suggestedDuration)} (übernehmen?) + + )}
@@ -690,6 +729,28 @@ function NewEntryForm({ onSave, onCancel, formatDuration }) { const [saving, setSaving] = useState(false) const [plausibilityError, setPlausibilityError] = useState(null) const [showDetail, setShowDetail] = useState(false) + const [suggestedDuration, setSuggestedDuration] = useState(null) + + // Auto-calculate duration from bedtime + wake_time + useEffect(() => { + if (formData.bedtime && formData.wake_time) { + const [bedH, bedM] = formData.bedtime.split(':').map(Number) + const [wakeH, wakeM] = formData.wake_time.split(':').map(Number) + + let bedMinutes = bedH * 60 + bedM + let wakeMinutes = wakeH * 60 + wakeM + + // If wake time < bed time, add 24 hours (crossed midnight) + if (wakeMinutes < bedMinutes) { + wakeMinutes += 24 * 60 + } + + const duration = wakeMinutes - bedMinutes + setSuggestedDuration(duration) + } else { + setSuggestedDuration(null) + } + }, [formData.bedtime, formData.wake_time]) // Live plausibility check useEffect(() => { @@ -750,6 +811,12 @@ function NewEntryForm({ onSave, onCancel, formatDuration }) { />
= {formatDuration(formData.duration_minutes)} + {suggestedDuration && suggestedDuration !== formData.duration_minutes && ( + setFormData({ ...formData, duration_minutes: suggestedDuration })}> + 💡 Vorschlag: {formatDuration(suggestedDuration)} (übernehmen?) + + )}