1353 lines
54 KiB
PHP
1353 lines
54 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';
|
|
|
|
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): int
|
|
{
|
|
return DB::exec("DELETE FROM " . TBL_ANMELD . " 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 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]);
|
|
}
|
|
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 >= NOW()";
|
|
}
|
|
|
|
// 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 >= NOW()";
|
|
}
|
|
|
|
// 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]);
|
|
}
|
|
}
|
|
|
|
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(fn($r) => (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']];
|
|
}
|
|
}
|
|
|
|
// ---- 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 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;
|
|
}
|
|
}
|
|
|
|
// ---- 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,
|
|
'info@sternwarte-welzheim.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, fn($v) => is_string($v) && trim($v) !== ''));
|
|
$ccList = array_values(array_filter($ccList, fn($v) => is_string($v) && trim($v) !== ''));
|
|
$bccList = array_values(array_filter($bccList, fn($v) => is_string($v) && trim($v) !== ''));
|
|
|
|
if (empty($toList)) {
|
|
return false;
|
|
}
|
|
|
|
$result = sendmail(
|
|
$subject,
|
|
'info@sternwarte-welzheim.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',
|
|
'GET_FDATES' => 'Führungstermine für Kalenderansicht',
|
|
'GET_CALENTRIES' => 'Kalendereinträge 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',
|
|
'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']);
|
|
respond(['success' => true]);
|
|
case 'DELETEONE': // alias for legacy
|
|
RepoAnmeld::delete((int)$input['id']);
|
|
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']);
|
|
|
|
// 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);
|
|
$subject = 'Ihre Sonderführung am ' . date('d.m.Y', strtotime($input['termin']));
|
|
$body = "Hallo {$info['name']}, Ihre Sonderführung am " . $input['termin'] . " findet mit Mitarbeiter " . $input['mitarbeiter'] . " statt.";
|
|
$ok = Mailer::sendPlain($info['email'], $subject, $body, 'info@sternwarte-welzheim.de');
|
|
respond(['success' => $ok]);
|
|
case 'SENDMAIL2BEO':
|
|
$mail = RepoBeos::email($input['ma']);
|
|
$vor = RepoBeos::vorname($input['ma']);
|
|
if (!$mail) respondError('Mitarbeiter nicht gefunden', 404);
|
|
$info = RepoSoFue::getByTermin($input['termin']);
|
|
if (!$info) respondError('Führung nicht gefunden', 404);
|
|
$subject = 'Sonderführung am ' . date('d.m.Y', strtotime($input['termin']));
|
|
$body = "Hallo $vor, du hast eine Sonderführung am {$input['termin']}. Teilnehmer: " . ($info['anzahl'] ?? '-');
|
|
$ok = Mailer::sendPlain($mail, $subject, $body, 'info@sternwarte-welzheim.de');
|
|
respond(['success' => $ok]);
|
|
case 'SENDMAIL2LISTE':
|
|
$info = RepoSoFue::getById((int)$input['id']);
|
|
if (!$info) respondError('Führung nicht gefunden', 404);
|
|
$to = $input['to'] ?? LISTE_EMAIL;
|
|
$subject = 'Neue Anfrage Sonderführung ' . date('d.m.Y', strtotime($info['wtermin']));
|
|
$body = 'Neue Anfrage: ' . $info['name'] . ' Personen: ' . ($info['anzahl'] ?? '-');
|
|
$ok = Mailer::sendPlain($to, $subject, $body);
|
|
respond(['success' => $ok]);
|
|
|
|
// Kalender
|
|
case 'PUT2KALENDER':
|
|
if (!isset($input['id'], $input['termin'], $input['mitarbeiter'])) respondError('Missing fields');
|
|
error_log('Kalender-Eintrag: ' . $input['id'] . ' ' . $input['termin'] . ' ' . $input['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 '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':
|
|
respond(RepoStatistikJahre::getYearList());
|
|
case 'GET_ONE_A':
|
|
$datum = $input['datum'] ?? respondError('datum missing');
|
|
$result = RepoStatistikJahre::getByDate($datum);
|
|
if ($result) {
|
|
// Add bemG from Gesamt
|
|
$year = substr($datum, 0, 4);
|
|
$gesamt = RepoStatistikGesamt::getByYear((int)$year);
|
|
$result['bemG'] = $gesamt['bemerkung'] ?? '';
|
|
}
|
|
respond($result ?: ['error' => 'Not found']);
|
|
case 'GET_ALL_A':
|
|
$year = (int)($input['jahr'] ?? date('Y'));
|
|
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':
|
|
respond(RepoStatistikGesamt::getAll());
|
|
case 'GET_ONE_G':
|
|
$year = (int)($input['jahr'] ?? date('Y'));
|
|
$result = RepoStatistikGesamt::getByYear($year);
|
|
respond($result ?: ['error' => 'Not found']);
|
|
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]));
|
|
|
|
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);
|
|
}
|