b4a339392d
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>
2353 lines
99 KiB
PHP
2353 lines
99 KiB
PHP
<?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);
|
||
}
|