Add multi-sport tracking with configurable recovery bonuses

This commit is contained in:
2026-04-11 20:12:21 +02:00
parent 3e5cdfb717
commit 2cfd59871c
16 changed files with 926 additions and 35 deletions
+66 -22
View File
@@ -184,16 +184,9 @@ final class App
private function showDashboard(): void
{
$user = $this->requireUser();
$settings = $this->settings->forUser($user['username']);
$settings = $this->hydrateSettings($this->settings->forUser($user['username']));
$entries = $this->entries->all($user['username']);
$evaluatedEntries = [];
foreach ($entries as $entry) {
$evaluation = $this->scoring->evaluate($entry, $settings);
$evaluatedEntries[] = array_merge($entry, ['evaluation' => $evaluation]);
}
usort($evaluatedEntries, static fn (array $a, array $b): int => strcmp($a['date'], $b['date']));
$evaluatedEntries = $this->evaluateEntriesWithContext($entries, $settings);
$summary = $this->buildDashboardSummary($evaluatedEntries);
$chartData = $this->buildDashboardCharts($evaluatedEntries);
@@ -211,7 +204,7 @@ final class App
private function showTrack(): void
{
$user = $this->requireUser();
$settings = $this->settings->forUser($user['username']);
$settings = $this->hydrateSettings($this->settings->forUser($user['username']));
$date = (string) ($_GET['date'] ?? today());
if (!$this->isValidDate($date)) {
$date = today();
@@ -224,12 +217,15 @@ final class App
'sleep_hours' => 7,
'sleep_feeling' => 3,
'sport_minutes' => 0,
'sport_type' => '',
'sport_types' => [],
'walk_minutes' => 0,
'note' => '',
];
$entry = $this->scoring->normalize($entry);
$evaluation = $this->scoring->evaluate($entry, $settings);
$previousEntry = $this->entries->find($user['username'], shift_date($date, -1));
$evaluation = $this->scoring->evaluate($entry, $settings, $previousEntry);
View::render('track', [
'pageTitle' => 'Tag tracken',
@@ -238,11 +234,13 @@ final class App
'entry' => $entry,
'evaluation' => $evaluation,
'settings' => $settings,
'sportTypes' => normalized_sport_types($settings),
'trackMood' => $evaluation['sentiment'],
'topbarDate' => $entry['date'],
'trackPayload' => encode_payload([
'settings' => $settings,
'entry' => $entry,
'previousEntry' => $previousEntry !== null ? $this->scoring->normalize($previousEntry) : null,
]),
]);
}
@@ -252,7 +250,7 @@ final class App
$this->enforceCsrf();
$user = $this->requireUser();
$settings = $this->settings->forUser($user['username']);
$settings = $this->hydrateSettings($this->settings->forUser($user['username']));
$entry = $this->scoring->normalize([
'date' => $_POST['date'] ?? today(),
@@ -262,6 +260,7 @@ final class App
'sleep_hours' => $_POST['sleep_hours'] ?? 0,
'sleep_feeling' => $_POST['sleep_feeling'] ?? 3,
'sport_minutes' => $_POST['sport_minutes'] ?? 0,
'sport_types' => $_POST['sport_types'] ?? [],
'walk_minutes' => $_POST['walk_minutes'] ?? 0,
'note' => $_POST['note'] ?? '',
]);
@@ -271,7 +270,8 @@ final class App
redirect('/track');
}
$evaluation = $this->scoring->evaluate($entry, $settings);
$previousEntry = $this->entries->find($user['username'], shift_date($entry['date'], -1));
$evaluation = $this->scoring->evaluate($entry, $settings, $previousEntry);
$this->entries->save($user['username'], $entry['date'], $entry, $evaluation);
flash('success', 'Der Tag wurde gespeichert.');
@@ -281,16 +281,10 @@ final class App
private function showArchive(): void
{
$user = $this->requireUser();
$settings = $this->settings->forUser($user['username']);
$settings = $this->hydrateSettings($this->settings->forUser($user['username']));
$selectedDate = isset($_GET['date']) ? (string) $_GET['date'] : null;
$entries = $this->entries->all($user['username']);
$archive = [];
foreach ($entries as $entry) {
$archive[] = array_merge($entry, [
'evaluation' => $this->scoring->evaluate($entry, $settings),
]);
}
$archive = array_reverse($this->evaluateEntriesWithContext($entries, $settings));
$selectedEntry = null;
if ($selectedDate !== null) {
@@ -314,7 +308,7 @@ final class App
private function showOptions(): void
{
$user = $this->requireUser();
$settings = $this->settings->forUser($user['username']);
$settings = $this->hydrateSettings($this->settings->forUser($user['username']));
View::render('options', [
'pageTitle' => 'Optionen',
@@ -329,6 +323,10 @@ final class App
'sleep_hours' => 7,
'sleep_feeling' => 5,
'sport_minutes' => 999,
'sport_types' => array_map(
static fn (array $type): string => (string) ($type['id'] ?? ''),
normalized_sport_types($settings)
),
'walk_minutes' => 999,
'note' => 'x',
], $settings)['max_total'],
@@ -467,6 +465,15 @@ final class App
'value' => $entry['sport_minutes'] + $entry['walk_minutes'],
'sport' => $entry['sport_minutes'],
'walk' => $entry['walk_minutes'],
'sport_labels' => array_values(array_filter(array_map(
static fn (array $type): string => (string) ($type['label'] ?? ''),
$entry['sport_type_meta'] ?? []
))),
'sport_icons' => array_values(array_filter(array_map(
static fn (array $type): ?string => isset($type['icon']) ? sport_icon_path((string) $type['icon']) : null,
$entry['sport_type_meta'] ?? []
))),
'sport_bonus' => (float) ($entry['evaluation']['components']['sport_bonus'] ?? 0),
];
}, $recent),
];
@@ -542,9 +549,46 @@ final class App
];
}
$settings['sport_types'] = normalized_sport_types([
'sport_types' => is_array($input['sport_types'] ?? null)
? $input['sport_types']
: $defaults['sport_types'],
]);
return $settings;
}
private function hydrateSettings(array $settings): array
{
$settings['sport_types'] = normalized_sport_types($settings);
return $settings;
}
private function evaluateEntriesWithContext(array $entries, array $settings): array
{
$normalized = array_map(fn (array $entry): array => $this->scoring->normalize($entry), $entries);
usort($normalized, static fn (array $a, array $b): int => strcmp($a['date'], $b['date']));
$entryMap = [];
foreach ($normalized as $entry) {
$entryMap[$entry['date']] = $entry;
}
$evaluated = [];
foreach ($normalized as $entry) {
$previousEntry = $entryMap[shift_date($entry['date'], -1)] ?? null;
$evaluation = $this->scoring->evaluate($entry, $settings, $previousEntry);
$evaluated[] = array_merge($entry, [
'evaluation' => $evaluation,
'sport_type_meta' => find_sport_types($settings, $entry['sport_types']),
]);
}
return $evaluated;
}
private function sendSecurityHeaders(): void
{
header('Referrer-Policy: strict-origin-when-cross-origin');