Refine balance scoring and dashboard views
This commit is contained in:
+67
-3
@@ -547,6 +547,34 @@ body.page-dashboard .content {
|
||||
.compare-day__line.score-2 .compare-day__marker { top: 12%; }
|
||||
.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-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; }
|
||||
@@ -1559,6 +1587,7 @@ body.page-dashboard .content {
|
||||
}
|
||||
|
||||
.range-score-strip {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
align-items: stretch;
|
||||
@@ -1569,13 +1598,14 @@ body.page-dashboard .content {
|
||||
|
||||
.range-score-strip--week {
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
padding-left: 2.45rem;
|
||||
}
|
||||
|
||||
.range-score-strip--month {
|
||||
display: flex;
|
||||
gap: 0.18rem;
|
||||
overflow: visible;
|
||||
padding-inline: 0.8rem;
|
||||
padding-inline: 2.45rem 0.8rem;
|
||||
}
|
||||
|
||||
.range-score-day {
|
||||
@@ -1635,11 +1665,15 @@ body.page-dashboard .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-height: min(68vh, 56rem);
|
||||
overflow: auto;
|
||||
overflow: visible;
|
||||
padding-right: 0.15rem;
|
||||
}
|
||||
|
||||
.range-day-list--month {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
||||
}
|
||||
|
||||
.range-day-card {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
@@ -1713,6 +1747,18 @@ body.page-dashboard .content {
|
||||
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 {
|
||||
font-size: clamp(1rem, 2.2vw, 1.25rem);
|
||||
}
|
||||
@@ -1739,6 +1785,15 @@ body.page-dashboard .content {
|
||||
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 {
|
||||
margin-top: 0.16rem;
|
||||
color: rgba(239, 247, 255, 0.64);
|
||||
@@ -2248,6 +2303,15 @@ body.page-dashboard .content {
|
||||
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 {
|
||||
position: relative;
|
||||
top: auto;
|
||||
|
||||
+30
-12
@@ -425,7 +425,10 @@
|
||||
let minValue = Math.min(...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);
|
||||
maxValue = Math.min(10, maxValue + 1.5);
|
||||
} else {
|
||||
@@ -435,7 +438,10 @@
|
||||
|
||||
if ((maxValue - minValue) < 3) {
|
||||
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);
|
||||
maxValue = Math.min(10, center + 1.5);
|
||||
} else {
|
||||
@@ -701,7 +707,7 @@
|
||||
}
|
||||
|
||||
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`;
|
||||
|
||||
if (!item.entry) {
|
||||
@@ -732,7 +738,7 @@
|
||||
</div>
|
||||
<div class="calendar-detail__score">
|
||||
<span data-calendar-score>${formatNumber(Number(latestVisibleEntry.score))}</span>
|
||||
<small>Punkte</small>
|
||||
<small>% Bilanz</small>
|
||||
</div>
|
||||
<a class="ghost-link calendar-detail__link" data-calendar-link href="${dashboardDayPath(latestVisibleEntry.date)}">Tag öffnen</a>
|
||||
`;
|
||||
@@ -1070,6 +1076,12 @@
|
||||
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) => {
|
||||
if (!element) {
|
||||
return;
|
||||
@@ -1113,6 +1125,10 @@
|
||||
const metric = stepper.dataset.stepperMetric || "mood";
|
||||
input.value = String(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;
|
||||
plus.disabled = current >= 2;
|
||||
const ring = stepper.querySelector(".overlay-signal-card__ring");
|
||||
@@ -1434,20 +1450,22 @@
|
||||
let pointerStartX = 0;
|
||||
let pointerStartY = 0;
|
||||
let dragging = false;
|
||||
let ignoreSwipe = false;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (deltaX < 0 && swipeContainer.dataset.prevDate) {
|
||||
window.location.href = dashboardDayPath(swipeContainer.dataset.prevDate);
|
||||
} else if (deltaX > 0 && swipeContainer.dataset.nextDate) {
|
||||
if (deltaX < 0 && 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 => {
|
||||
ignoreSwipe = Boolean(event.target.closest("a, button, input, textarea, select, label, [data-event-editable]"));
|
||||
dragging = true;
|
||||
pointerStartX = event.clientX;
|
||||
pointerStartY = event.clientY;
|
||||
@@ -1471,12 +1489,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowLeft" && swipeContainer.dataset.prevDate) {
|
||||
window.location.href = dashboardDayPath(swipeContainer.dataset.prevDate);
|
||||
if (event.key === "ArrowLeft" && swipeContainer.dataset.nextDate) {
|
||||
window.location.href = dashboardDayPath(swipeContainer.dataset.nextDate);
|
||||
}
|
||||
|
||||
if (event.key === "ArrowRight" && swipeContainer.dataset.nextDate) {
|
||||
window.location.href = dashboardDayPath(swipeContainer.dataset.nextDate);
|
||||
if (event.key === "ArrowRight" && swipeContainer.dataset.prevDate) {
|
||||
window.location.href = dashboardDayPath(swipeContainer.dataset.prevDate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+82
-29
@@ -1547,6 +1547,7 @@ final class App
|
||||
$isPersisted = isset($entryMap[$dayDate]);
|
||||
$hasContent = $isPersisted || $this->entryHasContent($entry);
|
||||
$visualScore = $this->dashboardVisualScore($entry, $isPersisted);
|
||||
$lineLevel = $this->dashboardLineLevel($entry, $isPersisted);
|
||||
|
||||
$days[] = [
|
||||
'date' => $dayDate,
|
||||
@@ -1556,9 +1557,9 @@ final class App
|
||||
'is_current' => $dayDate === $date,
|
||||
'has_content' => $hasContent,
|
||||
'visual_score' => $visualScore,
|
||||
'score_level' => $visualScore,
|
||||
'line_level' => $this->dashboardLineLevel($entry, $isPersisted),
|
||||
'line_tone' => $visualScore === null ? 'empty' : signal_value_class($visualScore),
|
||||
'score_level' => $lineLevel,
|
||||
'line_level' => $lineLevel,
|
||||
'line_tone' => $lineLevel === null ? 'empty' : signal_value_class($lineLevel),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1608,7 +1609,7 @@ final class App
|
||||
$iso = $day->format('Y-m-d');
|
||||
$entry = $entryMap[$iso] ?? null;
|
||||
$hasContent = $entry !== null && $this->entryHasContent($entry);
|
||||
$visualScore = $entry !== null ? $this->dashboardVisualScore($entry, true) : null;
|
||||
$lineLevel = $entry !== null ? $this->dashboardLineLevel($entry, true) : null;
|
||||
|
||||
$days[] = [
|
||||
'date' => $iso,
|
||||
@@ -1617,8 +1618,8 @@ final class App
|
||||
'day' => $day->format('j'),
|
||||
'entry' => $entry,
|
||||
'has_content' => $hasContent,
|
||||
'score_level' => $visualScore,
|
||||
'line_tone' => $visualScore === null ? 'empty' : signal_value_class($visualScore),
|
||||
'score_level' => $lineLevel,
|
||||
'line_tone' => $lineLevel === null ? 'empty' : signal_value_class($lineLevel),
|
||||
'is_current' => $iso === $selectedDate,
|
||||
];
|
||||
}
|
||||
@@ -1721,15 +1722,15 @@ final class App
|
||||
$iso = $day->format('Y-m-d');
|
||||
$entry = $entryMap[$iso] ?? null;
|
||||
$hasContent = $entry !== null && $this->entryHasContent($entry);
|
||||
$visualScore = $entry !== null ? $this->dashboardVisualScore($entry, true) : null;
|
||||
$lineLevel = $entry !== null ? $this->dashboardLineLevel($entry, true) : null;
|
||||
$days[] = [
|
||||
'date' => $iso,
|
||||
'day' => $day->format('j'),
|
||||
'weekday' => format_display_date($iso, true),
|
||||
'entry' => $entry,
|
||||
'has_content' => $hasContent,
|
||||
'score_level' => $visualScore,
|
||||
'line_tone' => $visualScore === null ? 'empty' : signal_value_class($visualScore),
|
||||
'score_level' => $lineLevel,
|
||||
'line_tone' => $lineLevel === null ? 'empty' : signal_value_class($lineLevel),
|
||||
'is_future' => $iso > $selectedDate,
|
||||
];
|
||||
}
|
||||
@@ -1787,14 +1788,18 @@ final class App
|
||||
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)));
|
||||
|
||||
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
|
||||
{
|
||||
return signal_value_class($this->dashboardVisualScore($entry, $isPersisted) ?? 0);
|
||||
return signal_value_class($this->dashboardLineLevel($entry, $isPersisted) ?? 0);
|
||||
}
|
||||
|
||||
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)
|
||||
: 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
|
||||
? round(array_sum(array_map(static fn (array $entry): int => (int) $entry['mood'], $entries)) / $count, 1)
|
||||
: 0.0;
|
||||
@@ -2566,6 +2575,7 @@ final class App
|
||||
return [
|
||||
'tracked_days' => $count,
|
||||
'average_score' => $avgScore,
|
||||
'average_balance' => $avgBalance,
|
||||
'average_mood' => $avgMood,
|
||||
'average_stress' => $avgStress,
|
||||
'streak' => $this->calculateStreak($entries),
|
||||
@@ -2825,13 +2835,20 @@ final class App
|
||||
|
||||
return [
|
||||
'calendar' => array_map(static function (array $entry): array {
|
||||
$balance = is_array($entry['evaluation']['balance'] ?? null) ? $entry['evaluation']['balance'] : [];
|
||||
return [
|
||||
'date' => $entry['date'],
|
||||
'score' => $entry['evaluation']['total'],
|
||||
'max' => $entry['evaluation']['max_total'],
|
||||
'score' => (float) ($balance['percentage'] ?? $entry['evaluation']['percentage'] ?? 0),
|
||||
'max' => 100,
|
||||
'label' => $entry['evaluation']['label'],
|
||||
];
|
||||
}, $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 {
|
||||
return [
|
||||
'date' => $entry['date'],
|
||||
@@ -2915,13 +2932,24 @@ final class App
|
||||
$settings['sleep'] = [
|
||||
'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']['energy_multiplier'] = max(0, min(10, (int) ($input['scoring']['energy_multiplier'] ?? 2)));
|
||||
$settings['scoring']['stress_multiplier'] = max(0, min(10, (int) ($input['scoring']['stress_multiplier'] ?? 2)));
|
||||
$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'] ?? ($settings['scoring']['energy_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']['sleep_feeling_multiplier'] = max(0, min(10, (int) ($input['scoring']['sleep_feeling_multiplier'] ?? 2)));
|
||||
$settings['scoring']['journal_points'] = max(0, min(20, (int) ($input['scoring']['journal_points'] ?? 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'] ?? ($settings['scoring']['journal_points'] ?? 2))));
|
||||
$stepBonus = is_array($settings['scoring']['step_bonus'] ?? null) ? $settings['scoring']['step_bonus'] : $defaults['scoring']['step_bonus'];
|
||||
$settings['scoring']['step_bonus'] = [
|
||||
'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['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) {
|
||||
$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) {
|
||||
@@ -2946,25 +2976,35 @@ final class App
|
||||
$settings['scoring'][$bandKey][$index] = [
|
||||
'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']))),
|
||||
'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) {
|
||||
$currentRating = $settings['ratings'][$index] ?? $defaultRating;
|
||||
$settings['ratings'][$index] = [
|
||||
'label' => trim((string) ($input['ratings'][$index]['label'] ?? $defaultRating['label'])) ?: $defaultRating['label'],
|
||||
'min' => max(0, min(999, (int) ($input['ratings'][$index]['min'] ?? $defaultRating['min']))),
|
||||
'max' => max(0, min(999, (int) ($input['ratings'][$index]['max'] ?? $defaultRating['max']))),
|
||||
'label' => trim((string) ($input['ratings'][$index]['label'] ?? $currentRating['label'] ?? $defaultRating['label'])) ?: $defaultRating['label'],
|
||||
'min' => max(0, min(999, (int) ($input['ratings'][$index]['min'] ?? $currentRating['min'] ?? $defaultRating['min']))),
|
||||
'max' => max(0, min(999, (int) ($input['ratings'][$index]['max'] ?? $currentRating['max'] ?? $defaultRating['max']))),
|
||||
];
|
||||
}
|
||||
|
||||
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] = [
|
||||
'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)),
|
||||
'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([
|
||||
'sport_types' => $sportTypesProvided && is_array($input['sport_types'] ?? null)
|
||||
? $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'])));
|
||||
@@ -2983,7 +3023,9 @@ final class App
|
||||
}
|
||||
|
||||
$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,
|
||||
];
|
||||
|
||||
@@ -3002,6 +3044,17 @@ final class App
|
||||
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['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(
|
||||
Defaults::settings()['tracking'],
|
||||
is_array($settings['tracking'] ?? null) ? $settings['tracking'] : []
|
||||
|
||||
@@ -71,8 +71,9 @@ final class ScoringService
|
||||
'sleep_feeling' => $entry['sleep_feeling'] * (float) $scoring['sleep_feeling_multiplier'],
|
||||
'sport_minutes' => $this->bandPoints((int) $entry['sport_minutes'], $scoring['sport_bands']),
|
||||
'sport_bonus' => $sportBonus,
|
||||
'walk_minutes' => 0.0,
|
||||
'walk_minutes' => $this->walkPoints($entry, $settings),
|
||||
'step_bonus' => $this->stepBonusPoints($entry, $scoring['step_bonus'] ?? []),
|
||||
'daily_steps' => $this->stepTargetPoints((int) ($entry['health']['steps'] ?? 0), $scoring['walk_step_targets'] ?? []),
|
||||
'events' => $eventSignalPoints,
|
||||
'alcohol' => !empty($entry['alcohol']) ? ((float) abs((float) ($scoring['alcohol_penalty'] ?? 5)) * -1) : 0.0,
|
||||
'note' => $entry['note'] === '' ? 0 : (float) $scoring['journal_points'],
|
||||
@@ -92,6 +93,8 @@ final class ScoringService
|
||||
(5 * (float) $scoring['sleep_feeling_multiplier']) +
|
||||
$this->maxBandPoints($scoring['sport_bands']) +
|
||||
$this->maxSportBonusPoints($settings) +
|
||||
$this->maxWalkPoints($entry, $settings) +
|
||||
$this->maxStepTargetPoints($scoring['walk_step_targets'] ?? []) +
|
||||
max(0.0, (float) ($scoring['step_bonus']['points'] ?? 0)) +
|
||||
($eventSignalPoints !== 0.0 ? 8.0 : 0.0) +
|
||||
(float) $scoring['journal_points'],
|
||||
@@ -123,11 +126,50 @@ final class ScoringService
|
||||
'guardrail' => $guardrail,
|
||||
'sentiment' => $this->sentimentForLabel($label, $ratings),
|
||||
'percentage' => $maxTotal > 0 ? round(($total / $maxTotal) * 100, 1) : 0,
|
||||
'balance' => $this->dayBalance($entry, $components, $settings),
|
||||
'sport_type' => $sportTypes[0] ?? null,
|
||||
'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
|
||||
{
|
||||
if ($events === []) {
|
||||
@@ -249,6 +291,20 @@ final class ScoringService
|
||||
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
|
||||
{
|
||||
if ($targets === []) {
|
||||
|
||||
@@ -22,6 +22,16 @@ final class Defaults
|
||||
'sleep' => [
|
||||
'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' => [
|
||||
'pain_enabled' => false,
|
||||
],
|
||||
|
||||
@@ -116,7 +116,7 @@ $jsVersion = is_file(base_path('assets/js/app.js')) ? (string) filemtime(base_pa
|
||||
|
||||
<?php if (!$immersiveDashboard): ?>
|
||||
<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>
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -11,6 +11,24 @@ $dayHealth = is_array($dayEntry['health'] ?? null) ? $dayEntry['health'] : [];
|
||||
$daySteps = (int) ($dayHealth['steps'] ?? 0);
|
||||
$dayStepBonus = (float) ($dayEntry['evaluation']['components']['step_bonus'] ?? 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>
|
||||
@@ -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>
|
||||
|
||||
<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): ?>
|
||||
<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')) ?>">
|
||||
@@ -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>
|
||||
<?php if ($daySteps > 0): ?>
|
||||
<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>
|
||||
<?php if ($dayStepBonus > 0): ?>
|
||||
<span class="day-chip day-chip--bonus">+<?= e(format_points($dayStepBonus)) ?> Punkt</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="day-summary-card__chips"><span class="day-chip day-chip--score">Bilanz <?= e($formatBalanceValue($dayEntry)) ?></span></span>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
<h3><?= e($label) ?></h3>
|
||||
<p>
|
||||
<p data-stepper-label>
|
||||
<?= e(signal_labels_for_metric($metric)[$value]) ?>
|
||||
</p>
|
||||
</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-plus>+</button>
|
||||
</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>
|
||||
</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-plus>+</button>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -496,6 +520,7 @@ $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hou
|
||||
</header>
|
||||
|
||||
<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): ?>
|
||||
<a class="range-score-day<?= !empty($day['is_current']) ? ' is-current' : '' ?><?= empty($day['has_content']) ? ' is-empty' : '' ?>" href="/?view=week&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')) ?>">
|
||||
@@ -527,6 +552,7 @@ $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hou
|
||||
|
||||
<div class="range-day-card__body">
|
||||
<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>
|
||||
|
||||
<?php if ($events !== []): ?>
|
||||
@@ -587,6 +613,7 @@ $optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hou
|
||||
</header>
|
||||
|
||||
<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): ?>
|
||||
<a class="range-score-day<?= empty($day['has_content']) ? ' is-empty' : '' ?><?= !empty($day['is_future']) ? ' is-future' : '' ?>" href="/?view=month&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')) ?>">
|
||||
@@ -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&date=<?= e(rawurlencode((string) $day['date'])) ?>">
|
||||
<div class="range-day-card__body">
|
||||
<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>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -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>
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
@@ -204,6 +216,19 @@
|
||||
<form method="post" action="/options" class="stack-form stack-form--spacious">
|
||||
<?= csrf_field() ?>
|
||||
<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>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>
|
||||
@@ -213,16 +238,30 @@
|
||||
|
||||
<div class="options-panel" data-options-panel="stats" hidden>
|
||||
<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">
|
||||
<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>Ø 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>Ø 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>
|
||||
</section>
|
||||
<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"><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 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>
|
||||
|
||||
Reference in New Issue
Block a user