DB4js als _all zusammengefasst

Beoanswer ist nun OK
This commit is contained in:
rxf
2025-11-17 14:22:52 +01:00
parent ad0f7b2912
commit 3e8b1f9691
14 changed files with 1528 additions and 81 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ download
*.log
webseiten
sternwarte/beoanswer/.env.production

View File

@@ -5,6 +5,35 @@ include 'config_stern.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
// Parameter
@@ -435,14 +464,15 @@ function getOneRecordTermin($termin) {
return $erg;
}
/*
$_POST = json_decode(file_get_contents('php://input'), true);
$erg = "";
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$cmd = $_POST["cmd"];
/*
*/
$x = "[";
foreach ($_POST as $key => $value) {
if(gettype($value) == "array") {
@@ -451,7 +481,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$x = $x . $key . " => " . $value . ",";
}
$x = $x . "]";
*/
switch ($cmd) {
case 'GET_ANMELD':
$erg = getAnmeldungen($_POST['id']);
@@ -567,7 +597,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
default:
$erg = ['error' => 'Unknown POST-Command', 'cmd' => $cmd, 'params' => $x];
}
} else {
/*
$x = "[";
foreach ($_GET as $key => $value) {
@@ -575,6 +604,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
}
$x = $x . "]";
*/
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
$cmd = $_GET['cmd'];
switch ($cmd) {
case 'GET_FDATES':

725
sternwarte/DB4js_all.php Normal file
View 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);
}

View File

@@ -1,7 +1,7 @@
{
"name": "beoanswer_react",
"private": true,
"version": "1.0.2",
"version": "1.0.3",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -17,8 +17,8 @@ function AppContent() {
const [name, setName] = useState("")
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [mitsend, setMitsend] = useState(false)
const [mitback, setMitback] = useState(false)
//const [mitsend, setMitsend] = useState(false)
//const [mitback, setMitback] = useState(false)
const version = packageJson.version
const vdate = new Date().toLocaleDateString('de-DE')
@@ -66,6 +66,8 @@ function AppContent() {
headers['Authorization'] = `Basic ${credentials}`
}
console.log(formData)
const response = await fetch(APIURL, {
method: 'POST',
headers: headers,
@@ -160,7 +162,7 @@ function AppContent() {
}
const setBackButton = () => {
setMitback(true)
// setMitback(true)
}
// Welche Komponeneten werden angezeigt:

View 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 */
}

View File

@@ -2,6 +2,7 @@ import { useState } from 'react'
import { useFormData } from '../FormContext'
import Modal from './Modal'
import ConfirmModal from './ConfirmModal'
import './LastButtons.css';
export default function LastButtons({ mitSend, mitBack, handleBack}) {
@@ -13,6 +14,8 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
const [isModalHtml, setIsModalHtml] = useState(false)
const [showConfirmModal, setShowConfirmModal] = useState(false)
const [isSuccessModal, setIsSuccessModal] = useState(false)
const [isWideModal, setIsWideModal] = useState(false)
const handleSenden = async () => {
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.')
}
// FormData für PHP Backend erstellen
const backendData = new FormData()
backendData.append('cmd', 'UPDATEAFTER')
backendData.append('id', id)
// JSON-Objekt statt FormData erstellen
const backendData = {
cmd: 'UPDATEAFTER',
id: id
}
// Formulardaten zu Backend-Feldern mappen
// Basis-Status
if (formData.stattgefunden === 'ja') {
backendData.append('stattgefunden', '1')
backendData.stattgefunden = '1'
// Spenden-Informationen
if (formData.spendenArt) {
switch (formData.spendenArt) {
case 'bar':
backendData.append('bezahlt', `Kasse ${formData.betrag})`)
backendData.bezahlt = `Kasse ${formData.betrag}`
break
case 'ueber':
backendData.append('bezahlt', 'Überweisung')
backendData.bezahlt = 'Überweisung'
break
case 'kasse':
backendData.append('bezahlt', 'Spendenkässle')
backendData.bezahlt = 'Spendenkässle'
break
case 'keine':
backendData.append('bezahlt', 'keine')
backendData.bezahlt = 'keine'
break
}
}
} else if (formData.stattgefunden === 'nein') {
backendData.append('stattgefunden', '0')
backendData.append('bezahlt', 'keine')
backendData.stattgefunden = '0'
backendData.bezahlt = 'keine'
// Grund für Ausfall
if (formData.abgesagt === 'abgesagt') {
backendData.append('status', 3)
backendData.status = 3
} 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
backendData.append('remark', formData.bemerkungen || '')
backendData.remark = formData.bemerkungen || ''
// Besucher
backendData.append('besucher', formData.besucher || '0')
backendData.besucher = formData.besucher || '0'
// // Bearbeitungsdatum setzen
// const now = new Date().toISOString().slice(0, 19).replace('T', ' ')
// backendData.append('bearbeitet_am', now)
// Debug: FormData kann nicht direkt geloggt werden, deshalb iterieren
console.log("=== FORM DATA DEBUG ===")
// Debug: JSON-Daten loggen
console.log("=== JSON DATA DEBUG ===")
console.log("Original formData aus Context:", formData)
console.log("URL ID:", id)
console.log("Backend FormData Inhalt:")
for (let [key, value] of backendData.entries()) {
console.log(` ${key}: ${value}`)
}
console.log("Backend JSON Daten:", JSON.stringify(backendData, null, 2))
console.log("========================")
// HTTP Basic Authentication Header
const headers = {}
// HTTP Headers mit Basic Authentication und Content-Type
const headers = {
'Content-Type': 'application/json'
}
if (username && password) {
const credentials = btoa(`${username}:${password}`)
headers['Authorization'] = `Basic ${credentials}`
}
// Backend-Aufruf
// Backend-Aufruf mit JSON
const response = await fetch(APIURL, {
method: 'POST',
headers: headers,
body: backendData
body: JSON.stringify(backendData)
})
if (!response.ok) {
@@ -135,9 +135,13 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
responseText.trim() === 'true'
if (isSuccess) {
// E-Mail-Benachrichtigung senden (nicht blockierend)
sendEmailNotification(id, formData, backendData, APIURL, headers)
setModalType('success')
setModalMessage('✅ Daten erfolgreich gespeichert!')
setIsSuccessModal(true)
setIsWideModal(false)
setShowModal(true)
} else {
@@ -149,12 +153,93 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
setModalType('error')
setModalMessage(`❌ Fehler beim Speichern: ${error.message}`)
setIsSuccessModal(false)
setIsWideModal(false)
setShowModal(true)
} finally {
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 = () => {
setShowConfirmModal(true)
}
@@ -178,41 +263,41 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
setShowConfirmModal(false)
}
const handleAnleitung = () => {
// Ö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'
const handleAnleitung = async () => {
try {
// Versuche ein Popup-Fenster zu öffnen
const anleitungWindow = window.open(anleitungUrl, 'anleitung', windowFeatures)
// Anleitung soll im großen Modal erscheinen
setIsWideModal(true)
// Respect Vite base path in production (vite.config.js base: '/beoanswer/')
const base = (import.meta.env && import.meta.env.BASE_URL) ? import.meta.env.BASE_URL : '/'
const normalizedBase = base.endsWith('/') ? base : base + '/'
const url = `${normalizedBase}Anleitung.html?v=${Date.now()}` // cache-bust
// Fallback: Wenn Popup blockiert wird, öffne in neuem Tab
if (!anleitungWindow) {
window.open(anleitungUrl, '_blank')
// 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
console.warn('Anleitung konnte nicht in neuem Fenster geöffnet werden:', error)
if (!response.ok) {
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')
setModalMessage(`
📋 Anleitung:
1. **Fand statt?** - Wählen Sie "ja" oder "nein"
2. **Bei "ja":**
- Anzahl Besucher eingeben
- 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
`)
setModalMessage(anleitungContent)
setIsModalHtml(true)
setShowModal(true)
} catch (error) {
console.error('Fehler beim Laden der Anleitung:', error)
setModalType('error')
setModalMessage('❌ Anleitung konnte nicht geladen werden.')
setIsModalHtml(false)
setIsWideModal(false)
setShowModal(true)
}
}
@@ -244,6 +329,7 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
setModalMessage('')
setIsModalHtml(false)
setIsSuccessModal(false)
setIsWideModal(false)
}
}
@@ -282,6 +368,7 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
onClose={closeModal}
type={modalType}
isHtml={isModalHtml}
className={isWideModal ? 'custom-modal' : ''}
/>
)}

View File

@@ -2,7 +2,19 @@ import React from 'react'
// Import des CSS direkt hier
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
const handleOverlayClick = (e) => {
@@ -33,7 +45,7 @@ export default function Modal({ isOpen = true, onClose, title, children, message
// CSS-Klasse basierend auf type
const getModalClass = () => {
return `modal-content modal-${type}`
return `modal-content modal-${type}${className ? ' ' + className : ''}`
}
const displayTitle = title || getDefaultTitle()
@@ -57,12 +69,12 @@ export default function Modal({ isOpen = true, onClose, title, children, message
return (
<div className="modal-overlay" onClick={handleOverlayClick} onKeyDown={handleKeyDown} tabIndex={0}>
<div className={getModalClass()}>
<div className={getModalClass()} style={style}>
<div className="modal-header">
<h3 className="modal-title">{displayTitle}</h3>
<button className="modal-close" onClick={onClose}>&times;</button>
</div>
<div className="modal-body">
<div className={`modal-body${bodyClassName ? ' ' + bodyClassName : ''}`} style={bodyStyle}>
{getDisplayContent()}
</div>
<div className="modal-footer">

View File

@@ -15,20 +15,28 @@ Dieses Programm kann auch die Überwachung machen, dass Einträge in der DB gel
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
- Start
*/
"use strict"
const DEVELOP=0; // 1 -> Entwicklung 0-> Produktion
const DAYS=2;
const nodemailer = require('nodemailer');
const moment = require('moment');
const axios = require('axios');
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 DB_host = DEVELOP ? 'localhost' : 'localhost';
const DB_port = DEVELOP ? 3306 : 3306;
@@ -85,12 +93,12 @@ function send2BEO(info) {
// to: info.email,
to: 'rexfue@gmail.com',
subject: 'Sonderführung vom '+info.date,
text: 'Hallo ' + info.name + '(' + info.email + '),\n\n'
+ 'Du hattest gestern Führung! '
text: 'Hallo ' + info.name + ',\n\n'
+ 'du hattest gestern Führung! '
+ 'Bitte fülle folgendes Webformular aus:\n\n'
+ Url + beo_Url + info.id
+ '\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'
};
@@ -121,4 +129,8 @@ async function main() {
console.log("All done");
}
const argv = require('minimist')(process.argv.slice(2));
const DAYS = argv.d || 1;
main().catch(console.error);

View 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.

View File

@@ -1,10 +1,11 @@
<?php
$typ=$_GET['typ'] ?? 'regular';
$tit = "";
if ($typ == 'regular') {
$titel = 'Anmeldungen zur regulären Führung';
} elseif ($typ == 'sonnen') {
$titel = 'Anmeldungen zur Sonnenführung';
$tit = "-Sonne";
} else {
http_response_code(400);
die('Ungültiger Typ.');
@@ -16,7 +17,7 @@ if ($typ == 'regular') {
<head>
<meta charset="UTF-8">
<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 -->
</head>
<body>

View File

@@ -11,6 +11,8 @@ function sendmail($subject, $from, $body, $cc=[], $bcc=[], $to=[]) {
$ret = [];
$ret['error'] = false;
$develop = 'true';
$mail = new PHPMailer(true);
try {

View 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); });

View 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); });