Refine balance scoring and dashboard views

This commit is contained in:
2026-05-21 12:19:52 +02:00
parent 0fb8adbb14
commit abcd35714f
8 changed files with 316 additions and 48 deletions
+67 -3
View File
@@ -547,6 +547,34 @@ body.page-dashboard .content {
.compare-day__line.score-2 .compare-day__marker { top: 12%; } .compare-day__line.score-2 .compare-day__marker { top: 12%; }
.compare-day__line.score-empty .compare-day__marker { top: 50%; } .compare-day__line.score-empty .compare-day__marker { top: 50%; }
.score-scale {
position: absolute;
inset: 0 auto 0 0;
display: flex;
flex-direction: column;
justify-content: space-between;
width: 2.2rem;
padding-block: 0.25rem;
color: rgba(239, 247, 255, 0.52);
font-size: 0.66rem;
line-height: 1;
pointer-events: none;
}
.score-scale--day {
left: calc(50% - 9.8rem);
}
.score-scale--range {
left: 0.35rem;
top: 1rem;
bottom: 2rem;
}
.score-scale--month {
bottom: 1rem;
}
.tone-neg2 { background: #ff8f8f !important; border-color: rgba(255, 143, 143, 0.6) !important; } .tone-neg2 { background: #ff8f8f !important; border-color: rgba(255, 143, 143, 0.6) !important; }
.tone-neg1 { background: #ffbf8d !important; border-color: rgba(255, 191, 141, 0.6) !important; } .tone-neg1 { background: #ffbf8d !important; border-color: rgba(255, 191, 141, 0.6) !important; }
.tone-zero { background: #8be4ff !important; border-color: rgba(139, 228, 255, 0.6) !important; } .tone-zero { background: #8be4ff !important; border-color: rgba(139, 228, 255, 0.6) !important; }
@@ -1559,6 +1587,7 @@ body.page-dashboard .content {
} }
.range-score-strip { .range-score-strip {
position: relative;
display: grid; display: grid;
gap: 0.55rem; gap: 0.55rem;
align-items: stretch; align-items: stretch;
@@ -1569,13 +1598,14 @@ body.page-dashboard .content {
.range-score-strip--week { .range-score-strip--week {
grid-template-columns: repeat(7, minmax(0, 1fr)); grid-template-columns: repeat(7, minmax(0, 1fr));
padding-left: 2.45rem;
} }
.range-score-strip--month { .range-score-strip--month {
display: flex; display: flex;
gap: 0.18rem; gap: 0.18rem;
overflow: visible; overflow: visible;
padding-inline: 0.8rem; padding-inline: 2.45rem 0.8rem;
} }
.range-score-day { .range-score-day {
@@ -1635,11 +1665,15 @@ body.page-dashboard .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
max-height: min(68vh, 56rem); overflow: visible;
overflow: auto;
padding-right: 0.15rem; padding-right: 0.15rem;
} }
.range-day-list--month {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}
.range-day-card { .range-day-card {
display: block; display: block;
overflow: hidden; overflow: hidden;
@@ -1713,6 +1747,18 @@ body.page-dashboard .content {
line-height: 1.45; line-height: 1.45;
} }
.range-day-card__score {
display: inline-flex;
width: fit-content;
margin: 0 0 0.5rem;
padding: 0.34rem 0.62rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
color: rgba(239, 247, 255, 0.82);
font-size: 0.82rem;
font-weight: 700;
}
.range-day-card--summary-only .range-day-card__summary { .range-day-card--summary-only .range-day-card__summary {
font-size: clamp(1rem, 2.2vw, 1.25rem); font-size: clamp(1rem, 2.2vw, 1.25rem);
} }
@@ -1739,6 +1785,15 @@ body.page-dashboard .content {
display: block; display: block;
} }
.signal-scale {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
width: min(100%, 13rem);
color: rgba(239, 247, 255, 0.52);
font-size: 0.68rem;
text-align: center;
}
.range-moment-list__item span span { .range-moment-list__item span span {
margin-top: 0.16rem; margin-top: 0.16rem;
color: rgba(239, 247, 255, 0.64); color: rgba(239, 247, 255, 0.64);
@@ -2248,6 +2303,15 @@ body.page-dashboard .content {
padding: 0.95rem; padding: 0.95rem;
} }
.range-score-strip--week,
.range-score-strip--month {
padding-left: 2.35rem;
}
.range-day-list--month {
grid-template-columns: 1fr;
}
.dashboard-topbar { .dashboard-topbar {
position: relative; position: relative;
top: auto; top: auto;
+30 -12
View File
@@ -425,7 +425,10 @@
let minValue = Math.min(...values); let minValue = Math.min(...values);
let maxValue = Math.max(...values); let maxValue = Math.max(...values);
if (seriesName === "mood" || seriesName === "stress" || seriesName === "pain") { if (seriesName === "balance") {
minValue = -2;
maxValue = 2;
} else if (seriesName === "mood" || seriesName === "stress" || seriesName === "pain") {
minValue = Math.max(1, minValue - 1.5); minValue = Math.max(1, minValue - 1.5);
maxValue = Math.min(10, maxValue + 1.5); maxValue = Math.min(10, maxValue + 1.5);
} else { } else {
@@ -435,7 +438,10 @@
if ((maxValue - minValue) < 3) { if ((maxValue - minValue) < 3) {
const center = (maxValue + minValue) / 2; const center = (maxValue + minValue) / 2;
if (seriesName === "mood" || seriesName === "stress" || seriesName === "pain") { if (seriesName === "balance") {
minValue = -2;
maxValue = 2;
} else if (seriesName === "mood" || seriesName === "stress" || seriesName === "pain") {
minValue = Math.max(1, center - 1.5); minValue = Math.max(1, center - 1.5);
maxValue = Math.min(10, center + 1.5); maxValue = Math.min(10, center + 1.5);
} else { } else {
@@ -701,7 +707,7 @@
} }
const title = item.entry const title = item.entry
? `${item.date}: ${formatNumber(Number(item.entry.score))} Punkte · ${item.entry.label}` ? `${item.date}: ${formatNumber(Number(item.entry.score))}% Bilanz · ${item.entry.label}`
: `${item.date}: kein Eintrag`; : `${item.date}: kein Eintrag`;
if (!item.entry) { if (!item.entry) {
@@ -732,7 +738,7 @@
</div> </div>
<div class="calendar-detail__score"> <div class="calendar-detail__score">
<span data-calendar-score>${formatNumber(Number(latestVisibleEntry.score))}</span> <span data-calendar-score>${formatNumber(Number(latestVisibleEntry.score))}</span>
<small>Punkte</small> <small>% Bilanz</small>
</div> </div>
<a class="ghost-link calendar-detail__link" data-calendar-link href="${dashboardDayPath(latestVisibleEntry.date)}">Tag öffnen</a> <a class="ghost-link calendar-detail__link" data-calendar-link href="${dashboardDayPath(latestVisibleEntry.date)}">Tag öffnen</a>
`; `;
@@ -1070,6 +1076,12 @@
return "tone-zero"; return "tone-zero";
}; };
const signalLabels = {
mood: { "-2": "sehr niedrig", "-1": "niedrig", 0: "neutral", 1: "hoch", 2: "sehr hoch" },
energy: { "-2": "leer", "-1": "matt", 0: "okay", 1: "wach", 2: "kraftvoll" },
stress: { "-2": "sehr ruhig", "-1": "ruhig", 0: "neutral", 1: "angespannt", 2: "sehr angespannt" },
};
const setHidden = (element, hidden) => { const setHidden = (element, hidden) => {
if (!element) { if (!element) {
return; return;
@@ -1113,6 +1125,10 @@
const metric = stepper.dataset.stepperMetric || "mood"; const metric = stepper.dataset.stepperMetric || "mood";
input.value = String(current); input.value = String(current);
value.textContent = `${current > 0 ? "+" : ""}${current}`; value.textContent = `${current > 0 ? "+" : ""}${current}`;
const label = stepper.querySelector("[data-stepper-label]");
if (label) {
label.textContent = signalLabels[metric]?.[current] || signalLabels.mood[current] || "neutral";
}
minus.disabled = current <= -2; minus.disabled = current <= -2;
plus.disabled = current >= 2; plus.disabled = current >= 2;
const ring = stepper.querySelector(".overlay-signal-card__ring"); const ring = stepper.querySelector(".overlay-signal-card__ring");
@@ -1434,20 +1450,22 @@
let pointerStartX = 0; let pointerStartX = 0;
let pointerStartY = 0; let pointerStartY = 0;
let dragging = false; let dragging = false;
let ignoreSwipe = false;
const handleSwipe = (deltaX, deltaY) => { const handleSwipe = (deltaX, deltaY) => {
if (Math.abs(deltaX) < 70 || Math.abs(deltaY) > 50) { if (ignoreSwipe || document.body.classList.contains("is-dashboard-overlay-open") || Math.abs(deltaX) < 70 || Math.abs(deltaY) > 50) {
return; return;
} }
if (deltaX < 0 && swipeContainer.dataset.prevDate) { if (deltaX < 0 && swipeContainer.dataset.nextDate) {
window.location.href = dashboardDayPath(swipeContainer.dataset.prevDate);
} else if (deltaX > 0 && swipeContainer.dataset.nextDate) {
window.location.href = dashboardDayPath(swipeContainer.dataset.nextDate); window.location.href = dashboardDayPath(swipeContainer.dataset.nextDate);
} else if (deltaX > 0 && swipeContainer.dataset.prevDate) {
window.location.href = dashboardDayPath(swipeContainer.dataset.prevDate);
} }
}; };
swipeContainer.addEventListener("pointerdown", event => { swipeContainer.addEventListener("pointerdown", event => {
ignoreSwipe = Boolean(event.target.closest("a, button, input, textarea, select, label, [data-event-editable]"));
dragging = true; dragging = true;
pointerStartX = event.clientX; pointerStartX = event.clientX;
pointerStartY = event.clientY; pointerStartY = event.clientY;
@@ -1471,12 +1489,12 @@
return; return;
} }
if (event.key === "ArrowLeft" && swipeContainer.dataset.prevDate) { if (event.key === "ArrowLeft" && swipeContainer.dataset.nextDate) {
window.location.href = dashboardDayPath(swipeContainer.dataset.prevDate); window.location.href = dashboardDayPath(swipeContainer.dataset.nextDate);
} }
if (event.key === "ArrowRight" && swipeContainer.dataset.nextDate) { if (event.key === "ArrowRight" && swipeContainer.dataset.prevDate) {
window.location.href = dashboardDayPath(swipeContainer.dataset.nextDate); window.location.href = dashboardDayPath(swipeContainer.dataset.prevDate);
} }
}); });
} }
+82 -29
View File
@@ -1547,6 +1547,7 @@ final class App
$isPersisted = isset($entryMap[$dayDate]); $isPersisted = isset($entryMap[$dayDate]);
$hasContent = $isPersisted || $this->entryHasContent($entry); $hasContent = $isPersisted || $this->entryHasContent($entry);
$visualScore = $this->dashboardVisualScore($entry, $isPersisted); $visualScore = $this->dashboardVisualScore($entry, $isPersisted);
$lineLevel = $this->dashboardLineLevel($entry, $isPersisted);
$days[] = [ $days[] = [
'date' => $dayDate, 'date' => $dayDate,
@@ -1556,9 +1557,9 @@ final class App
'is_current' => $dayDate === $date, 'is_current' => $dayDate === $date,
'has_content' => $hasContent, 'has_content' => $hasContent,
'visual_score' => $visualScore, 'visual_score' => $visualScore,
'score_level' => $visualScore, 'score_level' => $lineLevel,
'line_level' => $this->dashboardLineLevel($entry, $isPersisted), 'line_level' => $lineLevel,
'line_tone' => $visualScore === null ? 'empty' : signal_value_class($visualScore), 'line_tone' => $lineLevel === null ? 'empty' : signal_value_class($lineLevel),
]; ];
} }
@@ -1608,7 +1609,7 @@ final class App
$iso = $day->format('Y-m-d'); $iso = $day->format('Y-m-d');
$entry = $entryMap[$iso] ?? null; $entry = $entryMap[$iso] ?? null;
$hasContent = $entry !== null && $this->entryHasContent($entry); $hasContent = $entry !== null && $this->entryHasContent($entry);
$visualScore = $entry !== null ? $this->dashboardVisualScore($entry, true) : null; $lineLevel = $entry !== null ? $this->dashboardLineLevel($entry, true) : null;
$days[] = [ $days[] = [
'date' => $iso, 'date' => $iso,
@@ -1617,8 +1618,8 @@ final class App
'day' => $day->format('j'), 'day' => $day->format('j'),
'entry' => $entry, 'entry' => $entry,
'has_content' => $hasContent, 'has_content' => $hasContent,
'score_level' => $visualScore, 'score_level' => $lineLevel,
'line_tone' => $visualScore === null ? 'empty' : signal_value_class($visualScore), 'line_tone' => $lineLevel === null ? 'empty' : signal_value_class($lineLevel),
'is_current' => $iso === $selectedDate, 'is_current' => $iso === $selectedDate,
]; ];
} }
@@ -1721,15 +1722,15 @@ final class App
$iso = $day->format('Y-m-d'); $iso = $day->format('Y-m-d');
$entry = $entryMap[$iso] ?? null; $entry = $entryMap[$iso] ?? null;
$hasContent = $entry !== null && $this->entryHasContent($entry); $hasContent = $entry !== null && $this->entryHasContent($entry);
$visualScore = $entry !== null ? $this->dashboardVisualScore($entry, true) : null; $lineLevel = $entry !== null ? $this->dashboardLineLevel($entry, true) : null;
$days[] = [ $days[] = [
'date' => $iso, 'date' => $iso,
'day' => $day->format('j'), 'day' => $day->format('j'),
'weekday' => format_display_date($iso, true), 'weekday' => format_display_date($iso, true),
'entry' => $entry, 'entry' => $entry,
'has_content' => $hasContent, 'has_content' => $hasContent,
'score_level' => $visualScore, 'score_level' => $lineLevel,
'line_tone' => $visualScore === null ? 'empty' : signal_value_class($visualScore), 'line_tone' => $lineLevel === null ? 'empty' : signal_value_class($lineLevel),
'is_future' => $iso > $selectedDate, 'is_future' => $iso > $selectedDate,
]; ];
} }
@@ -1787,14 +1788,18 @@ final class App
return null; return null;
} }
if (is_array($entry['evaluation']['balance'] ?? null)) {
return max(-2, min(2, (int) ($entry['evaluation']['balance']['level'] ?? 0)));
}
$percentage = max(0.0, min(100.0, (float) ($entry['evaluation']['percentage'] ?? 0))); $percentage = max(0.0, min(100.0, (float) ($entry['evaluation']['percentage'] ?? 0)));
return (int) round($percentage / 5); return max(-2, min(2, (int) round(($percentage - 50.0) / 25.0)));
} }
private function dashboardLineTone(array $entry, bool $isPersisted = false): string private function dashboardLineTone(array $entry, bool $isPersisted = false): string
{ {
return signal_value_class($this->dashboardVisualScore($entry, $isPersisted) ?? 0); return signal_value_class($this->dashboardLineLevel($entry, $isPersisted) ?? 0);
} }
private function dashboardEventFromPost(array $input): array private function dashboardEventFromPost(array $input): array
@@ -2555,6 +2560,10 @@ final class App
? round(array_sum(array_map(static fn (array $entry): float => (float) $entry['evaluation']['total'], $entries)) / $count, 1) ? round(array_sum(array_map(static fn (array $entry): float => (float) $entry['evaluation']['total'], $entries)) / $count, 1)
: 0.0; : 0.0;
$avgBalance = $count > 0
? round(array_sum(array_map(static fn (array $entry): float => (float) ($entry['evaluation']['balance']['raw'] ?? 0), $entries)) / $count, 1)
: 0.0;
$avgMood = $count > 0 $avgMood = $count > 0
? round(array_sum(array_map(static fn (array $entry): int => (int) $entry['mood'], $entries)) / $count, 1) ? round(array_sum(array_map(static fn (array $entry): int => (int) $entry['mood'], $entries)) / $count, 1)
: 0.0; : 0.0;
@@ -2566,6 +2575,7 @@ final class App
return [ return [
'tracked_days' => $count, 'tracked_days' => $count,
'average_score' => $avgScore, 'average_score' => $avgScore,
'average_balance' => $avgBalance,
'average_mood' => $avgMood, 'average_mood' => $avgMood,
'average_stress' => $avgStress, 'average_stress' => $avgStress,
'streak' => $this->calculateStreak($entries), 'streak' => $this->calculateStreak($entries),
@@ -2825,13 +2835,20 @@ final class App
return [ return [
'calendar' => array_map(static function (array $entry): array { 'calendar' => array_map(static function (array $entry): array {
$balance = is_array($entry['evaluation']['balance'] ?? null) ? $entry['evaluation']['balance'] : [];
return [ return [
'date' => $entry['date'], 'date' => $entry['date'],
'score' => $entry['evaluation']['total'], 'score' => (float) ($balance['percentage'] ?? $entry['evaluation']['percentage'] ?? 0),
'max' => $entry['evaluation']['max_total'], 'max' => 100,
'label' => $entry['evaluation']['label'], 'label' => $entry['evaluation']['label'],
]; ];
}, $calendar), }, $calendar),
'balance' => array_map(static function (array $entry): array {
return [
'date' => $entry['date'],
'value' => (float) ($entry['evaluation']['balance']['raw'] ?? 0),
];
}, $recent),
'mood' => array_map(static function (array $entry): array { 'mood' => array_map(static function (array $entry): array {
return [ return [
'date' => $entry['date'], 'date' => $entry['date'],
@@ -2915,13 +2932,24 @@ final class App
$settings['sleep'] = [ $settings['sleep'] = [
'optimal_hours' => max(1.0, min(16.0, round((float) ($input['sleep']['optimal_hours'] ?? ($settings['sleep']['optimal_hours'] ?? 7.0)), 1))), 'optimal_hours' => max(1.0, min(16.0, round((float) ($input['sleep']['optimal_hours'] ?? ($settings['sleep']['optimal_hours'] ?? 7.0)), 1))),
]; ];
$scoreMode = (string) ($input['display']['score_mode'] ?? ($settings['display']['score_mode'] ?? 'scale'));
$settings['display'] = [
'score_mode' => in_array($scoreMode, ['scale', 'percent', 'points'], true) ? $scoreMode : 'scale',
];
$settings['day_balance'] = [
'mood_weight' => max(0, min(10, (int) ($input['day_balance']['mood_weight'] ?? ($settings['day_balance']['mood_weight'] ?? 3)))),
'energy_weight' => max(0, min(10, (int) ($input['day_balance']['energy_weight'] ?? ($settings['day_balance']['energy_weight'] ?? 2)))),
'stress_weight' => max(0, min(10, (int) ($input['day_balance']['stress_weight'] ?? ($settings['day_balance']['stress_weight'] ?? 2)))),
'adjustment_cap' => max(0.0, min(2.0, round((float) ($input['day_balance']['adjustment_cap'] ?? ($settings['day_balance']['adjustment_cap'] ?? 1.0)), 1))),
'points_per_step' => max(1, min(50, (int) ($input['day_balance']['points_per_step'] ?? ($settings['day_balance']['points_per_step'] ?? 12)))),
];
$settings['scoring']['mood_multiplier'] = max(0, min(10, (int) ($input['scoring']['mood_multiplier'] ?? 3))); $settings['scoring']['mood_multiplier'] = max(0, min(10, (int) ($input['scoring']['mood_multiplier'] ?? ($settings['scoring']['mood_multiplier'] ?? 3))));
$settings['scoring']['energy_multiplier'] = max(0, min(10, (int) ($input['scoring']['energy_multiplier'] ?? 2))); $settings['scoring']['energy_multiplier'] = max(0, min(10, (int) ($input['scoring']['energy_multiplier'] ?? ($settings['scoring']['energy_multiplier'] ?? 2))));
$settings['scoring']['stress_multiplier'] = max(0, min(10, (int) ($input['scoring']['stress_multiplier'] ?? 2))); $settings['scoring']['stress_multiplier'] = max(0, min(10, (int) ($input['scoring']['stress_multiplier'] ?? ($settings['scoring']['stress_multiplier'] ?? 2))));
$settings['scoring']['pain_multiplier'] = max(0, min(10, (int) ($input['scoring']['pain_multiplier'] ?? ($settings['scoring']['pain_multiplier'] ?? 3)))); $settings['scoring']['pain_multiplier'] = max(0, min(10, (int) ($input['scoring']['pain_multiplier'] ?? ($settings['scoring']['pain_multiplier'] ?? 3))));
$settings['scoring']['sleep_feeling_multiplier'] = max(0, min(10, (int) ($input['scoring']['sleep_feeling_multiplier'] ?? 2))); $settings['scoring']['sleep_feeling_multiplier'] = max(0, min(10, (int) ($input['scoring']['sleep_feeling_multiplier'] ?? ($settings['scoring']['sleep_feeling_multiplier'] ?? 2))));
$settings['scoring']['journal_points'] = max(0, min(20, (int) ($input['scoring']['journal_points'] ?? 2))); $settings['scoring']['journal_points'] = max(0, min(20, (int) ($input['scoring']['journal_points'] ?? ($settings['scoring']['journal_points'] ?? 2))));
$stepBonus = is_array($settings['scoring']['step_bonus'] ?? null) ? $settings['scoring']['step_bonus'] : $defaults['scoring']['step_bonus']; $stepBonus = is_array($settings['scoring']['step_bonus'] ?? null) ? $settings['scoring']['step_bonus'] : $defaults['scoring']['step_bonus'];
$settings['scoring']['step_bonus'] = [ $settings['scoring']['step_bonus'] = [
'min' => max(0, min(100000, (int) ($input['scoring']['step_bonus']['min'] ?? $stepBonus['min'] ?? 10000))), 'min' => max(0, min(100000, (int) ($input['scoring']['step_bonus']['min'] ?? $stepBonus['min'] ?? 10000))),
@@ -2932,11 +2960,13 @@ final class App
$settings['scoring']['step_bonus']['max'] = $settings['scoring']['step_bonus']['min']; $settings['scoring']['step_bonus']['max'] = $settings['scoring']['step_bonus']['min'];
} }
$settings['tracking'] = [ $settings['tracking'] = [
'pain_enabled' => isset($input['tracking']['pain_enabled']) && $input['tracking']['pain_enabled'] === '1', 'pain_enabled' => array_key_exists('tracking', $input)
? (isset($input['tracking']['pain_enabled']) && $input['tracking']['pain_enabled'] === '1')
: !empty($settings['tracking']['pain_enabled']),
]; ];
foreach ($defaults['scoring']['sleep_duration_points'] as $key => $default) { foreach ($defaults['scoring']['sleep_duration_points'] as $key => $default) {
$settings['scoring']['sleep_duration_points'][$key] = max(0, min(20, (int) ($input['scoring']['sleep_duration_points'][$key] ?? $default))); $settings['scoring']['sleep_duration_points'][$key] = max(0, min(20, (int) ($input['scoring']['sleep_duration_points'][$key] ?? ($settings['scoring']['sleep_duration_points'][$key] ?? $default))));
} }
foreach (['sport_bands', 'walk_bands'] as $bandKey) { foreach (['sport_bands', 'walk_bands'] as $bandKey) {
@@ -2946,25 +2976,35 @@ final class App
$settings['scoring'][$bandKey][$index] = [ $settings['scoring'][$bandKey][$index] = [
'min' => max(0, min(2000, (int) ($input['scoring'][$bandKey][$index]['min'] ?? $currentBand['min'] ?? $defaultBand['min']))), 'min' => max(0, min(2000, (int) ($input['scoring'][$bandKey][$index]['min'] ?? $currentBand['min'] ?? $defaultBand['min']))),
'max' => max(0, min(2000, (int) ($input['scoring'][$bandKey][$index]['max'] ?? $currentBand['max'] ?? $defaultBand['max']))), 'max' => max(0, min(2000, (int) ($input['scoring'][$bandKey][$index]['max'] ?? $currentBand['max'] ?? $defaultBand['max']))),
'points' => max(0, min(20, (int) ($input['scoring'][$bandKey][$index]['points'] ?? $currentBand['points'] ?? $defaultBand['points']))), 'points' => max(-20, min(20, (int) ($input['scoring'][$bandKey][$index]['points'] ?? $currentBand['points'] ?? $defaultBand['points']))),
]; ];
} }
} }
foreach ($defaults['scoring']['walk_step_targets'] as $index => $defaultTarget) {
$currentTarget = $settings['scoring']['walk_step_targets'][$index] ?? $defaultTarget;
$settings['scoring']['walk_step_targets'][$index] = [
'steps' => max(0, min(100000, (int) ($input['scoring']['walk_step_targets'][$index]['steps'] ?? $currentTarget['steps'] ?? $defaultTarget['steps']))),
'points' => max(-20, min(20, (int) ($input['scoring']['walk_step_targets'][$index]['points'] ?? $currentTarget['points'] ?? $defaultTarget['points']))),
];
}
foreach ($defaults['ratings'] as $index => $defaultRating) { foreach ($defaults['ratings'] as $index => $defaultRating) {
$currentRating = $settings['ratings'][$index] ?? $defaultRating;
$settings['ratings'][$index] = [ $settings['ratings'][$index] = [
'label' => trim((string) ($input['ratings'][$index]['label'] ?? $defaultRating['label'])) ?: $defaultRating['label'], 'label' => trim((string) ($input['ratings'][$index]['label'] ?? $currentRating['label'] ?? $defaultRating['label'])) ?: $defaultRating['label'],
'min' => max(0, min(999, (int) ($input['ratings'][$index]['min'] ?? $defaultRating['min']))), 'min' => max(0, min(999, (int) ($input['ratings'][$index]['min'] ?? $currentRating['min'] ?? $defaultRating['min']))),
'max' => max(0, min(999, (int) ($input['ratings'][$index]['max'] ?? $defaultRating['max']))), 'max' => max(0, min(999, (int) ($input['ratings'][$index]['max'] ?? $currentRating['max'] ?? $defaultRating['max']))),
]; ];
} }
foreach ($defaults['guardrails'] as $index => $defaultGuardrail) { foreach ($defaults['guardrails'] as $index => $defaultGuardrail) {
$energyRaw = $input['guardrails'][$index]['energy_max'] ?? $defaultGuardrail['energy_max']; $currentGuardrail = $settings['guardrails'][$index] ?? $defaultGuardrail;
$energyRaw = $input['guardrails'][$index]['energy_max'] ?? $currentGuardrail['energy_max'] ?? $defaultGuardrail['energy_max'];
$settings['guardrails'][$index] = [ $settings['guardrails'][$index] = [
'mood_max' => max(1, min(10, (int) ($input['guardrails'][$index]['mood_max'] ?? $defaultGuardrail['mood_max']))), 'mood_max' => max(1, min(10, (int) ($input['guardrails'][$index]['mood_max'] ?? $currentGuardrail['mood_max'] ?? $defaultGuardrail['mood_max']))),
'energy_max' => $energyRaw === '' || $energyRaw === null ? null : max(1, min(10, (int) $energyRaw)), 'energy_max' => $energyRaw === '' || $energyRaw === null ? null : max(1, min(10, (int) $energyRaw)),
'cap_label' => trim((string) ($input['guardrails'][$index]['cap_label'] ?? $defaultGuardrail['cap_label'])) ?: $defaultGuardrail['cap_label'], 'cap_label' => trim((string) ($input['guardrails'][$index]['cap_label'] ?? $currentGuardrail['cap_label'] ?? $defaultGuardrail['cap_label'])) ?: $defaultGuardrail['cap_label'],
]; ];
} }
@@ -2974,7 +3014,7 @@ final class App
$settings['sport_types'] = normalized_sport_types([ $settings['sport_types'] = normalized_sport_types([
'sport_types' => $sportTypesProvided && is_array($input['sport_types'] ?? null) 'sport_types' => $sportTypesProvided && is_array($input['sport_types'] ?? null)
? $input['sport_types'] ? $input['sport_types']
: ($sportTypesProvided ? [] : $defaults['sport_types']), : ($sportTypesProvided ? [] : ($settings['sport_types'] ?? $defaults['sport_types'])),
]); ]);
$time = trim((string) ($input['notifications']['time'] ?? ($settings['notifications']['time'] ?? $defaults['notifications']['time']))); $time = trim((string) ($input['notifications']['time'] ?? ($settings['notifications']['time'] ?? $defaults['notifications']['time'])));
@@ -2983,7 +3023,9 @@ final class App
} }
$settings['notifications'] = [ $settings['notifications'] = [
'enabled' => isset($input['notifications']['enabled']) && $input['notifications']['enabled'] === '1', 'enabled' => array_key_exists('notifications', $input)
? (isset($input['notifications']['enabled']) && $input['notifications']['enabled'] === '1')
: !empty($settings['notifications']['enabled']),
'time' => $time, 'time' => $time,
]; ];
@@ -3002,6 +3044,17 @@ final class App
is_array($settings['sleep'] ?? null) ? $settings['sleep'] : [] is_array($settings['sleep'] ?? null) ? $settings['sleep'] : []
); );
$settings['sleep']['optimal_hours'] = max(1.0, min(16.0, round((float) ($settings['sleep']['optimal_hours'] ?? 7.0), 1))); $settings['sleep']['optimal_hours'] = max(1.0, min(16.0, round((float) ($settings['sleep']['optimal_hours'] ?? 7.0), 1)));
$settings['display'] = array_replace(
Defaults::settings()['display'],
is_array($settings['display'] ?? null) ? $settings['display'] : []
);
if (!in_array((string) ($settings['display']['score_mode'] ?? 'scale'), ['scale', 'percent', 'points'], true)) {
$settings['display']['score_mode'] = 'scale';
}
$settings['day_balance'] = array_replace(
Defaults::settings()['day_balance'],
is_array($settings['day_balance'] ?? null) ? $settings['day_balance'] : []
);
$settings['tracking'] = array_replace( $settings['tracking'] = array_replace(
Defaults::settings()['tracking'], Defaults::settings()['tracking'],
is_array($settings['tracking'] ?? null) ? $settings['tracking'] : [] is_array($settings['tracking'] ?? null) ? $settings['tracking'] : []
+57 -1
View File
@@ -71,8 +71,9 @@ final class ScoringService
'sleep_feeling' => $entry['sleep_feeling'] * (float) $scoring['sleep_feeling_multiplier'], 'sleep_feeling' => $entry['sleep_feeling'] * (float) $scoring['sleep_feeling_multiplier'],
'sport_minutes' => $this->bandPoints((int) $entry['sport_minutes'], $scoring['sport_bands']), 'sport_minutes' => $this->bandPoints((int) $entry['sport_minutes'], $scoring['sport_bands']),
'sport_bonus' => $sportBonus, 'sport_bonus' => $sportBonus,
'walk_minutes' => 0.0, 'walk_minutes' => $this->walkPoints($entry, $settings),
'step_bonus' => $this->stepBonusPoints($entry, $scoring['step_bonus'] ?? []), 'step_bonus' => $this->stepBonusPoints($entry, $scoring['step_bonus'] ?? []),
'daily_steps' => $this->stepTargetPoints((int) ($entry['health']['steps'] ?? 0), $scoring['walk_step_targets'] ?? []),
'events' => $eventSignalPoints, 'events' => $eventSignalPoints,
'alcohol' => !empty($entry['alcohol']) ? ((float) abs((float) ($scoring['alcohol_penalty'] ?? 5)) * -1) : 0.0, 'alcohol' => !empty($entry['alcohol']) ? ((float) abs((float) ($scoring['alcohol_penalty'] ?? 5)) * -1) : 0.0,
'note' => $entry['note'] === '' ? 0 : (float) $scoring['journal_points'], 'note' => $entry['note'] === '' ? 0 : (float) $scoring['journal_points'],
@@ -92,6 +93,8 @@ final class ScoringService
(5 * (float) $scoring['sleep_feeling_multiplier']) + (5 * (float) $scoring['sleep_feeling_multiplier']) +
$this->maxBandPoints($scoring['sport_bands']) + $this->maxBandPoints($scoring['sport_bands']) +
$this->maxSportBonusPoints($settings) + $this->maxSportBonusPoints($settings) +
$this->maxWalkPoints($entry, $settings) +
$this->maxStepTargetPoints($scoring['walk_step_targets'] ?? []) +
max(0.0, (float) ($scoring['step_bonus']['points'] ?? 0)) + max(0.0, (float) ($scoring['step_bonus']['points'] ?? 0)) +
($eventSignalPoints !== 0.0 ? 8.0 : 0.0) + ($eventSignalPoints !== 0.0 ? 8.0 : 0.0) +
(float) $scoring['journal_points'], (float) $scoring['journal_points'],
@@ -123,11 +126,50 @@ final class ScoringService
'guardrail' => $guardrail, 'guardrail' => $guardrail,
'sentiment' => $this->sentimentForLabel($label, $ratings), 'sentiment' => $this->sentimentForLabel($label, $ratings),
'percentage' => $maxTotal > 0 ? round(($total / $maxTotal) * 100, 1) : 0, 'percentage' => $maxTotal > 0 ? round(($total / $maxTotal) * 100, 1) : 0,
'balance' => $this->dayBalance($entry, $components, $settings),
'sport_type' => $sportTypes[0] ?? null, 'sport_type' => $sportTypes[0] ?? null,
'sport_types' => $sportTypes, 'sport_types' => $sportTypes,
]; ];
} }
private function dayBalance(array $entry, array $components, array $settings): array
{
$config = is_array($settings['day_balance'] ?? null) ? $settings['day_balance'] : [];
$moodWeight = max(0.0, (float) ($config['mood_weight'] ?? 3));
$energyWeight = max(0.0, (float) ($config['energy_weight'] ?? 2));
$stressWeight = max(0.0, (float) ($config['stress_weight'] ?? 2));
$weightTotal = max(1.0, $moodWeight + $energyWeight + $stressWeight);
$summary = is_array($entry['summary'] ?? null) ? $entry['summary'] : [];
$mood = normalize_signal_value($summary['mood'] ?? $entry['summary_mood'] ?? legacy_to_signal_scale($entry['mood'] ?? 5));
$energy = normalize_signal_value($summary['energy'] ?? $entry['summary_energy'] ?? legacy_to_signal_scale($entry['energy'] ?? 5));
$stress = normalize_signal_value($summary['stress'] ?? $entry['summary_stress'] ?? legacy_to_signal_scale($entry['stress'] ?? 5));
$base = (($mood * $moodWeight) + ($energy * $energyWeight) + ((-$stress) * $stressWeight)) / $weightTotal;
$adjustmentPoints = 0.0;
foreach ($components as $key => $value) {
if (in_array($key, ['mood', 'energy', 'stress', 'pain'], true)) {
continue;
}
$adjustmentPoints += (float) $value;
}
$pointsPerStep = max(1.0, (float) ($config['points_per_step'] ?? 12));
$cap = max(0.0, min(2.0, (float) ($config['adjustment_cap'] ?? 1.0)));
$adjustment = max(-$cap, min($cap, $adjustmentPoints / $pointsPerStep));
$raw = max(-2.0, min(2.0, $base + $adjustment));
$level = max(-2, min(2, (int) round($raw)));
return [
'base' => round($base, 2),
'adjustment' => round($adjustment, 2),
'raw' => round($raw, 2),
'level' => $level,
'percentage' => round((($raw + 2.0) / 4.0) * 100, 1),
'tone' => signal_value_class($level),
];
}
private function eventSignalPoints(array $events): float private function eventSignalPoints(array $events): float
{ {
if ($events === []) { if ($events === []) {
@@ -249,6 +291,20 @@ final class ScoringService
return $this->maxBandPoints($scoring['walk_bands'] ?? []); return $this->maxBandPoints($scoring['walk_bands'] ?? []);
} }
private function maxStepTargetPoints(array $targets): float
{
$max = 0.0;
foreach ($targets as $target) {
if (!is_array($target)) {
continue;
}
$max = max($max, (float) ($target['points'] ?? 0));
}
return $max;
}
private function stepTargetPoints(int $steps, array $targets): float private function stepTargetPoints(int $steps, array $targets): float
{ {
if ($targets === []) { if ($targets === []) {
+10
View File
@@ -22,6 +22,16 @@ final class Defaults
'sleep' => [ 'sleep' => [
'optimal_hours' => 7.0, 'optimal_hours' => 7.0,
], ],
'display' => [
'score_mode' => 'scale',
],
'day_balance' => [
'mood_weight' => 3,
'energy_weight' => 2,
'stress_weight' => 2,
'adjustment_cap' => 1.0,
'points_per_step' => 12,
],
'tracking' => [ 'tracking' => [
'pain_enabled' => false, 'pain_enabled' => false,
], ],
+1 -1
View File
@@ -116,7 +116,7 @@ $jsVersion = is_file(base_path('assets/js/app.js')) ? (string) filemtime(base_pa
<?php if (!$immersiveDashboard): ?> <?php if (!$immersiveDashboard): ?>
<footer class="site-footer glass-panel"> <footer class="site-footer glass-panel">
<a class="site-footer__link" href="https://git.hnz.io/hnzio/mood-tracking/releases" target="_blank" rel="noreferrer">Version 1.5</a> <a class="site-footer__link" href="https://git.hnz.io/hnzio/mood-tracking/releases" target="_blank" rel="noreferrer">Version 1.7.0</a>
<a class="site-footer__link" href="https://hnz.io" target="_blank" rel="noreferrer">(c) 2026 @hnz.io</a> <a class="site-footer__link" href="https://hnz.io" target="_blank" rel="noreferrer">(c) 2026 @hnz.io</a>
</footer> </footer>
<?php endif; ?> <?php endif; ?>
+29 -1
View File
@@ -11,6 +11,24 @@ $dayHealth = is_array($dayEntry['health'] ?? null) ? $dayEntry['health'] : [];
$daySteps = (int) ($dayHealth['steps'] ?? 0); $daySteps = (int) ($dayHealth['steps'] ?? 0);
$dayStepBonus = (float) ($dayEntry['evaluation']['components']['step_bonus'] ?? 0); $dayStepBonus = (float) ($dayEntry['evaluation']['components']['step_bonus'] ?? 0);
$optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hours'] ?? 7.0))); $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hours'] ?? 7.0)));
$formatBalanceValue = static function (?array $entry) use ($settings): string {
if ($entry === null) {
return '';
}
$mode = (string) ($settings['display']['score_mode'] ?? 'scale');
$balance = is_array($entry['evaluation']['balance'] ?? null) ? $entry['evaluation']['balance'] : [];
if ($mode === 'points') {
return format_points((float) ($entry['evaluation']['total'] ?? 0)) . ' Punkte';
}
if ($mode === 'percent') {
return format_points((float) ($balance['percentage'] ?? $entry['evaluation']['percentage'] ?? 0)) . ' %';
}
$level = max(-2, min(2, (int) ($balance['level'] ?? 0)));
return ($level > 0 ? '+' : '') . (string) $level;
};
?> ?>
<section class="dashboard-shell<?= $dayBackground !== null ? ' dashboard-shell--with-image' : '' ?>" data-dashboard-root> <section class="dashboard-shell<?= $dayBackground !== null ? ' dashboard-shell--with-image' : '' ?>" data-dashboard-root>
@@ -39,6 +57,7 @@ $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hou
<h1><?= e(format_display_date((string) $dayEntry['date'], false)) ?></h1> <h1><?= e(format_display_date((string) $dayEntry['date'], false)) ?></h1>
<nav class="dashboard-compare-strip" aria-label="Tagesvergleich"> <nav class="dashboard-compare-strip" aria-label="Tagesvergleich">
<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')) ?>">
@@ -54,11 +73,14 @@ $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hou
<strong class="day-summary-card__title"><?= $summaryComment !== '' ? e($summaryComment) : 'Tagesbilanz' ?></strong> <strong class="day-summary-card__title"><?= $summaryComment !== '' ? e($summaryComment) : 'Tagesbilanz' ?></strong>
<?php if ($daySteps > 0): ?> <?php if ($daySteps > 0): ?>
<span class="day-summary-card__chips"> <span class="day-summary-card__chips">
<span class="day-chip day-chip--score">Bilanz <?= e($formatBalanceValue($dayEntry)) ?></span>
<span class="day-chip"><?= e(number_format($daySteps, 0, ',', '.')) ?> Schritte</span> <span class="day-chip"><?= e(number_format($daySteps, 0, ',', '.')) ?> Schritte</span>
<?php if ($dayStepBonus > 0): ?> <?php if ($dayStepBonus > 0): ?>
<span class="day-chip day-chip--bonus">+<?= e(format_points($dayStepBonus)) ?> Punkt</span> <span class="day-chip day-chip--bonus">+<?= e(format_points($dayStepBonus)) ?> Punkt</span>
<?php endif; ?> <?php endif; ?>
</span> </span>
<?php else: ?>
<span class="day-summary-card__chips"><span class="day-chip day-chip--score">Bilanz <?= e($formatBalanceValue($dayEntry)) ?></span></span>
<?php endif; ?> <?php endif; ?>
</button> </button>
@@ -287,7 +309,7 @@ $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hou
<div class="overlay-signal-card overlay-signal-card--inline" data-stepper data-stepper-metric="<?= e($metric) ?>"> <div class="overlay-signal-card overlay-signal-card--inline" data-stepper data-stepper-metric="<?= e($metric) ?>">
<div> <div>
<h3><?= e($label) ?></h3> <h3><?= e($label) ?></h3>
<p> <p data-stepper-label>
<?= e(signal_labels_for_metric($metric)[$value]) ?> <?= e(signal_labels_for_metric($metric)[$value]) ?>
</p> </p>
</div> </div>
@@ -299,6 +321,7 @@ $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hou
<button type="button" data-stepper-minus>-</button> <button type="button" data-stepper-minus>-</button>
<button type="button" data-stepper-plus>+</button> <button type="button" data-stepper-plus>+</button>
</div> </div>
<div class="signal-scale" aria-hidden="true"><span>-2</span><span>-1</span><span>0</span><span>+1</span><span>+2</span></div>
<input type="hidden" name="<?= e($field) ?>" value="<?= e((string) $value) ?>" data-stepper-input> <input type="hidden" name="<?= e($field) ?>" value="<?= e((string) $value) ?>" data-stepper-input>
</div> </div>
</div> </div>
@@ -444,6 +467,7 @@ $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hou
<button type="button" data-stepper-minus>-</button> <button type="button" data-stepper-minus>-</button>
<button type="button" data-stepper-plus>+</button> <button type="button" data-stepper-plus>+</button>
</div> </div>
<div class="signal-scale" aria-hidden="true"><span>-2</span><span>-1</span><span>0</span><span>+1</span><span>+2</span></div>
<input type="hidden" name="<?= e($field) ?>" value="<?= e((string) $value) ?>" data-stepper-input> <input type="hidden" name="<?= e($field) ?>" value="<?= e((string) $value) ?>" data-stepper-input>
</div> </div>
</div> </div>
@@ -496,6 +520,7 @@ $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hou
</header> </header>
<nav class="range-score-strip range-score-strip--week glass-panel" aria-label="Tage dieser Woche"> <nav class="range-score-strip range-score-strip--week glass-panel" aria-label="Tage dieser Woche">
<span class="score-scale score-scale--range" aria-hidden="true"><span>+2</span><span>+1</span><span>0</span><span>-1</span><span>-2</span></span>
<?php foreach ($week['days'] as $day): ?> <?php foreach ($week['days'] as $day): ?>
<a class="range-score-day<?= !empty($day['is_current']) ? ' is-current' : '' ?><?= empty($day['has_content']) ? ' is-empty' : '' ?>" href="/?view=week&amp;date=<?= e(rawurlencode((string) $day['date'])) ?>" title="<?= e((string) $day['weekday']) ?>"> <a class="range-score-day<?= !empty($day['is_current']) ? ' is-current' : '' ?><?= empty($day['has_content']) ? ' is-empty' : '' ?>" href="/?view=week&amp;date=<?= e(rawurlencode((string) $day['date'])) ?>" title="<?= e((string) $day['weekday']) ?>">
<span class="compare-day__line<?= !empty($day['is_current']) ? ' is-primary' : '' ?> score-<?= e((string) ($day['score_level'] ?? 'empty')) ?> compare-tone-<?= e((string) ($day['line_tone'] ?? 'empty')) ?>"> <span class="compare-day__line<?= !empty($day['is_current']) ? ' is-primary' : '' ?> score-<?= e((string) ($day['score_level'] ?? 'empty')) ?> compare-tone-<?= e((string) ($day['line_tone'] ?? 'empty')) ?>">
@@ -527,6 +552,7 @@ $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hou
<div class="range-day-card__body"> <div class="range-day-card__body">
<p class="eyebrow"><?= e((string) $day['weekday']) ?></p> <p class="eyebrow"><?= e((string) $day['weekday']) ?></p>
<p class="range-day-card__score">Bilanz <?= e($formatBalanceValue($entry)) ?></p>
<p class="range-day-card__summary"><?= $summaryText !== '' ? e($summaryText) : 'Noch keine Tagesbilanz.' ?></p> <p class="range-day-card__summary"><?= $summaryText !== '' ? e($summaryText) : 'Noch keine Tagesbilanz.' ?></p>
<?php if ($events !== []): ?> <?php if ($events !== []): ?>
@@ -587,6 +613,7 @@ $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hou
</header> </header>
<nav class="range-score-strip range-score-strip--month glass-panel" aria-label="Tage dieses Monats"> <nav class="range-score-strip range-score-strip--month glass-panel" aria-label="Tage dieses Monats">
<span class="score-scale score-scale--range score-scale--month" aria-hidden="true"><span>+2</span><span>+1</span><span>0</span><span>-1</span><span>-2</span></span>
<?php foreach ($month['days'] as $day): ?> <?php foreach ($month['days'] as $day): ?>
<a class="range-score-day<?= empty($day['has_content']) ? ' is-empty' : '' ?><?= !empty($day['is_future']) ? ' is-future' : '' ?>" href="/?view=month&amp;date=<?= e(rawurlencode((string) $day['date'])) ?>" title="<?= e((string) $day['weekday']) ?>"> <a class="range-score-day<?= empty($day['has_content']) ? ' is-empty' : '' ?><?= !empty($day['is_future']) ? ' is-future' : '' ?>" href="/?view=month&amp;date=<?= e(rawurlencode((string) $day['date'])) ?>" title="<?= e((string) $day['weekday']) ?>">
<span class="compare-day__line score-<?= e((string) ($day['score_level'] ?? 'empty')) ?> compare-tone-<?= e((string) ($day['line_tone'] ?? 'empty')) ?>"> <span class="compare-day__line score-<?= e((string) ($day['score_level'] ?? 'empty')) ?> compare-tone-<?= e((string) ($day['line_tone'] ?? 'empty')) ?>">
@@ -616,6 +643,7 @@ $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hou
<a class="range-day-card range-day-card--summary-only range-day-card--<?= e($dayTone) ?> glass-panel" href="/?view=day&amp;date=<?= e(rawurlencode((string) $day['date'])) ?>"> <a class="range-day-card range-day-card--summary-only range-day-card--<?= e($dayTone) ?> glass-panel" href="/?view=day&amp;date=<?= e(rawurlencode((string) $day['date'])) ?>">
<div class="range-day-card__body"> <div class="range-day-card__body">
<p class="eyebrow"><?= e((string) $day['weekday']) ?></p> <p class="eyebrow"><?= e((string) $day['weekday']) ?></p>
<p class="range-day-card__score">Bilanz <?= e($formatBalanceValue($entry)) ?></p>
<p class="range-day-card__summary"><?= $summaryText !== '' ? e($summaryText) : 'Tagesbilanz' ?></p> <p class="range-day-card__summary"><?= $summaryText !== '' ? e($summaryText) : 'Tagesbilanz' ?></p>
</div> </div>
</a> </a>
+40 -1
View File
@@ -110,6 +110,18 @@
<label><span>Bonuspunkte</span><input type="number" name="settings[scoring][step_bonus][points]" value="<?= e((string) ($settings['scoring']['step_bonus']['points'] ?? 1)) ?>" min="0" max="20"></label> <label><span>Bonuspunkte</span><input type="number" name="settings[scoring][step_bonus][points]" value="<?= e((string) ($settings['scoring']['step_bonus']['points'] ?? 1)) ?>" min="0" max="20"></label>
</div> </div>
</div> </div>
<div class="settings-section">
<h4>Schritte-Zielkurve</h4>
<p class="helper-text">Diese Punkte fließen als kleiner Bonus oder Malus in die Tagesbilanz ein. Positive Werte motivieren, negative Werte markieren zu wenig oder zu viel Bewegung.</p>
<div class="band-grid">
<?php foreach (($settings['scoring']['walk_step_targets'] ?? []) as $index => $target): ?>
<div class="band-card">
<label><span>Schritte</span><input type="number" name="settings[scoring][walk_step_targets][<?= e((string) $index) ?>][steps]" value="<?= e((string) ($target['steps'] ?? 0)) ?>" min="0" max="100000"></label>
<label><span>Punkte</span><input type="number" name="settings[scoring][walk_step_targets][<?= e((string) $index) ?>][points]" value="<?= e((string) ($target['points'] ?? 0)) ?>" min="-20" max="20"></label>
</div>
<?php endforeach; ?>
</div>
</div>
<button class="primary-button" type="submit">Schritte speichern</button> <button class="primary-button" type="submit">Schritte speichern</button>
</form> </form>
</div> </div>
@@ -204,6 +216,19 @@
<form method="post" action="/options" class="stack-form stack-form--spacious"> <form method="post" action="/options" class="stack-form stack-form--spacious">
<?= csrf_field() ?> <?= csrf_field() ?>
<input type="hidden" name="form_name" value="settings"> <input type="hidden" name="form_name" value="settings">
<div class="settings-section">
<h4>Tagesbilanz als Hauptmetrik</h4>
<p class="helper-text">Stimmung, Energie und Stress bilden die Basis. Schlaf, Schritte, Sport, Spaziergang und Notizen verschieben den Tag nur gedeckelt in eine positivere oder negativere Richtung.</p>
<div class="field-grid field-grid--three">
<label><span>Gewicht Stimmung</span><input type="number" name="settings[day_balance][mood_weight]" value="<?= e((string) ($settings['day_balance']['mood_weight'] ?? 3)) ?>" min="0" max="10"></label>
<label><span>Gewicht Energie</span><input type="number" name="settings[day_balance][energy_weight]" value="<?= e((string) ($settings['day_balance']['energy_weight'] ?? 2)) ?>" min="0" max="10"></label>
<label><span>Gewicht Stress</span><input type="number" name="settings[day_balance][stress_weight]" value="<?= e((string) ($settings['day_balance']['stress_weight'] ?? 2)) ?>" min="0" max="10"></label>
</div>
<div class="field-grid field-grid--two">
<label><span>Max. Bonus/Malus in Stufen</span><input type="number" name="settings[day_balance][adjustment_cap]" value="<?= e((string) ($settings['day_balance']['adjustment_cap'] ?? 1.0)) ?>" min="0" max="2" step="0.1"></label>
<label><span>Punkte pro Stufenverschiebung</span><input type="number" name="settings[day_balance][points_per_step]" value="<?= e((string) ($settings['day_balance']['points_per_step'] ?? 12)) ?>" min="1" max="50"></label>
</div>
</div>
<div class="settings-section"><h4>Bewertungsskala</h4><div class="band-grid"><?php foreach ($settings['ratings'] as $index => $rating): ?><div class="band-card"><label><span>Label</span><input type="text" name="settings[ratings][<?= e((string) $index) ?>][label]" value="<?= e($rating['label']) ?>"></label><label><span>Min</span><input type="number" name="settings[ratings][<?= e((string) $index) ?>][min]" value="<?= e((string) $rating['min']) ?>"></label><label><span>Max</span><input type="number" name="settings[ratings][<?= e((string) $index) ?>][max]" value="<?= e((string) $rating['max']) ?>"></label></div><?php endforeach; ?></div></div> <div class="settings-section"><h4>Bewertungsskala</h4><div class="band-grid"><?php foreach ($settings['ratings'] as $index => $rating): ?><div class="band-card"><label><span>Label</span><input type="text" name="settings[ratings][<?= e((string) $index) ?>][label]" value="<?= e($rating['label']) ?>"></label><label><span>Min</span><input type="number" name="settings[ratings][<?= e((string) $index) ?>][min]" value="<?= e((string) $rating['min']) ?>"></label><label><span>Max</span><input type="number" name="settings[ratings][<?= e((string) $index) ?>][max]" value="<?= e((string) $rating['max']) ?>"></label></div><?php endforeach; ?></div></div>
<div class="settings-section"><h4>Schutzregeln</h4><div class="band-grid"><?php foreach ($settings['guardrails'] as $index => $guardrail): ?><div class="band-card"><label><span>Stimmung max</span><input type="number" name="settings[guardrails][<?= e((string) $index) ?>][mood_max]" value="<?= e((string) $guardrail['mood_max']) ?>" min="1" max="10"></label><label><span>Energie max</span><input type="number" name="settings[guardrails][<?= e((string) $index) ?>][energy_max]" value="<?= e((string) ($guardrail['energy_max'] ?? '')) ?>" min="1" max="10"></label><label><span>Maximales Label</span><input type="text" name="settings[guardrails][<?= e((string) $index) ?>][cap_label]" value="<?= e($guardrail['cap_label']) ?>"></label></div><?php endforeach; ?></div></div> <div class="settings-section"><h4>Schutzregeln</h4><div class="band-grid"><?php foreach ($settings['guardrails'] as $index => $guardrail): ?><div class="band-card"><label><span>Stimmung max</span><input type="number" name="settings[guardrails][<?= e((string) $index) ?>][mood_max]" value="<?= e((string) $guardrail['mood_max']) ?>" min="1" max="10"></label><label><span>Energie max</span><input type="number" name="settings[guardrails][<?= e((string) $index) ?>][energy_max]" value="<?= e((string) ($guardrail['energy_max'] ?? '')) ?>" min="1" max="10"></label><label><span>Maximales Label</span><input type="text" name="settings[guardrails][<?= e((string) $index) ?>][cap_label]" value="<?= e($guardrail['cap_label']) ?>"></label></div><?php endforeach; ?></div></div>
<label><span>Tagebuchpunkte bei nicht-leerer Notiz</span><input type="number" name="settings[scoring][journal_points]" value="<?= e((string) $settings['scoring']['journal_points']) ?>" min="0" max="20"></label> <label><span>Tagebuchpunkte bei nicht-leerer Notiz</span><input type="number" name="settings[scoring][journal_points]" value="<?= e((string) $settings['scoring']['journal_points']) ?>" min="0" max="20"></label>
@@ -213,16 +238,30 @@
<div class="options-panel" data-options-panel="stats" hidden> <div class="options-panel" data-options-panel="stats" hidden>
<h2>Statistik</h2> <h2>Statistik</h2>
<form method="post" action="/options" class="stack-form stack-form--spacious stats-settings-form">
<?= csrf_field() ?>
<input type="hidden" name="form_name" value="settings">
<div class="settings-section">
<h4>Statistik-Darstellung</h4>
<label><span>Hauptwert anzeigen als</span><select name="settings[display][score_mode]">
<?php foreach (['scale' => '5-Stufen-Bilanz', 'percent' => 'Prozentwert', 'points' => 'Punkte'] as $mode => $label): ?>
<option value="<?= e($mode) ?>" <?= ($settings['display']['score_mode'] ?? 'scale') === $mode ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select></label>
</div>
<button class="primary-button" type="submit">Statistik speichern</button>
</form>
<section class="stats-grid"> <section class="stats-grid">
<article class="metric-card glass-panel"><span>Getrackte Tage</span><strong><?= e((string) $statsSummary['tracked_days']) ?></strong></article> <article class="metric-card glass-panel"><span>Getrackte Tage</span><strong><?= e((string) $statsSummary['tracked_days']) ?></strong></article>
<article class="metric-card glass-panel"><span>Ø Score</span><strong><?= e(format_points((float) $statsSummary['average_score'])) ?></strong></article> <article class="metric-card glass-panel"><span>Ø Score</span><strong><?= e(format_points((float) $statsSummary['average_score'])) ?></strong></article>
<article class="metric-card glass-panel"><span>Ø Bilanz</span><strong><?= ((float) $statsSummary['average_balance']) > 0 ? '+' : '' ?><?= e(format_points((float) $statsSummary['average_balance'])) ?></strong></article>
<article class="metric-card glass-panel"><span>Ø Stimmung</span><strong><?= e(format_points((float) $statsSummary['average_mood'])) ?>/10</strong></article> <article class="metric-card glass-panel"><span>Ø Stimmung</span><strong><?= e(format_points((float) $statsSummary['average_mood'])) ?>/10</strong></article>
<article class="metric-card glass-panel"><span>Ø Stress</span><strong><?= e(format_points((float) $statsSummary['average_stress'])) ?>/10</strong></article> <article class="metric-card glass-panel"><span>Ø Stress</span><strong><?= e(format_points((float) $statsSummary['average_stress'])) ?>/10</strong></article>
<article class="metric-card glass-panel"><span>Serie</span><strong><?= e((string) $statsSummary['streak']) ?> Tage</strong></article> <article class="metric-card glass-panel"><span>Serie</span><strong><?= e((string) $statsSummary['streak']) ?> Tage</strong></article>
</section> </section>
<section class="dashboard-grid dashboard-grid--embedded-stats"> <section class="dashboard-grid dashboard-grid--embedded-stats">
<article class="glass-panel chart-card chart-card--calendar"><div class="section-head"><div><p class="eyebrow">Kalender</p><h3>Gesamtstimmung pro Tag</h3></div></div><div id="calendar-heatmap" class="calendar-heatmap" data-payload="<?= e($statsChartPayload) ?>"></div></article> <article class="glass-panel chart-card chart-card--calendar"><div class="section-head"><div><p class="eyebrow">Kalender</p><h3>Gesamtstimmung pro Tag</h3></div></div><div id="calendar-heatmap" class="calendar-heatmap" data-payload="<?= e($statsChartPayload) ?>"></div></article>
<article class="glass-panel chart-card"><div class="section-head"><div><p class="eyebrow">Trend</p><h3>Tagesstimmung</h3></div></div><div class="line-chart" data-chart-type="line" data-series="mood" data-payload="<?= e($statsChartPayload) ?>"></div></article> <article class="glass-panel chart-card"><div class="section-head"><div><p class="eyebrow">Trend</p><h3>Errechnete Tagesbilanz</h3></div></div><div class="line-chart" data-chart-type="line" data-series="balance" data-payload="<?= e($statsChartPayload) ?>"></div></article>
<article class="glass-panel chart-card"><div class="section-head"><div><p class="eyebrow">Belastung</p><h3>Stressverlauf</h3></div></div><div class="line-chart" data-chart-type="line" data-series="stress" data-payload="<?= e($statsChartPayload) ?>"></div></article> <article class="glass-panel chart-card"><div class="section-head"><div><p class="eyebrow">Belastung</p><h3>Stressverlauf</h3></div></div><div class="line-chart" data-chart-type="line" data-series="stress" data-payload="<?= e($statsChartPayload) ?>"></div></article>
<article class="glass-panel chart-card chart-card--wide"><div class="section-head"><div><p class="eyebrow">Aktivität</p><h3>Sport und Spaziergang</h3></div></div><div class="bar-chart" data-chart-type="bars" data-series="sport" data-payload="<?= e($statsChartPayload) ?>"></div></article> <article class="glass-panel chart-card chart-card--wide"><div class="section-head"><div><p class="eyebrow">Aktivität</p><h3>Sport und Spaziergang</h3></div></div><div class="bar-chart" data-chart-type="bars" data-series="sport" data-payload="<?= e($statsChartPayload) ?>"></div></article>
</section> </section>