Improve day swipe and sleep handling

This commit is contained in:
2026-05-21 13:00:10 +02:00
parent 2047cae61c
commit a087eb508b
5 changed files with 206 additions and 53 deletions
+59 -2
View File
@@ -462,6 +462,51 @@ body.page-dashboard .content {
padding-top: 6rem; padding-top: 6rem;
} }
.dashboard-day-slider {
--day-prev-hint: 0;
--day-next-hint: 0;
position: relative;
overflow: hidden;
border-radius: 1.8rem;
margin-bottom: 2rem;
}
.day-slide-hint {
position: absolute;
top: 50%;
z-index: 0;
display: inline-flex;
align-items: center;
min-height: 3rem;
max-width: min(12rem, 42vw);
padding: 0.8rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.9);
font-size: 0.86rem;
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.01em;
opacity: 0;
pointer-events: none;
transform: translateY(-50%) scale(0.94);
transition: opacity 120ms ease, transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.day-slide-hint--prev {
left: 0.75rem;
opacity: var(--day-prev-hint);
transform: translateY(-50%) translateX(calc((1 - var(--day-prev-hint)) * -0.7rem)) scale(calc(0.94 + (var(--day-prev-hint) * 0.06)));
}
.day-slide-hint--next {
right: 0.75rem;
opacity: var(--day-next-hint);
text-align: right;
transform: translateY(-50%) translateX(calc((1 - var(--day-next-hint)) * 0.7rem)) scale(calc(0.94 + (var(--day-next-hint) * 0.06)));
}
.dashboard-compare-strip { .dashboard-compare-strip {
position: relative; position: relative;
width: min(100%, 20rem); width: min(100%, 20rem);
@@ -475,8 +520,11 @@ body.page-dashboard .content {
} }
.dashboard-day__hero[data-day-slider] { .dashboard-day__hero[data-day-slider] {
transform: translateX(var(--day-slider-offset, 0)); position: relative;
transition: transform 180ms ease; z-index: 1;
margin-bottom: 0;
transform: translate3d(var(--day-slider-offset, 0), 0, 0) scale(var(--day-slider-scale, 1));
transition: transform 260ms cubic-bezier(0.2, 0.8, 0.2, 1);
will-change: transform; will-change: transform;
} }
@@ -1028,6 +1076,15 @@ body.page-dashboard .content {
background: linear-gradient(135deg, rgba(54, 147, 173, 0.92), rgba(38, 106, 135, 0.92)); background: linear-gradient(135deg, rgba(54, 147, 173, 0.92), rgba(38, 106, 135, 0.92));
} }
.sleep-phase-bar__segment--total {
border-radius: 999px 0 0 999px;
background: linear-gradient(135deg, rgba(90, 188, 242, 0.9), rgba(44, 126, 190, 0.9));
}
.sleep-phase-bar__segment--total.is-after-phase {
border-radius: 0;
}
.sleep-phase-bar__segment--rest { .sleep-phase-bar__segment--rest {
min-width: 0; min-width: 0;
padding: 0; padding: 0;
+69 -12
View File
@@ -1223,7 +1223,7 @@
valueInput.placeholder = config.placeholder; valueInput.placeholder = config.placeholder;
valueInput.required = !!config.showValue; valueInput.required = !!config.showValue;
valueInput.value = config.showValue ? valueInput.value : ""; valueInput.value = config.showValue ? valueInput.value : "";
valueInput.step = type === "sleep" ? "0.25" : "1"; valueInput.step = type === "sleep" ? "0.01" : "1";
} }
if (unitInput) { if (unitInput) {
unitInput.value = config.unit; unitInput.value = config.unit;
@@ -1454,11 +1454,61 @@
let dragging = false; let dragging = false;
let didSwipe = false; let didSwipe = false;
let activePointerId = null; let activePointerId = null;
let currentOffset = 0;
let targetOffset = 0;
let animationFrame = null;
const prefetchedDays = new Set();
const setSlideProgress = offset => {
const progress = Math.min(1, Math.abs(offset) / 120);
daySlider.style.setProperty("--day-slider-offset", `${offset.toFixed(1)}px`);
daySlider.style.setProperty("--day-slider-scale", (1 - (progress * 0.025)).toFixed(3));
swipeContainer.style.setProperty("--day-prev-hint", offset > 0 ? progress.toFixed(3) : "0");
swipeContainer.style.setProperty("--day-next-hint", offset < 0 ? progress.toFixed(3) : "0");
};
const animateSlide = () => {
currentOffset += (targetOffset - currentOffset) * 0.34;
if (Math.abs(targetOffset - currentOffset) < 0.4) {
currentOffset = targetOffset;
}
setSlideProgress(currentOffset);
if (currentOffset !== targetOffset) {
animationFrame = window.requestAnimationFrame(animateSlide);
} else {
animationFrame = null;
}
};
const setTargetOffset = offset => {
targetOffset = offset;
if (animationFrame === null) {
animationFrame = window.requestAnimationFrame(animateSlide);
}
};
const preloadDay = date => {
if (!date || prefetchedDays.has(date)) {
return;
}
prefetchedDays.add(date);
window.fetch(dashboardDayPath(date), {
credentials: "same-origin",
cache: "force-cache",
priority: "low"
}).catch(() => {});
};
preloadDay(swipeContainer.dataset.prevDate);
preloadDay(swipeContainer.dataset.nextDate);
const resetStrip = () => { const resetStrip = () => {
dayStrip.classList.remove("is-dragging"); dayStrip.classList.remove("is-dragging");
daySlider.classList.remove("is-dragging"); daySlider.classList.remove("is-dragging");
daySlider.style.setProperty("--day-slider-offset", "0px"); setTargetOffset(0);
}; };
const handleSwipe = (deltaX, deltaY) => { const handleSwipe = (deltaX, deltaY) => {
@@ -1469,18 +1519,18 @@
if (deltaX < 0 && swipeContainer.dataset.nextDate) { if (deltaX < 0 && swipeContainer.dataset.nextDate) {
didSwipe = true; didSwipe = true;
daySlider.style.setProperty("--day-slider-offset", "-120%"); setSlideProgress(-window.innerWidth);
window.location.href = dashboardDayPath(swipeContainer.dataset.nextDate); window.location.href = dashboardDayPath(swipeContainer.dataset.nextDate);
} else if (deltaX > 0 && swipeContainer.dataset.prevDate) { } else if (deltaX > 0 && swipeContainer.dataset.prevDate) {
didSwipe = true; didSwipe = true;
daySlider.style.setProperty("--day-slider-offset", "120%"); setSlideProgress(window.innerWidth);
window.location.href = dashboardDayPath(swipeContainer.dataset.prevDate); window.location.href = dashboardDayPath(swipeContainer.dataset.prevDate);
} else { } else {
resetStrip(); resetStrip();
} }
}; };
dayStrip.addEventListener("pointerdown", event => { daySlider.addEventListener("pointerdown", event => {
if (event.target.closest("input, textarea, select, label, [data-stepper], .dashboard-overlay")) { if (event.target.closest("input, textarea, select, label, [data-stepper], .dashboard-overlay")) {
dragging = false; dragging = false;
return; return;
@@ -1493,10 +1543,10 @@
pointerStartY = event.clientY; pointerStartY = event.clientY;
dayStrip.classList.add("is-dragging"); dayStrip.classList.add("is-dragging");
daySlider.classList.add("is-dragging"); daySlider.classList.add("is-dragging");
dayStrip.setPointerCapture?.(event.pointerId); daySlider.setPointerCapture?.(event.pointerId);
}); });
dayStrip.addEventListener("pointermove", event => { daySlider.addEventListener("pointermove", event => {
if (!dragging || (activePointerId !== null && event.pointerId !== activePointerId)) { if (!dragging || (activePointerId !== null && event.pointerId !== activePointerId)) {
return; return;
} }
@@ -1510,10 +1560,17 @@
return; return;
} }
daySlider.style.setProperty("--day-slider-offset", `${Math.max(-120, Math.min(120, deltaX))}px`); const dampedOffset = Math.sign(deltaX) * Math.min(148, Math.pow(Math.abs(deltaX), 0.88) * 1.6);
setTargetOffset(dampedOffset);
if (deltaX < -32) {
preloadDay(swipeContainer.dataset.nextDate);
} else if (deltaX > 32) {
preloadDay(swipeContainer.dataset.prevDate);
}
}); });
dayStrip.addEventListener("pointerup", event => { daySlider.addEventListener("pointerup", event => {
if (!dragging) { if (!dragging) {
return; return;
} }
@@ -1523,13 +1580,13 @@
handleSwipe(event.clientX - pointerStartX, event.clientY - pointerStartY); handleSwipe(event.clientX - pointerStartX, event.clientY - pointerStartY);
}); });
dayStrip.addEventListener("pointercancel", () => { daySlider.addEventListener("pointercancel", () => {
dragging = false; dragging = false;
activePointerId = null; activePointerId = null;
resetStrip(); resetStrip();
}); });
dayStrip.addEventListener("lostpointercapture", () => { daySlider.addEventListener("lostpointercapture", () => {
if (!dragging) { if (!dragging) {
return; return;
} }
@@ -1539,7 +1596,7 @@
resetStrip(); resetStrip();
}); });
dayStrip.addEventListener("click", event => { daySlider.addEventListener("click", event => {
if (!didSwipe) { if (!didSwipe) {
return; return;
} }
+11 -4
View File
@@ -739,7 +739,9 @@ final class App
if ($name === 'sleep_analysis') { if ($name === 'sleep_analysis') {
foreach ($data as $point) { foreach ($data as $point) {
$date = $this->healthPointDate($point['date'] ?? ($point['sleepEnd'] ?? ($point['endDate'] ?? null))); $start = $this->healthDateTime($point['sleepStart'] ?? ($point['inBedStart'] ?? ($point['startDate'] ?? null)));
$end = $this->healthDateTime($point['sleepEnd'] ?? ($point['inBedEnd'] ?? ($point['endDate'] ?? null)));
$date = ($end ?? $this->healthDateTime($point['date'] ?? null))?->format('Y-m-d');
if ($date === null) { if ($date === null) {
continue; continue;
} }
@@ -770,8 +772,6 @@ final class App
} }
} }
$start = $this->healthDateTime($point['sleepStart'] ?? ($point['inBedStart'] ?? ($point['startDate'] ?? null)));
$end = $this->healthDateTime($point['sleepEnd'] ?? ($point['inBedEnd'] ?? ($point['endDate'] ?? null)));
if ($start !== null && ($bucket['start'] === null || $start < $bucket['start'])) { if ($start !== null && ($bucket['start'] === null || $start < $bucket['start'])) {
$bucket['start'] = $start; $bucket['start'] = $start;
} }
@@ -1816,7 +1816,7 @@ final class App
$comment = trim((string) ($input['event_comment'] ?? '')); $comment = trim((string) ($input['event_comment'] ?? ''));
$value = max(0, min(50000, (float) ($input['event_value'] ?? 0))); $value = max(0, min(50000, $this->localizedFloat($input['event_value'] ?? 0)));
if (in_array($type, ['walk', 'sport', 'sleep'], true) && $value <= 0) { if (in_array($type, ['walk', 'sport', 'sleep'], true) && $value <= 0) {
throw new RuntimeException('Für diesen Moment braucht es einen Wert oder eine Dauer.'); throw new RuntimeException('Für diesen Moment braucht es einen Wert oder eine Dauer.');
} }
@@ -1847,6 +1847,13 @@ final class App
]; ];
} }
private function localizedFloat(mixed $value): float
{
$normalized = str_replace(',', '.', trim((string) $value));
return is_numeric($normalized) ? (float) $normalized : 0.0;
}
private function dashboardMediaDirectory(string $username): string private function dashboardMediaDirectory(string $username): string
{ {
return storage_path('users/' . normalize_username($username) . '/media'); return storage_path('users/' . normalize_username($username) . '/media');
+17
View File
@@ -115,6 +115,23 @@ function format_points(float $value): string
return number_format($rounded, 1, ',', '.'); return number_format($rounded, 1, ',', '.');
} }
function format_duration_hours(float $hours): string
{
$minutes = max(0, (int) round($hours * 60));
$wholeHours = intdiv($minutes, 60);
$remainingMinutes = $minutes % 60;
if ($wholeHours <= 0) {
return $remainingMinutes . ' min';
}
if ($remainingMinutes === 0) {
return $wholeHours . ' h';
}
return $wholeHours . ' h ' . $remainingMinutes . ' min';
}
function normalize_username(string $username): string function normalize_username(string $username): string
{ {
return strtolower(trim($username)); return strtolower(trim($username));
+50 -35
View File
@@ -55,20 +55,24 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
<?php if ($dashboardView === 'day'): ?> <?php if ($dashboardView === 'day'): ?>
<div class="dashboard-day" data-day-swipe data-prev-date="<?= e($dashboardPrevDate) ?>" data-next-date="<?= e($dashboardNextDate) ?>"> <div class="dashboard-day" data-day-swipe data-prev-date="<?= e($dashboardPrevDate) ?>" data-next-date="<?= e($dashboardNextDate) ?>">
<div class="dashboard-day__hero" data-day-slider> <div class="dashboard-day-slider" data-day-slider-shell>
<p class="dashboard-day__eyebrow"><?= e((string) $dayWeekday) ?></p> <span class="day-slide-hint day-slide-hint--prev" data-day-slide-prev-hint>Vorherigen Tag laden</span>
<h1><?= e(format_display_date((string) $dayEntry['date'], false)) ?></h1> <span class="day-slide-hint day-slide-hint--next" data-day-slide-next-hint>Nächster Tag laden</span>
<div class="dashboard-day__hero" data-day-slider>
<p class="dashboard-day__eyebrow"><?= e((string) $dayWeekday) ?></p>
<h1><?= e(format_display_date((string) $dayEntry['date'], false)) ?></h1>
<nav class="dashboard-compare-strip" aria-label="Tagesvergleich" data-day-strip> <nav class="dashboard-compare-strip" aria-label="Tagesvergleich" data-day-strip>
<span class="score-scale score-scale--day" aria-hidden="true"><span>+2</span><span>+1</span><span>0</span><span>-1</span><span>-2</span></span> <span class="score-scale score-scale--day" aria-hidden="true"><span>+2</span><span>+1</span><span>0</span><span>-1</span><span>-2</span></span>
<?php foreach ($dashboardCompareDays as $compareDay): ?> <?php foreach ($dashboardCompareDays as $compareDay): ?>
<a class="compare-day offset-<?= e((string) ($compareDay['offset'] ?? 0)) ?><?= !empty($compareDay['is_current']) ? ' is-current' : '' ?><?= empty($compareDay['has_content']) ? ' is-empty' : '' ?>" href="/?view=day&amp;date=<?= e(rawurlencode((string) $compareDay['date'])) ?>"> <a class="compare-day offset-<?= e((string) ($compareDay['offset'] ?? 0)) ?><?= !empty($compareDay['is_current']) ? ' is-current' : '' ?><?= empty($compareDay['has_content']) ? ' is-empty' : '' ?>" href="/?view=day&amp;date=<?= e(rawurlencode((string) $compareDay['date'])) ?>">
<span class="compare-day__line<?= empty($compareDay['has_content']) ? ' is-empty' : '' ?><?= !empty($compareDay['is_current']) ? ' is-primary' : '' ?> score-<?= e((string) ($compareDay['score_level'] ?? 'empty')) ?> compare-tone-<?= e((string) ($compareDay['line_tone'] ?? 'empty')) ?>"> <span class="compare-day__line<?= empty($compareDay['has_content']) ? ' is-empty' : '' ?><?= !empty($compareDay['is_current']) ? ' is-primary' : '' ?> score-<?= e((string) ($compareDay['score_level'] ?? 'empty')) ?> compare-tone-<?= e((string) ($compareDay['line_tone'] ?? 'empty')) ?>">
<span class="compare-day__marker"></span> <span class="compare-day__marker"></span>
</span> </span>
</a> </a>
<?php endforeach; ?> <?php endforeach; ?>
</nav> </nav>
</div>
</div> </div>
<button class="day-summary-card glass-panel<?= $dashboardHasContent ? ' is-filled' : '' ?>" type="button" data-summary-overlay-open> <button class="day-summary-card glass-panel<?= $dashboardHasContent ? ' is-filled' : '' ?>" type="button" data-summary-overlay-open>
@@ -124,24 +128,26 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
'walk', 'sleep' => trim($eventValueText), 'walk', 'sleep' => trim($eventValueText),
default => trim($eventValueText . ($sportType !== null ? ' · ' . (string) ($sportType['label'] ?? '') : '')), default => trim($eventValueText . ($sportType !== null ? ' · ' . (string) ($sportType['label'] ?? '') : '')),
}; ?> }; ?>
<?php $showEventComment = in_array($eventType, ['sport', 'walk', 'sleep'], true) && $eventComment !== '' && !$isImportedHealth; ?> <?php $showEventComment = in_array($eventType, ['sport', 'walk', 'sleep'], true) && $eventComment !== ''; ?>
<?php <?php
$sleepPhases = ['deep' => (float) ($item['sleep_deep'] ?? 0), 'rem' => (float) ($item['sleep_rem'] ?? 0), 'core' => (float) ($item['sleep_core'] ?? 0)]; $sleepPhases = ['deep' => (float) ($item['sleep_deep'] ?? 0), 'rem' => (float) ($item['sleep_rem'] ?? 0), 'core' => (float) ($item['sleep_core'] ?? 0)];
$sleepPhaseSource = trim($eventComment . ' ' . (string) ($item['duration_label'] ?? '') . ' ' . (string) ($item['distance_label'] ?? '') . ' ' . (string) ($item['energy_label'] ?? '') . ' ' . (string) ($item['heart_rate_label'] ?? '')); $sleepPhaseSource = trim($eventComment . ' ' . (string) ($item['duration_label'] ?? '') . ' ' . (string) ($item['distance_label'] ?? '') . ' ' . (string) ($item['energy_label'] ?? '') . ' ' . (string) ($item['heart_rate_label'] ?? ''));
if ($eventType === 'sleep' && array_sum($sleepPhases) <= 0 && $sleepPhaseSource !== '') { if ($eventType === 'sleep' && array_sum($sleepPhases) <= 0 && $sleepPhaseSource !== '') {
if (preg_match('/(?:Tief|Tiefschlaf)\s*:?[\s]+([0-9]+(?:[,.][0-9]+)?)/u', $sleepPhaseSource, $match) === 1) { if (preg_match('/(?:Tief|Tiefschlaf)\s*:?\s*([0-9]+(?:[,.][0-9]+)?)/u', $sleepPhaseSource, $match) === 1) {
$sleepPhases['deep'] = (float) str_replace(',', '.', $match[1]); $sleepPhases['deep'] = (float) str_replace(',', '.', $match[1]);
} }
if (preg_match('/REM(?:-Schlaf)?\s*:?[\s]+([0-9]+(?:[,.][0-9]+)?)/u', $sleepPhaseSource, $match) === 1) { if (preg_match('/REM(?:-Schlaf)?\s*:?\s*([0-9]+(?:[,.][0-9]+)?)/u', $sleepPhaseSource, $match) === 1) {
$sleepPhases['rem'] = (float) str_replace(',', '.', $match[1]); $sleepPhases['rem'] = (float) str_replace(',', '.', $match[1]);
} }
if (preg_match('/(?:Kern|Kernschlaf)\s*:?[\s]+([0-9]+(?:[,.][0-9]+)?)/u', $sleepPhaseSource, $match) === 1) { if (preg_match('/(?:Kern|Kernschlaf)\s*:?\s*([0-9]+(?:[,.][0-9]+)?)/u', $sleepPhaseSource, $match) === 1) {
$sleepPhases['core'] = (float) str_replace(',', '.', $match[1]); $sleepPhases['core'] = (float) str_replace(',', '.', $match[1]);
} }
} }
$sleepPhaseTotal = max(0.0, array_sum($sleepPhases)); $sleepPhaseTotal = max(0.0, array_sum($sleepPhases));
$sleepBarTotal = $eventType === 'sleep' ? max((float) ($item['value'] ?? 0), $sleepPhaseTotal, $optimalSleepHours / 0.75) : 0.0; $sleepActualTotal = $eventType === 'sleep' ? max((float) ($item['value'] ?? 0), $sleepPhaseTotal) : 0.0;
$sleepPhaseRemainder = max(0.0, $sleepBarTotal - $sleepPhaseTotal); $sleepBarTotal = $eventType === 'sleep' ? max($sleepActualTotal, $optimalSleepHours / 0.75) : 0.0;
$sleepUnclassified = max(0.0, $sleepActualTotal - $sleepPhaseTotal);
$sleepPhaseRemainder = max(0.0, $sleepBarTotal - $sleepActualTotal);
$sleepOptimalPercent = $sleepBarTotal > 0 ? max(0, min(100, ($optimalSleepHours / $sleepBarTotal) * 100)) : 0; $sleepOptimalPercent = $sleepBarTotal > 0 ? max(0, min(100, ($optimalSleepHours / $sleepBarTotal) * 100)) : 0;
$sleepPhaseLeft = 0.0; $sleepPhaseLeft = 0.0;
?> ?>
@@ -212,24 +218,33 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
<p class="timeline-card__value"><?= e((string) $item['comment']) ?></p> <p class="timeline-card__value"><?= e((string) $item['comment']) ?></p>
<?php endif; ?> <?php endif; ?>
<?php if ($eventType === 'sleep' && $sleepPhaseTotal > 0): ?> <?php if ($eventType === 'sleep' && $sleepActualTotal > 0): ?>
<div class="sleep-phase-bar" aria-label="Schlafphasen" style="--sleep-optimal-left: <?= e((string) $sleepOptimalPercent) ?>%"> <div class="sleep-phase-bar" aria-label="Schlafphasen" style="--sleep-optimal-left: <?= e((string) $sleepOptimalPercent) ?>%">
<?php foreach (['deep' => ['Tief', 'deep'], 'rem' => ['REM', 'rem'], 'core' => ['Kern', 'core']] as $phase => [$label, $class]): ?> <?php if ($sleepPhaseTotal > 0): ?>
<?php $phaseHours = max(0.0, (float) ($sleepPhases[$phase] ?? 0)); ?> <?php foreach (['deep' => ['Tief', 'deep'], 'rem' => ['REM', 'rem'], 'core' => ['Kern', 'core']] as $phase => [$label, $class]): ?>
<?php if ($phaseHours <= 0) { continue; } ?> <?php $phaseHours = max(0.0, (float) ($sleepPhases[$phase] ?? 0)); ?>
<?php $phasePercent = $sleepBarTotal > 0 ? max(0.5, min(100, ($phaseHours / $sleepBarTotal) * 100)) : 0; ?> <?php if ($phaseHours <= 0) { continue; } ?>
<?php $phaseLeftPercent = $sleepBarTotal > 0 ? max(0, min(100, ($sleepPhaseLeft / $sleepBarTotal) * 100)) : 0; ?> <?php $phasePercent = $sleepBarTotal > 0 ? max(0.5, min(100, ($phaseHours / $sleepBarTotal) * 100)) : 0; ?>
<span class="sleep-phase-bar__segment sleep-phase-bar__segment--<?= e($class) ?><?= $phasePercent < 13 ? ' is-compact' : '' ?>" style="--sleep-segment-left: <?= e((string) $phaseLeftPercent) ?>%; --sleep-segment-width: <?= e((string) $phasePercent) ?>%" title="<?= e($label) ?>: <?= e(format_points($phaseHours)) ?> h" data-tooltip="<?= e($label) ?>: <?= e(format_points($phaseHours)) ?> h"> <?php $phaseLeftPercent = $sleepBarTotal > 0 ? max(0, min(100, ($sleepPhaseLeft / $sleepBarTotal) * 100)) : 0; ?>
<strong><?= e($label) ?></strong> <?= e(format_points($phaseHours)) ?> h <span class="sleep-phase-bar__segment sleep-phase-bar__segment--<?= e($class) ?><?= $phasePercent < 13 ? ' is-compact' : '' ?>" style="--sleep-segment-left: <?= e((string) $phaseLeftPercent) ?>%; --sleep-segment-width: <?= e((string) $phasePercent) ?>%" title="<?= e($label) ?>: <?= e(format_duration_hours($phaseHours)) ?>" data-tooltip="<?= e($label) ?>: <?= e(format_duration_hours($phaseHours)) ?>">
<strong><?= e($label) ?></strong> <?= e(format_duration_hours($phaseHours)) ?>
</span>
<?php $sleepPhaseLeft += $phaseHours; ?>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($sleepUnclassified > 0): ?>
<?php $unclassifiedPercent = $sleepBarTotal > 0 ? max(0.5, min(100, ($sleepUnclassified / $sleepBarTotal) * 100)) : 0; ?>
<?php $unclassifiedLeftPercent = $sleepBarTotal > 0 ? max(0, min(100, ($sleepPhaseTotal / $sleepBarTotal) * 100)) : 0; ?>
<span class="sleep-phase-bar__segment sleep-phase-bar__segment--total<?= $sleepPhaseTotal > 0 ? ' is-after-phase' : '' ?><?= $unclassifiedPercent < 13 ? ' is-compact' : '' ?>" style="--sleep-segment-left: <?= e((string) $unclassifiedLeftPercent) ?>%; --sleep-segment-width: <?= e((string) $unclassifiedPercent) ?>%" title="Schlaf: <?= e(format_duration_hours($sleepUnclassified)) ?>" data-tooltip="Schlaf: <?= e(format_duration_hours($sleepUnclassified)) ?>">
<strong>Schlaf</strong> <?= e(format_duration_hours($sleepUnclassified)) ?>
</span> </span>
<?php $sleepPhaseLeft += $phaseHours; ?> <?php endif; ?>
<?php endforeach; ?>
<?php if ($sleepPhaseRemainder > 0): ?> <?php if ($sleepPhaseRemainder > 0): ?>
<?php $remainderPercent = $sleepBarTotal > 0 ? max(0.5, min(100, ($sleepPhaseRemainder / $sleepBarTotal) * 100)) : 0; ?> <?php $remainderPercent = $sleepBarTotal > 0 ? max(0.5, min(100, ($sleepPhaseRemainder / $sleepBarTotal) * 100)) : 0; ?>
<?php $remainderLeftPercent = $sleepBarTotal > 0 ? max(0, min(100, ($sleepPhaseTotal / $sleepBarTotal) * 100)) : 0; ?> <?php $remainderLeftPercent = $sleepBarTotal > 0 ? max(0, min(100, ($sleepActualTotal / $sleepBarTotal) * 100)) : 0; ?>
<span class="sleep-phase-bar__segment sleep-phase-bar__segment--rest" style="--sleep-segment-left: <?= e((string) $remainderLeftPercent) ?>%; --sleep-segment-width: <?= e((string) $remainderPercent) ?>%" title="Bis Ziel-/Skalenende: <?= e(format_points($sleepPhaseRemainder)) ?> h" data-tooltip="Bis Ziel-/Skalenende: <?= e(format_points($sleepPhaseRemainder)) ?> h"></span> <span class="sleep-phase-bar__segment sleep-phase-bar__segment--rest" style="--sleep-segment-left: <?= e((string) $remainderLeftPercent) ?>%; --sleep-segment-width: <?= e((string) $remainderPercent) ?>%" title="Bis Ziel-/Skalenende: <?= e(format_duration_hours($sleepPhaseRemainder)) ?>" data-tooltip="Bis Ziel-/Skalenende: <?= e(format_duration_hours($sleepPhaseRemainder)) ?>"></span>
<?php endif; ?> <?php endif; ?>
<span class="sleep-phase-bar__target"><span><?= e(format_points($optimalSleepHours)) ?> h</span></span> <span class="sleep-phase-bar__target"><span><?= e(format_duration_hours($optimalSleepHours)) ?></span></span>
</div> </div>
<?php endif; ?> <?php endif; ?>
@@ -421,7 +436,7 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
</label> </label>
<label data-moment-value-field> <label data-moment-value-field>
<span data-moment-value-label>Wert</span> <span data-moment-value-label>Wert</span>
<input type="number" name="event_value" min="0" max="50000" step="0.25" placeholder="optional" data-moment-value-input> <input type="number" name="event_value" min="0" max="50000" step="0.01" placeholder="optional" data-moment-value-input>
</label> </label>
</div> </div>
@@ -573,7 +588,7 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
$eventTone = signal_value_class($eventScore); $eventTone = signal_value_class($eventScore);
$sportType = $eventType === 'sport' ? find_sport_type($settings, (string) ($event['sport_type_id'] ?? '')) : null; $sportType = $eventType === 'sport' ? find_sport_type($settings, (string) ($event['sport_type_id'] ?? '')) : null;
$eventValue = (float) ($event['value'] ?? 0); $eventValue = (float) ($event['value'] ?? 0);
$eventValueText = $eventValue > 0 ? rtrim(rtrim(number_format($eventValue, 2, ',', '.'), '0'), ',') . ' ' . (string) ($event['unit'] ?? '') : ''; $eventValueText = $eventValue > 0 ? ($eventType === 'sleep' ? format_duration_hours($eventValue) : rtrim(rtrim(number_format($eventValue, 2, ',', '.'), '0'), ',') . ' ' . (string) ($event['unit'] ?? '')) : '';
$eventTitle = trim((string) ($event['comment'] ?? '')) !== '' ? trim((string) $event['comment']) : day_event_type_label($eventType); $eventTitle = trim((string) ($event['comment'] ?? '')) !== '' ? trim((string) $event['comment']) : day_event_type_label($eventType);
$eventDetail = $eventValueText; $eventDetail = $eventValueText;
@@ -581,7 +596,7 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
$eventTitle = (string) ($sportType['label'] ?? 'Sport'); $eventTitle = (string) ($sportType['label'] ?? 'Sport');
} }
if ($eventType === 'sleep') { if ($eventType === 'sleep' && trim((string) ($event['comment'] ?? '')) === '') {
$eventTitle = 'Schlaf'; $eventTitle = 'Schlaf';
} }
?> ?>