From 8b2b421f6354f7c540ef75bf4aad50e0c05ddfce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Sat, 8 Aug 2026 12:10:11 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20JSON-Bridge=20f=C3=BCr=20die=20Logbuch-?= =?UTF-8?q?App=20inkl.=20Spende-Feld?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bringt die bislang nur im Working Tree vorhandene Logbuch-Unterstützung in die Versionsverwaltung: RepoLogbuch (Liste, Anlegen, Ändern, Löschen, BEOs/Objekte, Fahrkosten, Backup) samt der LB_*-Kommandos und der zugehörigen Tabellenkonstanten. Neu darin sind die Spalten Spende und SpendeBetrag: SELECT/INSERT/UPDATE sind erweitert, spendeValues() setzt beides auf NULL außer bei ArtFuehrung 'SF', prüft den Wert gegen eine Whitelist und übernimmt den Betrag nur bei 'bar'. Co-Authored-By: Claude Opus 5 (1M context) --- sternwarte/DB4js_all.php | 742 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 731 insertions(+), 11 deletions(-) diff --git a/sternwarte/DB4js_all.php b/sternwarte/DB4js_all.php index ba87e4e..aa545f8 100644 --- a/sternwarte/DB4js_all.php +++ b/sternwarte/DB4js_all.php @@ -123,6 +123,12 @@ const TBL_SOFIANMELD = 'sofianmeld'; const TBL_FDATES = 'fdates'; const TBL_SONNEANMELD = 'sonneanmeld'; +// ---- Logbuch-Tabellen ---- +const TBL_LOGBUCH = 'logbuch'; +const TBL_LOGBUCH_BEOS = 'logbuch_beos'; +const TBL_LOGBUCH_OBJEKTE = 'logbuch_objekte'; +const TBL_OBJEKTE = 'objekte'; + const URL_KALENDER = 'https://sternwarte-welzheim.de/kalender/'; const URL_BEO_FORM = 'beoform/beoFormular.php?id='; const LISTE_EMAIL = 'sofue-liste@sternwarte-welzheim.de'; @@ -547,6 +553,10 @@ class RepoBeos { return DB::one("SELECT * FROM " . TBL_BEOS . " WHERE name=?", [$name]); } + public static function getByNamePW(string $kurz, string $what): ?array + { + return DB::one("SELECT $what FROM " . TBL_BEOS . " WHERE kürzel=?", [$kurz]); + } public static function vorname(string $name): string { $r = self::getByName($name); @@ -893,6 +903,563 @@ class RepoStatistikGesamt } } +// ---- Logbuch Repository (liest direkt aus der logbuch-Tabelle) ---- +class RepoLogbuch +{ + public static function firstYear(): ?int + { + $r = DB::one("SELECT MIN(YEAR(Beginn)) AS y FROM logbuch"); + return isset($r['y']) && $r['y'] !== null ? (int)$r['y'] : null; + } + + public static function yearList(): array + { + $rows = DB::all("SELECT DISTINCT YEAR(Beginn) AS jahr FROM logbuch ORDER BY jahr DESC"); + return array_map(function($r) { return (int)$r['jahr']; }, $rows); + } + + public static function getByYear(int $year): array + { + // Laufenden Monat nicht anzeigen – Daten erst ab dem 1. des Folgemonats + $dateFilter = ($year == (int)date('Y')) ? " AND Beginn < DATE_FORMAT(NOW(), '%Y-%m-01')" : ""; + + $sql = + "SELECT MONTH(Beginn) AS monat," . + " SUM(CASE WHEN ArtFuehrung IN ('RF','PrF','SonF','Sonst') THEN Besucher ELSE 0 END) AS besucherNormal," . + " SUM(CASE WHEN ArtFuehrung IN ('SF','BEOS') THEN Besucher ELSE 0 END) AS besucherSonder," . + " SUM(CASE WHEN ArtFuehrung = 'ToT' THEN Besucher ELSE 0 END) AS besucherToT," . + " SUM(CASE WHEN ArtFuehrung NOT IN ('TD','Beob') THEN 1 ELSE 0 END) AS fuehrungen," . + " SUM(CASE WHEN ArtFuehrung = 'Beob' THEN 1 ELSE 0 END) AS beobachtungen," . + " SUM(CASE WHEN ArtFuehrung = 'TD' THEN 1 ELSE 0 END) AS techdienst" . + " FROM logbuch WHERE YEAR(Beginn) = ?" . $dateFilter . + " GROUP BY MONTH(Beginn)"; + $rows = DB::all($sql, [$year]); + + // Index by month number + $byMonth = []; + foreach ($rows as $r) { + $byMonth[(int)$r['monat']] = $r; + } + + // Always return all 12 months so statistic.js positions bars correctly + $data = []; + for ($m = 1; $m <= 12; $m++) { + if (isset($byMonth[$m])) { + $r = $byMonth[$m]; + $data[] = [ + 'datum' => sprintf('%04d-%02d-01', $year, $m), + 'besucherNormal' => (int)$r['besucherNormal'], + 'besucherSonder' => (int)$r['besucherSonder'], + 'besucherToT' => (int)$r['besucherToT'], + 'fuehrungen' => (int)$r['fuehrungen'], + 'beobachtungen' => (int)$r['beobachtungen'], + 'techdienst' => (int)$r['techdienst'], + 'beoTage' => 0, + 'beoZeit' => 0, + 'bemerkung' => '', + ]; + } else { + $data[] = [ + 'datum' => sprintf('%04d-%02d-01', $year, $m), + 'besucherNormal' => 0, + 'besucherSonder' => 0, + 'besucherToT' => 0, + 'fuehrungen' => 0, + 'beobachtungen' => 0, + 'techdienst' => 0, + 'beoTage' => 0, + 'beoZeit' => 0, + 'bemerkung' => '', + ]; + } + } + + $sumB = array_sum(array_map( + function($r) { return $r['besucherNormal'] + $r['besucherSonder'] + $r['besucherToT']; }, + $data + )); + $sumA = array_sum(array_map( + function($r) { return $r['fuehrungen'] + $r['beobachtungen'] + $r['techdienst']; }, + $data + )); + + $gesamt = DB::one("SELECT bemerkung FROM StatistikGesamt WHERE jahr=?", [$year]); + return [ + 'data' => $data, + 'sumB' => $sumB, + 'sumA' => $sumA, + 'sumBZ' => 0, + 'sumBT' => 0, + 'bemG' => $gesamt['bemerkung'] ?? '', + ]; + } + + public static function yearlySummary(): array + { + $sql = + "SELECT YEAR(Beginn) AS jahr," . + " SUM(Besucher) AS besucher," . + " COUNT(*) AS aktivitaeten," . + " '' AS bemerkung" . + " FROM logbuch GROUP BY YEAR(Beginn) ORDER BY jahr DESC"; + return DB::all($sql); + } + + public static function getByMonth(string $datum): ?array + { + $year = (int)substr($datum, 0, 4); + $month = (int)substr($datum, 5, 2); + $sql = + "SELECT COUNT(*) AS cnt," . + " SUM(CASE WHEN ArtFuehrung IN ('RF','PrF','SonF','Sonst') THEN COALESCE(Besucher,0) ELSE 0 END) AS besucherNormal," . + " SUM(CASE WHEN ArtFuehrung IN ('SF','BEOS') THEN COALESCE(Besucher,0) ELSE 0 END) AS besucherSonder," . + " SUM(CASE WHEN ArtFuehrung = 'ToT' THEN COALESCE(Besucher,0) ELSE 0 END) AS besucherToT," . + " SUM(CASE WHEN ArtFuehrung NOT IN ('TD','Beob') THEN 1 ELSE 0 END) AS fuehrungen," . + " SUM(CASE WHEN ArtFuehrung = 'Beob' THEN 1 ELSE 0 END) AS beobachtungen," . + " SUM(CASE WHEN ArtFuehrung = 'TD' THEN 1 ELSE 0 END) AS techdienst" . + " FROM logbuch WHERE YEAR(Beginn)=? AND MONTH(Beginn)=?"; + $r = DB::one($sql, [$year, $month]); + if ((int)($r['cnt'] ?? 0) === 0) return null; + return [ + 'datum' => sprintf('%04d-%02d-01', $year, $month), + 'besucherNormal' => (int)($r['besucherNormal'] ?? 0), + 'besucherSonder' => (int)($r['besucherSonder'] ?? 0), + 'besucherToT' => (int)($r['besucherToT'] ?? 0), + 'fuehrungen' => (int)($r['fuehrungen'] ?? 0), + 'beobachtungen' => (int)($r['beobachtungen'] ?? 0), + 'techdienst' => (int)($r['techdienst'] ?? 0), + 'beoTage' => 0, + 'beoZeit' => 0.0, + 'bemerkung' => '', + 'bemG' => '', + ]; + } + + // ---- Logbuch-CRUD & Auth (für Logbuch-App) ---- + + private static function checkAccess(int $logbuchId, int $userId, string $userRole, string $action): void + { + $row = DB::one("SELECT ID FROM " . TBL_LOGBUCH . " WHERE ID = ?", [$logbuchId]); + if (!$row) { + respondError('Eintrag nicht gefunden', 404); + } + if (strpos($userRole, 'admin') !== false) return; + $beoRow = DB::one( + "SELECT COUNT(*) AS cnt FROM " . TBL_LOGBUCH_BEOS . " WHERE LogbuchID = ? AND BeoID = ?", + [$logbuchId, $userId] + ); + if ((int)($beoRow['cnt'] ?? 0) === 0) { + respondError('Keine Berechtigung zum ' . $action . ' dieses Eintrags', 403); + } + } + + public static function getByKuerzel(string $kuerzel): ?array + { + return DB::one( + "SELECT id, name, vorname, `kürzel`, pw, MustChangePassword, role FROM " . TBL_BEOS . " WHERE `kürzel` = ?", + [$kuerzel] + ); + } + + public static function getByName(string $name): ?array + { + return DB::one( + "SELECT id, name, vorname, `kürzel`, pw, MustChangePassword, role FROM " . TBL_BEOS . " WHERE LOWER(name) = LOWER(?)", + [$name] + ); + } + + public static function updatePw(int $id, string $pwHash): void + { + DB::exec("UPDATE " . TBL_BEOS . " SET pw = ?, MustChangePassword = 0 WHERE id = ?", [$pwHash, $id]); + } + + public static function resetPw(int $id): void + { + DB::exec("UPDATE " . TBL_BEOS . " SET pw = NULL, MustChangePassword = 1 WHERE id = ?", [$id]); + } + + public static function listUsers(): array + { + $rows = DB::all( + "SELECT id, `kürzel`, name, vorname, role, (pw IS NOT NULL) AS hasPw FROM " . TBL_BEOS . " ORDER BY name, vorname" + ); + return array_map(function ($r) { + $r['hasPw'] = (bool)$r['hasPw']; + return $r; + }, $rows); + } + + public static function listLogbuch(array $p): array + { + $kuppel = $p['kuppel'] ?? 'West'; + $limit = min((int)($p['limit'] ?? 10), 500); + $offset = max(0, (int)($p['offset'] ?? 0)); + $month = $p['month'] ?? ''; + $order = strtoupper($p['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC'; + $search = trim($p['search'] ?? ''); + + $baseSelect = + "SELECT l.ID, l.Kuppel, l.ArtFuehrung," + . " DATE_FORMAT(l.Beginn, '%Y-%m-%dT%H:%i') AS Beginn," + . " DATE_FORMAT(l.Ende, '%Y-%m-%dT%H:%i') AS Ende," + . " l.Besucher, l.Bemerkungen, l.SonderName, l.Spende, l.SpendeBetrag," + . " l.WetterTemp, l.WetterFeuchte, l.WetterDruck," + . " l.created_by, l.created_at," + . " creator.kuerzel AS created_by_kuerzel," + . " GROUP_CONCAT(DISTINCT bk.kuerzel ORDER BY bk.kuerzel SEPARATOR ', ') AS BEOs," + . " GROUP_CONCAT(DISTINCT o.Name ORDER BY o.Name SEPARATOR ', ') AS Objekte" + . " FROM " . TBL_LOGBUCH . " l" + . " LEFT JOIN (SELECT id, `kürzel` AS kuerzel FROM " . TBL_BEOS . ") creator ON creator.id = l.created_by" + . " LEFT JOIN " . TBL_LOGBUCH_BEOS . " lb ON lb.LogbuchID = l.ID" + . " LEFT JOIN (SELECT id, `kürzel` AS kuerzel FROM " . TBL_BEOS . ") bk ON bk.id = lb.BeoID" + . " LEFT JOIN " . TBL_LOGBUCH_OBJEKTE . " lo ON lo.LogbuchID = l.ID" + . " LEFT JOIN " . TBL_OBJEKTE . " o ON o.ID = lo.ObjektID" + . " WHERE l.Kuppel = ?"; + + $baseParams = [$kuppel]; + $monthCond = ''; + + if ($month && preg_match('/^\d{4}-\d{2}$/', $month)) { + [$y, $m] = explode('-', $month); + $y = (int)$y; + $m = (int)$m; + $start = sprintf('%d-%02d-01', $y, $m); + $nextM = $m === 12 ? 1 : $m + 1; + $nextY = $m === 12 ? $y + 1 : $y; + $end = sprintf('%d-%02d-01', $nextY, $nextM); + $monthCond = " AND l.Beginn >= ? AND l.Beginn < ?"; + $baseParams[] = $start; + $baseParams[] = $end; + } + + if ($search !== '') { + $pattern = '%' . $search . '%'; + $listSql = $baseSelect . $monthCond + . " GROUP BY l.ID" + . " HAVING (MAX(l.Bemerkungen) LIKE ? OR BEOs LIKE ? OR Objekte LIKE ?)" + . " ORDER BY l.Beginn $order LIMIT $limit OFFSET $offset"; + $listParams = array_merge($baseParams, [$pattern, $pattern, $pattern]); + + $countSql = + "SELECT COUNT(*) AS total FROM (" + . "SELECT l.ID FROM " . TBL_LOGBUCH . " l" + . " LEFT JOIN " . TBL_LOGBUCH_BEOS . " lb ON lb.LogbuchID = l.ID" + . " LEFT JOIN (SELECT id, `kürzel` AS kuerzel FROM " . TBL_BEOS . ") bk ON bk.id = lb.BeoID" + . " LEFT JOIN " . TBL_LOGBUCH_OBJEKTE . " lo ON lo.LogbuchID = l.ID" + . " LEFT JOIN " . TBL_OBJEKTE . " o ON o.ID = lo.ObjektID" + . " WHERE l.Kuppel = ?" . ($monthCond ? $monthCond : '') + . " GROUP BY l.ID" + . " HAVING (MAX(l.Bemerkungen) LIKE ?" + . " OR GROUP_CONCAT(DISTINCT bk.kuerzel ORDER BY bk.kuerzel SEPARATOR ', ') LIKE ?" + . " OR GROUP_CONCAT(DISTINCT o.Name ORDER BY o.Name SEPARATOR ', ') LIKE ?)" + . ") AS sub"; + $countParams = array_merge($baseParams, [$pattern, $pattern, $pattern]); + } else { + $listSql = $baseSelect . $monthCond + . " GROUP BY l.ID ORDER BY l.Beginn $order LIMIT $limit OFFSET $offset"; + $listParams = $baseParams; + + $countSql = "SELECT COUNT(*) AS total FROM " . TBL_LOGBUCH . " WHERE Kuppel = ?"; + $countParams = [$kuppel]; + if ($monthCond) { + $countSql .= " AND Beginn >= ? AND Beginn < ?"; + $countParams = array_merge($countParams, array_slice($baseParams, 1)); + } + } + + $entries = DB::all($listSql, $listParams); + $countRow = DB::one($countSql, $countParams); + + return ['entries' => $entries, 'total' => (int)($countRow['total'] ?? 0)]; + } + + private static function upsertObjekte(int $logbuchId, array $objekte, string $kategorie): void + { + foreach ($objekte as $obj) { + $objektId = (int)($obj['ID'] ?? 0); + if (!$objektId) { + $existing = DB::one( + "SELECT ID, Kategorie FROM " . TBL_OBJEKTE . " WHERE LOWER(Name) = LOWER(?)", + [$obj['Name']] + ); + if ($existing) { + $objektId = (int)$existing['ID']; + $currentKats = $existing['Kategorie'] ? explode(',', (string)$existing['Kategorie']) : []; + if (!in_array($kategorie, $currentKats, true)) { + $currentKats[] = $kategorie; + sort($currentKats); + DB::exec("UPDATE " . TBL_OBJEKTE . " SET Kategorie = ? WHERE ID = ?", [implode(',', $currentKats), $objektId]); + } + } else { + DB::exec("INSERT INTO " . TBL_OBJEKTE . " (Name, Kategorie) VALUES (?, ?)", [$obj['Name'], $kategorie]); + $objektId = (int)DB::insertId(); + } + } + DB::exec("UPDATE " . TBL_OBJEKTE . " SET LastUsed = NOW() WHERE ID = ?", [$objektId]); + DB::exec( + "INSERT INTO " . TBL_LOGBUCH_OBJEKTE . " (LogbuchID, ObjektID) VALUES (?, ?)", + [$logbuchId, $objektId] + ); + } + } + + /** Spende nur bei Sonderführung; Betrag nur bei 'bar'. Rückgabe: [Spende, SpendeBetrag] */ + private static function spendeValues(array $p): array + { + if (($p['ArtFuehrung'] ?? '') !== 'SF') return [null, null]; + $art = in_array($p['Spende'] ?? null, ['bar', 'ueberw', 'kasse', 'keine'], true) + ? $p['Spende'] + : null; + $betrag = ($art === 'bar' && isset($p['SpendeBetrag'])) + ? round((float)$p['SpendeBetrag'], 2) + : null; + return [$art, $betrag]; + } + + public static function createLogbuch(array $p): int + { + $wetter = is_array($p['Wetter'] ?? null) ? $p['Wetter'] : []; + [$spende, $spendeBetrag] = self::spendeValues($p); + $db = DB::conn(); + $db->beginTransaction(); + try { + DB::exec( + "INSERT INTO " . TBL_LOGBUCH + . " (Kuppel, ArtFuehrung, SonderName, Spende, SpendeBetrag, Beginn, Ende, Besucher," + . " Bemerkungen, WetterTemp, WetterFeuchte, WetterDruck, created_by)" + . " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + $p['Kuppel'], + $p['ArtFuehrung'], + $p['SonderName'] ?? null, + $spende, + $spendeBetrag, + $p['Beginn'], + $p['Ende'], + (int)($p['Besucher'] ?? 0), + isset($p['Bemerkungen']) ? substr((string)$p['Bemerkungen'], 0, 500) : null, + isset($wetter['temp']) ? (float)$wetter['temp'] : null, + isset($wetter['feuchte']) ? (float)$wetter['feuchte'] : null, + isset($wetter['druck']) ? (float)$wetter['druck'] : null, + (int)$p['created_by'], + ] + ); + $logbuchId = (int)DB::insertId(); + + foreach (($p['beoIds'] ?? []) as $beoId) { + DB::exec( + "INSERT INTO " . TBL_LOGBUCH_BEOS . " (LogbuchID, BeoID) VALUES (?, ?)", + [$logbuchId, (int)$beoId] + ); + } + + $kategorie = ($p['ArtFuehrung'] === 'SonF') ? 'sonne' : 'stern'; + self::upsertObjekte($logbuchId, $p['objekte'] ?? [], $kategorie); + + $db->commit(); + return $logbuchId; + } catch (Throwable $e) { + $db->rollBack(); + throw $e; + } + } + + public static function updateLogbuch(int $id, int $userId, string $userRole, array $p): void + { + self::checkAccess($id, $userId, $userRole, 'Ändern'); + + $wetter = is_array($p['Wetter'] ?? null) ? $p['Wetter'] : []; + [$spende, $spendeBetrag] = self::spendeValues($p); + $db = DB::conn(); + $db->beginTransaction(); + try { + DB::exec( + "UPDATE " . TBL_LOGBUCH + . " SET Kuppel=?, ArtFuehrung=?, SonderName=?, Spende=?, SpendeBetrag=?, Beginn=?, Ende=?, Besucher=?," + . " Bemerkungen=?, WetterTemp=?, WetterFeuchte=?, WetterDruck=? WHERE ID=?", + [ + $p['Kuppel'], + $p['ArtFuehrung'], + $p['SonderName'] ?? null, + $spende, + $spendeBetrag, + $p['Beginn'], + $p['Ende'], + (int)($p['Besucher'] ?? 0), + isset($p['Bemerkungen']) ? substr((string)$p['Bemerkungen'], 0, 500) : null, + isset($wetter['temp']) ? (float)$wetter['temp'] : null, + isset($wetter['feuchte']) ? (float)$wetter['feuchte'] : null, + isset($wetter['druck']) ? (float)$wetter['druck'] : null, + $id, + ] + ); + + DB::exec("DELETE FROM " . TBL_LOGBUCH_BEOS . " WHERE LogbuchID = ?", [$id]); + DB::exec("DELETE FROM " . TBL_LOGBUCH_OBJEKTE . " WHERE LogbuchID = ?", [$id]); + + foreach (($p['beoIds'] ?? []) as $beoId) { + DB::exec( + "INSERT INTO " . TBL_LOGBUCH_BEOS . " (LogbuchID, BeoID) VALUES (?, ?)", + [$id, (int)$beoId] + ); + } + + $kategorie = ($p['ArtFuehrung'] === 'SonF') ? 'sonne' : 'stern'; + self::upsertObjekte($id, $p['objekte'] ?? [], $kategorie); + + $db->commit(); + } catch (Throwable $e) { + $db->rollBack(); + throw $e; + } + } + + public static function deleteLogbuch(int $id, int $userId, string $userRole): void + { + self::checkAccess($id, $userId, $userRole, 'Löschen'); + DB::exec("DELETE FROM " . TBL_LOGBUCH . " WHERE ID = ?", [$id]); + } + + public static function listBeos(): array + { + return DB::all( + "SELECT id AS ID, `kürzel` AS Kuerzel," + . " CONCAT(IFNULL(vorname, ''), IF(vorname IS NOT NULL, ' ', ''), name) AS Name" + . " FROM " . TBL_BEOS + . " WHERE `kürzel` IS NOT NULL AND FIND_IN_SET('guide', role) > 0" + . " ORDER BY name ASC" + ); + } + + // Alle BEOs mit Adressfeldern (für die km-Berechnung zur Sternwarte Welzheim) + public static function listBeoAdressen(): array + { + return DB::all( + "SELECT id AS ID, `kürzel` AS Kuerzel, name AS Name," + . " adresse AS Adresse, plz AS Plz, ort AS Ort, km AS Km" + . " FROM " . TBL_BEOS + . " ORDER BY name ASC" + ); + } + + // Straßenentfernung (km) eines BEOs zur Sternwarte setzen (NULL = unbekannt) + public static function updateBeoKm(int $id, ?float $km): void + { + DB::exec("UPDATE " . TBL_BEOS . " SET km = ? WHERE id = ?", [$km, $id]); + } + + public static function listObjekte(string $kategorie = 'stern'): array + { + return DB::all("SELECT ID, Name FROM " . TBL_OBJEKTE . " WHERE FIND_IN_SET(?, Kategorie) > 0 ORDER BY LastUsed DESC LIMIT 100", [$kategorie]); + } + + public static function listObjekteAdmin(): array + { + return DB::all("SELECT ID, Name, LastUsed, Kategorie FROM " . TBL_OBJEKTE . " ORDER BY Name ASC"); + } + + public static function createObjekt(string $name, string $kategorie = 'stern'): int + { + DB::exec("INSERT INTO " . TBL_OBJEKTE . " (Name, Kategorie) VALUES (?, ?)", [$name, $kategorie]); + return (int)DB::insertId(); + } + + public static function updateObjekt(int $id, string $name, ?string $kategorie = null): void + { + if ($kategorie !== null) { + DB::exec("UPDATE " . TBL_OBJEKTE . " SET Name = ?, Kategorie = ? WHERE ID = ?", [$name, $kategorie, $id]); + } else { + DB::exec("UPDATE " . TBL_OBJEKTE . " SET Name = ? WHERE ID = ?", [$name, $id]); + } + } + + public static function deleteObjekt(int $id): void + { + DB::exec("DELETE FROM " . TBL_OBJEKTE . " WHERE ID = ?", [$id]); + } + + public static function fahrkosten(string $ab, string $bis): array + { + return DB::all( + "SELECT b.id AS ID, b.`kürzel` AS Kuerzel," + . " CONCAT(IFNULL(b.vorname, ''), IF(b.vorname IS NOT NULL, ' ', ''), b.name) AS Name," + . " b.km AS Km," + . " COUNT(DISTINCT l.ID) AS Anzahl" + . " FROM " . TBL_BEOS . " b" + . " JOIN " . TBL_LOGBUCH_BEOS . " lb ON lb.BeoID = b.id" + . " JOIN " . TBL_LOGBUCH . " l ON l.ID = lb.LogbuchID" + . " WHERE l.Beginn >= ? AND l.Beginn <= ?" + . " GROUP BY b.id, b.`kürzel`, b.name, b.vorname, b.km" + . " ORDER BY b.name ASC", + [$ab . ' 00:00:00', $bis . ' 23:59:59'] + ); + } + + public static function statistik(int $year): array + { + $monthly = DB::all( + "SELECT MONTH(Beginn) AS monat," + . " COUNT(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF') THEN 1 END) AS tageFuehrungen," + . " COUNT(CASE WHEN ArtFuehrung = 'Beob' THEN 1 END) AS tageBeob," + . " COUNT(CASE WHEN ArtFuehrung = 'TD' THEN 1 END) AS tageTD," + . " COUNT(CASE WHEN ArtFuehrung = 'Sonst' THEN 1 END) AS tageSonst," + . " COUNT(CASE WHEN ArtFuehrung = 'BEOS' THEN 1 END) AS tageBEOS," + . " COUNT(CASE WHEN ArtFuehrung = 'ToT' THEN 1 END) AS tagesToT," + . " COUNT(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','Beob','TD','Sonst','BEOS','ToT') THEN 1 END) AS tageGesamt," + . " SUM(CASE WHEN ArtFuehrung = 'RF' THEN Besucher ELSE 0 END) AS besucherRF," + . " SUM(CASE WHEN ArtFuehrung = 'SF' THEN Besucher ELSE 0 END) AS besucherSF," + . " SUM(CASE WHEN ArtFuehrung = 'SonF' THEN Besucher ELSE 0 END) AS besucherSonF," + . " SUM(CASE WHEN ArtFuehrung = 'PrF' THEN Besucher ELSE 0 END) AS besucherPrF," + . " SUM(CASE WHEN ArtFuehrung = 'ToT' THEN Besucher ELSE 0 END) AS besucherToT," + . " SUM(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','ToT') THEN Besucher ELSE 0 END) AS besucherGesamt" + . " FROM " . TBL_LOGBUCH . " WHERE YEAR(Beginn) = ?" + . " GROUP BY MONTH(Beginn) ORDER BY monat", + [$year] + ); + + $monthly = array_map(function ($r) { + return array_map(function ($v) { return is_numeric($v) ? (int)$v : $v; }, $r); + }, $monthly); + + $cumRow = DB::one( + "SELECT SUM(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','ToT') THEN Besucher ELSE 0 END) AS total" + . " FROM " . TBL_LOGBUCH . " WHERE YEAR(Beginn) = ?", + [$year] + ); + $tageRow = DB::one( + "SELECT COUNT(*) AS tage FROM " . TBL_LOGBUCH + . " WHERE YEAR(Beginn) = ? AND ArtFuehrung IN ('RF','SF','SonF','PrF','Beob','TD','Sonst','BEOS','ToT')", + [$year] + ); + + return [ + 'monthly' => $monthly, + 'cumulative' => (int)($cumRow['total'] ?? 0), + 'tage' => (int)($tageRow['tage'] ?? 0), + 'year' => $year, + ]; + } + + public static function backupData(): array + { + $logbuchTables = [TBL_LOGBUCH, TBL_LOGBUCH_BEOS, TBL_LOGBUCH_OBJEKTE, TBL_OBJEKTE]; + $db = DB::conn(); + $tables = []; + foreach ($logbuchTables as $name) { + $createStmt = $db->query("SHOW CREATE TABLE `$name`"); + $createRow = $createStmt ? $createStmt->fetch(PDO::FETCH_ASSOC) : false; + $createSql = $createRow ? array_values($createRow)[1] : ''; + + $rowsStmt = $db->query("SELECT * FROM `$name`"); + $rows = $rowsStmt ? $rowsStmt->fetchAll(PDO::FETCH_ASSOC) : []; + + $tables[] = ['name' => $name, 'createSql' => $createSql, 'rows' => $rows]; + } + return ['tables' => $tables]; + } +} + // ---- Kalender Repository ---- class RepoKalender { @@ -1103,6 +1670,27 @@ class Commands 'UPDATETLNFD' => 'Teilnehmer-Datum aktualisieren', 'SEND_MAIL_HTML' => 'Mail versenden (Text)', 'GET_ALLTEILN' => 'Alle Teilnehmer ab Datum', + // Logbuch-Befehle + 'LB_AUTH_KUERZEL' => 'Logbuch: BEO-Auth nach Kürzel', + 'LB_AUTH_NAME' => 'Logbuch: BEO-Auth nach Name', + 'LB_UPDATE_PW' => 'Logbuch: Passwort setzen', + 'LB_RESET_PW' => 'Logbuch: Passwort zurücksetzen', + 'LB_LIST_USERS' => 'Logbuch: Alle Benutzer', + 'LB_GET_BEO_ADRESSEN' => 'Logbuch: BEOs mit Adressen (km-Berechnung)', + 'LB_UPDATE_BEO_KM' => 'Logbuch: km-Entfernung eines BEOs setzen', + 'LB_LIST_LOGBUCH' => 'Logbuch: Einträge abrufen', + 'LB_CREATE_LOGBUCH' => 'Logbuch: Eintrag anlegen', + 'LB_UPDATE_LOGBUCH' => 'Logbuch: Eintrag ändern', + 'LB_DELETE_LOGBUCH' => 'Logbuch: Eintrag löschen', + 'LB_GET_BEOS' => 'Logbuch: Guide-BEOs abrufen', + 'LB_GET_OBJEKTE' => 'Logbuch: Objekte (zuletzt genutzt)', + 'LB_CREATE_OBJEKT' => 'Logbuch: Objekt anlegen', + 'LB_UPDATE_OBJEKT' => 'Logbuch: Objekt umbenennen', + 'LB_DELETE_OBJEKT' => 'Logbuch: Objekt löschen', + 'LB_LIST_OBJEKTE_ADMIN' => 'Logbuch: Alle Objekte (Admin)', + 'LB_FAHRKOSTEN' => 'Logbuch: Fahrkosten-Auswertung', + 'LB_STATISTIK' => 'Logbuch: Jahresstatistik', + 'LB_BACKUP_DATA' => 'Logbuch: Backup-Daten (Schema + Rows)', 'LIST_COMMANDS' => 'Liste aller Kommandos' ]; } @@ -1229,6 +1817,9 @@ try { case 'GET_ONEBEO': $r = RepoBeos::getByName($input['name']); respond($r ?: ['error' => 'Not found']); + case 'GET_ONEBEOPW': + $r = RepoBeos::getByNamePW($input['kurz'], $input['what']); + respond($r ?: ['error' => 'Not found']); // Sonderführungen case 'GET_ONE': @@ -1282,7 +1873,8 @@ try { case 'SENDMAILZUSAGE': $info = RepoSoFue::getById((int)$input['id']); if (!$info) respondError('Führung nicht gefunden', 404); - $ma = RepoBeos::getByName($input['mitarbeiter']); + $ma_first = trim(explode(',', $input['mitarbeiter'])[0]); + $ma = RepoBeos::getByName($ma_first); $ma_name = $ma['name']; $ma_vorname = $ma['vorname']; $gender = $ma['gender'] == 'm'; @@ -1464,19 +2056,29 @@ Spendenbescheinigung: \t" . $info['spende'] . " // Statistik - Jahre case 'GET_YEARS': - respond(RepoStatistikJahre::getYearList()); + $fromSta = RepoStatistikJahre::getYearList(); + $fromLog = RepoLogbuch::yearList(); + $all = array_values(array_unique(array_merge($fromLog, $fromSta))); + rsort($all); + respond($all); case 'GET_ONE_A': - $datum = $input['datum'] ?? respondError('datum missing'); + $datum = $input['datum'] ?? $_GET['datum'] ?? null; + if (!$datum) respondError('datum missing'); + $year = (int)substr($datum, 0, 4); + if ($year >= 2026) { + respond(RepoLogbuch::getByMonth($datum)); + } $result = RepoStatistikJahre::getByDate($datum); if ($result) { - // Add bemG from Gesamt - $year = substr($datum, 0, 4); - $gesamt = RepoStatistikGesamt::getByYear((int)$year); + $gesamt = RepoStatistikGesamt::getByYear($year); $result['bemG'] = $gesamt['bemerkung'] ?? ''; } - respond($result ?: ['error' => 'Not found']); + respond($result); case 'GET_ALL_A': - $year = (int)($input['jahr'] ?? date('Y')); + $year = (int)($input['jahr'] ?? $_GET['jahr'] ?? date('Y')); + if ($year >= 2026) { + respond(RepoLogbuch::getByYear($year)); + } respond(RepoStatistikJahre::getByYear($year)); case 'CRUP_A': if (!isset($input['toInsert'])) respondError('toInsert missing'); @@ -1484,11 +2086,43 @@ Spendenbescheinigung: \t" . $info['spende'] . " // Statistik - Gesamt case 'GET_ALL_G': - respond(RepoStatistikGesamt::getAll()); + $gesamt = RepoStatistikGesamt::getAll(); + // Für Jahre >= 2026: Daten live aus logbuch statt aus StatistikGesamt + $logSummary = RepoLogbuch::yearlySummary(); + $logMap = []; + foreach ($logSummary as $r) { if ((int)$r['jahr'] >= 2026) $logMap[(int)$r['jahr']] = $r; } + $merged = []; + $existingYears = []; + foreach ($gesamt['data'] as $r) { + $j = (int)$r['jahr']; + $existingYears[] = $j; + $merged[] = ($j >= 2026 && isset($logMap[$j])) ? $logMap[$j] : $r; + } + foreach ($logMap as $j => $r) { + if (!in_array($j, $existingYears)) { $merged[] = $r; } + } + usort($merged, function($a, $b) { return (int)$b['jahr'] - (int)$a['jahr']; }); + $gesamt['data'] = $merged; + $gesamt['sumB'] = array_sum(array_column($merged, 'besucher')); + $gesamt['sumA'] = array_sum(array_column($merged, 'aktivitaeten')); + // lastDate auf das neueste Jahr (ggf. aus logbuch) aktualisieren + if (!empty($logMap)) { + $lbLastYear = max(array_keys($logMap)); + if ($lbLastYear > (int)$gesamt['lastDate']['lastYear']) { + $lbYearData = RepoLogbuch::getByYear($lbLastYear); + $gesamt['lastDate'] = ['lastYear' => $lbLastYear, 'fullYear' => $lbYearData['data']]; + } + } + respond($gesamt); case 'GET_ONE_G': $year = (int)($input['jahr'] ?? date('Y')); - $result = RepoStatistikGesamt::getByYear($year); - respond($result ?: ['error' => 'Not found']); + if ($year >= 2026) { + $rows = RepoLogbuch::yearlySummary(); + $result = null; + foreach ($rows as $r) { if ((int)$r['jahr'] === $year) { $result = $r; break; } } + respond($result); + } + respond(RepoStatistikGesamt::getByYear($year)); case 'CRUP_G': if (!isset($input['toInsert'])) respondError('toInsert missing'); respond(RepoStatistikGesamt::createOrUpdate($input['toInsert'])); @@ -1536,6 +2170,92 @@ Spendenbescheinigung: \t" . $info['spende'] . " $sql = "SELECT * FROM " . TBL_ANMELD . " WHERE fdatum >= ? ORDER BY fid ASC"; respond(DB::all($sql, [$fdatum])); + // ---- Logbuch ---- + case 'LB_AUTH_KUERZEL': + $kuerzel = $input['kuerzel'] ?? respondError('kuerzel missing'); + respond(['beo' => RepoLogbuch::getByKuerzel((string)$kuerzel)]); + case 'LB_AUTH_NAME': + $name = $input['name'] ?? respondError('name missing'); + respond(['beo' => RepoLogbuch::getByName((string)$name)]); + case 'LB_UPDATE_PW': + $id = (int)($input['id'] ?? respondError('id missing')); + $pw = $input['pw'] ?? respondError('pw missing'); + RepoLogbuch::updatePw($id, (string)$pw); + respond(['ok' => true]); + case 'LB_RESET_PW': + $id = (int)($input['id'] ?? respondError('id missing')); + RepoLogbuch::resetPw($id); + respond(['ok' => true]); + case 'LB_LIST_USERS': + respond(RepoLogbuch::listUsers()); + case 'LB_LIST_LOGBUCH': + respond(RepoLogbuch::listLogbuch($input)); + case 'LB_CREATE_LOGBUCH': + $newId = RepoLogbuch::createLogbuch($input); + respond(['id' => $newId], 201); + case 'LB_UPDATE_LOGBUCH': + $id = (int)($input['id'] ?? respondError('id missing')); + $userId = (int)($input['user_id'] ?? respondError('user_id missing')); + $userRole = (string)($input['user_role'] ?? ''); + RepoLogbuch::updateLogbuch($id, $userId, $userRole, $input); + respond(['ok' => true]); + case 'LB_DELETE_LOGBUCH': + $id = (int)($input['id'] ?? respondError('id missing')); + $userId = (int)($input['user_id'] ?? respondError('user_id missing')); + $userRole = (string)($input['user_role'] ?? ''); + RepoLogbuch::deleteLogbuch($id, $userId, $userRole); + respond(['ok' => true]); + case 'LB_GET_BEOS': + respond(RepoLogbuch::listBeos()); + case 'LB_GET_BEO_ADRESSEN': + respond(RepoLogbuch::listBeoAdressen()); + case 'LB_UPDATE_BEO_KM': + $id = (int)($input['id'] ?? respondError('id missing')); + $km = (array_key_exists('km', $input) && $input['km'] !== null && $input['km'] !== '') + ? (float)$input['km'] : null; + RepoLogbuch::updateBeoKm($id, $km); + respond(['ok' => true]); + case 'LB_GET_OBJEKTE': + $kategorie = in_array($input['kategorie'] ?? '', ['stern', 'sonne']) ? (string)$input['kategorie'] : 'stern'; + respond(RepoLogbuch::listObjekte($kategorie)); + case 'LB_CREATE_OBJEKT': + $name = trim((string)($input['name'] ?? respondError('name missing'))); + if ($name === '') respondError('name darf nicht leer sein'); + $kategorie = in_array($input['kategorie'] ?? '', ['stern', 'sonne']) ? (string)$input['kategorie'] : 'stern'; + $newId = RepoLogbuch::createObjekt($name, $kategorie); + respond(['ID' => $newId, 'Name' => $name, 'Kategorie' => $kategorie], 201); + case 'LB_UPDATE_OBJEKT': + $id = (int)($input['id'] ?? respondError('id missing')); + $name = trim((string)($input['name'] ?? respondError('name missing'))); + if ($name === '') respondError('name darf nicht leer sein'); + $kategorie = in_array($input['kategorie'] ?? '', ['stern', 'sonne']) ? (string)$input['kategorie'] : null; + RepoLogbuch::updateObjekt($id, $name, $kategorie); + respond(['ID' => $id, 'Name' => $name]); + case 'LB_DELETE_OBJEKT': + $id = (int)($input['id'] ?? respondError('id missing')); + RepoLogbuch::deleteObjekt($id); + respond(['ok' => true]); + case 'LB_LIST_OBJEKTE_ADMIN': + respond(RepoLogbuch::listObjekteAdmin()); + case 'LB_FAHRKOSTEN': + $ab = $input['ab'] ?? respondError('ab missing'); + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', (string)$ab)) respondError('ab muss YYYY-MM-DD sein'); + $bis = $input['bis'] ?? date('Y-m-d'); + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', (string)$bis)) respondError('bis muss YYYY-MM-DD sein'); + respond(RepoLogbuch::fahrkosten((string)$ab, (string)$bis)); + case 'LB_STATISTIK': + $year = (int)($input['year'] ?? date('Y')); + respond(RepoLogbuch::statistik($year)); + case 'LB_BACKUP_DATA': + respond(RepoLogbuch::backupData()); + + case 'LB_UNUSED_OBJEKTE': + respond(DB::all( + "SELECT o.ID, o.Name, o.LastUsed FROM " . TBL_OBJEKTE . " o" . + " LEFT JOIN " . TBL_LOGBUCH_OBJEKTE . " lo ON lo.ObjektID = o.ID" . + " WHERE lo.ObjektID IS NULL ORDER BY o.Name" + )); + case 'LIST_COMMANDS': respond(['commands' => Commands::MAP, 'count' => count(Commands::MAP)]);