Improve day swipe and sleep handling
This commit is contained in:
+59
-2
@@ -462,6 +462,51 @@ body.page-dashboard .content {
|
||||
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 {
|
||||
position: relative;
|
||||
width: min(100%, 20rem);
|
||||
@@ -475,8 +520,11 @@ body.page-dashboard .content {
|
||||
}
|
||||
|
||||
.dashboard-day__hero[data-day-slider] {
|
||||
transform: translateX(var(--day-slider-offset, 0));
|
||||
transition: transform 180ms ease;
|
||||
position: relative;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1028,6 +1076,15 @@ body.page-dashboard .content {
|
||||
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 {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
|
||||
+69
-12
@@ -1223,7 +1223,7 @@
|
||||
valueInput.placeholder = config.placeholder;
|
||||
valueInput.required = !!config.showValue;
|
||||
valueInput.value = config.showValue ? valueInput.value : "";
|
||||
valueInput.step = type === "sleep" ? "0.25" : "1";
|
||||
valueInput.step = type === "sleep" ? "0.01" : "1";
|
||||
}
|
||||
if (unitInput) {
|
||||
unitInput.value = config.unit;
|
||||
@@ -1454,11 +1454,61 @@
|
||||
let dragging = false;
|
||||
let didSwipe = false;
|
||||
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 = () => {
|
||||
dayStrip.classList.remove("is-dragging");
|
||||
daySlider.classList.remove("is-dragging");
|
||||
daySlider.style.setProperty("--day-slider-offset", "0px");
|
||||
setTargetOffset(0);
|
||||
};
|
||||
|
||||
const handleSwipe = (deltaX, deltaY) => {
|
||||
@@ -1469,18 +1519,18 @@
|
||||
|
||||
if (deltaX < 0 && swipeContainer.dataset.nextDate) {
|
||||
didSwipe = true;
|
||||
daySlider.style.setProperty("--day-slider-offset", "-120%");
|
||||
setSlideProgress(-window.innerWidth);
|
||||
window.location.href = dashboardDayPath(swipeContainer.dataset.nextDate);
|
||||
} else if (deltaX > 0 && swipeContainer.dataset.prevDate) {
|
||||
didSwipe = true;
|
||||
daySlider.style.setProperty("--day-slider-offset", "120%");
|
||||
setSlideProgress(window.innerWidth);
|
||||
window.location.href = dashboardDayPath(swipeContainer.dataset.prevDate);
|
||||
} else {
|
||||
resetStrip();
|
||||
}
|
||||
};
|
||||
|
||||
dayStrip.addEventListener("pointerdown", event => {
|
||||
daySlider.addEventListener("pointerdown", event => {
|
||||
if (event.target.closest("input, textarea, select, label, [data-stepper], .dashboard-overlay")) {
|
||||
dragging = false;
|
||||
return;
|
||||
@@ -1493,10 +1543,10 @@
|
||||
pointerStartY = event.clientY;
|
||||
dayStrip.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)) {
|
||||
return;
|
||||
}
|
||||
@@ -1510,10 +1560,17 @@
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@@ -1523,13 +1580,13 @@
|
||||
handleSwipe(event.clientX - pointerStartX, event.clientY - pointerStartY);
|
||||
});
|
||||
|
||||
dayStrip.addEventListener("pointercancel", () => {
|
||||
daySlider.addEventListener("pointercancel", () => {
|
||||
dragging = false;
|
||||
activePointerId = null;
|
||||
resetStrip();
|
||||
});
|
||||
|
||||
dayStrip.addEventListener("lostpointercapture", () => {
|
||||
daySlider.addEventListener("lostpointercapture", () => {
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
@@ -1539,7 +1596,7 @@
|
||||
resetStrip();
|
||||
});
|
||||
|
||||
dayStrip.addEventListener("click", event => {
|
||||
daySlider.addEventListener("click", event => {
|
||||
if (!didSwipe) {
|
||||
return;
|
||||
}
|
||||
|
||||
+11
-4
@@ -739,7 +739,9 @@ final class App
|
||||
|
||||
if ($name === 'sleep_analysis') {
|
||||
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) {
|
||||
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'])) {
|
||||
$bucket['start'] = $start;
|
||||
}
|
||||
@@ -1816,7 +1816,7 @@ final class App
|
||||
|
||||
$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) {
|
||||
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
|
||||
{
|
||||
return storage_path('users/' . normalize_username($username) . '/media');
|
||||
|
||||
@@ -115,6 +115,23 @@ function format_points(float $value): string
|
||||
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
|
||||
{
|
||||
return strtolower(trim($username));
|
||||
|
||||
@@ -55,20 +55,24 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
|
||||
|
||||
<?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__hero" data-day-slider>
|
||||
<p class="dashboard-day__eyebrow"><?= e((string) $dayWeekday) ?></p>
|
||||
<h1><?= e(format_display_date((string) $dayEntry['date'], false)) ?></h1>
|
||||
<div class="dashboard-day-slider" data-day-slider-shell>
|
||||
<span class="day-slide-hint day-slide-hint--prev" data-day-slide-prev-hint>Vorherigen Tag laden</span>
|
||||
<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>
|
||||
<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): ?>
|
||||
<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&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__marker"></span>
|
||||
</span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
<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>
|
||||
<?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&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__marker"></span>
|
||||
</span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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),
|
||||
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
|
||||
$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'] ?? ''));
|
||||
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]);
|
||||
}
|
||||
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]);
|
||||
}
|
||||
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]);
|
||||
}
|
||||
}
|
||||
$sleepPhaseTotal = max(0.0, array_sum($sleepPhases));
|
||||
$sleepBarTotal = $eventType === 'sleep' ? max((float) ($item['value'] ?? 0), $sleepPhaseTotal, $optimalSleepHours / 0.75) : 0.0;
|
||||
$sleepPhaseRemainder = max(0.0, $sleepBarTotal - $sleepPhaseTotal);
|
||||
$sleepActualTotal = $eventType === 'sleep' ? max((float) ($item['value'] ?? 0), $sleepPhaseTotal) : 0.0;
|
||||
$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;
|
||||
$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>
|
||||
<?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) ?>%">
|
||||
<?php foreach (['deep' => ['Tief', 'deep'], 'rem' => ['REM', 'rem'], 'core' => ['Kern', 'core']] as $phase => [$label, $class]): ?>
|
||||
<?php $phaseHours = max(0.0, (float) ($sleepPhases[$phase] ?? 0)); ?>
|
||||
<?php if ($phaseHours <= 0) { continue; } ?>
|
||||
<?php $phasePercent = $sleepBarTotal > 0 ? max(0.5, min(100, ($phaseHours / $sleepBarTotal) * 100)) : 0; ?>
|
||||
<?php $phaseLeftPercent = $sleepBarTotal > 0 ? max(0, min(100, ($sleepPhaseLeft / $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">
|
||||
<strong><?= e($label) ?></strong> <?= e(format_points($phaseHours)) ?> h
|
||||
<?php if ($sleepPhaseTotal > 0): ?>
|
||||
<?php foreach (['deep' => ['Tief', 'deep'], 'rem' => ['REM', 'rem'], 'core' => ['Kern', 'core']] as $phase => [$label, $class]): ?>
|
||||
<?php $phaseHours = max(0.0, (float) ($sleepPhases[$phase] ?? 0)); ?>
|
||||
<?php if ($phaseHours <= 0) { continue; } ?>
|
||||
<?php $phasePercent = $sleepBarTotal > 0 ? max(0.5, min(100, ($phaseHours / $sleepBarTotal) * 100)) : 0; ?>
|
||||
<?php $phaseLeftPercent = $sleepBarTotal > 0 ? max(0, min(100, ($sleepPhaseLeft / $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_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>
|
||||
<?php $sleepPhaseLeft += $phaseHours; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($sleepPhaseRemainder > 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; ?>
|
||||
<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>
|
||||
<?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_duration_hours($sleepPhaseRemainder)) ?>" data-tooltip="Bis Ziel-/Skalenende: <?= e(format_duration_hours($sleepPhaseRemainder)) ?>"></span>
|
||||
<?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>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -421,7 +436,7 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
|
||||
</label>
|
||||
<label data-moment-value-field>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -573,7 +588,7 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
|
||||
$eventTone = signal_value_class($eventScore);
|
||||
$sportType = $eventType === 'sport' ? find_sport_type($settings, (string) ($event['sport_type_id'] ?? '')) : null;
|
||||
$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);
|
||||
$eventDetail = $eventValueText;
|
||||
|
||||
@@ -581,7 +596,7 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
|
||||
$eventTitle = (string) ($sportType['label'] ?? 'Sport');
|
||||
}
|
||||
|
||||
if ($eventType === 'sleep') {
|
||||
if ($eventType === 'sleep' && trim((string) ($event['comment'] ?? '')) === '') {
|
||||
$eventTitle = 'Schlaf';
|
||||
}
|
||||
?>
|
||||
|
||||
Reference in New Issue
Block a user