DB4js als _all zusammengefasst
Beoanswer ist nun OK
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ download
|
|||||||
*.log
|
*.log
|
||||||
webseiten
|
webseiten
|
||||||
|
|
||||||
|
sternwarte/beoanswer/.env.production
|
||||||
|
|||||||
@@ -5,6 +5,35 @@ include 'config_stern.php';
|
|||||||
include 'phpmailer/dosendmail.php';
|
include 'phpmailer/dosendmail.php';
|
||||||
|
|
||||||
|
|
||||||
|
// ===== Request-Daten verarbeiten =====
|
||||||
|
// Unterstützt sowohl JSON als auch FormData
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
|
// Prüfe Content-Type
|
||||||
|
$contentType = $_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? '';
|
||||||
|
|
||||||
|
if (strpos($contentType, 'application/json') !== false) {
|
||||||
|
// JSON-Daten
|
||||||
|
$jsonData = file_get_contents('php://input');
|
||||||
|
$_POST = json_decode($jsonData, true);
|
||||||
|
|
||||||
|
// Debug-Logging
|
||||||
|
error_log("=== PHP JSON DEBUG ===");
|
||||||
|
error_log("Content-Type: " . $contentType);
|
||||||
|
error_log("Raw JSON: " . $jsonData);
|
||||||
|
error_log("Decoded POST: " . print_r($_POST, true));
|
||||||
|
error_log("=====================");
|
||||||
|
}
|
||||||
|
// Bei FormData ist $_POST bereits automatisch gefüllt
|
||||||
|
else {
|
||||||
|
error_log("=== PHP FormData DEBUG ===");
|
||||||
|
error_log("Content-Type: " . $contentType);
|
||||||
|
error_log("POST data: " . print_r($_POST, true));
|
||||||
|
error_log("==========================");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ab hier bleibt alles gleich
|
||||||
|
$cmd = $_POST["cmd"] ?? '';
|
||||||
|
|
||||||
// Holen der Einträge in der anmelde-Datenbank für den selektierten Tag
|
// Holen der Einträge in der anmelde-Datenbank für den selektierten Tag
|
||||||
// Parameter
|
// Parameter
|
||||||
@@ -435,14 +464,15 @@ function getOneRecordTermin($termin) {
|
|||||||
return $erg;
|
return $erg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
$_POST = json_decode(file_get_contents('php://input'), true);
|
$_POST = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
$erg = "";
|
$erg = "";
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
$cmd = $_POST["cmd"];
|
$cmd = $_POST["cmd"];
|
||||||
/*
|
*/
|
||||||
|
|
||||||
$x = "[";
|
$x = "[";
|
||||||
foreach ($_POST as $key => $value) {
|
foreach ($_POST as $key => $value) {
|
||||||
if(gettype($value) == "array") {
|
if(gettype($value) == "array") {
|
||||||
@@ -451,7 +481,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
|||||||
$x = $x . $key . " => " . $value . ",";
|
$x = $x . $key . " => " . $value . ",";
|
||||||
}
|
}
|
||||||
$x = $x . "]";
|
$x = $x . "]";
|
||||||
*/
|
|
||||||
switch ($cmd) {
|
switch ($cmd) {
|
||||||
case 'GET_ANMELD':
|
case 'GET_ANMELD':
|
||||||
$erg = getAnmeldungen($_POST['id']);
|
$erg = getAnmeldungen($_POST['id']);
|
||||||
@@ -567,7 +597,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
|||||||
default:
|
default:
|
||||||
$erg = ['error' => 'Unknown POST-Command', 'cmd' => $cmd, 'params' => $x];
|
$erg = ['error' => 'Unknown POST-Command', 'cmd' => $cmd, 'params' => $x];
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
/*
|
/*
|
||||||
$x = "[";
|
$x = "[";
|
||||||
foreach ($_GET as $key => $value) {
|
foreach ($_GET as $key => $value) {
|
||||||
@@ -575,6 +604,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
|||||||
}
|
}
|
||||||
$x = $x . "]";
|
$x = $x . "]";
|
||||||
*/
|
*/
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
|
||||||
$cmd = $_GET['cmd'];
|
$cmd = $_GET['cmd'];
|
||||||
switch ($cmd) {
|
switch ($cmd) {
|
||||||
case 'GET_FDATES':
|
case 'GET_FDATES':
|
||||||
|
|||||||
725
sternwarte/DB4js_all.php
Normal file
725
sternwarte/DB4js_all.php
Normal file
@@ -0,0 +1,725 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Fehlerbehandlung ----
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', 0); // Keine direkten Fehlerausgaben
|
||||||
|
|
||||||
|
// ---- Konstanten für Tabellen ----
|
||||||
|
const TBL_SOFUE = 'SoFue2';
|
||||||
|
const TBL_ANMELD = 'anmeldungen';
|
||||||
|
const TBL_FDATUM = 'fdatum1';
|
||||||
|
const TBL_BEOS = 'beos';
|
||||||
|
const TBL_SOFIANMELD = 'sofianmeld';
|
||||||
|
|
||||||
|
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 = 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';
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getById(int $id): ?array
|
||||||
|
{
|
||||||
|
return DB::one("SELECT * FROM " . TBL_ANMELD . " WHERE id=?", [$id]);
|
||||||
|
}
|
||||||
|
public static function countByFid(int $fid): int
|
||||||
|
{
|
||||||
|
$r = DB::one("SELECT SUM(anzahl) c FROM " . TBL_ANMELD . " WHERE fid=?", [$fid]);
|
||||||
|
return (int)($r['c'] ?? 0);
|
||||||
|
}
|
||||||
|
public static function countByDate(string $date): int
|
||||||
|
{
|
||||||
|
$r = DB::one("SELECT SUM(a.anzahl) c FROM " . TBL_ANMELD . " a JOIN " . TBL_FDATUM . " f ON a.fid=f.id WHERE f.datum=?", [$date]);
|
||||||
|
return (int)($r['c'] ?? 0);
|
||||||
|
}
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getById(int $id): ?array
|
||||||
|
{
|
||||||
|
return DB::one("SELECT * FROM " . TBL_FDATUM . " WHERE id=?", [$id]);
|
||||||
|
}
|
||||||
|
public static function getByDate(string $date): ?array
|
||||||
|
{
|
||||||
|
return DB::one("SELECT * FROM " . TBL_FDATUM . " WHERE datum=?", [$date]);
|
||||||
|
}
|
||||||
|
public static function fidByDate(string $date): ?int
|
||||||
|
{
|
||||||
|
$r = self::getByDate($date);
|
||||||
|
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);
|
||||||
|
return $r['uhrzeit'] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 getRecords(string $status = 'all', int $rows = 10, int $page = 1, ?string $termin = null): array
|
||||||
|
{
|
||||||
|
$offset = ($page - 1) * $rows;
|
||||||
|
$params = [];
|
||||||
|
$sql = "SELECT * FROM " . TBL_SOFUE . " WHERE deleted=0";
|
||||||
|
if ($status !== 'all') {
|
||||||
|
$sql .= " AND status=?";
|
||||||
|
$params[] = (int)$status;
|
||||||
|
}
|
||||||
|
if ($termin) {
|
||||||
|
$sql .= " AND wtermin=?";
|
||||||
|
$params[] = $termin;
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY wtermin DESC, id DESC LIMIT ? OFFSET ?";
|
||||||
|
$params[] = $rows;
|
||||||
|
$params[] = $offset;
|
||||||
|
return DB::all($sql, $params);
|
||||||
|
}
|
||||||
|
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)]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Command Registry (Beschreibung für LIST_COMMANDS) ----
|
||||||
|
class Commands
|
||||||
|
{
|
||||||
|
public const MAP = [
|
||||||
|
'PING' => 'Health-Check',
|
||||||
|
'GET_ANMELD' => 'Liste Anmeldungen für fid',
|
||||||
|
'GET_ONEANMELD' => 'Eine Anmeldung nach id',
|
||||||
|
'GET_COUNTS' => 'Summe Anmeldungen für fid',
|
||||||
|
'GET_COUNTS_DATE' => 'Summe Anmeldungen für Datum (YYYY-MM-DD)',
|
||||||
|
'INSERT_TLN' => 'Anmeldung anlegen',
|
||||||
|
'UPDATE_TLN' => 'Anmeldung ändern',
|
||||||
|
'DELETE_TLN' => 'Anmeldung löschen',
|
||||||
|
'GET_SOFIANMELD' => 'Alle/sofue_id bezogene Sonderführungs-Anmeldungen',
|
||||||
|
'GET_ONESOFIANMELD' => 'Eine Sonderführungs-Anmeldung',
|
||||||
|
'GET_SOFIANMELD_COUNT' => 'Zähler Sonderführungs-Anmeldungen',
|
||||||
|
'INSERT_SOFIANMELD' => 'Neue Sonderführungs-Anmeldung',
|
||||||
|
'UPDATE_SOFIANMELD' => 'Sonderführungs-Anmeldung ändern',
|
||||||
|
'DELETE_SOFIANMELD' => 'Sonderführungs-Anmeldung löschen',
|
||||||
|
'GET_TERMINE' => 'Termine öffentliche Führungen',
|
||||||
|
'GET_ONETERMIN' => 'Termin nach id',
|
||||||
|
'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_ONETERMIN_SOFUE' => 'Sonderführung nach Termin',
|
||||||
|
'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',
|
||||||
|
'SEND_CONFIRMATION' => 'Einfache Bestätigungs-Mail',
|
||||||
|
'SENDMAILZUSAGE' => 'Zusage an Anfragenden',
|
||||||
|
'SENDMAIL2BEO' => 'Mail an Mitarbeiter',
|
||||||
|
'SENDMAIL2LISTE' => 'Mail an Verteiler',
|
||||||
|
'PUT2KALENDER' => 'Kalender-Eintrag (Platzhalter)',
|
||||||
|
'LIST_COMMANDS' => 'Liste aller Kommandos'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Dispatcher ----
|
||||||
|
try {
|
||||||
|
ensureAuth();
|
||||||
|
|
||||||
|
switch ($cmd) {
|
||||||
|
case 'PING':
|
||||||
|
respond(['pong' => true, 'timestamp' => date('c')]);
|
||||||
|
|
||||||
|
// Öffentliche Führungen
|
||||||
|
case 'GET_ANMELD':
|
||||||
|
respond(RepoAnmeld::getByFid((int)$input['id']));
|
||||||
|
case 'GET_ONEANMELD':
|
||||||
|
$r = RepoAnmeld::getById((int)$input['id']);
|
||||||
|
respond($r ?: ['error' => 'Not found']);
|
||||||
|
case 'GET_COUNTS':
|
||||||
|
respond(['count' => RepoAnmeld::countByFid((int)$input['fid'])]);
|
||||||
|
case 'GET_COUNTS_DATE':
|
||||||
|
respond(['count' => RepoAnmeld::countByDate($input['date'])]);
|
||||||
|
case 'INSERT_TLN':
|
||||||
|
if (!isset($input['name'], $input['email'], $input['anzahl'], $input['fid'])) respondError('Missing fields');
|
||||||
|
if (!filter_var($input['email'], FILTER_VALIDATE_EMAIL)) respondError('Invalid email');
|
||||||
|
$id = RepoAnmeld::insert($input);
|
||||||
|
respond(['success' => true, 'id' => $id]);
|
||||||
|
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]);
|
||||||
|
|
||||||
|
// Sonderführungs-Anmeldungen
|
||||||
|
case 'GET_SOFIANMELD':
|
||||||
|
if (isset($input['sofue_id'])) respond(RepoSoFiAnmeld::getBySoFue((int)$input['sofue_id']));
|
||||||
|
respond(RepoSoFiAnmeld::getAll());
|
||||||
|
case 'GET_ONESOFIANMELD':
|
||||||
|
$r = RepoSoFiAnmeld::getById((int)$input['id']);
|
||||||
|
respond($r ?: ['error' => 'Not found']);
|
||||||
|
case 'GET_SOFIANMELD_COUNT':
|
||||||
|
respond(['count' => RepoSoFiAnmeld::countBySoFue((int)$input['sofue_id'])]);
|
||||||
|
case 'INSERT_SOFIANMELD':
|
||||||
|
if (!isset($input['name'], $input['email'], $input['anzahl'], $input['sofue_id'])) respondError('Missing fields');
|
||||||
|
if (!filter_var($input['email'], FILTER_VALIDATE_EMAIL)) respondError('Invalid email');
|
||||||
|
$id = RepoSoFiAnmeld::insert($input);
|
||||||
|
respond(['success' => true, 'id' => $id]);
|
||||||
|
case 'UPDATE_SOFIANMELD':
|
||||||
|
if (!isset($input['id'])) respondError('id missing');
|
||||||
|
RepoSoFiAnmeld::update((int)$input['id'], $input);
|
||||||
|
respond(['success' => true]);
|
||||||
|
case 'DELETE_SOFIANMELD':
|
||||||
|
RepoSoFiAnmeld::delete((int)$input['id']);
|
||||||
|
respond(['success' => true]);
|
||||||
|
|
||||||
|
// Termine
|
||||||
|
case 'GET_TERMINE':
|
||||||
|
respond(RepoTermine::getAll(($input['includeOld'] ?? 'false') === 'true'));
|
||||||
|
case 'GET_ONETERMIN':
|
||||||
|
$r = RepoTermine::getById((int)$input['id']);
|
||||||
|
respond($r ?: ['error' => 'Not found']);
|
||||||
|
case 'GET_FID':
|
||||||
|
respond(['fid' => RepoTermine::fidByDate($input['datum'])]);
|
||||||
|
case 'GET_TIME':
|
||||||
|
respond(['time' => RepoTermine::timeByDate($input['date'], $input['typ'] ?? '')]);
|
||||||
|
|
||||||
|
// 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_ONETERMIN_SOFUE':
|
||||||
|
$r = RepoSoFue::getByTermin($input['termin']);
|
||||||
|
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 'SEND_CONFIRMATION':
|
||||||
|
if (!isset($input['to'], $input['subject'], $input['body'])) respondError('Missing mail fields');
|
||||||
|
$ok = Mailer::sendPlain($input['to'], $input['subject'], $input['body']);
|
||||||
|
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 '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);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "beoanswer_react",
|
"name": "beoanswer_react",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ function AppContent() {
|
|||||||
const [name, setName] = useState("")
|
const [name, setName] = useState("")
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [mitsend, setMitsend] = useState(false)
|
//const [mitsend, setMitsend] = useState(false)
|
||||||
const [mitback, setMitback] = useState(false)
|
//const [mitback, setMitback] = useState(false)
|
||||||
|
|
||||||
const version = packageJson.version
|
const version = packageJson.version
|
||||||
const vdate = new Date().toLocaleDateString('de-DE')
|
const vdate = new Date().toLocaleDateString('de-DE')
|
||||||
@@ -66,6 +66,8 @@ function AppContent() {
|
|||||||
headers['Authorization'] = `Basic ${credentials}`
|
headers['Authorization'] = `Basic ${credentials}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(formData)
|
||||||
|
|
||||||
const response = await fetch(APIURL, {
|
const response = await fetch(APIURL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers,
|
headers: headers,
|
||||||
@@ -160,7 +162,7 @@ function AppContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setBackButton = () => {
|
const setBackButton = () => {
|
||||||
setMitback(true)
|
// setMitback(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Welche Komponeneten werden angezeigt:
|
// Welche Komponeneten werden angezeigt:
|
||||||
|
|||||||
14
sternwarte/beoanswer/src/components/LastButtons.css
Normal file
14
sternwarte/beoanswer/src/components/LastButtons.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.modal-content.custom-modal {
|
||||||
|
width: 95vw; /* Nearly full window width */
|
||||||
|
max-width: none; /* Remove base max-width constraint */
|
||||||
|
height: 85vh; /* Tall modal */
|
||||||
|
max-height: 85vh; /* Cap at viewport height */
|
||||||
|
display: flex; /* Allow header/body/footer layout */
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content.custom-modal .modal-body {
|
||||||
|
flex: 1; /* Fill remaining space */
|
||||||
|
overflow: auto; /* Scroll body when content is taller than container */
|
||||||
|
text-align: left; /* Better for long instructions */
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
|||||||
import { useFormData } from '../FormContext'
|
import { useFormData } from '../FormContext'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import ConfirmModal from './ConfirmModal'
|
import ConfirmModal from './ConfirmModal'
|
||||||
|
import './LastButtons.css';
|
||||||
|
|
||||||
export default function LastButtons({ mitSend, mitBack, handleBack}) {
|
export default function LastButtons({ mitSend, mitBack, handleBack}) {
|
||||||
|
|
||||||
@@ -13,7 +14,9 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
|
|||||||
const [isModalHtml, setIsModalHtml] = useState(false)
|
const [isModalHtml, setIsModalHtml] = useState(false)
|
||||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||||
const [isSuccessModal, setIsSuccessModal] = useState(false)
|
const [isSuccessModal, setIsSuccessModal] = useState(false)
|
||||||
|
const [isWideModal, setIsWideModal] = useState(false)
|
||||||
|
|
||||||
|
|
||||||
const handleSenden = async () => {
|
const handleSenden = async () => {
|
||||||
console.log("Alle Formulardaten: ", formData)
|
console.log("Alle Formulardaten: ", formData)
|
||||||
|
|
||||||
@@ -37,77 +40,74 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
|
|||||||
throw new Error('Keine ID in der URL gefunden.')
|
throw new Error('Keine ID in der URL gefunden.')
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormData für PHP Backend erstellen
|
// JSON-Objekt statt FormData erstellen
|
||||||
const backendData = new FormData()
|
const backendData = {
|
||||||
backendData.append('cmd', 'UPDATEAFTER')
|
cmd: 'UPDATEAFTER',
|
||||||
backendData.append('id', id)
|
id: id
|
||||||
|
}
|
||||||
|
|
||||||
// Formulardaten zu Backend-Feldern mappen
|
// Formulardaten zu Backend-Feldern mappen
|
||||||
// Basis-Status
|
// Basis-Status
|
||||||
if (formData.stattgefunden === 'ja') {
|
if (formData.stattgefunden === 'ja') {
|
||||||
backendData.append('stattgefunden', '1')
|
backendData.stattgefunden = '1'
|
||||||
|
|
||||||
// Spenden-Informationen
|
// Spenden-Informationen
|
||||||
if (formData.spendenArt) {
|
if (formData.spendenArt) {
|
||||||
switch (formData.spendenArt) {
|
switch (formData.spendenArt) {
|
||||||
case 'bar':
|
case 'bar':
|
||||||
backendData.append('bezahlt', `Kasse ${formData.betrag}€)`)
|
backendData.bezahlt = `Kasse ${formData.betrag}€`
|
||||||
break
|
break
|
||||||
case 'ueber':
|
case 'ueber':
|
||||||
backendData.append('bezahlt', 'Überweisung')
|
backendData.bezahlt = 'Überweisung'
|
||||||
break
|
break
|
||||||
case 'kasse':
|
case 'kasse':
|
||||||
backendData.append('bezahlt', 'Spendenkässle')
|
backendData.bezahlt = 'Spendenkässle'
|
||||||
break
|
break
|
||||||
case 'keine':
|
case 'keine':
|
||||||
backendData.append('bezahlt', 'keine')
|
backendData.bezahlt = 'keine'
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (formData.stattgefunden === 'nein') {
|
} else if (formData.stattgefunden === 'nein') {
|
||||||
backendData.append('stattgefunden', '0')
|
backendData.stattgefunden = '0'
|
||||||
backendData.append('bezahlt', 'keine')
|
backendData.bezahlt = 'keine'
|
||||||
|
|
||||||
// Grund für Ausfall
|
// Grund für Ausfall
|
||||||
if (formData.abgesagt === 'abgesagt') {
|
if (formData.abgesagt === 'abgesagt') {
|
||||||
backendData.append('status', 3)
|
backendData.status = 3
|
||||||
} else if (formData.abgesagt === 'verschoben') {
|
} else if (formData.abgesagt === 'verschoben') {
|
||||||
backendData.append('wtermin', formData.neuesDatum || '1900-01-01 00:00:00')
|
backendData.wtermin = formData.neuesDatum || '1900-01-01 00:00:00'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bemerkungen
|
// Bemerkungen
|
||||||
backendData.append('remark', formData.bemerkungen || '')
|
backendData.remark = formData.bemerkungen || ''
|
||||||
// Besucher
|
// Besucher
|
||||||
backendData.append('besucher', formData.besucher || '0')
|
backendData.besucher = formData.besucher || '0'
|
||||||
|
|
||||||
// // Bearbeitungsdatum setzen
|
// Debug: JSON-Daten loggen
|
||||||
// const now = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
console.log("=== JSON DATA DEBUG ===")
|
||||||
// backendData.append('bearbeitet_am', now)
|
|
||||||
|
|
||||||
// Debug: FormData kann nicht direkt geloggt werden, deshalb iterieren
|
|
||||||
console.log("=== FORM DATA DEBUG ===")
|
|
||||||
console.log("Original formData aus Context:", formData)
|
console.log("Original formData aus Context:", formData)
|
||||||
console.log("URL ID:", id)
|
console.log("URL ID:", id)
|
||||||
console.log("Backend FormData Inhalt:")
|
console.log("Backend JSON Daten:", JSON.stringify(backendData, null, 2))
|
||||||
for (let [key, value] of backendData.entries()) {
|
|
||||||
console.log(` ${key}: ${value}`)
|
|
||||||
}
|
|
||||||
console.log("========================")
|
console.log("========================")
|
||||||
|
|
||||||
// HTTP Basic Authentication Header
|
// HTTP Headers mit Basic Authentication und Content-Type
|
||||||
const headers = {}
|
const headers = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
if (username && password) {
|
if (username && password) {
|
||||||
const credentials = btoa(`${username}:${password}`)
|
const credentials = btoa(`${username}:${password}`)
|
||||||
headers['Authorization'] = `Basic ${credentials}`
|
headers['Authorization'] = `Basic ${credentials}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backend-Aufruf
|
// Backend-Aufruf mit JSON
|
||||||
const response = await fetch(APIURL, {
|
const response = await fetch(APIURL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: backendData
|
body: JSON.stringify(backendData)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -135,9 +135,13 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
|
|||||||
responseText.trim() === 'true'
|
responseText.trim() === 'true'
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
|
// E-Mail-Benachrichtigung senden (nicht blockierend)
|
||||||
|
sendEmailNotification(id, formData, backendData, APIURL, headers)
|
||||||
|
|
||||||
setModalType('success')
|
setModalType('success')
|
||||||
setModalMessage('✅ Daten erfolgreich gespeichert!')
|
setModalMessage('✅ Daten erfolgreich gespeichert!')
|
||||||
setIsSuccessModal(true)
|
setIsSuccessModal(true)
|
||||||
|
setIsWideModal(false)
|
||||||
setShowModal(true)
|
setShowModal(true)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -149,12 +153,93 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
|
|||||||
setModalType('error')
|
setModalType('error')
|
||||||
setModalMessage(`❌ Fehler beim Speichern: ${error.message}`)
|
setModalMessage(`❌ Fehler beim Speichern: ${error.message}`)
|
||||||
setIsSuccessModal(false)
|
setIsSuccessModal(false)
|
||||||
|
setIsWideModal(false)
|
||||||
setShowModal(true)
|
setShowModal(true)
|
||||||
} finally {
|
} finally {
|
||||||
setIsSending(false)
|
setIsSending(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// E-Mail-Benachrichtigung senden (asynchron, nicht blockierend)
|
||||||
|
const sendEmailNotification = async (id, formData, backendData, apiUrl, headers) => {
|
||||||
|
try {
|
||||||
|
// Details zur Sonderführung laden (für BEO und Besucher-Name)
|
||||||
|
let beoName = 'unbekannt'
|
||||||
|
let visitorName = 'unbekannt'
|
||||||
|
let termin = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const detailsResp = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({ cmd: 'GET_ONE', id })
|
||||||
|
})
|
||||||
|
if (detailsResp.ok) {
|
||||||
|
const details = await detailsResp.json()
|
||||||
|
// Backend kann Objekt oder Array liefern; robust extrahieren
|
||||||
|
const d = Array.isArray(details) ? (details[0] || {}) : (details || {})
|
||||||
|
// Felder: mitarbeiter (BEO), name/vorname (Besucher), wtermin (Termin)
|
||||||
|
beoName = d.mitarbeiter || beoName
|
||||||
|
const vn = d.vorname || ''
|
||||||
|
const nn = d.name || ''
|
||||||
|
visitorName = (vn + ' ' + nn).trim() || visitorName
|
||||||
|
termin = d.wtermin || ''
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Konnte Details für E-Mail nicht laden:', e)
|
||||||
|
}
|
||||||
|
// E-Mail-Betreff und Inhalt erstellen
|
||||||
|
const stattgefunden = formData.stattgefunden === 'ja' ? 'Ja' : 'Nein'
|
||||||
|
const besucher = formData.besucher || '0'
|
||||||
|
const spenden = backendData.bezahlt || 'keine'
|
||||||
|
const bemerkungen = formData.bemerkungen || 'keine'
|
||||||
|
|
||||||
|
const subject = `Sonderführung vom ${termin} - Nachbearbeitung`
|
||||||
|
const body = `Nachbearbeitung für Sonderführung ID ${id}
|
||||||
|
|
||||||
|
BEO: ${beoName}
|
||||||
|
Besucher: ${visitorName}
|
||||||
|
Termin: ${termin}
|
||||||
|
|
||||||
|
Stattgefunden: ${stattgefunden}
|
||||||
|
Anzahl Besucher: ${besucher}
|
||||||
|
Spenden: ${spenden}
|
||||||
|
Bemerkungen: ${bemerkungen}
|
||||||
|
|
||||||
|
Status: ${formData.stattgefunden === 'ja' ? 'Durchgeführt' :
|
||||||
|
formData.abgesagt === 'abgesagt' ? 'Abgesagt' :
|
||||||
|
formData.abgesagt === 'verschoben' ? `Verschoben auf ${formData.neuesDatum}` :
|
||||||
|
'Unbekannt'}
|
||||||
|
|
||||||
|
Diese E-Mail wurde automatisch vom Nachbearbeitungs-System generiert.
|
||||||
|
`
|
||||||
|
|
||||||
|
// E-Mail-Command an Backend senden
|
||||||
|
const emailData = {
|
||||||
|
cmd: 'SEND_CONFIRMATION',
|
||||||
|
to: 'rxf@gmx.de',
|
||||||
|
subject: subject,
|
||||||
|
body: body
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailResponse = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
body: JSON.stringify(emailData)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (emailResponse.ok) {
|
||||||
|
console.log('✅ E-Mail-Benachrichtigung erfolgreich gesendet')
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ E-Mail-Benachrichtigung konnte nicht gesendet werden:', emailResponse.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Fehler beim E-Mail-Versand nicht kritisch - nur loggen
|
||||||
|
console.warn('⚠️ E-Mail-Benachrichtigung fehlgeschlagen:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleAbbruch = () => {
|
const handleAbbruch = () => {
|
||||||
setShowConfirmModal(true)
|
setShowConfirmModal(true)
|
||||||
}
|
}
|
||||||
@@ -178,41 +263,41 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
|
|||||||
setShowConfirmModal(false)
|
setShowConfirmModal(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAnleitung = () => {
|
const handleAnleitung = async () => {
|
||||||
// Öffne die HTML-Anleitung in einem neuen Fenster/Tab
|
|
||||||
const anleitungUrl = '/anleitung.html'
|
|
||||||
const windowFeatures = 'width=800,height=600,scrollbars=yes,resizable=yes,toolbar=no,menubar=no,location=no'
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Versuche ein Popup-Fenster zu öffnen
|
// Anleitung soll im großen Modal erscheinen
|
||||||
const anleitungWindow = window.open(anleitungUrl, 'anleitung', windowFeatures)
|
setIsWideModal(true)
|
||||||
|
// Respect Vite base path in production (vite.config.js base: '/beoanswer/')
|
||||||
// Fallback: Wenn Popup blockiert wird, öffne in neuem Tab
|
const base = (import.meta.env && import.meta.env.BASE_URL) ? import.meta.env.BASE_URL : '/'
|
||||||
if (!anleitungWindow) {
|
const normalizedBase = base.endsWith('/') ? base : base + '/'
|
||||||
window.open(anleitungUrl, '_blank')
|
const url = `${normalizedBase}Anleitung.html?v=${Date.now()}` // cache-bust
|
||||||
|
|
||||||
|
// Fetch Anleitung.html relative to app base
|
||||||
|
let response = await fetch(url)
|
||||||
|
|
||||||
|
// Fallback: try relative path without base if first attempt failed
|
||||||
|
if (!response.ok) {
|
||||||
|
const fallbackUrl = `Anleitung.html?v=${Date.now()}`
|
||||||
|
response = await fetch(fallbackUrl)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
// Letzter Fallback: Als Modal anzeigen
|
if (!response.ok) {
|
||||||
console.warn('Anleitung konnte nicht in neuem Fenster geöffnet werden:', error)
|
throw new Error(`Fehler beim Laden der Anleitung (${response.status}): ${response.url}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const anleitungContent = await response.text()
|
||||||
|
|
||||||
|
// Display the content in the modal
|
||||||
setModalType('info')
|
setModalType('info')
|
||||||
setModalMessage(`
|
setModalMessage(anleitungContent)
|
||||||
📋 Anleitung:
|
setIsModalHtml(true)
|
||||||
|
setShowModal(true)
|
||||||
1. **Fand statt?** - Wählen Sie "ja" oder "nein"
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Anleitung:', error)
|
||||||
2. **Bei "ja":**
|
setModalType('error')
|
||||||
- Anzahl Besucher eingeben
|
setModalMessage('❌ Anleitung konnte nicht geladen werden.')
|
||||||
- Spenden-Art auswählen
|
|
||||||
- Bei Barspende: Betrag eingeben
|
|
||||||
- Optional: Bemerkungen
|
|
||||||
|
|
||||||
3. **Bei "nein":**
|
|
||||||
- "abgesagt" oder "verschoben" wählen
|
|
||||||
- Bei verschoben: neues Datum eingeben
|
|
||||||
|
|
||||||
4. **Senden** - Speichert alle Daten im System
|
|
||||||
`)
|
|
||||||
setIsModalHtml(false)
|
setIsModalHtml(false)
|
||||||
|
setIsWideModal(false)
|
||||||
setShowModal(true)
|
setShowModal(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,6 +329,7 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
|
|||||||
setModalMessage('')
|
setModalMessage('')
|
||||||
setIsModalHtml(false)
|
setIsModalHtml(false)
|
||||||
setIsSuccessModal(false)
|
setIsSuccessModal(false)
|
||||||
|
setIsWideModal(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +368,7 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
|
|||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
type={modalType}
|
type={modalType}
|
||||||
isHtml={isModalHtml}
|
isHtml={isModalHtml}
|
||||||
|
className={isWideModal ? 'custom-modal' : ''}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,19 @@ import React from 'react'
|
|||||||
// Import des CSS direkt hier
|
// Import des CSS direkt hier
|
||||||
import './Modal.css'
|
import './Modal.css'
|
||||||
|
|
||||||
export default function Modal({ isOpen = true, onClose, title, children, message, type = 'info', isHtml = false }) {
|
export default function Modal({
|
||||||
|
isOpen = true,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
message,
|
||||||
|
type = 'info',
|
||||||
|
isHtml = false,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
bodyClassName,
|
||||||
|
bodyStyle
|
||||||
|
}) {
|
||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
|
|
||||||
const handleOverlayClick = (e) => {
|
const handleOverlayClick = (e) => {
|
||||||
@@ -33,7 +45,7 @@ export default function Modal({ isOpen = true, onClose, title, children, message
|
|||||||
|
|
||||||
// CSS-Klasse basierend auf type
|
// CSS-Klasse basierend auf type
|
||||||
const getModalClass = () => {
|
const getModalClass = () => {
|
||||||
return `modal-content modal-${type}`
|
return `modal-content modal-${type}${className ? ' ' + className : ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayTitle = title || getDefaultTitle()
|
const displayTitle = title || getDefaultTitle()
|
||||||
@@ -57,12 +69,12 @@ export default function Modal({ isOpen = true, onClose, title, children, message
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal-overlay" onClick={handleOverlayClick} onKeyDown={handleKeyDown} tabIndex={0}>
|
<div className="modal-overlay" onClick={handleOverlayClick} onKeyDown={handleKeyDown} tabIndex={0}>
|
||||||
<div className={getModalClass()}>
|
<div className={getModalClass()} style={style}>
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h3 className="modal-title">{displayTitle}</h3>
|
<h3 className="modal-title">{displayTitle}</h3>
|
||||||
<button className="modal-close" onClick={onClose}>×</button>
|
<button className="modal-close" onClick={onClose}>×</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className={`modal-body${bodyClassName ? ' ' + bodyClassName : ''}`} style={bodyStyle}>
|
||||||
{getDisplayContent()}
|
{getDisplayContent()}
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
|
|||||||
@@ -15,20 +15,28 @@ Dieses Programm kann auch die Überwachung machen, dass Einträge in der DB gel
|
|||||||
|
|
||||||
|
|
||||||
Versions:
|
Versions:
|
||||||
|
V 1.0.1 2025-11-17 rxf
|
||||||
|
- Übergabe der Tage bis zu 'gestern' als CXommandline Parameter: '-d x'. Ohne -d wird 1 angesetzt.
|
||||||
|
|
||||||
|
V 1.0.0 2025-11-15 rxf
|
||||||
|
- Mit Tricks kann das nun DOCH realisiert werden:
|
||||||
|
Auf externem Rechner (z.Zt. 'strato_1'; Miet-Server von rxf) wird der cron angestoßen,
|
||||||
|
der das Programm hier (checkfuehrung.js) auf dem Sternwartemserver aufruft.
|
||||||
|
Das zugehörige 'beoanswer' ist eine Webseute, die auch hier auf dem Sternwarte-Server
|
||||||
|
gehostet ist.
|
||||||
|
|
||||||
V 0.0 2019-02-04 rxf
|
V 0.0 2019-02-04 rxf
|
||||||
- Start
|
- Start
|
||||||
*/
|
*/
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
const DEVELOP=0; // 1 -> Entwicklung 0-> Produktion
|
const DEVELOP=0; // 1 -> Entwicklung 0-> Produktion
|
||||||
const DAYS=2;
|
|
||||||
|
|
||||||
const nodemailer = require('nodemailer');
|
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const mysql = require('mysql2/promise');
|
const mysql = require('mysql2/promise');
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
|
||||||
const beo_Url = 'beoanswer/beoanswer.php?id=';
|
const beo_Url = 'beoanswer/?id=';
|
||||||
const Url = DEVELOP ? 'http://localhost:8081/' : 'https://sternwarte-welzheim.de/';
|
const Url = DEVELOP ? 'http://localhost:8081/' : 'https://sternwarte-welzheim.de/';
|
||||||
const DB_host = DEVELOP ? 'localhost' : 'localhost';
|
const DB_host = DEVELOP ? 'localhost' : 'localhost';
|
||||||
const DB_port = DEVELOP ? 3306 : 3306;
|
const DB_port = DEVELOP ? 3306 : 3306;
|
||||||
@@ -85,12 +93,12 @@ function send2BEO(info) {
|
|||||||
// to: info.email,
|
// to: info.email,
|
||||||
to: 'rexfue@gmail.com',
|
to: 'rexfue@gmail.com',
|
||||||
subject: 'Sonderführung vom '+info.date,
|
subject: 'Sonderführung vom '+info.date,
|
||||||
text: 'Hallo ' + info.name + '(' + info.email + '),\n\n'
|
text: 'Hallo ' + info.name + ',\n\n'
|
||||||
+ 'Du hattest gestern Führung! '
|
+ 'du hattest gestern Führung! '
|
||||||
+ 'Bitte fülle folgendes Webformular aus:\n\n'
|
+ 'Bitte fülle folgendes Webformular aus:\n\n'
|
||||||
+ Url + beo_Url + info.id
|
+ Url + beo_Url + info.id
|
||||||
+ '\n\nBitte nur über diesen Link zugreifen (oder exakt abschreiben),\n'
|
+ '\n\nBitte nur über diesen Link zugreifen (oder exakt abschreiben),\n'
|
||||||
+ 'da sonst die Zuordnung nicht hergestellt werden kann.\n'
|
+ 'da sonst die Zuordnung nicht hergestellt werden kann.\n\n'
|
||||||
+ 'Besten Dank.\n\nGrüße vom Sonderführungsteam'
|
+ 'Besten Dank.\n\nGrüße vom Sonderführungsteam'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,4 +129,8 @@ async function main() {
|
|||||||
console.log("All done");
|
console.log("All done");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const argv = require('minimist')(process.argv.slice(2));
|
||||||
|
|
||||||
|
const DAYS = argv.d || 1;
|
||||||
|
|
||||||
main().catch(console.error);
|
main().catch(console.error);
|
||||||
|
|||||||
263
sternwarte/docs/API_DB4js_all.md
Normal file
263
sternwarte/docs/API_DB4js_all.md
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
# Sternwarte API – DB4js_all.php
|
||||||
|
|
||||||
|
Vereinheitlichte Backend-API für öffentliche Führungen, Sonderführungen, Mitarbeiter (BEO), Anmeldungen und Statistiken.
|
||||||
|
|
||||||
|
## Überblick
|
||||||
|
Die Datei `DB4js_all.php` bündelt ehemals mehrere Endpunkte:
|
||||||
|
- `DB4js.php` (öffentliche Führungen + Anmeldungen)
|
||||||
|
- `sofueDB.php` (Sonderführungen)
|
||||||
|
- `anmeldDB.php` (Anmeldungen alte Variante)
|
||||||
|
- `sofianmeldDB.php` (Sonderführungs-Anmeldungen)
|
||||||
|
- `statisticDB.php` (Statistiken)
|
||||||
|
|
||||||
|
Alle Aufrufe erfolgen über einen einzigen HTTP-POST (oder wenige GET) Request an die Datei mit dem Parameter `cmd`.
|
||||||
|
Antwortformat immer JSON (`Content-Type: application/json; charset=utf-8`).
|
||||||
|
|
||||||
|
## Authentifizierung (optional)
|
||||||
|
Falls die Environment-Variablen `API_USER` und `API_PASS` gesetzt sind, **muss** Basic-Auth verwendet werden.
|
||||||
|
|
||||||
|
Header Beispiel:
|
||||||
|
```
|
||||||
|
Authorization: Basic base64(API_USER:API_PASS)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ohne gesetzte ENV-Variablen ist die API offen (nur interne Nutzung empfohlen).
|
||||||
|
|
||||||
|
## Allgemeines Request-Format
|
||||||
|
```http
|
||||||
|
POST /api/DB4js_all.php
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"cmd": "GET_ANMELD",
|
||||||
|
"id": 42
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Erfolgreiche Antworten: HTTP 200. Fehler: passende HTTP-Status (z.B. 400, 401, 404, 422, 500) + `{"error": "Beschreibung"}`.
|
||||||
|
|
||||||
|
## Fehlerstruktur
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Message",
|
||||||
|
"...optional": "Zusatzinformationen"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kommandoliste
|
||||||
|
| Command | Beschreibung |
|
||||||
|
|---------|--------------|
|
||||||
|
| PING | Health-Check (liefert Zeitstempel) |
|
||||||
|
| LIST_COMMANDS | Liefert alle verfügbaren Kommandos mit Beschreibung |
|
||||||
|
| GET_ANMELD | Liste öffentlicher Anmeldungen für `fid` |
|
||||||
|
| GET_ONEANMELD | Einzelne Anmeldung per `id` |
|
||||||
|
| GET_COUNTS | Anzahl Anmeldungen für `fid` |
|
||||||
|
| GET_COUNTS_DATE | Anzahl Anmeldungen für Datum `date` (YYYY-MM-DD) |
|
||||||
|
| INSERT_TLN | Neue öffentliche Anmeldung erstellen |
|
||||||
|
| UPDATE_TLN | Öffentliche Anmeldung ändern |
|
||||||
|
| DELETE_TLN | Öffentliche Anmeldung löschen |
|
||||||
|
| GET_SOFIANMELD | Sonderführungs-Anmeldungen; optional `sofue_id` |
|
||||||
|
| GET_ONESOFIANMELD | Einzelne Sonderführungs-Anmeldung per `id` |
|
||||||
|
| GET_SOFIANMELD_COUNT | Anzahl Sonderführungs-Anmeldungen pro `sofue_id` |
|
||||||
|
| INSERT_SOFIANMELD | Neue Sonderführungs-Anmeldung |
|
||||||
|
| UPDATE_SOFIANMELD | Sonderführungs-Anmeldung ändern |
|
||||||
|
| DELETE_SOFIANMELD | Sonderführungs-Anmeldung löschen |
|
||||||
|
| GET_TERMINE | Öffentliche Führungstermine, optional `includeOld` |
|
||||||
|
| GET_ONETERMIN | Termin per `id` |
|
||||||
|
| GET_FID | Führungs-ID zu Datum `datum` |
|
||||||
|
| GET_TIME | Uhrzeit zu Datum `date` + optional `typ` ("sonnen") |
|
||||||
|
| GET_BEOS | Alle BEOs; optional `onlyguides` und `what` (Spalten) |
|
||||||
|
| GET_ONEBEO | Einzelner BEO per `name` |
|
||||||
|
| GET_ONE | Sonderführung per `id` |
|
||||||
|
| GET_ONETERMIN_SOFUE | Sonderführung per `termin` (Datum) |
|
||||||
|
| GET_MANY | Gefilterte Sonderführungen (status, rows, page, termin) |
|
||||||
|
| UPDATE | Standard-Update einer Sonderführung |
|
||||||
|
| UPDATEAFTER | Nachbearbeitung (stattgefunden, besucher, remark, bezahlt, status) |
|
||||||
|
| DELETE | Sonderführung Soft-Delete |
|
||||||
|
| GET_STATISTIK_SOFUE | Monatsstatistik Sonderführungen Jahr `year` |
|
||||||
|
| GET_STATISTIK_ANMELD | Monatsstatistik öffentliche Führungen Jahr `year` |
|
||||||
|
| GET_STATISTIK_BEO | BEO-Führungsstatistik Jahr `year` |
|
||||||
|
| GET_STATISTIK_GESAMT | Gesamtstatistik Jahr `year` |
|
||||||
|
| SEND_CONFIRMATION | Einfache Text-Mail |
|
||||||
|
| SENDMAILZUSAGE | Zusage-Mail an Anfragenden |
|
||||||
|
| SENDMAIL2BEO | Mail an Mitarbeiter (BEO) |
|
||||||
|
| SENDMAIL2LISTE | Anfrage an Verteilerliste |
|
||||||
|
| PUT2KALENDER | Placeholder für Kalender-Eintrag |
|
||||||
|
|
||||||
|
## Parameter & Beispiele
|
||||||
|
|
||||||
|
### 1. Öffentliche Anmeldungen
|
||||||
|
#### GET_ANMELD
|
||||||
|
```json
|
||||||
|
{ "cmd": "GET_ANMELD", "id": 17 }
|
||||||
|
```
|
||||||
|
Antwort: Liste von Anmeldungen.
|
||||||
|
|
||||||
|
#### INSERT_TLN
|
||||||
|
Pflichtfelder: `name`, `email`, `anzahl`, `fid`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "INSERT_TLN",
|
||||||
|
"name": "Müller",
|
||||||
|
"email": "mueller@example.com",
|
||||||
|
"anzahl": 4,
|
||||||
|
"fid": 17,
|
||||||
|
"remarks": "Kommt etwas früher"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Antwort: `{ "success": true, "id": 123 }`
|
||||||
|
|
||||||
|
### 2. Sonderführungs-Anmeldungen
|
||||||
|
#### INSERT_SOFIANMELD
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "INSERT_SOFIANMELD",
|
||||||
|
"name": "Schule ABC",
|
||||||
|
"email": "lehrer@schule.de",
|
||||||
|
"anzahl": 22,
|
||||||
|
"sofue_id": 55,
|
||||||
|
"remarks": "Viele Fragen erwartet"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Termine
|
||||||
|
#### GET_TERMINE
|
||||||
|
```json
|
||||||
|
{ "cmd": "GET_TERMINE", "includeOld": "false" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. BEOs
|
||||||
|
#### GET_BEOS
|
||||||
|
Hinweis: In der Tabelle `beos` heißt die E-Mail-Spalte `email_1`. Aus Kompatibilitätsgründen akzeptiert die API auch `email` und liefert diese als Alias zurück.
|
||||||
|
```json
|
||||||
|
{ "cmd": "GET_BEOS", "onlyguides": "true", "what": "id,name,email" }
|
||||||
|
```
|
||||||
|
Oder explizit mit Originalspalte:
|
||||||
|
```json
|
||||||
|
{ "cmd": "GET_BEOS", "onlyguides": true, "what": "id,name,email_1" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Sonderführungen
|
||||||
|
#### GET_MANY
|
||||||
|
Filterbar über Status/Termin/Pagination.
|
||||||
|
```json
|
||||||
|
{ "cmd": "GET_MANY", "status": "2", "rows": 20, "page": 1 }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### UPDATE (Standard)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "UPDATE",
|
||||||
|
"id": 55,
|
||||||
|
"mitarbeiter": "beo_k1",
|
||||||
|
"status": 2,
|
||||||
|
"bemerkung": "Bestätigt",
|
||||||
|
"wtermin": "2025-12-03 19:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### UPDATEAFTER (Nachbearbeitung)
|
||||||
|
Optional Felder: `stattgefunden`, `besucher`, `remark`, `bezahlt`, `status`, `wtermin`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "UPDATEAFTER",
|
||||||
|
"id": 55,
|
||||||
|
"stattgefunden": 1,
|
||||||
|
"besucher": 27,
|
||||||
|
"remark": "Sehr interessiert",
|
||||||
|
"bezahlt": "Kasse 50€"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Statistiken
|
||||||
|
#### Gesamtstatistik
|
||||||
|
```json
|
||||||
|
{ "cmd": "GET_STATISTIK_GESAMT", "year": 2025 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Mail
|
||||||
|
#### SEND_CONFIRMATION
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "SEND_CONFIRMATION",
|
||||||
|
"to": "rxf@gmx.de",
|
||||||
|
"subject": "Test",
|
||||||
|
"body": "Hallo Welt"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## GET Fallback / Health
|
||||||
|
Ein GET ohne `cmd` liefert einfachen Status:
|
||||||
|
```http
|
||||||
|
GET /api/DB4js_all.php
|
||||||
|
```
|
||||||
|
Antwort:
|
||||||
|
```json
|
||||||
|
{ "status": "ok", "message": "API erreichbar" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Typische Fehlerfälle
|
||||||
|
| Status | Ursache | Beispiel |
|
||||||
|
|--------|---------|---------|
|
||||||
|
| 401 | Ungültige oder fehlende Basic-Auth | `{ "error": "Unauthorized" }` |
|
||||||
|
| 422 | Fehlender `cmd` oder Pflichtfelder | `{ "error": "Command missing" }` |
|
||||||
|
| 404 | Datensatz nicht gefunden | `{ "error": "Not found" }` |
|
||||||
|
| 500 | Interner Fehler | `{ "error": "Internal error" }` |
|
||||||
|
|
||||||
|
## Sicherheit
|
||||||
|
- Alle DB-Zugriffe über PDO Prepared Statements.
|
||||||
|
- Optional Basic-Auth.
|
||||||
|
- Keine direkte Ausgabe interner Fehlermeldungen an Client.
|
||||||
|
- E-Mail-Versand minimalistisch (kein HTML-Injection-Risiko durch Plaintext).
|
||||||
|
|
||||||
|
## Migration Hinweise
|
||||||
|
| Alt-Datei | Abgedeckt durch | Hinweise |
|
||||||
|
|----------|-----------------|----------|
|
||||||
|
| DB4js.php | Commands für öffentliche Anmeldungen & Termine | Parameter unverändert nutzbar |
|
||||||
|
| sofueDB.php | Sonderführungen & Nachbearbeitung | `UPDATEAFTER` ersetzt alte updateAfter-Version |
|
||||||
|
| anmeldDB.php | Enthalten in öffentlichen Anmeldungen | Zusammengeführt |
|
||||||
|
| sofianmeldDB.php | SoFi-Anmeldungen | eigener Command-Namespace |
|
||||||
|
| statisticDB.php | Statistik-Commands | Jahr-Parameter optional |
|
||||||
|
|
||||||
|
## Beispiel: JS Fetch
|
||||||
|
```js
|
||||||
|
async function getAnmeldungen(fid) {
|
||||||
|
const res = await fetch('/api/DB4js_all.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ cmd: 'GET_ANMELD', id: fid })
|
||||||
|
});
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Beispiel: curl
|
||||||
|
```bash
|
||||||
|
curl -X POST https://example.com/api/DB4js_all.php \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"cmd":"PING"}'
|
||||||
|
```
|
||||||
|
Mit Basic-Auth:
|
||||||
|
```bash
|
||||||
|
curl -X POST https://example.com/api/DB4js_all.php \
|
||||||
|
-u "$API_USER:$API_PASS" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"cmd":"GET_TERMINE"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Erweiterung neuer Commands
|
||||||
|
1. Eintrag in `Commands::MAP` hinzufügen.
|
||||||
|
2. Switch-Case im Dispatcher erweitern.
|
||||||
|
3. Validierung & Antwortformat konsistent halten.
|
||||||
|
|
||||||
|
## Known Limitations / TODO
|
||||||
|
- Kein Rate Limiting.
|
||||||
|
- Kein Pagination bei öffentlichen Anmeldungen (nur Sonderführungen umgesetzt).
|
||||||
|
- Kein zentraler Logger für Nutzungsstatistik.
|
||||||
|
- Kalender-Funktion ist Platzhalter.
|
||||||
|
|
||||||
|
## Changelog (Unified Version)
|
||||||
|
- Erstveröffentlichung: Zusammenführung aller Endpoints, PDO, Auth, Validation.
|
||||||
|
|
||||||
|
---
|
||||||
|
Bei Fragen oder für neue Features bitte erweitern oder Issue anlegen.
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
$typ=$_GET['typ'] ?? 'regular';
|
$typ=$_GET['typ'] ?? 'regular';
|
||||||
|
$tit = "";
|
||||||
if ($typ == 'regular') {
|
if ($typ == 'regular') {
|
||||||
$titel = 'Anmeldungen zur regulären Führung';
|
$titel = 'Anmeldungen zur regulären Führung';
|
||||||
} elseif ($typ == 'sonnen') {
|
} elseif ($typ == 'sonnen') {
|
||||||
$titel = 'Anmeldungen zur Sonnenführung';
|
$titel = 'Anmeldungen zur Sonnenführung';
|
||||||
|
$tit = "-Sonne";
|
||||||
} else {
|
} else {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
die('Ungültiger Typ.');
|
die('Ungültiger Typ.');
|
||||||
@@ -16,7 +17,7 @@ if ($typ == 'regular') {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Anmeldungen</title>
|
<title>Anmeldungen<?echo $tit?></title>
|
||||||
<link rel="stylesheet" href="css/anmeld.css"> <!-- Falls du ein Stylesheet hast -->
|
<link rel="stylesheet" href="css/anmeld.css"> <!-- Falls du ein Stylesheet hast -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ function sendmail($subject, $from, $body, $cc=[], $bcc=[], $to=[]) {
|
|||||||
$ret = [];
|
$ret = [];
|
||||||
$ret['error'] = false;
|
$ret['error'] = false;
|
||||||
|
|
||||||
|
$develop = 'true';
|
||||||
|
|
||||||
$mail = new PHPMailer(true);
|
$mail = new PHPMailer(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
147
sternwarte/tests/integration_api.test.cjs
Normal file
147
sternwarte/tests/integration_api.test.cjs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/*
|
||||||
|
Integration smoke tests for DB4js_all.php (CommonJS)
|
||||||
|
Env:
|
||||||
|
- API_URL (default: https://sternwarte-welzheim.de/DB4js_all.php)
|
||||||
|
- API_USER / API_PASS (optional Basic Auth)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
const { URL } = require('url');
|
||||||
|
|
||||||
|
const API_URL = process.env.API_URL || 'https://sternwarte-welzheim.de/DB4js_all.php';
|
||||||
|
const API_USER = process.env.API_USER || '';
|
||||||
|
const API_PASS = process.env.API_PASS || '';
|
||||||
|
|
||||||
|
function postJSON(urlStr, bodyObj) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(urlStr);
|
||||||
|
const body = JSON.stringify(bodyObj || {});
|
||||||
|
const isHttps = url.protocol === 'https:';
|
||||||
|
const lib = isHttps ? https : http;
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(body)
|
||||||
|
};
|
||||||
|
if (API_USER && API_PASS) {
|
||||||
|
const token = Buffer.from(`${API_USER}:${API_PASS}`).toString('base64');
|
||||||
|
headers['Authorization'] = `Basic ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = lib.request({
|
||||||
|
method: 'POST',
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || (isHttps ? 443 : 80),
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
headers,
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
timeout: 15000,
|
||||||
|
}, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.setEncoding('utf8');
|
||||||
|
res.on('data', c => data += c);
|
||||||
|
res.on('end', () => {
|
||||||
|
let json = null;
|
||||||
|
try { json = JSON.parse(data); } catch (e) {}
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
return reject(new Error(`HTTP ${res.statusCode}: ${data}`));
|
||||||
|
}
|
||||||
|
if (!json) return reject(new Error(`Non-JSON response: ${data}`));
|
||||||
|
resolve(json);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.on('timeout', () => { req.destroy(new Error('Request timeout')); });
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert(cond, msg) {
|
||||||
|
if (!cond) throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const failures = [];
|
||||||
|
let step = 0;
|
||||||
|
function record(name, fn) {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(fn)
|
||||||
|
.then(() => console.log(`✔ ${++step}. ${name}`))
|
||||||
|
.catch((err) => { failures.push({ name, err }); console.error(`✖ ${++step}. ${name} ->`, err.message); });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`API_URL=${API_URL}`);
|
||||||
|
|
||||||
|
await record('PING returns pong + timestamp', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'PING' });
|
||||||
|
assert(r && r.pong === true, 'pong !== true');
|
||||||
|
assert(typeof r.timestamp === 'string' && r.timestamp.length > 0, 'timestamp missing');
|
||||||
|
});
|
||||||
|
|
||||||
|
await record('LIST_COMMANDS returns command map', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'LIST_COMMANDS' });
|
||||||
|
assert(r && r.commands && typeof r.count === 'number', 'invalid LIST_COMMANDS payload');
|
||||||
|
});
|
||||||
|
|
||||||
|
await record('GET_BEOS with alias email works', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_BEOS', onlyguides: true, what: 'id,name,email' });
|
||||||
|
assert(Array.isArray(r), 'GET_BEOS did not return array');
|
||||||
|
if (r.length > 0) {
|
||||||
|
const it = r[0];
|
||||||
|
assert('id' in it, 'beo item missing id');
|
||||||
|
assert('name' in it, 'beo item missing name');
|
||||||
|
assert(('email' in it) || ('email_1' in it), 'beo item missing email/email_1');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await record('GET_BEOS with non-existing field falls back', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_BEOS', onlyguides: 'true', what: 'id,name,doesnotexist' });
|
||||||
|
assert(Array.isArray(r), 'GET_BEOS did not return array on fallback');
|
||||||
|
});
|
||||||
|
|
||||||
|
let sampleDate = null;
|
||||||
|
await record('GET_TERMINE returns array', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_TERMINE' });
|
||||||
|
assert(Array.isArray(r), 'GET_TERMINE did not return array');
|
||||||
|
if (r.length > 0) {
|
||||||
|
const d = r.find(x => typeof x.datum === 'string');
|
||||||
|
if (d) sampleDate = d.datum;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sampleDate) {
|
||||||
|
await record('GET_TIME for sample date', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_TIME', date: sampleDate });
|
||||||
|
assert(r && typeof r.time === 'string', 'GET_TIME missing time');
|
||||||
|
});
|
||||||
|
|
||||||
|
let sampleFid = null;
|
||||||
|
await record('GET_FID for sample date', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_FID', datum: sampleDate });
|
||||||
|
assert(r && ('fid' in r), 'GET_FID missing fid');
|
||||||
|
sampleFid = r.fid;
|
||||||
|
});
|
||||||
|
|
||||||
|
await record('GET_COUNTS_DATE for sample date', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_COUNTS_DATE', date: sampleDate });
|
||||||
|
assert(r && typeof r.count === 'number', 'GET_COUNTS_DATE invalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sampleFid) {
|
||||||
|
await record('GET_COUNTS for sample fid', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_COUNTS', fid: sampleFid });
|
||||||
|
assert(r && typeof r.count === 'number', 'GET_COUNTS invalid');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error(`\n${failures.length} test(s) failed:`);
|
||||||
|
failures.forEach((f, i) => console.error(` ${i + 1}) ${f.name}: ${f.err && f.err.stack || f.err}`));
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log('\nAll integration tests passed.');
|
||||||
|
}
|
||||||
|
})().catch((e) => { console.error('Fatal error:', e); process.exit(1); });
|
||||||
151
sternwarte/tests/integration_api.test.js
Normal file
151
sternwarte/tests/integration_api.test.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/*
|
||||||
|
Integration smoke tests for DB4js_all.php
|
||||||
|
- Uses HTTPS POST JSON to remote API (or custom API_URL)
|
||||||
|
- No external deps (Node core only)
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- API_URL (default: https://sternwarte-welzheim.de/DB4js_all.php)
|
||||||
|
- API_USER / API_PASS (optional Basic Auth)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
const { URL } = require('url');
|
||||||
|
|
||||||
|
const API_URL = process.env.API_URL || 'https://sternwarte-welzheim.de/DB4js_all.php';
|
||||||
|
const API_USER = process.env.API_USER || '';
|
||||||
|
const API_PASS = process.env.API_PASS || '';
|
||||||
|
|
||||||
|
function postJSON(urlStr, bodyObj) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(urlStr);
|
||||||
|
const body = JSON.stringify(bodyObj || {});
|
||||||
|
const isHttps = url.protocol === 'https:';
|
||||||
|
const lib = isHttps ? https : http;
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(body)
|
||||||
|
};
|
||||||
|
if (API_USER && API_PASS) {
|
||||||
|
const token = Buffer.from(`${API_USER}:${API_PASS}`).toString('base64');
|
||||||
|
headers['Authorization'] = `Basic ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = lib.request({
|
||||||
|
method: 'POST',
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || (isHttps ? 443 : 80),
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
headers,
|
||||||
|
rejectUnauthorized: false, // allow self-signed (if any)
|
||||||
|
timeout: 15000,
|
||||||
|
}, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.setEncoding('utf8');
|
||||||
|
res.on('data', c => data += c);
|
||||||
|
res.on('end', () => {
|
||||||
|
let json = null;
|
||||||
|
try { json = JSON.parse(data); } catch (e) {}
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
return reject(new Error(`HTTP ${res.statusCode}: ${data}`));
|
||||||
|
}
|
||||||
|
if (!json) return reject(new Error(`Non-JSON response: ${data}`));
|
||||||
|
resolve(json);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.on('timeout', () => { req.destroy(new Error('Request timeout')); });
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert(cond, msg) {
|
||||||
|
if (!cond) throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const failures = [];
|
||||||
|
let step = 0;
|
||||||
|
function record(name, fn) {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(fn)
|
||||||
|
.then(() => console.log(`✔ ${++step}. ${name}`))
|
||||||
|
.catch((err) => { failures.push({ name, err }); console.error(`✖ ${++step}. ${name} ->`, err.message); });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`API_URL=${API_URL}`);
|
||||||
|
|
||||||
|
await record('PING returns pong + timestamp', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'PING' });
|
||||||
|
assert(r && r.pong === true, 'pong !== true');
|
||||||
|
assert(typeof r.timestamp === 'string' && r.timestamp.length > 0, 'timestamp missing');
|
||||||
|
});
|
||||||
|
|
||||||
|
await record('LIST_COMMANDS returns command map', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'LIST_COMMANDS' });
|
||||||
|
assert(r && r.commands && typeof r.count === 'number', 'invalid LIST_COMMANDS payload');
|
||||||
|
});
|
||||||
|
|
||||||
|
await record('GET_BEOS with alias email works', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_BEOS', onlyguides: true, what: 'id,name,email' });
|
||||||
|
assert(Array.isArray(r), 'GET_BEOS did not return array');
|
||||||
|
if (r.length > 0) {
|
||||||
|
const it = r[0];
|
||||||
|
assert('id' in it, 'beo item missing id');
|
||||||
|
assert('name' in it, 'beo item missing name');
|
||||||
|
assert(('email' in it) || ('email_1' in it), 'beo item missing email/email_1');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await record('GET_BEOS with non-existing field falls back', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_BEOS', onlyguides: 'true', what: 'id,name,doesnotexist' });
|
||||||
|
assert(Array.isArray(r), 'GET_BEOS did not return array on fallback');
|
||||||
|
});
|
||||||
|
|
||||||
|
let sampleDate = null;
|
||||||
|
await record('GET_TERMINE returns array', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_TERMINE' });
|
||||||
|
assert(Array.isArray(r), 'GET_TERMINE did not return array');
|
||||||
|
if (r.length > 0) {
|
||||||
|
// pick first with possible datum field
|
||||||
|
const d = r.find(x => typeof x.datum === 'string');
|
||||||
|
if (d) sampleDate = d.datum;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sampleDate) {
|
||||||
|
await record('GET_TIME for sample date', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_TIME', date: sampleDate });
|
||||||
|
assert(r && typeof r.time === 'string', 'GET_TIME missing time');
|
||||||
|
});
|
||||||
|
|
||||||
|
let sampleFid = null;
|
||||||
|
await record('GET_FID for sample date', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_FID', datum: sampleDate });
|
||||||
|
assert(r && ('fid' in r), 'GET_FID missing fid');
|
||||||
|
sampleFid = r.fid;
|
||||||
|
});
|
||||||
|
|
||||||
|
await record('GET_COUNTS_DATE for sample date', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_COUNTS_DATE', date: sampleDate });
|
||||||
|
assert(r && typeof r.count === 'number', 'GET_COUNTS_DATE invalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sampleFid) {
|
||||||
|
await record('GET_COUNTS for sample fid', async () => {
|
||||||
|
const r = await postJSON(API_URL, { cmd: 'GET_COUNTS', fid: sampleFid });
|
||||||
|
assert(r && typeof r.count === 'number', 'GET_COUNTS invalid');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error(`\n${failures.length} test(s) failed:`);
|
||||||
|
failures.forEach((f, i) => console.error(` ${i + 1}) ${f.name}: ${f.err && f.err.stack || f.err}`));
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log('\nAll integration tests passed.');
|
||||||
|
}
|
||||||
|
})().catch((e) => { console.error('Fatal error:', e); process.exit(1); });
|
||||||
Reference in New Issue
Block a user