dashboard - first draft of complete redesign
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../contentpanel/insertion/util/contentpanel_helpers.php';
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../contentpanel/insertion/util/news.php';
|
||||
require_once __DIR__ . '/../../contentpanel/insertion/util/announcements.php';
|
||||
require_once __DIR__ . '/../../../general_files/ini/runtime_env.php';
|
||||
|
||||
const DASHBOARD_TIMEZONE = 'Europe/Berlin';
|
||||
@@ -14,141 +17,157 @@ function dashboard_now(): DateTimeImmutable
|
||||
return new DateTimeImmutable('now', new DateTimeZone(DASHBOARD_TIMEZONE));
|
||||
}
|
||||
|
||||
function dashboard_html($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function dashboard_fetch_content(PDO $pdo, DateTimeImmutable $now): array
|
||||
{
|
||||
$now = $now->setTimezone(new DateTimeZone(DASHBOARD_TIMEZONE));
|
||||
$today = $now->format('Y-m-d');
|
||||
$payload = ['date' => $today, 'service' => null, 'announcements' => [], 'errors' => []];
|
||||
|
||||
$serviceStatement = $pdo->prepare(
|
||||
'SELECT bibelwort, bibelvers, eingang_nummer, eingang_titel
|
||||
FROM gottesdienst
|
||||
WHERE bibeldatum = ?'
|
||||
);
|
||||
$serviceStatement->execute([$today]);
|
||||
$service = $serviceStatement->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
|
||||
$announcementStatement = $pdo->prepare(
|
||||
'SELECT ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort, ansage_titel, ansage_untertitel
|
||||
FROM ansagen
|
||||
WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ?
|
||||
ORDER BY ansage_datum_von ASC'
|
||||
);
|
||||
$announcementStatement->execute([$today, $today]);
|
||||
$announcements = $announcementStatement->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($announcements as &$announcement) {
|
||||
$announcement = dashboard_prepare_announcement($announcement, $now);
|
||||
// Unabhängige Abfragen: ein fehlerhafter Bereich blockiert den anderen nicht.
|
||||
try {
|
||||
$statement = $pdo->prepare(
|
||||
'SELECT bibelwort, bibelvers FROM gottesdienst
|
||||
WHERE bibeldatum = ? ORDER BY id ASC LIMIT 1'
|
||||
);
|
||||
$statement->execute([$today]);
|
||||
$service = $statement->fetch(PDO::FETCH_ASSOC);
|
||||
if ($service) {
|
||||
// Leitgedanken-Import: bibelwort = Referenz, bibelvers = eigentlicher Text.
|
||||
$payload['service'] = [
|
||||
'reference' => trim((string) ($service['bibelwort'] ?? '')),
|
||||
'text' => trim((string) ($service['bibelvers'] ?? '')),
|
||||
];
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Dashboard Bibelwort: ' . $exception->getMessage());
|
||||
$payload['errors'][] = 'service';
|
||||
}
|
||||
unset($announcement);
|
||||
|
||||
contentpanel_cleanup_expired_news();
|
||||
try {
|
||||
$statement = $pdo->prepare(
|
||||
'SELECT ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort,
|
||||
ansage_titel, ansage_untertitel
|
||||
FROM ansagen WHERE COALESCE(ansage_datum_bis, ansage_datum_von) >= ?
|
||||
ORDER BY ansage_datum_von ASC NULLS LAST, ansage_uhrzeit ASC NULLS LAST, id ASC'
|
||||
);
|
||||
contentpanel_cleanup_expired_announcements($pdo, $today);
|
||||
$statement->execute([$today]);
|
||||
foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$payload['announcements'][] = dashboard_prepare_announcement($row, $now);
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Dashboard Ansagen: ' . $exception->getMessage());
|
||||
$payload['errors'][] = 'announcements';
|
||||
}
|
||||
|
||||
return [
|
||||
'date_label' => dashboard_date_label($now),
|
||||
'service' => [
|
||||
'bibelwort' => (string) ($service['bibelwort'] ?? 'Bibelwort nicht eingetragen'),
|
||||
'bibelvers' => (string) ($service['bibelvers'] ?? 'Bibelvers nicht gefunden.'),
|
||||
'eingang_nummer' => (string) ($service['eingang_nummer'] ?? 'N/A'),
|
||||
'eingang_titel' => (string) ($service['eingang_titel'] ?? 'Eingangslied nicht gefunden'),
|
||||
],
|
||||
'announcements' => $announcements,
|
||||
'posters' => dashboard_list_posters(),
|
||||
'month_images' => dashboard_month_images(),
|
||||
];
|
||||
return $payload;
|
||||
}
|
||||
|
||||
function dashboard_date_label(DateTimeImmutable $date): string
|
||||
function dashboard_date(string $raw): ?DateTimeImmutable
|
||||
{
|
||||
static $months = [
|
||||
1 => 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
|
||||
];
|
||||
|
||||
return sprintf('%02d. %s', (int) $date->format('d'), $months[(int) $date->format('n')]);
|
||||
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $raw, new DateTimeZone(DASHBOARD_TIMEZONE));
|
||||
return $date && $date->format('Y-m-d') === $raw ? $date : null;
|
||||
}
|
||||
|
||||
function dashboard_prepare_announcement(array $announcement, DateTimeImmutable $now): array
|
||||
{
|
||||
$startRaw = substr((string) ($announcement['ansage_datum_von'] ?? ''), 0, 10);
|
||||
$endRaw = substr((string) ($announcement['ansage_datum_bis'] ?? ''), 0, 10);
|
||||
$timeRaw = substr((string) ($announcement['ansage_uhrzeit'] ?? ''), 0, 5);
|
||||
|
||||
$startDate = DateTimeImmutable::createFromFormat('!Y-m-d', $startRaw, $now->getTimezone());
|
||||
$tomorrow = $now->setTime(0, 0)->modify('+1 day');
|
||||
|
||||
if ($startDate && $startDate->format('Y-m-d') === $now->format('Y-m-d')) {
|
||||
$dateLabel = 'Heute';
|
||||
} elseif ($startDate && $startDate->format('Y-m-d') === $tomorrow->format('Y-m-d')) {
|
||||
$dateLabel = 'Morgen';
|
||||
} elseif ($startDate) {
|
||||
static $weekdays = [1 => 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
$dateLabel = $weekdays[(int) $startDate->format('N')] . ' ' . $startDate->format('d.m.');
|
||||
$now = $now->setTimezone(new DateTimeZone(DASHBOARD_TIMEZONE));
|
||||
$start = substr((string) ($announcement['ansage_datum_von'] ?? ''), 0, 10);
|
||||
$end = substr((string) ($announcement['ansage_datum_bis'] ?? ''), 0, 10);
|
||||
$today = $now->format('Y-m-d');
|
||||
$startDate = dashboard_date($start);
|
||||
$endDate = dashboard_date($end);
|
||||
if ($start === $today) {
|
||||
$label = 'Heute';
|
||||
} elseif ($start === $now->modify('+1 day')->format('Y-m-d')) {
|
||||
$label = 'Morgen';
|
||||
} else {
|
||||
$dateLabel = $startRaw;
|
||||
$days = [1 => 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
$label = $startDate ? $days[(int) $startDate->format('N')] . ' ' . $startDate->format('d.m.') : '';
|
||||
}
|
||||
|
||||
$endLabel = '';
|
||||
if ($endRaw !== '') {
|
||||
$endDate = DateTimeImmutable::createFromFormat('!Y-m-d', $endRaw, $now->getTimezone());
|
||||
$endLabel = $endDate ? 'bis ' . $endDate->format('d.m.') : 'bis ' . $endRaw;
|
||||
}
|
||||
|
||||
$meta = [];
|
||||
if ($timeRaw !== '') {
|
||||
$meta[] = $timeRaw . ' Uhr';
|
||||
}
|
||||
$endLabel = $endDate && $end !== $start ? 'bis ' . $endDate->format('d.m.') : '';
|
||||
$time = substr((string) ($announcement['ansage_uhrzeit'] ?? ''), 0, 5);
|
||||
$place = trim((string) ($announcement['ansage_ort'] ?? ''));
|
||||
if ($place !== '') {
|
||||
$meta[] = $place;
|
||||
}
|
||||
|
||||
return [
|
||||
'date_label' => $dateLabel,
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
'date_label' => $label,
|
||||
'end_label' => $endLabel,
|
||||
'title' => (string) ($announcement['ansage_titel'] ?? ''),
|
||||
'subtitle' => (string) ($announcement['ansage_untertitel'] ?? ''),
|
||||
'meta' => implode(', ', $meta),
|
||||
'time' => $time,
|
||||
'place' => $place,
|
||||
'title' => trim((string) ($announcement['ansage_titel'] ?? '')),
|
||||
'subtitle' => trim((string) ($announcement['ansage_untertitel'] ?? '')),
|
||||
'meta' => implode(' · ', array_filter([$label, $endLabel, $time !== '' ? $time . ' Uhr' : '', $place])),
|
||||
];
|
||||
}
|
||||
|
||||
function dashboard_list_posters(): array
|
||||
function dashboard_image(string $path, string $url): ?array
|
||||
{
|
||||
$files = glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/*.png') ?: [];
|
||||
natsort($files);
|
||||
|
||||
$posters = [];
|
||||
foreach ($files as $file) {
|
||||
if (!is_file($file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$basename = basename($file);
|
||||
$posters[] = [
|
||||
'url' => DASHBOARD_NEWS_URL . rawurlencode($basename) . '?v=' . (int) filemtime($file),
|
||||
'alt' => 'Aushang',
|
||||
];
|
||||
if (!is_file($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_values($posters);
|
||||
$size = @getimagesize($path);
|
||||
if (!$size || $size[0] < 1 || $size[1] < 1) {
|
||||
return null;
|
||||
}
|
||||
// Neu erzeugte, aber identische PNGs dürfen keinen sichtbaren Austausch auslösen.
|
||||
$version = @hash_file('sha256', $path);
|
||||
if ($version === false) {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
'url' => $url . '?v=' . $version,
|
||||
'width' => $size[0],
|
||||
'height' => $size[1],
|
||||
];
|
||||
}
|
||||
|
||||
function dashboard_month_images(): array
|
||||
function dashboard_list_posters(DateTimeImmutable $now, string $directory = CONTENTPANEL_NEWS_PNG_DIRECTORY): array
|
||||
{
|
||||
$today = $now->setTimezone(new DateTimeZone(DASHBOARD_TIMEZONE))->format('Y-m-d');
|
||||
$files = glob($directory . '/*.png') ?: [];
|
||||
natsort($files);
|
||||
$posters = [];
|
||||
foreach ($files as $file) {
|
||||
$name = basename($file);
|
||||
if (!preg_match('/^(.+)_page_([1-9][0-9]*)\\.png$/D', $name, $match)
|
||||
|| !contentpanel_is_valid_news_file_base($match[1])) {
|
||||
continue;
|
||||
}
|
||||
// Dieselbe inklusive Ablauffrist wie im Contentpanel. Nur filtern, nicht löschen.
|
||||
$expiry = substr($match[1], 0, 10);
|
||||
if (!dashboard_date($expiry) || $expiry < $today) {
|
||||
continue;
|
||||
}
|
||||
$image = dashboard_image($file, DASHBOARD_NEWS_URL . rawurlencode($name));
|
||||
if ($image) {
|
||||
$title = str_replace('_', ' ', substr($match[1], 11));
|
||||
$posters[] = $image + [
|
||||
'document' => $match[1],
|
||||
'kind' => 'poster', 'expires' => $expiry, 'page' => (int) $match[2],
|
||||
'title' => $title, 'alt' => $title . ' · Seite ' . (int) $match[2],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $posters;
|
||||
}
|
||||
|
||||
function dashboard_month_images(?string $directory = null): array
|
||||
{
|
||||
$directory ??= __DIR__ . '/../month';
|
||||
$images = [];
|
||||
foreach ([1, 2] as $page) {
|
||||
$filename = 'terminuebersicht-' . $page . '.png';
|
||||
$path = __DIR__ . '/../month/' . $filename;
|
||||
$version = is_file($path) ? (int) filemtime($path) : 0;
|
||||
$images[] = [
|
||||
'page' => $page,
|
||||
'url' => 'month/' . $filename . '?v=' . $version,
|
||||
];
|
||||
$image = dashboard_image($directory . '/' . $filename, 'month/' . $filename);
|
||||
if ($image) {
|
||||
$images[] = $image + [
|
||||
'kind' => 'month', 'page' => $page, 'title' => 'Monatsplan',
|
||||
'alt' => 'Monatsplan · Seite ' . $page,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
@@ -159,17 +178,12 @@ function dashboard_fetch_indoor_temperature(): array
|
||||
$url = $baseUrl . '/api/states/' . rawurlencode(DASHBOARD_HA_ENTITY_ID);
|
||||
$data = dashboard_http_json($url, ['Authorization: Bearer ' . $token]);
|
||||
|
||||
if (!array_key_exists('state', $data)) {
|
||||
if (!isset($data['state']) || !is_numeric($data['state'])) {
|
||||
throw new RuntimeException('Unerwartete Home-Assistant-Antwort.');
|
||||
}
|
||||
|
||||
$value = $data['state'];
|
||||
if (is_numeric($value)) {
|
||||
$value = round((float) $value, 1);
|
||||
}
|
||||
|
||||
return [
|
||||
'value' => $value,
|
||||
'value' => round((float) $data['state'], 1),
|
||||
'unit' => (string) ($data['attributes']['unit_of_measurement'] ?? '°C'),
|
||||
];
|
||||
}
|
||||
@@ -186,7 +200,6 @@ function dashboard_fetch_outdoor_weather(): array
|
||||
|
||||
$data = dashboard_http_json('https://api.openweathermap.org/data/2.5/weather?' . $query);
|
||||
$temperature = $data['main']['temp'] ?? null;
|
||||
$icon = (string) ($data['weather'][0]['icon'] ?? '');
|
||||
|
||||
if (!is_numeric($temperature)) {
|
||||
throw new RuntimeException('Unerwartete OpenWeather-Antwort.');
|
||||
@@ -194,27 +207,9 @@ function dashboard_fetch_outdoor_weather(): array
|
||||
|
||||
return [
|
||||
'temperature' => (int) round((float) $temperature),
|
||||
'icon' => dashboard_weather_kind($icon),
|
||||
];
|
||||
}
|
||||
|
||||
function dashboard_weather_kind(string $icon): string
|
||||
{
|
||||
return match ($icon) {
|
||||
'04d', '04n', '03d', '03n' => 'cloudy',
|
||||
'02d' => 'cloudy_day',
|
||||
'02n' => 'cloudy_night',
|
||||
'01d' => 'sun',
|
||||
'01n' => 'moon',
|
||||
'09d', '09n', '10n' => 'rain_shower',
|
||||
'10d' => 'rain',
|
||||
'11d', '11n' => 'thunder',
|
||||
'13d', '13n' => 'snowy',
|
||||
'50d', '50n' => 'mist',
|
||||
default => 'cloudy',
|
||||
};
|
||||
}
|
||||
|
||||
function dashboard_http_json(string $url, array $headers = []): array
|
||||
{
|
||||
$handle = curl_init($url);
|
||||
|
||||
Reference in New Issue
Block a user