Files
sternwarte/sternwarte/DB4js_all.php
T
admin b4a339392d feat: Sonderführungen aus dem Logbuch in SoFue2 nachtragen
RepoSoFue::ausLogbuch() sucht zu einem Logbuch-Eintrag der Art 'SF' den
passenden Datensatz über DATE(wtermin) — bei mehreren Führungen an einem
Tag gewinnt die zeitlich nächstgelegene — und setzt über das vorhandene
updateAfter() stattgefunden, anzahl_echt, bezahlt und remarks.

Aufgerufen wird das im Dispatcher nach dem Commit der Logbuch-
Transaktion; die Methode wirft nie, sondern liefert einen Status, der als
Feld 'sofue' in der Antwort zurückgeht.

bezahlt wird wie im beoanswer-Formular geschrieben (Kasse 50€,
Überweisung, Spendenkässle, keine). Da SoFue2 latin1 ist und remarks nur
100 Zeichen fasst, kürzt cp1252Safe() den Text und entfernt nicht
darstellbare Zeichen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 12:57:36 +02:00

2353 lines
99 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* DB4js_all.php
* Vereinheitlichte API für alle bisherigen Einzel-Endpoints:
* - Öffentliche Führungen (anmeldungen)
* - Sonderführungen (SoFue2 + sofianmeld)
* - Termine (fdatum1)
* - BEOs (beos)
* - Statistiken
* - Kalender-Platzhalter
*
* Verbesserungen gegenüber Vorgängerversionen:
* - PDO Prepared Statements (SQL-Injection Schutz)
* - Einheitliche Fehler- / Antwortstruktur (JSON)
* - Zentrale Dispatch-Funktion (Command -> Handler)
* - Optionale Basic-Auth (Umgebungsvariablen API_USER / API_PASS)
* - Eingabevalidierung & Typ-Casts
* - Fallback auf config_stern.php wenn keine ENV-Variablen gesetzt
* - Saubere Trennung von Repositories / Services / Controller
* - Erweiterbares Command-Register (self::COMMANDS)
* - Konsistente UTF-8 Header & CORS
* - Logging von Fehlern ohne interne Details an Client
*
* Rückwärtskompatible Commands (Alte Aufrufe funktionieren weiter):
* GET_ANMELD, GET_ONEANMELD, GET_COUNTS, GET_COUNTS_DATE,
* INSERT_TLN, UPDATE_TLN, DELETE_TLN,
* GET_SOFIANMELD, GET_ONESOFIANMELD, GET_SOFIANMELD_COUNT,
* INSERT_SOFIANMELD, UPDATE_SOFIANMELD, DELETE_SOFIANMELD,
* GET_TERMINE, GET_ONETERMIN, GET_FID, GET_TIME,
* GET_BEOS, GET_ONEBEO,
* GET_ONE, GET_ONETERMIN_SOFUE, GET_MANY, UPDATE, UPDATEAFTER, DELETE,
* GET_STATISTIK_SOFUE, GET_STATISTIK_ANMELD, GET_STATISTIK_BEO, GET_STATISTIK_GESAMT,
* SEND_CONFIRMATION, SENDMAILZUSAGE, SENDMAIL2BEO, SENDMAIL2LISTE,
* PUT2KALENDER
* Zusätzliche neue Commands:
* PING -> Gesundheitscheck
* LIST_COMMANDS -> gibt alle verfügbaren Kommandos inkl. Beschreibung zurück
*/
// ---- Basis HTTP / CORS ----
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
// Früher Lebenszeichen-Check: hilft zu unterscheiden, ob der Parser/Include scheitert
if (isset($_GET['alive']) && $_GET['alive'] === '1') {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['alive' => true, 'ts' => date('c')]);
exit;
}
// ---- Fehlerbehandlung ----
error_reporting(E_ALL);
ini_set('display_errors', 0); // Keine direkten Fehlerausgaben
ini_set('log_errors', '1');
// Schreibe Fehler in ein projektlokales Logfile, damit /var/log nicht nötig ist
// Stelle sicher, dass der Webserver Schreibrechte hat (www-data/apache user)
ini_set('error_log', __DIR__ . '/db4js_error.log');
// Signalisiere dem config_stern.php, dass kein mysqli-Connect durchgeführt werden soll
if (!defined('DB4JS_ALL')) {
define('DB4JS_ALL', true);
}
// Lokales Logging initialisieren: Logdatei anlegen, wenn möglich
function setupLocalLogging(): void {
try {
$logFile = ini_get('error_log');
if (!$logFile) {
$logFile = __DIR__ . '/db4js_error.log';
ini_set('error_log', $logFile);
}
if (!file_exists($logFile)) {
// create empty file
@file_put_contents($logFile, "");
}
// Testeintrag
@error_log('DB4js_all LOG-INIT OK -> ' . $logFile);
} catch (Throwable $e) {
// still attempt a fallback
@file_put_contents(__DIR__ . '/db4js_error.log', 'DB4js_all LOG-INIT FAIL: ' . $e->getMessage() . "\n", FILE_APPEND);
}
}
setupLocalLogging();
// Zusätzliche robuste Fehler- und Request-Logs für Server-Diagnose
register_shutdown_function(function () {
$err = error_get_last();
if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
error_log('DB4js_all FATAL: ' . $err['message'] . ' @' . ($err['file'] ?? '-') . ':' . ($err['line'] ?? '-'));
// Liefere eine JSON-Antwort, damit Clients keinen leeren Body sehen
http_response_code(500);
echo json_encode(['error' => 'Internal error (fatal)', 'details' => 'See server logs'], JSON_UNESCAPED_UNICODE);
}
});
// Basis-Request-Logging (Content-Type, Methode, Rohdaten) zur 500-Analyse
try {
$ct = $_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? '';
$meth = $_SERVER['REQUEST_METHOD'] ?? '';
$rawPreview = '';
$rawBody = file_get_contents('php://input');
if ($rawBody !== false) {
$rawPreview = substr($rawBody, 0, 512);
}
error_log('DB4js_all REQ: method=' . $meth . ' ct=' . $ct . ' raw=' . $rawPreview);
} catch (Throwable $e) {
// Ignoriere Logging-Fehler
}
// ---- Konstanten für Tabellen ----
const TBL_SOFUE = 'SoFue2';
const TBL_ANMELD = 'anmeldungen';
const TBL_FDATUM = 'fdatum1';
const TBL_SONNEDATUM = 'sonnedatum';
const TBL_BEOS = 'beos';
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';
// ---- Utility: Einheitliche Antwort ----
function respond($data, int $status = 200)
{
http_response_code($status);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
exit;
}
function respondError(string $message, int $status = 400, array $extra = [])
{
$payload = array_merge(['error' => $message], $extra);
respond($payload, $status);
}
// ---- Auth (optional) ----
function ensureAuth(): void
{
$apiUser = getenv('API_USER');
$apiPass = getenv('API_PASS');
if (!$apiUser || !$apiPass) { // Auth deaktiviert wenn ENV nicht gesetzt
return;
}
if (!isset($_SERVER['HTTP_AUTHORIZATION'])) {
respondError('Unauthorized', 401);
}
if (!preg_match('/Basic\s+(.*)$/i', $_SERVER['HTTP_AUTHORIZATION'], $m)) {
respondError('Invalid auth header', 401);
}
$decoded = base64_decode(trim($m[1]));
if (!$decoded || strpos($decoded, ':') === false) {
respondError('Invalid credentials format', 401);
}
[$user, $pass] = explode(':', $decoded, 2);
if (!hash_equals($apiUser, $user) || !hash_equals($apiPass, $pass)) {
respondError('Authentication failed', 401);
}
}
// ---- Input Laden (JSON bevorzugt, Fallback FormData/Query) ----
$raw = $rawBody ?? file_get_contents('php://input');
$input = [];
if ($raw !== false && strlen(trim($raw)) > 0) {
$decoded = json_decode($raw, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
$input = $decoded;
}
}
if (empty($input)) { // Fallback auf $_POST
$input = $_POST;
}
$method = $_SERVER['REQUEST_METHOD'];
// ---- Command extrahieren ----
$cmd = $input['cmd'] ?? ($method === 'GET' ? ($_GET['cmd'] ?? null) : null);
if ($method === 'GET' && !$cmd) { // einfache GET Health Check
respond(['status' => 'ok', 'message' => 'API erreichbar']);
}
if (!$cmd) {
respondError('Command missing', 422);
}
// ---- Datenbank (PDO) ----
class DB
{
private static $pdo = null; // untyped for PHP 7.2 compatibility
public static function conn(): PDO
{
if (self::$pdo === null) {
// require_once __DIR__ . '/config_stern.php';
include "config_stern.php";
// config_stern.php sollte $host,$dbase,$user,$pass setzen
$hostEnv = getenv('DB_HOST') ?: ($host ?? 'localhost');
$nameEnv = getenv('DB_NAME') ?: ($dbase ?? 'sternwarte');
$userEnv = getenv('DB_USER') ?: ($user ?? 'root');
$passEnv = getenv('DB_PASS') ?: ($pass ?? '');
$dsn = "mysql:host=$hostEnv;dbname=$nameEnv;charset=utf8mb4";
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
self::$pdo = new PDO($dsn, $userEnv, $passEnv, $opt);
} catch (Throwable $e) {
error_log('DB CONNECT ERROR: ' . $e->getMessage());
respondError('Database connection failed', 500);
}
}
return self::$pdo;
}
public static function all(string $sql, array $params = []): array
{
$st = self::conn()->prepare($sql);
$st->execute($params);
return $st->fetchAll();
}
//"SELECT * FROM SoFue2 WHERE deleted=0 AND status=? AND wtermin >= NOW() ORDER BY wtermin DESC, id DESC LIMIT ? OFFSET ?"
public static function one(string $sql, array $params = []): ?array
{
$st = self::conn()->prepare($sql);
$st->execute($params);
$row = $st->fetch();
return $row === false ? null : $row;
}
public static function exec(string $sql, array $params = []): int
{
$st = self::conn()->prepare($sql);
$st->execute($params);
return $st->rowCount();
}
public static function insertId(): string
{
return self::conn()->lastInsertId();
}
}
// ---- Repositories ----
class RepoAnmeld
{
public static function getByFid(int $fid): array
{
return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE fid=? ORDER BY angemeldet DESC", [$fid]);
}
public static function getByDate(string $date, string $typ): array
{
$table = ($typ === 'regular') ? TBL_ANMELD : TBL_SONNEANMELD;
// expects $date as YYYYMMDD numeric string
$dateNum = (int)preg_replace('/[^0-9]/', '', (string)$date);
return DB::all("SELECT * FROM " . $table . " WHERE fdatum=? ORDER BY angemeldet DESC", [$dateNum]);
}
public static function getById(int $id, string $typ = ''): ?array
{
$table = ($typ === 'sonnen') ? TBL_SONNEANMELD : TBL_ANMELD;
return DB::one("SELECT * FROM " . $table . " WHERE id=?", [$id]);
}
public static function getByName(string $name): array
{
return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE name=? OR vorname=?", [$name, $name]);
}
public static function countByFid(int $fid, string $typ): int
{
$table = ($typ == 'regular') ? TBL_ANMELD : TBL_SONNEANMELD;
$r = DB::one("SELECT SUM(anzahl) c FROM " . $table . " WHERE fid=?", [$fid]);
return (int)($r['c'] ?? 0);
}
public static function countByDate(string $date, string $typ): int
{
$dateNum = (int)preg_replace('/[^0-9]/', '', (string)$date);
$table = ($typ == 'regular') ? TBL_ANMELD : TBL_SONNEANMELD;
$r = DB::one("SELECT SUM(anzahl) c FROM " . $table . " WHERE fdatum=?", [$dateNum]);
return (int)($r['c'] ?? 0);
}
public static function lastAnmeldungAfter(string $date, string $typ): ?int
{
$table = ($typ == 'regular') ? TBL_ANMELD : TBL_SONNEANMELD;
$dateNum = (int)preg_replace('/[^0-9]/', '', (string)$date);
$r = DB::one("SELECT MAX(fdatum) lastdate FROM " . $table . " WHERE fdatum>=? AND anzahl!=0", [$dateNum]);
return isset($r['lastdate']) ? (int)$r['lastdate'] : null;
}
public static function getNew(string $special, string $date)
{
// Implement only the commonly used variant from legacy: 'alllater'
$dateNum = (int)preg_replace('/[^0-9]/', '', (string)$date);
if ($special === 'alllater') {
return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE fdatum > ? AND name != '-' ORDER BY fdatum", [$dateNum]);
}
if ($special === 'normal') {
return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE fdatum = ?", [$dateNum]);
}
if ($special === 'all') {
return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE name != '-'", []);
}
if ($special === 'abgesagt') {
// Legacy had '=1'; be robust and accept non-null values as abgesagt
return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE name != '-' AND (abgesagt = 1 OR abgesagt IS NOT NULL)");
}
if ($special === 'nichtda') {
// Keep simple and approximate legacy behavior: older than yesterday and not participated
return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE fdatum < ? AND COALESCE(teilgenommen,0)=0 AND name != '-'", [(int)date('Ymd', strtotime('-1 day'))]);
}
if ($special === 'zualt') {
// Interpret date as day-offset
$days = (int)$date;
$cut = (int)date('Ymd', strtotime("-$days day"));
return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE fdatum <= ? AND name != '-'", [$cut]);
}
return [];
}
public static function insert(array $d): int
{
$sql = "INSERT INTO " . TBL_ANMELD . " (name,vorname,strasse,plz,stadt,telefon,email,anzahl,remarks,fid,angemeldet) VALUES (?,?,?,?,?,?,?,?,?,?,CURDATE())";
DB::exec($sql, [
$d['name'],
$d['vorname'] ?? '',
$d['strasse'] ?? '',
(int)($d['plz'] ?? 0),
$d['stadt'] ?? '',
$d['telefon'] ?? '',
$d['email'],
(int)$d['anzahl'],
$d['remarks'] ?? '',
(int)$d['fid']
]);
return (int)DB::insertId();
}
public static function update(int $id, array $d): int
{
$sql = "UPDATE " . TBL_ANMELD . " SET name=?,vorname=?,strasse=?,plz=?,stadt=?,telefon=?,email=?,anzahl=?,remarks=?,fid=? WHERE id=?";
return DB::exec($sql, [
$d['name'],
$d['vorname'] ?? '',
$d['strasse'] ?? '',
(int)($d['plz'] ?? 0),
$d['stadt'] ?? '',
$d['telefon'] ?? '',
$d['email'],
(int)$d['anzahl'],
$d['remarks'] ?? '',
(int)$d['fid'],
$id
]);
}
public static function delete(int $id, string $typ): int
{
$table = ($typ == 'regular') ? TBL_ANMELD : TBL_SONNEANMELD;
return DB::exec("DELETE FROM " . $table . " WHERE id=?", [$id]);
}
public static function bulkUpdateField(array $ids, string $field, $value): int
{
// Whitelist to avoid arbitrary column updates
$allowed = ['abgesagt', 'teilgenommen', 'remarks'];
if (!in_array($field, $allowed, true)) {
return 0;
}
$ids = array_values(array_filter(array_map('intval', $ids), function ($v) { return $v > 0; }));
if (empty($ids)) return 0;
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$sql = "UPDATE " . TBL_ANMELD . " SET $field=? WHERE id IN ($placeholders)";
$params = array_merge([$value], $ids);
return DB::exec($sql, $params);
}
}
class RepoSoFiAnmeld
{
public static function getAll(): array
{
return DB::all("SELECT * FROM " . TBL_SOFIANMELD . " ORDER BY angemeldet DESC");
}
public static function getBySoFue(int $sid): array
{
return DB::all("SELECT * FROM " . TBL_SOFIANMELD . " WHERE sofue_id=? ORDER BY angemeldet DESC", [$sid]);
}
public static function getById(int $id): ?array
{
return DB::one("SELECT * FROM " . TBL_SOFIANMELD . " WHERE id=?", [$id]);
}
public static function countBySoFue(int $sid): int
{
$r = DB::one("SELECT SUM(anzahl) c FROM " . TBL_SOFIANMELD . " WHERE sofue_id=?", [$sid]);
return (int)($r['c'] ?? 0);
}
public static function insert(array $d): int
{
$sql = "INSERT INTO " . TBL_SOFIANMELD . " (name,vorname,strasse,plz,stadt,telefon,email,anzahl,remarks,sofue_id,angemeldet) VALUES (?,?,?,?,?,?,?,?,?,?,CURDATE())";
DB::exec($sql, [
$d['name'],
$d['vorname'] ?? '',
$d['strasse'] ?? '',
(int)($d['plz'] ?? 0),
$d['stadt'] ?? '',
$d['telefon'] ?? '',
$d['email'],
(int)$d['anzahl'],
$d['remarks'] ?? '',
(int)$d['sofue_id']
]);
return (int)DB::insertId();
}
public static function update(int $id, array $d): int
{
$sql = "UPDATE " . TBL_SOFIANMELD . " SET name=?,vorname=?,strasse=?,plz=?,stadt=?,telefon=?,email=?,anzahl=?,remarks=? WHERE id=?";
return DB::exec($sql, [
$d['name'],
$d['vorname'] ?? '',
$d['strasse'] ?? '',
(int)($d['plz'] ?? 0),
$d['stadt'] ?? '',
$d['telefon'] ?? '',
$d['email'],
(int)$d['anzahl'],
$d['remarks'] ?? '',
$id
]);
}
public static function delete(int $id): int
{
return DB::exec("DELETE FROM " . TBL_SOFIANMELD . " WHERE id=?", [$id]);
}
}
class RepoTermine
{
public static function getAll(bool $includeOld = false): array
{
$sql = "SELECT * FROM " . TBL_FDATUM;
if (!$includeOld) {
$sql .= " WHERE datum >= CURDATE()";
}
$sql .= " ORDER BY datum";
return DB::all($sql);
}
public static function getNextDates(int $amount = 50, ?string $fromDate = null, string $typ = 'regular'): array
{
// sanitize and clamp
$limit = max(1, min((int)$amount, 365));
// incoming date can be like YYYYMMDD or YYYY-MM-DD, keep only digits
$fromNum = $fromDate ? (int)preg_replace('/[^0-9]/', '', (string)$fromDate) : (int)date('Ymd');
if ($typ === 'sonnen') {
// Sonnenführungen: Tabelle 'sonnedatum' enthält Datumswerte
$sql = "SELECT datum FROM " . TBL_SONNEDATUM . " WHERE datum >= ? ORDER BY datum ASC LIMIT $limit";
return DB::all($sql, [$fromNum]);
}
// Reguläre Führungen aus fdatum1 mit Zusatzinfos
$sql = "SELECT wtag, datum, uhrzeit, grp FROM " . TBL_FDATUM . " WHERE datum >= ? ORDER BY datum ASC LIMIT $limit";
return DB::all($sql, [$fromNum]);
}
public static function getById(int $id): ?array
{
return DB::one("SELECT * FROM " . TBL_FDATUM . " WHERE id=?", [$id]);
}
public static function getByDate(string $date, string $typ): ?array
{
$table = ($typ == 'regular') ? TBL_FDATUM : TBL_SONNEDATUM;
return DB::one("SELECT * FROM " . TBL_FDATUM . " WHERE datum=?", [$date]);
}
public static function fidByDate(string $date, string $typ): ?int
{
$r = self::getByDate($date, $typ);
return $r ? (int)$r['id'] : null;
}
public static function timeByDate(string $date, string $typ = ''): string
{
if ($typ === 'sonnen') return '11 Uhr';
$r = self::getByDate($date, $typ);
return $r['uhrzeit'] ?? '';
}
public static function decCountByDate(string $date, int $anzahl): int
{
$dateNum = (int)preg_replace('/[^0-9]/', '', (string)$date);
// Ensure positive decrement
$anz = max(0, (int)$anzahl);
$sql = "UPDATE " . TBL_FDATUM . " SET count = CASE WHEN count >= ? THEN count - ? ELSE 0 END WHERE datum = ?";
return DB::exec($sql, [$anz, $anz, $dateNum]);
}
}
class RepoBeos
{
public static function getAll(bool $onlyGuides = false, string $fields = '*'): array
{
// sanitize requested fields to avoid SQL injection and schema mismatches
$select = '*';
if ($fields !== '*') {
$parts = array_map('trim', explode(',', (string)$fields));
$cols = [];
// Known aliases to keep backward compatibility
$aliasMap = [
'email' => 'email_1 AS email',
];
foreach ($parts as $p) {
if ($p === '') continue;
if (isset($aliasMap[$p])) {
$cols[] = $aliasMap[$p];
continue;
}
if (preg_match('/^[a-zA-Z0-9_]+$/', $p)) {
$cols[] = $p;
}
}
if (!empty($cols)) {
$select = implode(',', $cols);
}
}
$sql = "SELECT $select FROM " . TBL_BEOS;
if ($onlyGuides) {
$sql .= " WHERE gruppe != ''";
}
$sql .= " ORDER BY name";
try {
return DB::all($sql);
} catch (Throwable $e) {
// Fallback: falls Feldliste nicht passt, liefere alle Spalten
error_log('RepoBeos/getAll fallback to *: ' . $e->getMessage());
$sql = "SELECT * FROM " . TBL_BEOS;
if ($onlyGuides) {
$sql .= " WHERE gruppe != ''";
}
$sql .= " ORDER BY name";
return DB::all($sql);
}
}
public static function getById(int $id, string $fields = '*'): ?array
{
return DB::one("SELECT $fields FROM " . TBL_BEOS . " WHERE id=?", [$id]);
}
public static function getByName(string $name): ?array
{
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);
return $r['vorname'] ?? '';
}
public static function email(string $name): string
{
$r = self::getByName($name);
return $r['email_1'] ?? '';
}
}
class RepoFdates
{
public static function groupByDate(string $dateTime): ?string
{
$r = DB::one("SELECT grp FROM " . TBL_FDATES . " WHERE dateTime=?", [$dateTime]);
return $r['grp'] ?? null;
}
}
class RepoSoFue
{
public static function getById(int $id): ?array
{
return DB::one("SELECT * FROM " . TBL_SOFUE . " WHERE id=?", [$id]);
}
public static function getByTermin(string $termin): ?array
{
return DB::one("SELECT * FROM " . TBL_SOFUE . " WHERE wtermin=? AND deleted=0", [$termin]);
}
/**
* Passende Führung zum Beginn eines Logbuch-Eintrags: gleiches Datum, zeitlich am
* nächsten. Der Logbuch-Beginn liegt typisch 0-30 Minuten vor dem wtermin.
*/
public static function findByLogbuchBeginn(string $beginn): ?array
{
return DB::one(
"SELECT id, wtermin FROM " . TBL_SOFUE
. " WHERE deleted=0 AND DATE(wtermin) = DATE(?)"
. " ORDER BY ABS(TIMESTAMPDIFF(MINUTE, ?, wtermin)) ASC, id ASC LIMIT 1",
[$beginn, $beginn]
);
}
public static function getAfterDate(string $date)
{
// Accept YYYYMMDD or YYYY-MM-DD; compare by DATE(wtermin)
$digits = preg_replace('/[^0-9]/', '', (string)$date);
if (strlen($digits) >= 8) {
$norm = substr($digits, 0, 4) . '-' . substr($digits, 4, 2) . '-' . substr($digits, 6, 2);
} else {
$norm = date('Y-m-d');
}
$sql = "SELECT * FROM " . TBL_SOFUE . " WHERE deleted=0 AND DATE(wtermin) > ? ORDER BY wtermin";
return DB::all($sql, [$norm]);
}
public static function getRecords(string $status = 'all', int $rows = 10, int $page = 1, ?string $termin = null): array
{
// Lastdate: 9 Monate zurück
$lastdate = new DateTime();
$lastdate->sub(new DateInterval('P9M'));
$lastdateStr = $lastdate->format('Y-m-d');
$params = [];
$countSql = "SELECT COUNT(*) as count FROM " . TBL_SOFUE . " WHERE deleted=0";
// WHERE Bedingungen aufbauen
if ($status !== 'all') {
if ((int)$status === 4) {
$countSql .= " AND stattgefunden=1";
} else {
$countSql .= " AND status=?";
$params[] = (int)$status;
}
}
// Bei status=4 (stattgefunden) macht 'neu' keinen Sinn, ignoriere termin
// termin kann 'all', 'neu' sein - nur 'neu' wird behandelt
if ($termin === 'neu' && (int)$status !== 4) {
$countSql .= " AND wtermin >= CURDATE() - INTERVAL 1 DAY ";
}
// Lastdate-Filter auch beim Count hinzufügen
$countSql .= " AND DATE(wtermin) >= ?";
$params[] = $lastdateStr;
// Anzahl der Records holen
$countResult = DB::one($countSql, $params);
$count = (int)($countResult['count'] ?? 0);
// Anzahl der Seiten berechnen
$totalPages = $rows > 0 ? ceil($count / $rows) : 1;
// Falls angeforderte Seite > Anzahl der Seiten, letzte Seite verwenden
if ($page > $totalPages && $totalPages > 0) {
$page = $totalPages;
}
// Start-Record berechnen
$offset = $rows * ($page - 1);
if ($offset < 0) {
$offset = 0;
}
// Daten abrufen mit lastdate-Filter
$sql = "SELECT * FROM " . TBL_SOFUE . " WHERE deleted=0";
$dataParams = [];
if ($status !== 'all') {
if ((int)$status === 4) {
$sql .= " AND stattgefunden=1";
} else {
$sql .= " AND status=?";
$dataParams[] = (int)$status;
}
}
// Bei status=4 (stattgefunden) macht 'neu' keinen Sinn, ignoriere termin
// termin kann 'all', 'neu' sein - nur 'neu' wird behandelt
if ($termin === 'neu' && (int)$status !== 4) {
$sql .= " AND wtermin >= CURDATE() - INTERVAL 1 DAY ";
}
// Lastdate-Filter hinzufügen
$sql .= " AND DATE(wtermin) >= ?";
$dataParams[] = $lastdateStr;
$sql .= " ORDER BY wtermin DESC, id DESC LIMIT ? OFFSET ?";
$dataParams[] = $rows;
$dataParams[] = $offset;
$records = DB::all($sql, $dataParams);
// Response mit Pagination-Info
return [
'page' => $page,
'total' => $totalPages,
'records' => $count,
'rows' => $records
];
}
// "SELECT * FROM SoFue2 WHERE deleted=0 AND stattgefunden=1 AND wtermin >= NOW() AND DATE(wtermin) >= ? ORDER BY wtermin DESC, id DESC LIMIT ? OFFSET ?"
public static function update(int $id, array $d): int
{
$sql = "UPDATE " . TBL_SOFUE . " SET mitarbeiter=?,status=?,bemerkung=?,wtermin=?,atermin=?,erledigt_datum=? WHERE id=?";
return DB::exec($sql, [$d['mitarbeiter'] ?? '', (int)($d['status'] ?? 0), $d['bemerkung'] ?? '', $d['wtermin'] ?? null, $d['atermin'] ?? null, $d['erledigt_datum'] ?? null, $id]);
}
public static function updateAfter(int $id, array $d): int
{
$fields = [];
$params = [];
$map = ['stattgefunden' => 'stattgefunden', 'besucher' => 'anzahl_echt', 'remark' => 'remarks', 'bezahlt' => 'bezahlt', 'wtermin' => 'wtermin', 'status' => 'status'];
foreach ($map as $in => $col) {
if (array_key_exists($in, $d) && $d[$in] !== '' && $d[$in] !== null) {
$fields[] = "$col=?";
$params[] = in_array($in, ['besucher', 'status', 'stattgefunden']) ? (int)$d[$in] : $d[$in];
}
}
if (!$fields) return 0;
$params[] = $id;
$sql = "UPDATE " . TBL_SOFUE . " SET " . implode(',', $fields) . " WHERE id=?";
return DB::exec($sql, $params);
}
public static function delete(int $id): int
{
return DB::exec("UPDATE " . TBL_SOFUE . " SET deleted=1 WHERE id=?", [$id]);
}
/**
* Spende aus dem Logbuch als bezahlt-Text — gleiche Schreibweise wie das
* beoanswer-Formular (beoanswer/src/components/LastButtons.jsx).
*/
private static function bezahltText(array $p): string
{
switch ($p['Spende'] ?? '') {
case 'bar':
// Ganze Beträge ohne Nachkommastellen ("Kasse 50€"), sonst mit Komma
// ("Kasse 25,50€") — passt zu den Bestandswerten und in VARCHAR(15).
$betrag = (float)($p['SpendeBetrag'] ?? 0);
$text = fmod($betrag, 1.0) === 0.0
? number_format($betrag, 0, ',', '')
: number_format($betrag, 2, ',', '');
return 'Kasse ' . $text . '€';
case 'ueberw': return 'Überweisung';
case 'kasse': return 'Spendenkässle';
case 'keine': return 'keine';
default: return '';
}
}
/**
* Kürzt auf $max Zeichen und entfernt alles, was die latin1-Spalten von SoFue2
* nicht darstellen können (z. B. Emoji) — sonst scheitert der UPDATE im Strict Mode.
*/
private static function cp1252Safe(string $s, int $max): string
{
// 'none' lässt nicht abbildbare Zeichen (z. B. Emoji) ersatzlos entfallen,
// statt sie wie iconv//TRANSLIT durch '?' zu ersetzen.
$vorher = mb_substitute_character();
mb_substitute_character('none');
$cp = mb_convert_encoding($s, 'CP1252', 'UTF-8');
mb_substitute_character($vorher);
return mb_substr(mb_convert_encoding($cp, 'UTF-8', 'CP1252'), 0, $max);
}
/**
* Trägt eine im Logbuch erfasste Sonderführung im Register nach: stattgefunden,
* anzahl_echt, bezahlt und remarks. Der Datensatz wird über das Datum gefunden.
* Wirft nie — der Logbuch-Eintrag ist zu diesem Zeitpunkt bereits gespeichert.
*/
public static function ausLogbuch(array $p): array
{
if (($p['ArtFuehrung'] ?? '') !== 'SF') return ['status' => 'uebersprungen'];
$datum = substr((string)($p['Beginn'] ?? ''), 0, 10);
try {
$treffer = self::findByLogbuchBeginn((string)($p['Beginn'] ?? ''));
if (!$treffer) return ['status' => 'kein_termin', 'datum' => $datum];
self::updateAfter((int)$treffer['id'], [
'stattgefunden' => 1,
'besucher' => (int)($p['Besucher'] ?? 0),
'remark' => self::cp1252Safe((string)($p['Bemerkungen'] ?? ''), 100),
'bezahlt' => self::bezahltText($p),
]);
return [
'status' => 'ok',
'id' => (int)$treffer['id'],
'wtermin' => $treffer['wtermin'],
'datum' => $datum,
];
} catch (Throwable $e) {
error_log('SoFue-Rueckschreibung: ' . $e->getMessage());
return ['status' => 'fehler', 'datum' => $datum];
}
}
}
class RepoStatistik
{
public static function sofue(int $year): array
{
$sql = "SELECT MONTH(wtermin) m, COUNT(*) angefragt, SUM(CASE WHEN status>=2 THEN 1 ELSE 0 END) zugesagt, SUM(CASE WHEN status=1 THEN 1 ELSE 0 END) abgesagt, SUM(CASE WHEN stattgefunden=1 THEN 1 ELSE 0 END) stattgefunden FROM " . TBL_SOFUE . " WHERE YEAR(wtermin)=? AND deleted=0 GROUP BY MONTH(wtermin) ORDER BY m";
$rows = DB::all($sql, [$year]);
$base = [];
for ($i = 1; $i <= 12; $i++) {
$base[$i] = ['month' => $i, 'angefragt' => 0, 'zugesagt' => 0, 'abgesagt' => 0, 'stattgefunden' => 0];
}
foreach ($rows as $r) {
$base[(int)$r['m']] = ['month' => (int)$r['m'], 'angefragt' => (int)$r['angefragt'], 'zugesagt' => (int)$r['zugesagt'], 'abgesagt' => (int)$r['abgesagt'], 'stattgefunden' => (int)$r['stattgefunden']];
}
return ['year' => $year, 'data' => array_values($base)];
}
public static function anmeld(int $year): array
{
$sql = "SELECT MONTH(f.datum) m, COUNT(DISTINCT f.id) fuehrungen, COALESCE(SUM(a.anzahl),0) teilnehmer FROM " . TBL_FDATUM . " f LEFT JOIN " . TBL_ANMELD . " a ON f.id=a.fid WHERE YEAR(f.datum)=? GROUP BY MONTH(f.datum) ORDER BY m";
$rows = DB::all($sql, [$year]);
$base = [];
for ($i = 1; $i <= 12; $i++) {
$base[$i] = ['month' => $i, 'fuehrungen' => 0, 'teilnehmer' => 0];
}
foreach ($rows as $r) {
$base[(int)$r['m']] = ['month' => (int)$r['m'], 'fuehrungen' => (int)$r['fuehrungen'], 'teilnehmer' => (int)$r['teilnehmer']];
}
return ['year' => $year, 'data' => array_values($base)];
}
public static function beo(int $year): array
{
$sql = "SELECT mitarbeiter, COUNT(*) anzahl_fuehrungen, SUM(anzahl_echt) gesamt_besucher FROM " . TBL_SOFUE . " WHERE YEAR(wtermin)=? AND deleted=0 AND stattgefunden=1 AND mitarbeiter!='' GROUP BY mitarbeiter ORDER BY anzahl_fuehrungen DESC";
return DB::all($sql, [$year]);
}
public static function gesamt(int $year): array
{
$sofue = DB::one("SELECT COUNT(*) gesamt, SUM(CASE WHEN stattgefunden=1 THEN 1 ELSE 0 END) durch, SUM(CASE WHEN stattgefunden=1 THEN anzahl_echt ELSE 0 END) besucher FROM " . TBL_SOFUE . " WHERE YEAR(wtermin)=? AND deleted=0", [$year]);
$oeff = DB::one("SELECT COUNT(DISTINCT f.id) gesamt, COALESCE(SUM(a.anzahl),0) besucher FROM " . TBL_FDATUM . " f LEFT JOIN " . TBL_ANMELD . " a ON f.id=a.fid WHERE YEAR(f.datum)=?", [$year]);
return ['year' => $year, 'sonderfuehrungen' => ['gesamt' => (int)($sofue['gesamt'] ?? 0), 'durchgefuehrt' => (int)($sofue['durch'] ?? 0), 'besucher' => (int)($sofue['besucher'] ?? 0)], 'oeffentlich' => ['gesamt' => (int)($oeff['gesamt'] ?? 0), 'besucher' => (int)($oeff['besucher'] ?? 0)]];
}
}
// ---- Statistik Jahre Repository (StatistikJahre table) ----
class RepoStatistikJahre
{
const TBL = 'StatistikJahre';
public static function getByDate(string $datum): ?array
{
return DB::one("SELECT * FROM " . self::TBL . " WHERE datum=?", [$datum]);
}
public static function getByYear(int $year): array
{
$sql = "SELECT * FROM " . self::TBL . " WHERE YEAR(datum)=? ORDER BY datum";
$data = DB::all($sql, [$year]);
// Calculate sums
$sumB = 0;
$sumA = 0;
$sumBZ = 0;
$sumBT = 0;
foreach ($data as $row) {
$sumB += ($row['besucherNormal'] ?? 0) + ($row['besucherSonder'] ?? 0) + ($row['besucherToT'] ?? 0);
$sumA += ($row['fuehrungen'] ?? 0) + ($row['beobachtungen'] ?? 0) + ($row['techdienst'] ?? 0);
$sumBZ += ($row['beoZeit'] ?? 0);
$sumBT += ($row['beoTage'] ?? 0);
}
// Get Gesamt bemerkung
$gesamt = DB::one("SELECT bemerkung FROM StatistikGesamt WHERE jahr=?", [$year]);
$bemG = $gesamt['bemerkung'] ?? '';
return [
'data' => $data,
'sumB' => $sumB,
'sumA' => $sumA,
'sumBZ' => $sumBZ,
'sumBT' => $sumBT,
'bemG' => $bemG
];
}
public static function createOrUpdate(array $post): array
{
$existing = self::getByDate($post['datum']);
if (!$existing) {
// Insert
$sql = "INSERT INTO " . self::TBL . " (fuehrungen, beobachtungen, techdienst, besucherNormal, besucherSonder, besucherToT, bemerkung, datum, beoZeit, beoTage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
DB::exec($sql, [
$post['fueh'] ?? 0,
$post['beob'] ?? 0,
$post['tech'] ?? 0,
$post['besN'] ?? 0,
$post['besS'] ?? 0,
$post['besT'] ?? 0,
$post['beme'] ?? '',
$post['datum'],
$post['beoZ'] ?? 0,
$post['beoT'] ?? 0
]);
} else {
// Update
$sql = "UPDATE " . self::TBL . " SET fuehrungen=?, beobachtungen=?, techdienst=?, besucherNormal=?, besucherSonder=?, besucherToT=?, bemerkung=?, beoZeit=?, beoTage=? WHERE datum=?";
DB::exec($sql, [
$post['fueh'] ?? 0,
$post['beob'] ?? 0,
$post['tech'] ?? 0,
$post['besN'] ?? 0,
$post['besS'] ?? 0,
$post['besT'] ?? 0,
$post['beme'] ?? '',
$post['beoZ'] ?? 0,
$post['beoT'] ?? 0,
$post['datum']
]);
}
return ['datum' => $post['datum']];
}
public static function getYearList(): array
{
$sql = "SELECT YEAR(datum) as jahr FROM " . self::TBL . " GROUP BY YEAR(datum) ORDER BY YEAR(datum) DESC";
$rows = DB::all($sql);
return array_map(function($r) { return (int)$r['jahr']; }, $rows);
}
}
// ---- Statistik Gesamt Repository (StatistikGesamt table) ----
class RepoStatistikGesamt
{
const TBL = 'StatistikGesamt';
public static function getAll(): array
{
$data = DB::all("SELECT * FROM " . self::TBL . " ORDER BY jahr DESC");
// Get last date info
$lastYearRow = DB::one("SELECT MAX(YEAR(datum)) as lastYear FROM StatistikJahre");
$lastYear = $lastYearRow['lastYear'] ?? date('Y');
$fullYear = DB::all("SELECT * FROM StatistikJahre WHERE YEAR(datum)=?", [$lastYear]);
// Calculate sums
$sumB = DB::one("SELECT SUM(besucher) as sum FROM " . self::TBL)['sum'] ?? 0;
$sumA = DB::one("SELECT SUM(aktivitaeten) as sum FROM " . self::TBL)['sum'] ?? 0;
return [
'data' => $data,
'lastDate' => [
'lastYear' => (int)$lastYear,
'fullYear' => $fullYear
],
'sumB' => (int)$sumB,
'sumA' => (int)$sumA
];
}
public static function getByYear(int $year): ?array
{
return DB::one("SELECT * FROM " . self::TBL . " WHERE jahr=?", [$year]);
}
public static function createOrUpdate(array $post): array
{
$existing = self::getByYear((int)$post['jahr']);
if (!$existing) {
// Insert
$sql = "INSERT INTO " . self::TBL . " (aktivitaeten, besucher, bemerkung, jahr) VALUES (?, ?, ?, ?)";
DB::exec($sql, [
$post['suma'] ?? 0,
$post['sumb'] ?? 0,
$post['bemG'] ?? '',
$post['jahr']
]);
} else {
// Update
$sql = "UPDATE " . self::TBL . " SET aktivitaeten=?, besucher=?, bemerkung=? WHERE jahr=?";
DB::exec($sql, [
$post['suma'] ?? 0,
$post['sumb'] ?? 0,
$post['bemG'] ?? '',
$post['jahr']
]);
}
return ['datum' => $post['jahr']];
}
}
// ---- 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
{
const TBL = 'kalender';
public static function getEntries(string $start, string $end): array
{
$s = date('Ymd', strtotime($start));
$e = date('Ymd', strtotime($end));
return DB::all("SELECT * FROM " . self::TBL . " WHERE start >= ? AND start <= ?", [$s, $e]);
}
public static function getOneEntry(string $start): array
{
$end = new DateTime($start);
$end->modify('+1 day');
$end = $end->format('Y-m-d');
return RepoKalender::getEntries($start, $end);
}
public static function insert(array $data): bool
{
$sql = "INSERT INTO " . self::TBL . " (start, end, title, description) VALUES (?, ?, ?, ?)";
DB::exec($sql, [$data['start'], $data['end'] ?? $data['start'], $data['title'], $data['description'] ?? '']);
return true;
}
public static function delete(int $id): bool
{
DB::exec("DELETE FROM " . self::TBL . " WHERE id=?", [$id]);
return true;
}
public static function updateBeos(int $id, string $mitarbeiter): bool
{
// First, get the existing calendar entry
$existingEntry = DB::one("SELECT * FROM " . self::TBL . " WHERE id=?", [$id]);
if (!$existingEntry) {
error_log("RepoKalender::updateBeos - Calendar entry with ID {$id} not found.");
return false;
}
// Extract the original Sonderführung name from the existing title
// Expected format: "WK, SF [Sonderführung Name], [Old Mitarbeiter]"
$oldTitle = $existingEntry['title'];
$sofueName = '';
if (preg_match('/^WK, SF (.*), .*$/', $oldTitle, $matches)) {
$sofueName = trim($matches[1]);
}
$newTitle = '';
if (!empty($sofueName)) {
$newTitle = "WK, SF {$sofueName}, {$mitarbeiter}";
} else {
// Fallback: If we can't extract the original Sonderführung name,
// we'll try to keep the original structure if possible, or
// simply create a title indicating the BEO update.
// For now, let's just make it clear it's a BEO update.
$newTitle = "Kalender BEO: {$mitarbeiter}"; // More general fallback
error_log("RepoKalender::updateBeos - Could not parse original SF name from title '{$oldTitle}'. Using generic title fallback.");
}
$sql = "UPDATE " . self::TBL . " SET title=? WHERE id=?";
DB::exec($sql, [$newTitle, $id]);
error_log("Kalender-Eintrag ID {$id} BEOs aktualisiert zu: {$mitarbeiter} (Titel: '{$newTitle}')");
return true;
}
public static function findEntryBySofueIdAndTermin(int $sofueId, string $wtermin): ?int
{
// Fetch Sonderführung details to get its name
$sofue = RepoSoFue::getById($sofueId);
if (!$sofue) {
error_log("RepoKalender::findEntryBySofueIdAndTermin - Sofue ID {$sofueId} not found.");
return null;
}
$sofueName = trim($sofue['name']);
$searchTitlePart = "WK, SF " . $sofueName; // We'll look for this part in the title
// Convert wtermin to YYYY-MM-DD H:i format for comparison with kalender.start
$terminDate = new DateTime($wtermin);
$startDateStr = $terminDate->format('Y-m-d H:i');
// Find calendar entry that matches the start date and contains the sofue name in its title
// Use LIKE for title matching because the full title includes the BEO's name which might change.
$sql = "SELECT id FROM " . self::TBL . " WHERE start = ? AND title LIKE ?";
$params = [$startDateStr, "%{$searchTitlePart}%"];
$result = DB::one($sql, $params);
return $result ? (int)$result['id'] : null;
}
}
// ---- Email Service (einfach) ----
class Mailer
{
public static function sendPlain(string $to, string $subject, string $body, ?string $cc = null): bool
{
require_once __DIR__ . '/phpmailer/dosendmail.php';
$ccList = $cc ? [$cc] : [];
$result = sendmail(
$subject,
'sternwarte.welzheim@gmx.de',
$body,
$ccList,
[],
[$to]
);
if ($result['error']) {
error_log('Mailer Error: ' . ($result['errortext'] ?? 'Unknown error'));
return false;
}
return true;
}
public static function sendAdvanced(array $toList, string $subject, string $body, array $ccList = [], array $bccList = []): bool
{
require_once __DIR__ . '/phpmailer/dosendmail.php';
// sanitize lists
$toList = array_values(array_filter($toList, function($v) { return is_string($v) && trim($v) !== ''; }));
$ccList = array_values(array_filter($ccList, function($v) { return is_string($v) && trim($v) !== ''; }));
$bccList = array_values(array_filter($bccList, function($v) { return is_string($v) && trim($v) !== ''; }));
if (empty($toList)) {
return false;
}
$result = sendmail(
$subject,
'sternwarte.welzheim@gmx.de',
$body,
$ccList,
$bccList,
$toList
);
if ($result['error']) {
error_log('Mailer Error (adv): ' . ($result['errortext'] ?? 'Unknown error'));
return false;
}
return true;
}
}
// ---- Command Registry (Beschreibung für LIST_COMMANDS) ----
class Commands
{
public const MAP = [
'PING' => 'Health-Check',
'PING_LOG' => 'Schreibt Testzeile ins Log',
'READ_LOG' => 'Liest letzte N Zeilen aus lokalem Log',
'GET_ANMELD' => 'Liste Anmeldungen für fid/Datum',
'GET_ANMELDNEW' => 'Anmeldungen-Varianten (special/date)',
'GET_TEILN_ID' => 'Teilnehmer nach id',
'GET_TEILN_NAME' => 'Teilnehmer nach Name/Vorname',
'GET_COUNTS' => 'Summe Anmeldungen für fid/Datum',
'GET_COUNTS_DATE' => 'Summe Anmeldungen für Datum',
'UPDATE_TLN' => 'Anmeldung ändern',
'DELETE_TLN' => 'Anmeldung löschen',
'UPDATE_TLN_BULK' => 'Mehrere Anmeldungen Feld-Update',
'DELETEONE' => 'Teilnehmer löschen (Alias)',
'GET_LASTANMELDUNG' => 'Letzte Anmeldung (Datum) ab Start',
'UPDATECOUNT' => 'Zähler in fdatum1 für Datum reduzieren',
'GET_TERMINE' => 'Termine öffentliche Führungen',
'GET_DATES' => 'Nächste Führungstermine (limit + ab Datum)',
'GET_FID' => 'Fid für Datum',
'GET_TIME' => 'Uhrzeit für Datum',
'GET_BEOS' => 'Liste BEOs optional nur Guides',
'GET_ONEBEO' => 'Ein BEO nach name',
'GET_ONE' => 'Sonderführung nach id',
'GET_MANY' => 'Gefilterte Sonderführungen',
'UPDATE' => 'Sonderführung Standard-Update',
'UPDATEAFTER' => 'Nachbearbeitung Sonderführung',
'DELETE' => 'Sonderführung löschen (soft)',
'GET_STATISTIK_SOFUE' => 'Statistik Sonderführungen Jahr',
'GET_STATISTIK_ANMELD' => 'Statistik öffentliche Führungen Jahr',
'GET_STATISTIK_BEO' => 'Statistik BEO Jahr',
'GET_STATISTIK_GESAMT' => 'Gesamtstatistik Jahr',
'SENDMYMAIL' => 'Mail mit BCC versenden',
'SENDMAILZUSAGE' => 'Zusage an Anfragenden',
'SENDMAIL2BEO' => 'Mail an Mitarbeiter',
'SENDMAIL2LISTE' => 'Mail an Verteiler',
'PUT2KALENDER' => 'Kalender-Eintrag',
'UPDATE_KALENDER_BEO' => 'Kalender-Eintrag BEOS aktualisieren',
'GET_FDATES' => 'Führungstermine für Kalenderansicht',
'GET_CALENTRIES' => 'Kalendereinträge abrufen',
'GET_ONEENTRY' => 'einen Kalendereintrag abrufen',
'PUT_CALENTRY' => 'Kalendereintrag erstellen',
'DEL_CALENTRY' => 'Kalendereintrag löschen',
'GET_YEARS' => 'Liste verfügbare Jahre (Statistik)',
'GET_ONE_A' => 'Statistik-Eintrag für Datum',
'GET_ALL_A' => 'Alle Statistik-Einträge für Jahr',
'CRUP_A' => 'Statistik-Eintrag erstellen/aktualisieren (Jahre)',
'GET_ALL_G' => 'Gesamtstatistik alle Jahre',
'GET_ONE_G' => 'Gesamtstatistik für Jahr',
'CRUP_G' => 'Gesamtstatistik erstellen/aktualisieren',
'GET_TIME_BY_DATE' => 'Uhrzeit für Datum abrufen',
'DELETE_ENTRY' => 'Anmeldung löschen (Storno)',
'GET_FUEHRUNGEN' => 'Führungen in Zeitbereich',
'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 (trägt Sonderführungen in SoFue2 nach)',
'LB_UPDATE_LOGBUCH' => 'Logbuch: Eintrag ändern (trägt Sonderführungen in SoFue2 nach)',
'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'
];
}
// ---- Dispatcher ----
try {
ensureAuth();
$typ = $input['typ'] ?? 'regular';
switch ($cmd) {
case 'PING':
respond(['pong' => true, 'timestamp' => date('c')]);
case 'PING_LOG':
$logFile = ini_get('error_log') ?: (__DIR__ . '/db4js_error.log');
error_log('DB4js_all PING_LOG at ' . date('c'));
$ok = file_exists($logFile);
respond(['ok' => $ok, 'logfile' => $logFile]);
case 'READ_LOG':
$logFile = ini_get('error_log') ?: (__DIR__ . '/db4js_error.log');
$lines = (int)($input['lines'] ?? 100);
$lines = max(10, min($lines, 1000));
if (!is_readable($logFile)) {
respond(['error' => 'Logfile not readable', 'logfile' => $logFile], 404);
}
// Tail simple implementation
$content = @file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$out = [];
if (is_array($content)) {
$out = array_slice($content, -$lines);
}
respond(['logfile' => $logFile, 'lines' => $lines, 'data' => $out]);
// Öffentliche Führungen
case 'GET_ANMELD':
// Backcompat: if an 8+ digit numeric 'id' is passed, treat it as date (fdatum)
if (isset($input['fid'])) {
respond(RepoAnmeld::getByFid((int)$input['fid']));
}
if (isset($input['id'])) {
$digits = preg_replace('/[^0-9]/', '', (string)$input['id']);
if (strlen($digits) >= 8) {
// For sonnen type, use the appropriate table/repository
if ($typ === 'sonnen') {
// For Sonnenführungen, we need to get registrations by date from anmeldungen table
// The anmeldungen table stores all types, so we just query by date
respond(RepoAnmeld::getByDate($digits, $typ));
} else {
respond(RepoAnmeld::getByDate($digits, $typ));
}
} else {
respond(RepoAnmeld::getByFid((int)$input['id']));
}
}
respondError('id or fid missing');
case 'GET_ANMELDNEW':
$special = $input['special'] ?? 'alllater';
$date = $input['date'] ?? date('Ymd');
respond(RepoAnmeld::getNew($special, $date));
case 'GET_TEILN_ID':
$r = RepoAnmeld::getById((int)$input['id'], $typ);
respond($r ? [$r] : []);
case 'GET_TEILN_NAME':
respond(RepoAnmeld::getByName($input['name']));
case 'GET_COUNTS':
if (isset($input['fdate'])) {
respond(RepoAnmeld::countByDate($input['fdate'], $typ));
}
if (isset($input['date'])) {
respond(RepoAnmeld::countByDate($input['date'], $typ));
}
if (isset($input['fid'])) {
respond(RepoAnmeld::countByFid((int)$input['fid'], $typ));
}
if (isset($input['id'])) {
respond(RepoAnmeld::countByFid((int)$input['id'], $typ));
}
respondError('Missing identifier for GET_COUNTS');
case 'GET_COUNTS_DATE':
respond(['count' => RepoAnmeld::countByDate($input['date'], $typ)]);
case 'UPDATE_TLN':
if (!isset($input['id'])) respondError('id missing');
RepoAnmeld::update((int)$input['id'], $input);
respond(['success' => true]);
case 'DELETE_TLN':
RepoAnmeld::delete((int)$input['id'], $input['typ']);
respond(['success' => true]);
case 'DELETEONE': // alias for legacy
RepoAnmeld::delete((int)$input['id'], $input['typ']);
respond(['success' => true]);
case 'UPDATE_TLN_BULK':
if (!isset($input['ids'], $input['field'], $input['values'])) respondError('Missing fields');
$ids = is_array($input['ids']) ? $input['ids'] : [];
$val = is_array($input['values']) ? ($input['values'][0] ?? null) : $input['values'];
if ($val === null) respondError('Missing value');
$n = RepoAnmeld::bulkUpdateField($ids, (string)$input['field'], $val);
respond(['success' => $n > 0, 'updated' => $n]);
// Termine
case 'GET_TERMINE':
respond(RepoTermine::getAll(($input['includeOld'] ?? 'false') === 'true'));
case 'GET_DATES':
$amount = isset($input['anzahl']) ? (int)$input['anzahl'] : 50;
$from = $input['date'] ?? date('Ymd');
respond(RepoTermine::getNextDates($amount, $from, $typ));
case 'GET_FID':
respond(['fid' => RepoTermine::fidByDate($input['datum'], $typ)]);
case 'GET_TIME':
respond(['time' => RepoTermine::timeByDate($input['date'], $input['typ'] ?? '')]);
case 'GET_LASTANMELDUNG':
$val = RepoAnmeld::lastAnmeldungAfter($input['date'] ?? date('Ymd'), $typ);
respond($val);
case 'UPDATECOUNT':
if (!isset($input['date'], $input['anzahl'])) respondError('Missing fields');
RepoTermine::decCountByDate($input['date'], (int)$input['anzahl']);
respond(['success' => true]);
// BEOs
case 'GET_BEOS':
$og = $input['onlyguides'] ?? false;
$onlyGuides = ($og === true) || ($og === 1) || ($og === '1') || ($og === 'true');
respond(RepoBeos::getAll($onlyGuides, $input['what'] ?? '*'));
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':
$r = RepoSoFue::getById((int)$input['id']);
respond($r ?: ['error' => 'Not found']);
case 'GET_MANY':
respond(RepoSoFue::getRecords($input['status'] ?? 'all', (int)($input['rows'] ?? 10), (int)($input['page'] ?? 1), $input['termin'] ?? null));
case 'UPDATE':
if (!isset($input['id'])) respondError('id missing');
$old = RepoSoFue::getById((int)$input['id']);
RepoSoFue::update((int)$input['id'], $input);
if ($old && isset($input['mitarbeiter']) && $input['mitarbeiter'] !== ($old['mitarbeiter'] ?? '')) {
$mail = RepoBeos::email($input['mitarbeiter']);
if ($mail) {
Mailer::sendPlain($mail, 'Sonderführung aktualisiert', 'Sie haben eine aktualisierte Führung am ' . $input['wtermin']);
}
}
respond(['success' => true]);
case 'UPDATEAFTER':
if (!isset($input['id'])) respondError('id missing');
RepoSoFue::updateAfter((int)$input['id'], $input);
respond(['success' => true]);
case 'DELETE':
RepoSoFue::delete((int)$input['id']);
respond(['success' => true]);
// Statistiken
case 'GET_STATISTIK_SOFUE':
respond(RepoStatistik::sofue((int)($input['year'] ?? date('Y'))));
case 'GET_STATISTIK_ANMELD':
respond(RepoStatistik::anmeld((int)($input['year'] ?? date('Y'))));
case 'GET_STATISTIK_BEO':
respond(RepoStatistik::beo((int)($input['year'] ?? date('Y'))));
case 'GET_STATISTIK_GESAMT':
respond(RepoStatistik::gesamt((int)($input['year'] ?? date('Y'))));
// Mail
case 'SENDMYMAIL':
// Legacy-compatible mail send with BCC list
$subject = $input['subject'] ?? ($input['betreff'] ?? null);
if (!$subject) respondError('Missing subject');
$body = $input['body'] ?? '';
$to = $input['to'] ?? [];
if (!is_array($to)) $to = [$to];
$bcc = $input['bcc'] ?? [];
if (!is_array($bcc)) $bcc = [$bcc];
$cc = $input['cc'] ?? [];
if (!is_array($cc)) $cc = [$cc];
$ok = Mailer::sendAdvanced($to, $subject, $body, $cc, $bcc);
respond(['success' => $ok]);
case 'SENDMAILZUSAGE':
$info = RepoSoFue::getById((int)$input['id']);
if (!$info) respondError('Führung nicht gefunden', 404);
$ma_first = trim(explode(',', $input['mitarbeiter'])[0]);
$ma = RepoBeos::getByName($ma_first);
$ma_name = $ma['name'];
$ma_vorname = $ma['vorname'];
$gender = $ma['gender'] == 'm';
$ma_mail = $ma['email_1'];
$ge1 = $gender ? "unser ehrenamtlicher Mitarbeiter, Herr" : "unsere ehrenamtliche Mitarbeiterin, Frau";
$ge2 = $gender ? "ihn" : "sie";
$ge3 = $gender ? "Herrn" : "Frau";
$dt = date('d.m.Y H:i', strtotime($input['termin']));
$subject = 'ZUSAGE - Sternführung am ' . $dt . ' Uhr';
$body = "
Guten Tag,
für Ihren Wunschtermin, {$dt} Uhr, hat sich {$ge1} {$ma_vorname} {$ma_name} bereit erklärt,
die Sonderführung zu übernehmen. Sie erreichen {$ge2} über die e-mail-Adresse: {$ma_mail}
Um nähere Besuchsmodalitäten zu klären, bitten wir Sie, mit {$ge3} {$ma_name} Kontakt aufzunehmen.
Wir bitten Sie, die Spende in Höhe von €50.00 auf unten aufgeführtes Konto zu überweisen oder in bar zur Führung mitzubringen.
Gesellschaft zur Förderung des Planetariums Stuttgart und der Sternwarte Welzheim e.V.
BANKVERBINDUNG: Deutsche Bank AG Stuttgart
IBAN DE18 6007 0070 0122 0383 00
BIC: DEUTDESSXXX
Mit sternfreundlichen Grüßen
Reinhard X. Fürst
Sternwarte Welzheim
";
$ok = Mailer::sendPlain($info['email'], $subject, $body, 'rexfue@gmail.com');
respond(['success' => $ok]);
case 'SENDMAIL2BEO':
$mailNote = trim((string)($input['mail_note'] ?? ''));
$mailNoteBlock = $mailNote !== '' ? "\n " . $mailNote . "\n" : "\n";
$mail = RepoBeos::email($input['ma']);
$vor = RepoBeos::vorname($input['ma']);
$dt = date('d.m.Y H:i', strtotime($input['termin']));
if (!$mail) respondError('Mitarbeiter nicht gefunden', 404);
$info = RepoSoFue::getByTermin($input['termin']);
if (!$info) respondError('Führung nicht gefunden', 404);
$subject = 'Vereinbarte Sonderführung am ' . date('d.m.Y', strtotime($input['termin']));
$body = "
Hallo " . $vor .",
vielen Dank für die Bereitschaft, die Sonderführung am {$dt} zu übernehmen.
Bitte den Termin nicht vergessen und bitte ggf. auch das Teammitglied, das die
Führung mitmacht, informieren.
Der Termin wurde in den Sternwartenkalender eingetragen.
Die Kontaktdaten sind auf der Sonderführungsseite ( https://sternwarte-welzheim.de/intern/sofue/sofue.php ) zu finden.
{$mailNoteBlock}
Viele Grüße
Reinhard
Diese Meldung wurde automatisch erzeugt. Es kann nicht geantwortet werden.";
$ok = Mailer::sendPlain($mail, $subject, $body, 'rexfue@gmail.com');
respond(['success' => $ok]);
case 'SENDMAIL2LISTE':
$info = RepoSoFue::getById((int)$input['id']);
if (!$info) respondError('Führung nicht gefunden', 404);
$mailNote = trim((string)($input['mail_note'] ?? ''));
$to = $input['to'] ?? LISTE_EMAIL;
$subject = 'Neue Anfrage Sonderführung am ' . date('d.m.Y', strtotime($info['wtermin']));
$body = "
Liebe BEOs,
wer kann folgende Sonderführung übernehmen?
---------------------------------------------------------------------------------------------------";
$body = $body . "
Name, Vorname: " . $info['name'] . " " . $info['vorname'] . "
Verein / Organisation : " . $info['verein'] . "
Wunsch - Termin: " . $info['wtermin'] . "
Teilnehmerzahl ca.: " . $info['anzahl'] . "
Weitere Fragen oder Mitteilungen:
" . $info['mitteilung'] . "
Spendenbescheinigung: \t" . $info['spende'] . "
---------------------------------------------------------------------------------------------------";
if ($mailNote !== '') {
$body .= "\n\n" . $mailNote . "\n";
}
$body .= "\n\nViele Grüße\nReinhard\n";
$ok = Mailer::sendPlain($to, $subject, $body);
respond(['success' => $ok]);
case 'SEND_CONFIRMATION':
if (!isset($input['to'], $input['subject'], $input['body'])) respondError('Missing fields');
$ok = Mailer::sendPlain($input['to'],$input['subject'],$input['body']);
respond(['success' => $ok]);
// Kalender
case 'PUT2KALENDER':
if (!isset($input['id'], $input['termin'], $input['mitarbeiter'])) respondError('Missing fields');
// Sonderführung laden
$sofue = RepoSoFue::getById((int)$input['id']);
if (!$sofue) respondError('Sonderführung nicht gefunden', 404);
// Datum aus termin extrahieren und in YYYYMMDD Format konvertieren
$terminDate = new DateTime($input['termin']);
$dateStr = $terminDate->format('Y-m-d H:i');
$endTime = $terminDate->modify('+2hours');
$endStr = $endTime->format('Y-m-d H:i');
// Titel mit Mitarbeiter für Kalendereintrag erstellen
$title = "WK, SF " . trim($sofue['name']) . ", " . $input['mitarbeiter'];
// Kalendereintrag erstellen
RepoKalender::insert([
'start' => $dateStr,
'end' => $endStr,
'title' => $title,
'description' => ''
]);
error_log('Kalender-Eintrag erstellt: ' . $input['id'] . ' ' . $input['termin'] . ' ' . $input['mitarbeiter']);
respond(['success' => true]);
case 'UPDATE_KALENDER_BEO':
if (!isset($input['id'], $input['mitarbeiter'])) respondError('Missing fields for calendar update');
$sofueId = (int)$input['id'];
$mitarbeiter = $input['mitarbeiter'];
// Fetch Sonderführung details to get wtermin
$sofue = RepoSoFue::getById($sofueId);
if (!$sofue) respondError('Sonderführung not found for calendar update', 404);
$wtermin = $sofue['wtermin'];
// Find the calendar entry ID based on sofueId and wtermin
$kalenderId = RepoKalender::findEntryBySofueIdAndTermin($sofueId, $wtermin);
if (!$kalenderId) {
error_log("UPDATE_KALENDER_BEO: Could not find calendar entry for Sofue ID {$sofueId} and wtermin {$wtermin}.");
respondError('Corresponding calendar entry not found.', 404);
}
RepoKalender::updateBeos($kalenderId, $mitarbeiter);
respond(['success' => true]);
case 'GET_FDATES':
// Returns führungen for calendar display
if (!isset($input['start'], $input['end'])) respondError('start and end required');
// Convert ISO date strings to YYYYMMDD format
$startObj = new DateTime($input['start']);
$endObj = new DateTime($input['end']);
$s = $startObj->format('Ymd');
$e = $endObj->format('Ymd');
$sql = "SELECT * FROM " . TBL_FDATUM . " WHERE datum >= ? AND datum <= ? ORDER BY datum ASC";
$rows = DB::all($sql, [$s, $e]);
$result = [];
foreach ($rows as $r) {
$count = RepoAnmeld::countByDate($r['datum'], 'regular');
$result[] = [
'start' => $r['datum'],
'uhr' => substr($r['uhrzeit'] ?? '', 0, 2),
'title' => $r['grp'] ?? '',
'count' => $count
];
}
respond($result);
case 'GET_CALENTRIES':
if (!isset($input['start'], $input['end'])) respondError('start and end required');
respond(RepoKalender::getEntries($input['start'], $input['end']));
case 'GET_ONEENTRY':
if (!isset($input['date'])) respondError('date required');
respond(RepoKalender::getOneEntry($input['date']));
case 'PUT_CALENTRY':
RepoKalender::insert($input['data']);
respond(['success' => true]);
case 'DEL_CALENTRY':
RepoKalender::delete((int)$input['id']);
respond(['success' => true]);
// Statistik - Jahre
case 'GET_YEARS':
$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'] ?? $_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) {
$gesamt = RepoStatistikGesamt::getByYear($year);
$result['bemG'] = $gesamt['bemerkung'] ?? '';
}
respond($result);
case 'GET_ALL_A':
$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');
respond(RepoStatistikJahre::createOrUpdate($input['toInsert']));
// Statistik - Gesamt
case 'GET_ALL_G':
$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'));
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']));
// Storno module commands
case 'GET_TIME_BY_DATE':
$dt = $input['dt'] ?? respondError('dt missing');
$typ = $input['typ'] ?? 'regular';
if ($typ === 'sonnen') {
respond(['time' => '11 Uhr']);
}
$time = DB::one("SELECT uhrzeit FROM " . TBL_FDATUM . " WHERE datum=?", [$dt]);
respond(['time' => $time['uhrzeit'] ?? '']);
case 'DELETE_ENTRY':
$id = (int)($input['id'] ?? respondError('id missing'));
DB::exec("DELETE FROM " . TBL_ANMELD . " WHERE id=?", [$id]);
respond(['success' => true]);
case 'GET_FUEHRUNGEN':
$start = $input['start'] ?? respondError('start missing');
$end = $input['end'] ?? respondError('end missing');
$typ = $input['typ'] ?? 'regular';
$table = ($typ === 'sonnen') ? 'sonnedatum' : TBL_FDATUM;
$sql = "SELECT * FROM $table WHERE datum >= ? AND datum <= ? ORDER BY datum ASC";
respond(DB::all($sql, [$start, $end]));
case 'UPDATETLNFD':
if (!isset($input['id'], $input['fdatum'], $input['fid'])) respondError('Missing fields');
$sql = "UPDATE " . TBL_ANMELD . " SET fdatum=?, fid=?, abgesagt=NULL WHERE id=?";
DB::exec($sql, [$input['fdatum'], $input['fid'], $input['id']]);
respond(['success' => true]);
case 'SEND_MAIL_HTML':
if (!isset($input['subject'], $input['to'], $input['body_txt'])) respondError('Missing mail fields');
require_once __DIR__ . '/phpmailer/dosendmail.php';
// Note: body_html is ignored because sendmail doesn't support it
$result = sendmail(
$input['subject'],
'noreply@sternwarte-welzheim.de',
$input['body_txt'],
[],
[],
is_array($input['to']) ? $input['to'] : [$input['to']]
);
respond(['success' => !($result['error'] ?? false)]);
case 'GET_ALLTEILN':
$fdatum = $input['fdatum'] ?? respondError('fdatum missing');
$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);
// Erst nach dem Commit: eine Sonderführung wird im Register nachgetragen,
// ein Problem dabei darf den Logbuch-Eintrag nicht zurückrollen.
respond(['id' => $newId, 'sofue' => RepoSoFue::ausLogbuch($input)], 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, 'sofue' => RepoSoFue::ausLogbuch($input)]);
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)]);
default:
respondError('Unknown command', 400, ['cmd' => $cmd]);
}
} catch (Throwable $e) {
error_log('API ERROR: ' . $e->getMessage() . ' @' . $e->getFile() . ':' . $e->getLine());
respondError('Internal error', 500);
}