Add multi-sport tracking with configurable recovery bonuses
This commit is contained in:
+66
-22
@@ -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');
|
||||
|
||||
@@ -62,6 +62,21 @@ final class EntryRepository
|
||||
|
||||
private function parse(string $content, string $fallbackDate): ?array
|
||||
{
|
||||
$sportTypes = [];
|
||||
$sportTypesRaw = (string) ($this->extract('/^- Sportarten:\s*(.*)$/mu', $content) ?? '');
|
||||
if ($sportTypesRaw !== '') {
|
||||
preg_match_all('/\[([^\]]+)\]/u', $sportTypesRaw, $matches);
|
||||
$sportTypes = normalize_sport_type_selection($matches[1] ?? []);
|
||||
}
|
||||
|
||||
if ($sportTypes === []) {
|
||||
$sportType = (string) ($this->extract('/^- Sportart:\s*(.*)$/mu', $content) ?? '');
|
||||
if (preg_match('/\[(.+)\]\s*$/u', $sportType, $matches)) {
|
||||
$sportType = trim((string) ($matches[1] ?? ''));
|
||||
}
|
||||
$sportTypes = normalize_sport_type_selection($sportType);
|
||||
}
|
||||
|
||||
$entry = [
|
||||
'date' => $this->extract('/^Datum:\s*(\d{4}-\d{2}-\d{2})$/m', $content) ?? $fallbackDate,
|
||||
'mood' => (int) ($this->extract('/^- Stimmung:\s*(.+)$/m', $content) ?? 5),
|
||||
@@ -70,6 +85,8 @@ final class EntryRepository
|
||||
'sleep_hours' => (float) ($this->extract('/^- Schlafdauer:\s*(.+)$/m', $content) ?? 0),
|
||||
'sleep_feeling' => (int) ($this->extract('/^- Schlaf(?:gefühl|gefuehl):\s*(.+)$/mu', $content) ?? 3),
|
||||
'sport_minutes' => (int) ($this->extract('/^- Sport:\s*(.+)$/m', $content) ?? 0),
|
||||
'sport_type' => $sportTypes[0] ?? '',
|
||||
'sport_types' => $sportTypes,
|
||||
'walk_minutes' => (int) ($this->extract('/^- Spaziergang:\s*(.+)$/m', $content) ?? 0),
|
||||
'note' => $this->extractNote($content),
|
||||
];
|
||||
@@ -101,8 +118,14 @@ final class EntryRepository
|
||||
|
||||
private function toMarkdown(string $username, string $date, array $entry, array $evaluation): string
|
||||
{
|
||||
$sportTypes = $evaluation['sport_types'] ?? [];
|
||||
$sportTypeValues = array_map(
|
||||
static fn (array $type): string => (string) $type['label'] . ' [' . (string) $type['id'] . ']',
|
||||
array_filter($sportTypes, 'is_array')
|
||||
);
|
||||
|
||||
$lines = [
|
||||
'<!-- mood-tracker:v1 -->',
|
||||
'<!-- mood-tracker:v2 -->',
|
||||
'# Stimmungstracker',
|
||||
'Datum: ' . $date,
|
||||
'Benutzer: ' . normalize_username($username),
|
||||
@@ -114,6 +137,7 @@ final class EntryRepository
|
||||
'- Schlafdauer: ' . $entry['sleep_hours'],
|
||||
'- Schlafgefühl: ' . $entry['sleep_feeling'],
|
||||
'- Sport: ' . $entry['sport_minutes'],
|
||||
'- Sportarten: ' . implode(', ', $sportTypeValues),
|
||||
'- Spaziergang: ' . $entry['walk_minutes'],
|
||||
'',
|
||||
'## Bewertung',
|
||||
@@ -127,6 +151,7 @@ final class EntryRepository
|
||||
'- Schlafdauer: ' . format_points((float) $evaluation['components']['sleep_hours']),
|
||||
'- Schlafgefühl: ' . format_points((float) $evaluation['components']['sleep_feeling']),
|
||||
'- Sport: ' . format_points((float) $evaluation['components']['sport_minutes']),
|
||||
'- Sportbonus: ' . format_points((float) ($evaluation['components']['sport_bonus'] ?? 0)),
|
||||
'- Spaziergang: ' . format_points((float) $evaluation['components']['walk_minutes']),
|
||||
'- Notiz: ' . format_points((float) $evaluation['components']['note']),
|
||||
'',
|
||||
|
||||
@@ -6,6 +6,8 @@ final class ScoringService
|
||||
{
|
||||
public function normalize(array $input): array
|
||||
{
|
||||
$sportTypes = normalize_sport_type_selection($input['sport_types'] ?? ($input['sport_type'] ?? []));
|
||||
|
||||
return [
|
||||
'date' => $input['date'] ?? today(),
|
||||
'mood' => max(1, min(10, (int) ($input['mood'] ?? 5))),
|
||||
@@ -14,16 +16,21 @@ final class ScoringService
|
||||
'sleep_hours' => max(0, min(24, (float) ($input['sleep_hours'] ?? 0))),
|
||||
'sleep_feeling' => max(1, min(5, (int) ($input['sleep_feeling'] ?? 3))),
|
||||
'sport_minutes' => max(0, min(1440, (int) ($input['sport_minutes'] ?? 0))),
|
||||
'sport_type' => $sportTypes[0] ?? '',
|
||||
'sport_types' => $sportTypes,
|
||||
'walk_minutes' => max(0, min(1440, (int) ($input['walk_minutes'] ?? 0))),
|
||||
'note' => trim((string) ($input['note'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
public function evaluate(array $entry, array $settings): array
|
||||
public function evaluate(array $entry, array $settings, ?array $previousEntry = null): array
|
||||
{
|
||||
$entry = $this->normalize($entry);
|
||||
$previousEntry = $previousEntry !== null ? $this->normalize($previousEntry) : null;
|
||||
$scoring = $settings['scoring'];
|
||||
$ratings = $this->sortedRatings($settings['ratings'] ?? []);
|
||||
$sportTypes = find_sport_types($settings, $entry['sport_types']);
|
||||
$sportBonus = $this->sportBonusPoints($entry, $previousEntry, $settings, $sportTypes);
|
||||
|
||||
$components = [
|
||||
'mood' => $entry['mood'] * (float) $scoring['mood_multiplier'],
|
||||
@@ -32,6 +39,7 @@ final class ScoringService
|
||||
'sleep_hours' => $this->sleepDurationPoints((float) $entry['sleep_hours'], $scoring['sleep_duration_points']),
|
||||
'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' => $this->bandPoints((int) $entry['walk_minutes'], $scoring['walk_bands']),
|
||||
'note' => $entry['note'] === '' ? 0 : (float) $scoring['journal_points'],
|
||||
];
|
||||
@@ -44,6 +52,7 @@ final class ScoringService
|
||||
max(array_map('floatval', $scoring['sleep_duration_points'])) +
|
||||
(5 * (float) $scoring['sleep_feeling_multiplier']) +
|
||||
$this->maxBandPoints($scoring['sport_bands']) +
|
||||
$this->maxSportBonusPoints($settings) +
|
||||
$this->maxBandPoints($scoring['walk_bands']) +
|
||||
(float) $scoring['journal_points'],
|
||||
1
|
||||
@@ -74,6 +83,8 @@ final class ScoringService
|
||||
'guardrail' => $guardrail,
|
||||
'sentiment' => $this->sentimentForLabel($label, $ratings),
|
||||
'percentage' => $maxTotal > 0 ? round(($total / $maxTotal) * 100, 1) : 0,
|
||||
'sport_type' => $sportTypes[0] ?? null,
|
||||
'sport_types' => $sportTypes,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -135,6 +146,86 @@ final class ScoringService
|
||||
return $max;
|
||||
}
|
||||
|
||||
private function maxSportBonusPoints(array $settings): float
|
||||
{
|
||||
$max = 0.0;
|
||||
$perGroup = [];
|
||||
|
||||
foreach (normalized_sport_types($settings) as $type) {
|
||||
$bonus = (float) ($type['bonus_points'] ?? 0);
|
||||
if ($bonus <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($type['allow_consecutive'])) {
|
||||
$max += $bonus;
|
||||
continue;
|
||||
}
|
||||
|
||||
$group = (string) ($type['recovery_group'] ?? $type['id'] ?? '');
|
||||
if ($group === '') {
|
||||
$group = (string) ($type['id'] ?? '');
|
||||
}
|
||||
|
||||
$perGroup[$group] = max((float) ($perGroup[$group] ?? 0), $bonus);
|
||||
}
|
||||
|
||||
foreach ($perGroup as $bonus) {
|
||||
$max += $bonus;
|
||||
}
|
||||
|
||||
return $max;
|
||||
}
|
||||
|
||||
private function sportBonusPoints(array $entry, ?array $previousEntry, array $settings, array $currentSportTypes): float
|
||||
{
|
||||
if ((int) $entry['sport_minutes'] <= 0 || $currentSportTypes === []) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$previousGroups = [];
|
||||
if ($previousEntry !== null && (int) $previousEntry['sport_minutes'] > 0) {
|
||||
foreach (find_sport_types($settings, $previousEntry['sport_types']) as $type) {
|
||||
$group = (string) ($type['recovery_group'] ?? $type['id'] ?? '');
|
||||
if ($group !== '') {
|
||||
$previousGroups[$group] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$groupBonuses = [];
|
||||
$total = 0.0;
|
||||
|
||||
foreach ($currentSportTypes as $type) {
|
||||
$bonus = (float) ($type['bonus_points'] ?? 0);
|
||||
if ($bonus <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$group = (string) ($type['recovery_group'] ?? $type['id'] ?? '');
|
||||
if ($group === '') {
|
||||
$group = (string) ($type['id'] ?? '');
|
||||
}
|
||||
|
||||
if (!empty($type['allow_consecutive'])) {
|
||||
$total += $bonus;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($previousGroups[$group])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$groupBonuses[$group] = max((float) ($groupBonuses[$group] ?? 0), $bonus);
|
||||
}
|
||||
|
||||
foreach ($groupBonuses as $bonus) {
|
||||
$total += $bonus;
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
private function sortedRatings(array $ratings): array
|
||||
{
|
||||
usort($ratings, static fn (array $a, array $b): int => ((int) $a['min']) <=> ((int) $b['min']));
|
||||
|
||||
@@ -8,8 +8,13 @@ final class SettingsRepository
|
||||
{
|
||||
$path = $this->pathFor($username);
|
||||
$saved = decode_json_file($path, []);
|
||||
$settings = array_replace_recursive(Defaults::settings(), $saved);
|
||||
|
||||
return array_replace_recursive(Defaults::settings(), $saved);
|
||||
if (array_key_exists('sport_types', $saved) && is_array($saved['sport_types'])) {
|
||||
$settings['sport_types'] = $saved['sport_types'];
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function saveForUser(string $username, array $settings): void
|
||||
@@ -32,4 +37,3 @@ final class SettingsRepository
|
||||
return storage_path('users/' . normalize_username($username) . '/settings.json');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,48 @@ final class Defaults
|
||||
5 => 'sehr ausgeschlafen',
|
||||
],
|
||||
],
|
||||
'sport_types' => [
|
||||
[
|
||||
'id' => 'strength-home',
|
||||
'label' => 'Kraftsport (Keller)',
|
||||
'icon' => 'strength-home',
|
||||
'recovery_group' => 'kraftsport',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'strength-gym',
|
||||
'label' => 'Kraftsport (Gym)',
|
||||
'icon' => 'strength-gym',
|
||||
'recovery_group' => 'kraftsport',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'running',
|
||||
'label' => 'Joggen',
|
||||
'icon' => 'run',
|
||||
'recovery_group' => 'joggen',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'rowing',
|
||||
'label' => 'Rudergerät',
|
||||
'icon' => 'row',
|
||||
'recovery_group' => 'rudern',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'core',
|
||||
'label' => 'Core',
|
||||
'icon' => 'core',
|
||||
'recovery_group' => 'core',
|
||||
'bonus_points' => 2,
|
||||
'allow_consecutive' => true,
|
||||
],
|
||||
],
|
||||
'scoring' => [
|
||||
'mood_multiplier' => 3,
|
||||
'energy_multiplier' => 2,
|
||||
|
||||
+159
@@ -175,3 +175,162 @@ function mood_icon_path(string $sentiment): string
|
||||
{
|
||||
return icon_path('mood-' . $sentiment);
|
||||
}
|
||||
|
||||
function sport_icon_path(string $icon): string
|
||||
{
|
||||
return icon_path('sport-' . $icon);
|
||||
}
|
||||
|
||||
function sport_icon_options(): array
|
||||
{
|
||||
return [
|
||||
'strength-home' => 'Kraftsport daheim',
|
||||
'strength-gym' => 'Kraftsport im Gym',
|
||||
'run' => 'Joggen',
|
||||
'row' => 'Rudergerät',
|
||||
'core' => 'Core',
|
||||
];
|
||||
}
|
||||
|
||||
function normalize_sport_type_id(string $value): string
|
||||
{
|
||||
$value = trim(strtr($value, [
|
||||
'Ä' => 'ae',
|
||||
'Ö' => 'oe',
|
||||
'Ü' => 'ue',
|
||||
'ä' => 'ae',
|
||||
'ö' => 'oe',
|
||||
'ü' => 'ue',
|
||||
'ß' => 'ss',
|
||||
]));
|
||||
$value = strtolower($value);
|
||||
$value = strtr($value, [
|
||||
'--' => '-',
|
||||
]);
|
||||
$value = preg_replace('/[^a-z0-9]+/u', '-', $value) ?? '';
|
||||
|
||||
return trim($value, '-');
|
||||
}
|
||||
|
||||
function normalized_sport_types(array $settings): array
|
||||
{
|
||||
$types = $settings['sport_types'] ?? [];
|
||||
$normalized = [];
|
||||
$usedIds = [];
|
||||
$iconOptions = sport_icon_options();
|
||||
|
||||
foreach ($types as $index => $type) {
|
||||
if (!is_array($type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = trim((string) ($type['label'] ?? ''));
|
||||
if ($label === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidateId = trim((string) ($type['id'] ?? ''));
|
||||
if ($candidateId === '') {
|
||||
$candidateId = normalize_sport_type_id($label);
|
||||
}
|
||||
|
||||
if ($candidateId === '') {
|
||||
$candidateId = 'sportart';
|
||||
}
|
||||
|
||||
$id = $candidateId;
|
||||
$suffix = 2;
|
||||
while (isset($usedIds[$id])) {
|
||||
$id = $candidateId . '-' . $suffix;
|
||||
$suffix++;
|
||||
}
|
||||
|
||||
$usedIds[$id] = true;
|
||||
|
||||
$icon = trim((string) ($type['icon'] ?? 'run'));
|
||||
if (!array_key_exists($icon, $iconOptions)) {
|
||||
$icon = 'run';
|
||||
}
|
||||
|
||||
$group = trim((string) ($type['recovery_group'] ?? ''));
|
||||
if ($group === '') {
|
||||
$group = $id;
|
||||
}
|
||||
|
||||
$normalized[] = [
|
||||
'id' => $id,
|
||||
'label' => $label,
|
||||
'icon' => $icon,
|
||||
'recovery_group' => normalize_sport_type_id($group) ?: $id,
|
||||
'bonus_points' => max(0, min(20, (int) ($type['bonus_points'] ?? 0))),
|
||||
'allow_consecutive' => !empty($type['allow_consecutive']),
|
||||
];
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
function normalize_sport_type_selection(mixed $value): array
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (str_contains($value, ',')) {
|
||||
$value = array_map('trim', explode(',', $value));
|
||||
} else {
|
||||
$value = [$value];
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$normalized = [];
|
||||
foreach ($value as $item) {
|
||||
if (!is_string($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = trim($item);
|
||||
if ($id === '' || isset($normalized[$id])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalized[$id] = true;
|
||||
}
|
||||
|
||||
return array_keys($normalized);
|
||||
}
|
||||
|
||||
function find_sport_type(array $settings, ?string $id): ?array
|
||||
{
|
||||
if (!is_string($id) || trim($id) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (normalized_sport_types($settings) as $type) {
|
||||
if (($type['id'] ?? '') === $id) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function find_sport_types(array $settings, array $ids): array
|
||||
{
|
||||
$types = [];
|
||||
|
||||
foreach (normalize_sport_type_selection($ids) as $id) {
|
||||
$type = find_sport_type($settings, $id);
|
||||
if ($type !== null) {
|
||||
$types[] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user