feat: JSON-Bridge für die Logbuch-App inkl. Spende-Feld

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) <noreply@anthropic.com>
This commit is contained in:
2026-08-08 12:10:11 +02:00
parent fbce630b52
commit 8b2b421f63
+731 -11
View File
@@ -123,6 +123,12 @@ const TBL_SOFIANMELD = 'sofianmeld';
const TBL_FDATES = 'fdates'; const TBL_FDATES = 'fdates';
const TBL_SONNEANMELD = 'sonneanmeld'; 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_KALENDER = 'https://sternwarte-welzheim.de/kalender/';
const URL_BEO_FORM = 'beoform/beoFormular.php?id='; const URL_BEO_FORM = 'beoform/beoFormular.php?id=';
const LISTE_EMAIL = 'sofue-liste@sternwarte-welzheim.de'; const LISTE_EMAIL = 'sofue-liste@sternwarte-welzheim.de';
@@ -547,6 +553,10 @@ class RepoBeos
{ {
return DB::one("SELECT * FROM " . TBL_BEOS . " WHERE name=?", [$name]); 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 public static function vorname(string $name): string
{ {
$r = self::getByName($name); $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 ---- // ---- Kalender Repository ----
class RepoKalender class RepoKalender
{ {
@@ -1103,6 +1670,27 @@ class Commands
'UPDATETLNFD' => 'Teilnehmer-Datum aktualisieren', 'UPDATETLNFD' => 'Teilnehmer-Datum aktualisieren',
'SEND_MAIL_HTML' => 'Mail versenden (Text)', 'SEND_MAIL_HTML' => 'Mail versenden (Text)',
'GET_ALLTEILN' => 'Alle Teilnehmer ab Datum', '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' 'LIST_COMMANDS' => 'Liste aller Kommandos'
]; ];
} }
@@ -1229,6 +1817,9 @@ try {
case 'GET_ONEBEO': case 'GET_ONEBEO':
$r = RepoBeos::getByName($input['name']); $r = RepoBeos::getByName($input['name']);
respond($r ?: ['error' => 'Not found']); respond($r ?: ['error' => 'Not found']);
case 'GET_ONEBEOPW':
$r = RepoBeos::getByNamePW($input['kurz'], $input['what']);
respond($r ?: ['error' => 'Not found']);
// Sonderführungen // Sonderführungen
case 'GET_ONE': case 'GET_ONE':
@@ -1282,7 +1873,8 @@ try {
case 'SENDMAILZUSAGE': case 'SENDMAILZUSAGE':
$info = RepoSoFue::getById((int)$input['id']); $info = RepoSoFue::getById((int)$input['id']);
if (!$info) respondError('Führung nicht gefunden', 404); 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_name = $ma['name'];
$ma_vorname = $ma['vorname']; $ma_vorname = $ma['vorname'];
$gender = $ma['gender'] == 'm'; $gender = $ma['gender'] == 'm';
@@ -1464,19 +2056,29 @@ Spendenbescheinigung: \t" . $info['spende'] . "
// Statistik - Jahre // Statistik - Jahre
case 'GET_YEARS': 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': 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); $result = RepoStatistikJahre::getByDate($datum);
if ($result) { if ($result) {
// Add bemG from Gesamt $gesamt = RepoStatistikGesamt::getByYear($year);
$year = substr($datum, 0, 4);
$gesamt = RepoStatistikGesamt::getByYear((int)$year);
$result['bemG'] = $gesamt['bemerkung'] ?? ''; $result['bemG'] = $gesamt['bemerkung'] ?? '';
} }
respond($result ?: ['error' => 'Not found']); respond($result);
case 'GET_ALL_A': 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)); respond(RepoStatistikJahre::getByYear($year));
case 'CRUP_A': case 'CRUP_A':
if (!isset($input['toInsert'])) respondError('toInsert missing'); if (!isset($input['toInsert'])) respondError('toInsert missing');
@@ -1484,11 +2086,43 @@ Spendenbescheinigung: \t" . $info['spende'] . "
// Statistik - Gesamt // Statistik - Gesamt
case 'GET_ALL_G': 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': case 'GET_ONE_G':
$year = (int)($input['jahr'] ?? date('Y')); $year = (int)($input['jahr'] ?? date('Y'));
$result = RepoStatistikGesamt::getByYear($year); if ($year >= 2026) {
respond($result ?: ['error' => 'Not found']); $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': case 'CRUP_G':
if (!isset($input['toInsert'])) respondError('toInsert missing'); if (!isset($input['toInsert'])) respondError('toInsert missing');
respond(RepoStatistikGesamt::createOrUpdate($input['toInsert'])); respond(RepoStatistikGesamt::createOrUpdate($input['toInsert']));
@@ -1536,6 +2170,92 @@ Spendenbescheinigung: \t" . $info['spende'] . "
$sql = "SELECT * FROM " . TBL_ANMELD . " WHERE fdatum >= ? ORDER BY fid ASC"; $sql = "SELECT * FROM " . TBL_ANMELD . " WHERE fdatum >= ? ORDER BY fid ASC";
respond(DB::all($sql, [$fdatum])); 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': case 'LIST_COMMANDS':
respond(['commands' => Commands::MAP, 'count' => count(Commands::MAP)]); respond(['commands' => Commands::MAP, 'count' => count(Commands::MAP)]);