Refine balance scoring and dashboard views
This commit is contained in:
+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,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user