fix: empty string validation + auto-calculate sleep duration
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:
parent
1644b34d5c
commit
b22481d4ce
|
|
@ -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() {
|
|||
<NewEntryForm
|
||||
onSave={async (data) => {
|
||||
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
|
|||
/>
|
||||
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}>
|
||||
= {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>
|
||||
|
||||
|
|
@ -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 }) {
|
|||
/>
|
||||
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 4 }}>
|
||||
= {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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user