828 lines
19 KiB
PHP
828 lines
19 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
function base_path(string $path = ''): string
|
|
{
|
|
$base = dirname(__DIR__);
|
|
|
|
if ($path === '') {
|
|
return $base;
|
|
}
|
|
|
|
return $base . '/' . ltrim($path, '/');
|
|
}
|
|
|
|
function storage_path(string $path = ''): string
|
|
{
|
|
return base_path('storage/' . ltrim($path, '/'));
|
|
}
|
|
|
|
function e(mixed $value): string
|
|
{
|
|
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
}
|
|
|
|
function redirect(string $path): never
|
|
{
|
|
header('Location: ' . $path);
|
|
exit;
|
|
}
|
|
|
|
function json_response(array $payload, int $status = 200): never
|
|
{
|
|
http_response_code($status);
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
function request_path(): string
|
|
{
|
|
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
|
|
$path = is_string($path) ? $path : '/';
|
|
|
|
if ($path !== '/') {
|
|
$path = rtrim($path, '/');
|
|
}
|
|
|
|
return $path === '' ? '/' : $path;
|
|
}
|
|
|
|
function flash(string $type, string $message): void
|
|
{
|
|
$_SESSION['_flash'][] = [
|
|
'type' => $type,
|
|
'message' => $message,
|
|
];
|
|
}
|
|
|
|
function pull_flashes(): array
|
|
{
|
|
$flashes = $_SESSION['_flash'] ?? [];
|
|
unset($_SESSION['_flash']);
|
|
|
|
return is_array($flashes) ? $flashes : [];
|
|
}
|
|
|
|
function csrf_token(): string
|
|
{
|
|
if (empty($_SESSION['_csrf'])) {
|
|
$_SESSION['_csrf'] = bin2hex(random_bytes(32));
|
|
}
|
|
|
|
return (string) $_SESSION['_csrf'];
|
|
}
|
|
|
|
function csrf_field(): string
|
|
{
|
|
return '<input type="hidden" name="_token" value="' . e(csrf_token()) . '">';
|
|
}
|
|
|
|
function verify_csrf(?string $token): bool
|
|
{
|
|
if (!is_string($token) || $token === '') {
|
|
return false;
|
|
}
|
|
|
|
return hash_equals(csrf_token(), $token);
|
|
}
|
|
|
|
function verify_request_csrf(): bool
|
|
{
|
|
$headerToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;
|
|
|
|
if (is_string($headerToken) && $headerToken !== '') {
|
|
return verify_csrf($headerToken);
|
|
}
|
|
|
|
return verify_csrf($_POST['_token'] ?? null);
|
|
}
|
|
|
|
function is_active_path(string $path): bool
|
|
{
|
|
return request_path() === $path;
|
|
}
|
|
|
|
function format_points(float $value): string
|
|
{
|
|
$rounded = round($value, 1);
|
|
|
|
if (abs($rounded - round($rounded)) < 0.05) {
|
|
return (string) (int) round($rounded);
|
|
}
|
|
|
|
return number_format($rounded, 1, ',', '.');
|
|
}
|
|
|
|
function format_duration_hours(float $hours): string
|
|
{
|
|
$minutes = max(0, (int) round($hours * 60));
|
|
$wholeHours = intdiv($minutes, 60);
|
|
$remainingMinutes = $minutes % 60;
|
|
|
|
if ($wholeHours <= 0) {
|
|
return $remainingMinutes . ' min';
|
|
}
|
|
|
|
if ($remainingMinutes === 0) {
|
|
return $wholeHours . ' h';
|
|
}
|
|
|
|
return $wholeHours . ' h ' . $remainingMinutes . ' min';
|
|
}
|
|
|
|
function normalize_username(string $username): string
|
|
{
|
|
return strtolower(trim($username));
|
|
}
|
|
|
|
function today(): string
|
|
{
|
|
return date('Y-m-d');
|
|
}
|
|
|
|
function decode_json_file(string $path, array $fallback = []): array
|
|
{
|
|
if (!is_file($path)) {
|
|
return $fallback;
|
|
}
|
|
|
|
$decoded = json_decode((string) file_get_contents($path), true);
|
|
|
|
return is_array($decoded) ? $decoded : $fallback;
|
|
}
|
|
|
|
function request_json_body(): array
|
|
{
|
|
$raw = file_get_contents('php://input');
|
|
if (!is_string($raw) || trim($raw) === '') {
|
|
return [];
|
|
}
|
|
|
|
$decoded = json_decode($raw, true);
|
|
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
|
|
function encode_payload(array $payload): string
|
|
{
|
|
return base64_encode((string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
|
}
|
|
|
|
function shift_date(string $date, int $days): string
|
|
{
|
|
$current = DateTimeImmutable::createFromFormat('Y-m-d', $date);
|
|
|
|
if ($current === false) {
|
|
return today();
|
|
}
|
|
|
|
return $current->modify(($days >= 0 ? '+' : '') . $days . ' day')->format('Y-m-d');
|
|
}
|
|
|
|
function format_display_date(string $date, bool $withWeekday = true): string
|
|
{
|
|
$current = DateTimeImmutable::createFromFormat('Y-m-d', $date);
|
|
|
|
if ($current === false) {
|
|
return $date;
|
|
}
|
|
|
|
$weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
|
$months = [
|
|
1 => 'Januar',
|
|
2 => 'Februar',
|
|
3 => 'März',
|
|
4 => 'April',
|
|
5 => 'Mai',
|
|
6 => 'Juni',
|
|
7 => 'Juli',
|
|
8 => 'August',
|
|
9 => 'September',
|
|
10 => 'Oktober',
|
|
11 => 'November',
|
|
12 => 'Dezember',
|
|
];
|
|
|
|
$label = $current->format('j.') . ' ' . $months[(int) $current->format('n')] . ' ' . $current->format('Y');
|
|
|
|
if (!$withWeekday) {
|
|
return $label;
|
|
}
|
|
|
|
return $weekdays[(int) $current->format('w')] . ', ' . $label;
|
|
}
|
|
|
|
function format_compact_date(string $date): string
|
|
{
|
|
$current = DateTimeImmutable::createFromFormat('Y-m-d', $date);
|
|
|
|
if ($current === false) {
|
|
return $date;
|
|
}
|
|
|
|
return $current->format('d.m.Y');
|
|
}
|
|
|
|
function format_display_datetime(string $value): string
|
|
{
|
|
try {
|
|
$current = new DateTimeImmutable($value);
|
|
} catch (Throwable) {
|
|
return $value;
|
|
}
|
|
|
|
$months = [
|
|
1 => 'Januar',
|
|
2 => 'Februar',
|
|
3 => 'März',
|
|
4 => 'April',
|
|
5 => 'Mai',
|
|
6 => 'Juni',
|
|
7 => 'Juli',
|
|
8 => 'August',
|
|
9 => 'September',
|
|
10 => 'Oktober',
|
|
11 => 'November',
|
|
12 => 'Dezember',
|
|
];
|
|
|
|
return $current->format('j.') . ' ' . $months[(int) $current->format('n')] . ' ' . $current->format('Y') . ' um ' . $current->format('H:i');
|
|
}
|
|
|
|
function format_compact_datetime(string $value): string
|
|
{
|
|
try {
|
|
$current = new DateTimeImmutable($value);
|
|
} catch (Throwable) {
|
|
return $value;
|
|
}
|
|
|
|
return $current->format('d.m.Y · H:i');
|
|
}
|
|
|
|
function iso_week_key(string $date): string
|
|
{
|
|
$current = DateTimeImmutable::createFromFormat('Y-m-d', $date);
|
|
|
|
if ($current === false) {
|
|
return date('o-\K\W-W');
|
|
}
|
|
|
|
return $current->format('o-\K\W-W');
|
|
}
|
|
|
|
function month_key(string $date): string
|
|
{
|
|
$current = DateTimeImmutable::createFromFormat('Y-m-d', $date);
|
|
|
|
if ($current === false) {
|
|
return date('Y-m');
|
|
}
|
|
|
|
return $current->format('Y-m');
|
|
}
|
|
|
|
function iso_week_label(string $key): string
|
|
{
|
|
if (preg_match('/^(\d{4})-KW-(\d{2})$/', $key, $matches) === 1) {
|
|
return 'KW ' . $matches[2] . ' / ' . $matches[1];
|
|
}
|
|
|
|
return $key;
|
|
}
|
|
|
|
function month_label(string $key): string
|
|
{
|
|
if (preg_match('/^(\d{4})-(\d{2})$/', $key, $matches) !== 1) {
|
|
return $key;
|
|
}
|
|
|
|
$months = [
|
|
'01' => 'Januar',
|
|
'02' => 'Februar',
|
|
'03' => 'März',
|
|
'04' => 'April',
|
|
'05' => 'Mai',
|
|
'06' => 'Juni',
|
|
'07' => 'Juli',
|
|
'08' => 'August',
|
|
'09' => 'September',
|
|
'10' => 'Oktober',
|
|
'11' => 'November',
|
|
'12' => 'Dezember',
|
|
];
|
|
|
|
return ($months[$matches[2]] ?? $matches[2]) . ' ' . $matches[1];
|
|
}
|
|
|
|
function icon_path(string $name): string
|
|
{
|
|
return '/assets/icons/' . $name . '.svg';
|
|
}
|
|
|
|
function mood_icon_path(string $sentiment): string
|
|
{
|
|
return icon_path('mood-' . $sentiment);
|
|
}
|
|
|
|
function request_is_secure(): bool
|
|
{
|
|
$forwardedProto = strtolower((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''));
|
|
$forwardedSsl = strtolower((string) ($_SERVER['HTTP_X_FORWARDED_SSL'] ?? ''));
|
|
|
|
return (
|
|
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
|
|| $forwardedProto === 'https'
|
|
|| $forwardedSsl === 'on'
|
|
);
|
|
}
|
|
|
|
function app_origin(): string
|
|
{
|
|
$host = (string) ($_SERVER['HTTP_HOST'] ?? 'localhost');
|
|
$scheme = request_is_secure() ? 'https' : 'http';
|
|
|
|
return $scheme . '://' . $host;
|
|
}
|
|
|
|
function remember_me_lifetime(): int
|
|
{
|
|
return 60 * 60 * 24 * 30;
|
|
}
|
|
|
|
function remember_cookie_name(): string
|
|
{
|
|
return 'mood_remember';
|
|
}
|
|
|
|
function session_cookie_params_for(int $lifetime = 0): array
|
|
{
|
|
return [
|
|
'lifetime' => $lifetime,
|
|
'path' => '/',
|
|
'domain' => '',
|
|
'secure' => request_is_secure(),
|
|
'httponly' => true,
|
|
'samesite' => 'Lax',
|
|
];
|
|
}
|
|
|
|
function session_cookie_options_for(int $expires = 0): array
|
|
{
|
|
return [
|
|
'expires' => $expires,
|
|
'path' => '/',
|
|
'domain' => '',
|
|
'secure' => request_is_secure(),
|
|
'httponly' => true,
|
|
'samesite' => 'Lax',
|
|
];
|
|
}
|
|
|
|
function sport_icon_path(string $icon): string
|
|
{
|
|
return icon_path('sport-' . $icon);
|
|
}
|
|
|
|
function sport_icon_options(): array
|
|
{
|
|
return [
|
|
'strength' => 'Krafttraining',
|
|
'bike' => 'Radfahren',
|
|
'run' => 'Joggen',
|
|
'hike' => 'Wandern',
|
|
'swim' => 'Schwimmen',
|
|
'yoga' => 'Yoga',
|
|
'hiit' => 'HIIT / Workout',
|
|
'row' => 'Rudergerät',
|
|
'dance' => 'Tanzen',
|
|
'core' => 'Core',
|
|
'strength-home' => 'Krafttraining Zuhause',
|
|
'strength-gym' => 'Krafttraining Auswärts',
|
|
];
|
|
}
|
|
|
|
function sport_location_options(): array
|
|
{
|
|
return [
|
|
'' => 'ohne Angabe',
|
|
'home' => 'Zuhause',
|
|
'away' => 'Auswärts',
|
|
];
|
|
}
|
|
|
|
function sport_location_label(?string $value): string
|
|
{
|
|
$options = sport_location_options();
|
|
$value = is_string($value) ? trim($value) : '';
|
|
|
|
return $options[$value] ?? '';
|
|
}
|
|
|
|
function walk_mode_options(): array
|
|
{
|
|
return [
|
|
'time' => 'Spaziergang nach Zeit',
|
|
'steps' => 'Spaziergang nach Schritten',
|
|
];
|
|
}
|
|
|
|
function walk_mode_label(string $mode): string
|
|
{
|
|
return walk_mode_options()[$mode] ?? walk_mode_options()['time'];
|
|
}
|
|
|
|
function format_walk_value(array $entry): string
|
|
{
|
|
$mode = (string) ($entry['walk_mode'] ?? 'time');
|
|
|
|
if ($mode === 'steps') {
|
|
return number_format((int) ($entry['walk_steps'] ?? 0), 0, ',', '.') . ' Schritte';
|
|
}
|
|
|
|
return (string) ((int) ($entry['walk_minutes'] ?? 0)) . ' min';
|
|
}
|
|
|
|
function walk_chart_value(array $entry): int
|
|
{
|
|
$mode = (string) ($entry['walk_mode'] ?? 'time');
|
|
|
|
if ($mode === 'steps') {
|
|
return max(0, (int) round(((int) ($entry['walk_steps'] ?? 0)) / 200));
|
|
}
|
|
|
|
return max(0, (int) ($entry['walk_minutes'] ?? 0));
|
|
}
|
|
|
|
function base64url_encode(string $data): string
|
|
{
|
|
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
|
}
|
|
|
|
function base64url_decode(string $data): string
|
|
{
|
|
$padding = strlen($data) % 4;
|
|
if ($padding > 0) {
|
|
$data .= str_repeat('=', 4 - $padding);
|
|
}
|
|
|
|
$decoded = base64_decode(strtr($data, '-_', '+/'), true);
|
|
|
|
if ($decoded === false) {
|
|
throw new RuntimeException('Ungültige Base64url-Daten.');
|
|
}
|
|
|
|
return $decoded;
|
|
}
|
|
|
|
function uploaded_files(string $field): array
|
|
{
|
|
$raw = $_FILES[$field] ?? null;
|
|
if (!is_array($raw) || !isset($raw['name'])) {
|
|
return [];
|
|
}
|
|
|
|
if (!is_array($raw['name'])) {
|
|
return [[
|
|
'name' => (string) ($raw['name'] ?? ''),
|
|
'type' => (string) ($raw['type'] ?? ''),
|
|
'tmp_name' => (string) ($raw['tmp_name'] ?? ''),
|
|
'error' => (int) ($raw['error'] ?? UPLOAD_ERR_NO_FILE),
|
|
'size' => (int) ($raw['size'] ?? 0),
|
|
]];
|
|
}
|
|
|
|
$files = [];
|
|
foreach ($raw['name'] as $index => $name) {
|
|
$files[] = [
|
|
'name' => (string) ($name ?? ''),
|
|
'type' => (string) ($raw['type'][$index] ?? ''),
|
|
'tmp_name' => (string) ($raw['tmp_name'][$index] ?? ''),
|
|
'error' => (int) ($raw['error'][$index] ?? UPLOAD_ERR_NO_FILE),
|
|
'size' => (int) ($raw['size'][$index] ?? 0),
|
|
];
|
|
}
|
|
|
|
return $files;
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
$location = trim((string) ($type['location'] ?? ''));
|
|
if (!array_key_exists($location, sport_location_options())) {
|
|
$location = '';
|
|
}
|
|
|
|
$group = trim((string) ($type['recovery_group'] ?? ''));
|
|
if ($group === '') {
|
|
$group = $id;
|
|
}
|
|
|
|
$normalized[] = [
|
|
'id' => $id,
|
|
'label' => $label,
|
|
'icon' => $icon,
|
|
'location' => $location,
|
|
'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;
|
|
}
|
|
|
|
function signal_scale_options(): array
|
|
{
|
|
return [
|
|
-2 => 'sehr niedrig',
|
|
-1 => 'niedrig',
|
|
0 => 'neutral',
|
|
1 => 'hoch',
|
|
2 => 'sehr hoch',
|
|
];
|
|
}
|
|
|
|
function signal_labels_for_metric(string $metric): array
|
|
{
|
|
return match ($metric) {
|
|
'stress' => [
|
|
-2 => 'sehr ruhig',
|
|
-1 => 'ruhig',
|
|
0 => 'neutral',
|
|
1 => 'angespannt',
|
|
2 => 'sehr angespannt',
|
|
],
|
|
'energy' => [
|
|
-2 => 'leer',
|
|
-1 => 'matt',
|
|
0 => 'okay',
|
|
1 => 'wach',
|
|
2 => 'kraftvoll',
|
|
],
|
|
default => [
|
|
-2 => 'sehr niedrig',
|
|
-1 => 'niedrig',
|
|
0 => 'neutral',
|
|
1 => 'hoch',
|
|
2 => 'sehr hoch',
|
|
],
|
|
};
|
|
}
|
|
|
|
function normalize_signal_value(mixed $value): int
|
|
{
|
|
return max(-2, min(2, (int) $value));
|
|
}
|
|
|
|
function signal_to_legacy_scale(mixed $value): int
|
|
{
|
|
return match (normalize_signal_value($value)) {
|
|
-2 => 1,
|
|
-1 => 3,
|
|
0 => 5,
|
|
1 => 7,
|
|
2 => 9,
|
|
};
|
|
}
|
|
|
|
function legacy_to_signal_scale(mixed $value): int
|
|
{
|
|
$legacy = max(1, min(10, (int) $value));
|
|
|
|
return match (true) {
|
|
$legacy <= 2 => -2,
|
|
$legacy <= 4 => -1,
|
|
$legacy <= 6 => 0,
|
|
$legacy <= 8 => 1,
|
|
default => 2,
|
|
};
|
|
}
|
|
|
|
function day_event_type_options(): array
|
|
{
|
|
return [
|
|
'event' => [
|
|
'label' => 'Moment',
|
|
'icon' => '/assets/icons/activity-event.svg',
|
|
'unit' => '',
|
|
],
|
|
'walk' => [
|
|
'label' => 'Spaziergang',
|
|
'icon' => sport_icon_path('hike'),
|
|
'unit' => 'min',
|
|
],
|
|
'sport' => [
|
|
'label' => 'Sport',
|
|
'icon' => sport_icon_path('run'),
|
|
'unit' => 'min',
|
|
],
|
|
'sleep' => [
|
|
'label' => 'Schlaf',
|
|
'icon' => '/assets/icons/activity-sleep.svg',
|
|
'unit' => 'h',
|
|
],
|
|
'alcohol' => [
|
|
'label' => 'Alkohol',
|
|
'icon' => icon_path('alcohol'),
|
|
'unit' => '',
|
|
],
|
|
];
|
|
}
|
|
|
|
function day_event_type_label(string $type): string
|
|
{
|
|
return day_event_type_options()[$type]['label'] ?? day_event_type_options()['event']['label'];
|
|
}
|
|
|
|
function day_event_type_icon(string $type): string
|
|
{
|
|
return day_event_type_options()[$type]['icon'] ?? day_event_type_options()['event']['icon'];
|
|
}
|
|
|
|
function day_event_type_unit(string $type): string
|
|
{
|
|
return day_event_type_options()[$type]['unit'] ?? '';
|
|
}
|
|
|
|
function signal_badge_tone(int $value, string $metric): string
|
|
{
|
|
$value = normalize_signal_value($value);
|
|
|
|
if ($metric === 'stress') {
|
|
return match (true) {
|
|
$value <= -1 => 'good',
|
|
$value === 0 => 'neutral',
|
|
default => 'warn',
|
|
};
|
|
}
|
|
|
|
return match (true) {
|
|
$value <= -1 => 'warn',
|
|
$value === 0 => 'neutral',
|
|
default => 'good',
|
|
};
|
|
}
|
|
|
|
function signal_combo_score(mixed $mood, mixed $energy, mixed $stress): int
|
|
{
|
|
return max(-2, min(2, (int) round((
|
|
normalize_signal_value($mood) +
|
|
normalize_signal_value($energy) -
|
|
normalize_signal_value($stress)
|
|
) / 3)));
|
|
}
|
|
|
|
function day_entry_has_content(array $entry): bool
|
|
{
|
|
if (trim((string) ($entry['summary']['comment'] ?? '')) !== '') {
|
|
return true;
|
|
}
|
|
|
|
if (trim((string) ($entry['background_image'] ?? '')) !== '') {
|
|
return true;
|
|
}
|
|
|
|
if ((int) ($entry['health']['steps'] ?? 0) > 0) {
|
|
return true;
|
|
}
|
|
|
|
return count(array_filter(is_array($entry['events'] ?? null) ? $entry['events'] : [], 'is_array')) > 0;
|
|
}
|
|
|
|
function signal_value_class(int $value): string
|
|
{
|
|
return match (normalize_signal_value($value)) {
|
|
-2 => 'neg2',
|
|
-1 => 'neg1',
|
|
0 => 'zero',
|
|
1 => 'pos1',
|
|
2 => 'pos2',
|
|
};
|
|
}
|