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)
|
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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user