Compare commits
3 Commits
e3bb5d36b9
...
b78831266d
| Author | SHA1 | Date | |
|---|---|---|---|
| b78831266d | |||
| 3e8b1f9691 | |||
| ad0f7b2912 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ download
|
||||
*.log
|
||||
webseiten
|
||||
|
||||
sternwarte/beoanswer/.env.production
|
||||
|
||||
30
sternwarte/.gitignore
vendored
Normal file
30
sternwarte/.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Environment variables
|
||||
sternwarte/beoanswer/.env*
|
||||
|
||||
# CORS-Proxy Konfiguration (enthält Credentials)
|
||||
cors-config.php
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,11 +1,39 @@
|
||||
<?php
|
||||
# Hier werden die Anfragen vom Javascript verarbeitet und die
|
||||
# Datenbank bedient
|
||||
|
||||
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
|
||||
@@ -139,6 +167,10 @@ function getAllTeilnehmer($fdatum)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// die Daten der Führungen in dem Bereich start - end
|
||||
// in ein Array als ISO8601 holen
|
||||
// Parameter
|
||||
@@ -146,10 +178,14 @@ function getAllTeilnehmer($fdatum)
|
||||
// $end -> bis zu diesem Datum
|
||||
// Retunrn:
|
||||
// Array mit den Daten in ISO8601
|
||||
function getFuehrungen($start, $end) {
|
||||
function getFuehrungen($start, $end, $typ) {
|
||||
global $db;
|
||||
$erg = array();
|
||||
$sql_sel = "SELECT * FROM fdatum1 where datum >= '$start' AND datum <= '$end' ORDER BY datum ASC";
|
||||
$erg = [];
|
||||
$table = 'fdatum1';
|
||||
if ($typ == 'sonnen') {
|
||||
$table = 'sonnedatum';
|
||||
}
|
||||
$sql_sel = "SELECT * FROM $table where datum >= '$start' AND datum <= '$end' ORDER BY datum ASC";
|
||||
$result = mysqli_query($db, $sql_sel) or die(mysqli_error($db));
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
foreach ($row as $key => $value) {
|
||||
@@ -262,6 +298,17 @@ function getOneDate($id) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// aus fdatum die div. Datumsteile hole
|
||||
function getTimeByDate($dt, $typ) {
|
||||
global $db;
|
||||
if ($typ == 'sonnen')
|
||||
return '11 Uhr';
|
||||
$sql_stmt = "SELECT uhrzeit FROM fdatum1 WHERE datum='$dt'";
|
||||
$result = mysqli_query($db, $sql_stmt) or die(mysqli_error($db));
|
||||
$data = mysqli_fetch_assoc($result);
|
||||
return $data['uhrzeit'];
|
||||
}
|
||||
|
||||
function insertteilnehmer($data) {
|
||||
global $db;
|
||||
$name = $data['name'];
|
||||
@@ -417,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") {
|
||||
@@ -433,7 +481,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$x = $x . $key . " => " . $value . ",";
|
||||
}
|
||||
$x = $x . "]";
|
||||
*/
|
||||
|
||||
switch ($cmd) {
|
||||
case 'GET_ANMELD':
|
||||
$erg = getAnmeldungen($_POST['id']);
|
||||
@@ -473,6 +521,9 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
case 'GET_ONE_DATE':
|
||||
$erg = getOneDate($_POST['fid']);
|
||||
break;
|
||||
case 'GET_TIME_BY_DATE':
|
||||
$erg = getTimeByDate($_POST['dt'], $_POST['typ']);
|
||||
break;
|
||||
case 'GET_ALLTEILN':
|
||||
$erg = getAllTeilnehmer($_POST['fdatum']);
|
||||
break;
|
||||
@@ -503,7 +554,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$erg = getTeilnehmer(-1, true, false);
|
||||
break;
|
||||
case 'GET_FUEHRUNGEN':
|
||||
$erg = getFuehrungen($_POST['start'], $_POST['end']);
|
||||
$erg = getFuehrungen($_POST['start'], $_POST['end'], $_POST['typ']);
|
||||
break;
|
||||
case 'PUT_FDATES':
|
||||
$erg = putFdates($_POST['data']);
|
||||
@@ -546,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) {
|
||||
@@ -554,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
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",
|
||||
"private": true,
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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:
|
||||
|
||||
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 Modal from './Modal'
|
||||
import ConfirmModal from './ConfirmModal'
|
||||
import './LastButtons.css';
|
||||
|
||||
export default function LastButtons({ mitSend, mitBack, handleBack}) {
|
||||
|
||||
@@ -13,7 +14,9 @@ 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)
|
||||
|
||||
// Fallback: Wenn Popup blockiert wird, öffne in neuem Tab
|
||||
if (!anleitungWindow) {
|
||||
window.open(anleitungUrl, '_blank')
|
||||
// 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
|
||||
|
||||
// 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' : ''}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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}>×</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className={`modal-body${bodyClassName ? ' ' + bodyClassName : ''}`} style={bodyStyle}>
|
||||
{getDisplayContent()}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
|
||||
@@ -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);
|
||||
|
||||
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
|
||||
$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>
|
||||
|
||||
@@ -11,6 +11,8 @@ function sendmail($subject, $from, $body, $cc=[], $bcc=[], $to=[]) {
|
||||
$ret = [];
|
||||
$ret['error'] = false;
|
||||
|
||||
$develop = 'true';
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
.storno {
|
||||
font-size: 14px;
|
||||
min-height: 500px;
|
||||
min-height: 550px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -131,3 +131,8 @@ h5 {
|
||||
margin-left: 10px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
#abbrechen {
|
||||
background-color: gray;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
Für diese E-Mail Adresse ist keine Führung angemeldet !<br />
|
||||
</div>
|
||||
<div id="anmeldid" class="col-12 col-xm-8 text-center">
|
||||
Sie sind angemeldet für: <br /><br />
|
||||
Sie sind angemeldet für eine <span id="fart">Sternführung</span> am: <br /><br />
|
||||
<div id="anmeldung">
|
||||
2022-07-12 22:00 Uhr 4 Personen
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
$(document).ready(() => {
|
||||
|
||||
// Globale Konstanten und Variable
|
||||
// const ajaxURL="php/anmeldDB.php";
|
||||
const ajaxURL="../../DB4js.php";
|
||||
// const ajaxURL="php/anmeldDB.php";
|
||||
const ajaxURL = "../../DB4js.php";
|
||||
|
||||
const maxVisitors = 25
|
||||
const months2add = 3
|
||||
@@ -21,9 +21,9 @@ $(document).ready(() => {
|
||||
// Return:
|
||||
// angeforderte Daten als JSON
|
||||
const fetchFromDbase = async (body) => {
|
||||
const response = await fetch(ajaxURL,{
|
||||
const response = await fetch(ajaxURL, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/js'},
|
||||
headers: { 'Content-Type': 'application/js' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
let rt = await response.json();
|
||||
@@ -37,22 +37,41 @@ $(document).ready(() => {
|
||||
// Return:
|
||||
// angeforderte Daten als JSON
|
||||
const putToDbase = async (body) => {
|
||||
const response = await fetch(ajaxURL,{
|
||||
const response = await fetch(ajaxURL, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/js'},
|
||||
headers: { 'Content-Type': 'application/js' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Wochentag aus integer Datum extrahieren
|
||||
function getWochentag(datumInt) {
|
||||
const d = new Date(
|
||||
Math.floor(datumInt / 10000), // Jahr
|
||||
Math.floor((datumInt % 10000) / 100) - 1, // Monat (0-basiert)
|
||||
datumInt % 100 // Tag
|
||||
);
|
||||
return ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"][d.getDay()];
|
||||
}
|
||||
|
||||
// Aus dem integer Führungsdatum (aus anmeldungen) den Wochentag und
|
||||
// die Uhrzeit aus der Tabelle fdatum 1 holen
|
||||
const getFuhrzeit = async (dt, typ) => {
|
||||
return await fetchFromDbase({ cmd: 'GET_TIME_BY_DATE', dt: dt, typ: typ })
|
||||
|
||||
}
|
||||
// Das Führungsdatum extrahieren
|
||||
const buildDatum = async (tn, short) => {
|
||||
const person = tn.anzahl === '1' ? 'Person' : 'Personen'
|
||||
const datum = await fetchFromDbase({cmd: 'GET_ONE_DATE', fid: tn.fid})
|
||||
if(short) {
|
||||
return `${moment(datum.datum).format('DD.MM.YYYY')}`
|
||||
const uhrzeit = await getFuhrzeit(tn.fdatum, tn.typ)
|
||||
if (short) {
|
||||
return `${moment(tn.fdatum).format('DD.MM.YYYY')}`
|
||||
}
|
||||
return `${datum.wtag}, den ${moment(datum.datum).format('DD.MM.YYYY')} um ${datum.uhrzeit} mit ${tn.anzahl} ${person}`
|
||||
if(tn.typ === 'sonnen') {
|
||||
document.getElementById('fart').innerHTML = "Sonnenführung"
|
||||
}
|
||||
return `${getWochentag(tn.fdatum)}, den ${moment(tn.fdatum).format('DD.MM.YYYY')} um ${uhrzeit} mit ${tn.anzahl} ${person}`
|
||||
}
|
||||
|
||||
// Das Führungsdatum anzeigen
|
||||
@@ -67,13 +86,13 @@ $(document).ready(() => {
|
||||
}
|
||||
|
||||
const austragen = async (teilnehmer) => {
|
||||
// console.log("Austragen von ", teilnehmer)
|
||||
// return
|
||||
oldtndata = {...teilnehmer}
|
||||
let delstr = {cmd: 'DELETE_ENTRY', id: parseInt(teilnehmer.id)}
|
||||
// console.log("Austragen von ", teilnehmer)
|
||||
// return
|
||||
oldtndata = { ...teilnehmer }
|
||||
let delstr = { cmd: 'DELETE_ENTRY', id: parseInt(teilnehmer.id) }
|
||||
const erg1 = await putToDbase(delstr)
|
||||
console.log("Storno Ergebnis: ",erg1)
|
||||
if(erg1) {
|
||||
console.log("Storno Ergebnis: ", erg1)
|
||||
if (erg1) {
|
||||
$('#ausgetragen').show()
|
||||
$('#lower_text').show()
|
||||
sendEmail(teilnehmer, true)
|
||||
@@ -84,44 +103,48 @@ $(document).ready(() => {
|
||||
}
|
||||
|
||||
const umbuchen = async (teilnehmer) => {
|
||||
// console.log('Umbuchen von ',teilnehmer)
|
||||
// return
|
||||
oldtndata = {...teilnehmer}
|
||||
// console.log('Umbuchen von ',teilnehmer)
|
||||
// return
|
||||
oldtndata = { ...teilnehmer }
|
||||
let start = moment()
|
||||
start = start.add(vorlauf, 'h')
|
||||
let end = moment()
|
||||
end = end.add(months2add, 'M')
|
||||
let add = months2add
|
||||
if (teilnehmer.typ === 'sonnen') {
|
||||
add = 12
|
||||
}
|
||||
end = end.add(add, 'M')
|
||||
const anzahl = parseInt(teilnehmer.anzahl)
|
||||
let fuehrungen = await fetchFromDbase({cmd: 'GET_FUEHRUNGEN', start: start.format('YYYYMMDD'), end: end.format('YYYYMMDD')})
|
||||
let fuehrungen = await fetchFromDbase({ cmd: 'GET_FUEHRUNGEN', start: start.format('YYYYMMDD'), end: end.format('YYYYMMDD'), typ: teilnehmer.typ})
|
||||
let r = `<label for "ftermin" class="labeltext"><strong>Umbuchung auf</strong></label><br /><select name="ftermin" id="ftermin"><option>-- Bitte wählen Sie ein Datum aus--</option>`
|
||||
for (let f of fuehrungen) {
|
||||
if(f.datum == teilnehmer.fdatum) {
|
||||
if (f.datum == teilnehmer.fdatum) {
|
||||
continue
|
||||
}
|
||||
let count = await fetchFromDbase({cmd: 'GET_COUNTS_DATE', date: f.datum})
|
||||
let count = await fetchFromDbase({ cmd: 'GET_COUNTS_DATE', date: f.datum })
|
||||
count = count ? parseInt(count) : 0
|
||||
if(count + anzahl >= maxVisitors) {
|
||||
if (count + anzahl >= maxVisitors) {
|
||||
continue // wenn der Platz nicht reicht, nicht anzeigen
|
||||
}
|
||||
r += `<option id=${f.id} value=${f.datum}>`
|
||||
r += `${f.wtag.substring(0,2)} , ${moment(f.datum).format('DD.MM.YYYY')} ${f.uhrzeit} `
|
||||
r += `Frei ${(maxVisitors-count) > 0 ? maxVisitors-count : 0}</option>`
|
||||
r += `${getWochentag(f.datum).substring(0, 2)} , ${moment(f.datum).format('DD.MM.YYYY')} ${await getFuhrzeit(f.datum, teilnehmer.typ)} `
|
||||
r += `Frei ${(maxVisitors - count) > 0 ? maxVisitors - count : 0}</option>`
|
||||
}
|
||||
r += '</select>'
|
||||
$('#umbuchung').html(r)
|
||||
// $('#umgebucht').html('Bitte wählen Sie ein freies Datum über den kleine Pfeil rechts.')
|
||||
// $('#umgebucht').html('Bitte wählen Sie ein freies Datum über den kleine Pfeil rechts.')
|
||||
$('#umgebucht').show()
|
||||
|
||||
// Eventhandler für Auswahl eines Datums über die SelectBox:
|
||||
// Anzeigen der Anmeldungen dazu
|
||||
$('#ftermin').change( async () => {
|
||||
$('#ftermin').change(async () => {
|
||||
const x = $('#ftermin').find(':selected');
|
||||
const date = x[0].value
|
||||
teilnehmer.fdatum = date
|
||||
teilnehmer.fid = x[0].id
|
||||
const update = {cmd: 'UPDATETLNFD', fdatum: date, fid: x[0].id, id: teilnehmer.id}
|
||||
const update = { cmd: 'UPDATETLNFD', fdatum: date, fid: x[0].id, id: teilnehmer.id }
|
||||
const erg = await putToDbase(update)
|
||||
if(erg) {
|
||||
if (erg) {
|
||||
$('#umbuchung').hide()
|
||||
$('#umgebucht').html(`Sie wurden erfolgreich umgebucht auf <br /><br />${await buildDatum(teilnehmer, false)}`)
|
||||
$('#umgebucht').show()
|
||||
@@ -137,11 +160,11 @@ $(document).ready(() => {
|
||||
}
|
||||
|
||||
const aendern = async (teilnehmer) => {
|
||||
console.log('Ändern von ',teilnehmer)
|
||||
let count = await fetchFromDbase({cmd: 'GET_COUNTS_DATE', date: teilnehmer.fdatum})
|
||||
console.log('Ändern von ', teilnehmer)
|
||||
let count = await fetchFromDbase({ cmd: 'GET_COUNTS_DATE', date: teilnehmer.fdatum })
|
||||
let anzahl = parseInt(teilnehmer.anzahl)
|
||||
let max = anzahl
|
||||
if(count < maxVisitors) {
|
||||
if (count < maxVisitors) {
|
||||
max = maxVisitors - (count - anzahl)
|
||||
}
|
||||
if (max > 10) {
|
||||
@@ -149,24 +172,24 @@ $(document).ready(() => {
|
||||
}
|
||||
console.log("Max: ", max)
|
||||
$('#maxanzahl').text(`max. ${max}`)
|
||||
$('#personen').attr({max: max, min: 1, step: 1, value: anzahl})
|
||||
$('#personen').attr({ max: max, min: 1, step: 1, value: anzahl })
|
||||
|
||||
// Evelthandler Übernehmen geklicked
|
||||
$('#bsave').click(async () => {
|
||||
$('#save').hide()
|
||||
teilnehmer.anzahl = $('#personen').val()
|
||||
let e = await putToDbase({cmd: 'UPDATE_TLN', data: teilnehmer, id: teilnehmer.id})
|
||||
let e = await putToDbase({ cmd: 'UPDATE_TLN', data: teilnehmer, id: teilnehmer.id })
|
||||
$('#newperson').html(`Sie sind nun mit <strong>${teilnehmer.anzahl} </strong> Personen angemeldet.`)
|
||||
$('#bsave').hide()
|
||||
$('#newperson').show()
|
||||
$('#homebutton').show()
|
||||
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Emailadresse eingegeben
|
||||
// Emailadresse eingegeben
|
||||
const setEvent = (f) => {
|
||||
$(document).on("keypress", "input", async function (e) {
|
||||
if (e.which === 13) {
|
||||
@@ -196,15 +219,15 @@ $(document).ready(() => {
|
||||
$('#umbuchen').click(() => {
|
||||
umbuchen(f[idx])
|
||||
$('#anmeldid').hide()
|
||||
// $('#email').val("")
|
||||
// $('#email').val("")
|
||||
})
|
||||
// Ändern geklicked
|
||||
$('#aendern').click(() => {
|
||||
aendern(f[idx])
|
||||
$('#butgroup').hide()
|
||||
$('#aenderung').show()
|
||||
// $('#anmeldid').hide()
|
||||
// $('#email').val("")
|
||||
// $('#anmeldid').hide()
|
||||
// $('#email').val("")
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -214,20 +237,18 @@ $(document).ready(() => {
|
||||
sendEmail = async (tln, storno) => {
|
||||
let fdatum = await buildDatum(tln, false)
|
||||
let oldfdatum = await buildDatum(oldtndata, true)
|
||||
let subject = `${storno ? "Stornierung":"Umbuchung"} der Führung vom ${oldfdatum} auf der Sternwarte Welzheim`
|
||||
let subject = `${storno ? "Stornierung" : "Umbuchung"} der Führung vom ${oldfdatum} auf der Sternwarte Welzheim`
|
||||
let body_txt = `
|
||||
Sehr geehrte Dame, sehr geehrter Herr,
|
||||
|
||||
hiermit bestätigen wir die ${storno ? 'Stornierung' : 'Umbuchung'} Ihrer Führung auf der Sternwarte Welzheim vom`
|
||||
if(!storno) {
|
||||
if (!storno) {
|
||||
body_txt += ` ${oldfdatum}.
|
||||
|
||||
Sie wurden umgebucht auf:
|
||||
|
||||
${fdatum}
|
||||
|
||||
Bitte bringen Sie diese Bestätigung als Ausdruck oder digital zur Führung mit.
|
||||
|
||||
Die Führung findet NUR bei sternklarem Himmel statt. Falls der Himmel bedeckt ist
|
||||
und die Führung ausfällt, erhalten Sie bis spätestens eine Stunde vor Führungsbeginn
|
||||
eine Email. Sie können sich dann gerne zu einer anderen Führung neu anmelden.
|
||||
@@ -244,12 +265,12 @@ Mit freundlichen Grüßen
|
||||
Beobachterteam der Sternwarte Welzheim
|
||||
www.sternwarte-welzheim.de
|
||||
`
|
||||
let erg = await putToDbase({cmd: 'SEND_MAIL_HTML', subject: subject, to: [tln.email], body_txt: body_txt, body_html: ""})
|
||||
let erg = await putToDbase({ cmd: 'SEND_MAIL_HTML', subject: subject, to: [tln.email], body_txt: body_txt, body_html: "" })
|
||||
console.log("Antwort von sendmail_1: ", erg)
|
||||
|
||||
body_txt = `Die Führung vom ${oldfdatum} wurde ${storno ? 'storniert' : 'umgebucht'}
|
||||
`
|
||||
if(!storno) {
|
||||
if (!storno) {
|
||||
body_txt += `
|
||||
auf ${fdatum}`
|
||||
}
|
||||
@@ -258,12 +279,12 @@ auf ${fdatum}`
|
||||
Besucher: ${tln.name} ${tln.vorname}`
|
||||
|
||||
body_html = `Die Führung vom ${oldfdatum} wurde ${storno ? 'storniert' : 'umgebucht'}`
|
||||
if(!storno) {
|
||||
if (!storno) {
|
||||
body_html += ` auf ${fdatum}<br />`
|
||||
}
|
||||
body_html += `<br />Besucher: ${tln.name} ${tln.vorname}`
|
||||
body_html += `<br />Besucher: ${tln.name} ${tln.vorname}`
|
||||
|
||||
erg = await putToDbase({cmd: 'SEND_MAIL_HTML', subject: subject, to: ['rexfue@gmail.com'], body_txt: body_txt, body_html: body_html})
|
||||
erg = await putToDbase({ cmd: 'SEND_MAIL_HTML', subject: subject, to: ['rexfue@gmail.com'], body_txt: body_txt, body_html: body_html })
|
||||
console.log("Antwort von sendmail_2: ", erg)
|
||||
}
|
||||
|
||||
@@ -277,7 +298,7 @@ Besucher: ${tln.name} ${tln.vorname}`
|
||||
// und anzeigen
|
||||
// Params:
|
||||
// n -> Anzahl der zu holnden Daten
|
||||
async function main(){
|
||||
async function main() {
|
||||
console.log("Running...")
|
||||
$('.lastchange').text(`Letzte Änderungen: ${VDATE} rxf`)
|
||||
$('#anmeldid').hide()
|
||||
@@ -290,7 +311,7 @@ Besucher: ${tln.name} ${tln.vorname}`
|
||||
$('#versn').html("Version: " + VERSION + ' vom ' + VDATE);
|
||||
let fdatum = moment().format('YYYYMMDD')
|
||||
// alle Anmeldungen ab fdatum in ein Array holen
|
||||
let fuehrungen = await fetchFromDbase({cmd: 'GET_ALLTEILN', fdatum: fdatum})
|
||||
let fuehrungen = await fetchFromDbase({ cmd: 'GET_ALLTEILN', fdatum: fdatum })
|
||||
setEvent(fuehrungen)
|
||||
}
|
||||
|
||||
|
||||
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