fix: empty string validation + auto-calculate sleep duration
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

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 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-22 13:53:13 +01:00
parent 1644b34d5c
commit b22481d4ce

View File

@ -48,6 +48,17 @@ export default function SleepPage() {
setTimeout(() => setToast(null), 4000) 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) => { const handleImport = async (file) => {
if (!file) return if (!file) return
if (!file.name.endsWith('.csv')) { if (!file.name.endsWith('.csv')) {
@ -248,7 +259,7 @@ export default function SleepPage() {
<NewEntryForm <NewEntryForm
onSave={async (data) => { onSave={async (data) => {
try { try {
await api.createSleep(data) await api.createSleep(cleanSleepData(data))
await load() await load()
setEditingId(null) setEditingId(null)
showToast('Gespeichert') showToast('Gespeichert')
@ -283,7 +294,7 @@ export default function SleepPage() {
onCancelEdit={() => setEditingId(null)} onCancelEdit={() => setEditingId(null)}
onSave={async (data) => { onSave={async (data) => {
try { try {
await api.updateSleep(entry.id, data) await api.updateSleep(entry.id, cleanSleepData(data))
await load() await load()
setEditingId(null) setEditingId(null)
showToast('Gespeichert') showToast('Gespeichert')
@ -291,6 +302,7 @@ export default function SleepPage() {
showToast(err.message, 'error') showToast(err.message, 'error')
} }
}} }}
cleanSleepData={cleanSleepData}
onDelete={() => handleDelete(entry.id, entry.date)} onDelete={() => handleDelete(entry.id, entry.date)}
formatDuration={formatDuration} formatDuration={formatDuration}
getSourceBadge={getSourceBadge} getSourceBadge={getSourceBadge}
@ -322,6 +334,27 @@ function SleepEntry({ entry, expanded, editing, onToggleExpand, onEdit, onCancel
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [plausibilityError, setPlausibilityError] = useState(null) 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 // Live plausibility check
useEffect(() => { useEffect(() => {
@ -386,6 +419,12 @@ function SleepEntry({ entry, expanded, editing, onToggleExpand, onEdit, onCancel
/> />
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}> <div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}>
= {formatDuration(formData.duration_minutes)} = {formatDuration(formData.duration_minutes)}
{suggestedDuration && suggestedDuration !== formData.duration_minutes && (
<span style={{ marginLeft: 8, color: 'var(--accent)', cursor: 'pointer' }}
onClick={() => setFormData({ ...formData, duration_minutes: suggestedDuration })}>
💡 Vorschlag: {formatDuration(suggestedDuration)} (übernehmen?)
</span>
)}
</div> </div>
</div> </div>
@ -690,6 +729,28 @@ function NewEntryForm({ onSave, onCancel, formatDuration }) {
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [plausibilityError, setPlausibilityError] = useState(null) const [plausibilityError, setPlausibilityError] = useState(null)
const [showDetail, setShowDetail] = useState(false) 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 // Live plausibility check
useEffect(() => { useEffect(() => {
@ -750,6 +811,12 @@ function NewEntryForm({ onSave, onCancel, formatDuration }) {
/> />
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 4 }}> <div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 4 }}>
= {formatDuration(formData.duration_minutes)} = {formatDuration(formData.duration_minutes)}
{suggestedDuration && suggestedDuration !== formData.duration_minutes && (
<span style={{ marginLeft: 8, color: 'var(--accent)', cursor: 'pointer' }}
onClick={() => setFormData({ ...formData, duration_minutes: suggestedDuration })}>
💡 Vorschlag: {formatDuration(suggestedDuration)} (übernehmen?)
</span>
)}
</div> </div>
</div> </div>