Compare commits

...

3 Commits

Author SHA1 Message Date
rxf
b78831266d .gitignore dazu 2025-11-17 14:28:24 +01:00
rxf
3e8b1f9691 DB4js als _all zusammengefasst
Beoanswer ist nun OK
2025-11-17 14:22:52 +01:00
rxf
ad0f7b2912 storno an gemneinsame DB angepasst - **** WOP **** 2025-11-10 20:57:34 +01:00
18 changed files with 1663 additions and 139 deletions

1
.gitignore vendored
View File

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

30
sternwarte/.gitignore vendored Normal file
View 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?

View File

@@ -1,11 +1,39 @@
<?php <?php
# Hier werden die Anfragen vom Javascript verarbeitet und die
# Datenbank bedient # Datenbank bedient
include 'config_stern.php'; include 'config_stern.php';
include 'phpmailer/dosendmail.php'; include 'phpmailer/dosendmail.php';
// ===== Request-Daten verarbeiten =====
// Unterstützt sowohl JSON als auch FormData
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Prüfe Content-Type
$contentType = $_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? '';
if (strpos($contentType, 'application/json') !== false) {
// JSON-Daten
$jsonData = file_get_contents('php://input');
$_POST = json_decode($jsonData, true);
// Debug-Logging
error_log("=== PHP JSON DEBUG ===");
error_log("Content-Type: " . $contentType);
error_log("Raw JSON: " . $jsonData);
error_log("Decoded POST: " . print_r($_POST, true));
error_log("=====================");
}
// Bei FormData ist $_POST bereits automatisch gefüllt
else {
error_log("=== PHP FormData DEBUG ===");
error_log("Content-Type: " . $contentType);
error_log("POST data: " . print_r($_POST, true));
error_log("==========================");
}
}
// Ab hier bleibt alles gleich
$cmd = $_POST["cmd"] ?? '';
// Holen der Einträge in der anmelde-Datenbank für den selektierten Tag // Holen der Einträge in der anmelde-Datenbank für den selektierten Tag
// Parameter // Parameter
@@ -139,6 +167,10 @@ function getAllTeilnehmer($fdatum)
// die Daten der Führungen in dem Bereich start - end // die Daten der Führungen in dem Bereich start - end
// in ein Array als ISO8601 holen // in ein Array als ISO8601 holen
// Parameter // Parameter
@@ -146,10 +178,14 @@ function getAllTeilnehmer($fdatum)
// $end -> bis zu diesem Datum // $end -> bis zu diesem Datum
// Retunrn: // Retunrn:
// Array mit den Daten in ISO8601 // Array mit den Daten in ISO8601
function getFuehrungen($start, $end) { function getFuehrungen($start, $end, $typ) {
global $db; global $db;
$erg = array(); $erg = [];
$sql_sel = "SELECT * FROM fdatum1 where datum >= '$start' AND datum <= '$end' ORDER BY datum ASC"; $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)); $result = mysqli_query($db, $sql_sel) or die(mysqli_error($db));
while ($row = mysqli_fetch_assoc($result)) { while ($row = mysqli_fetch_assoc($result)) {
foreach ($row as $key => $value) { foreach ($row as $key => $value) {
@@ -262,6 +298,17 @@ function getOneDate($id) {
return $data; 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) { function insertteilnehmer($data) {
global $db; global $db;
$name = $data['name']; $name = $data['name'];
@@ -417,14 +464,15 @@ function getOneRecordTermin($termin) {
return $erg; return $erg;
} }
/*
$_POST = json_decode(file_get_contents('php://input'), true); $_POST = json_decode(file_get_contents('php://input'), true);
$erg = ""; $erg = "";
if ($_SERVER['REQUEST_METHOD'] == 'POST') { if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$cmd = $_POST["cmd"]; $cmd = $_POST["cmd"];
/* */
$x = "["; $x = "[";
foreach ($_POST as $key => $value) { foreach ($_POST as $key => $value) {
if(gettype($value) == "array") { if(gettype($value) == "array") {
@@ -433,7 +481,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$x = $x . $key . " => " . $value . ","; $x = $x . $key . " => " . $value . ",";
} }
$x = $x . "]"; $x = $x . "]";
*/
switch ($cmd) { switch ($cmd) {
case 'GET_ANMELD': case 'GET_ANMELD':
$erg = getAnmeldungen($_POST['id']); $erg = getAnmeldungen($_POST['id']);
@@ -473,6 +521,9 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
case 'GET_ONE_DATE': case 'GET_ONE_DATE':
$erg = getOneDate($_POST['fid']); $erg = getOneDate($_POST['fid']);
break; break;
case 'GET_TIME_BY_DATE':
$erg = getTimeByDate($_POST['dt'], $_POST['typ']);
break;
case 'GET_ALLTEILN': case 'GET_ALLTEILN':
$erg = getAllTeilnehmer($_POST['fdatum']); $erg = getAllTeilnehmer($_POST['fdatum']);
break; break;
@@ -503,7 +554,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$erg = getTeilnehmer(-1, true, false); $erg = getTeilnehmer(-1, true, false);
break; break;
case 'GET_FUEHRUNGEN': case 'GET_FUEHRUNGEN':
$erg = getFuehrungen($_POST['start'], $_POST['end']); $erg = getFuehrungen($_POST['start'], $_POST['end'], $_POST['typ']);
break; break;
case 'PUT_FDATES': case 'PUT_FDATES':
$erg = putFdates($_POST['data']); $erg = putFdates($_POST['data']);
@@ -546,7 +597,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
default: default:
$erg = ['error' => 'Unknown POST-Command', 'cmd' => $cmd, 'params' => $x]; $erg = ['error' => 'Unknown POST-Command', 'cmd' => $cmd, 'params' => $x];
} }
} else {
/* /*
$x = "["; $x = "[";
foreach ($_GET as $key => $value) { foreach ($_GET as $key => $value) {
@@ -554,6 +604,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
} }
$x = $x . "]"; $x = $x . "]";
*/ */
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
$cmd = $_GET['cmd']; $cmd = $_GET['cmd'];
switch ($cmd) { switch ($cmd) {
case 'GET_FDATES': case 'GET_FDATES':

725
sternwarte/DB4js_all.php Normal file
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", "name": "beoanswer_react",
"private": true, "private": true,
"version": "1.0.2", "version": "1.0.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

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

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 { useFormData } from '../FormContext'
import Modal from './Modal' import Modal from './Modal'
import ConfirmModal from './ConfirmModal' import ConfirmModal from './ConfirmModal'
import './LastButtons.css';
export default function LastButtons({ mitSend, mitBack, handleBack}) { export default function LastButtons({ mitSend, mitBack, handleBack}) {
@@ -13,7 +14,9 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
const [isModalHtml, setIsModalHtml] = useState(false) const [isModalHtml, setIsModalHtml] = useState(false)
const [showConfirmModal, setShowConfirmModal] = useState(false) const [showConfirmModal, setShowConfirmModal] = useState(false)
const [isSuccessModal, setIsSuccessModal] = useState(false) const [isSuccessModal, setIsSuccessModal] = useState(false)
const [isWideModal, setIsWideModal] = useState(false)
const handleSenden = async () => { const handleSenden = async () => {
console.log("Alle Formulardaten: ", formData) console.log("Alle Formulardaten: ", formData)
@@ -37,77 +40,74 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
throw new Error('Keine ID in der URL gefunden.') throw new Error('Keine ID in der URL gefunden.')
} }
// FormData für PHP Backend erstellen // JSON-Objekt statt FormData erstellen
const backendData = new FormData() const backendData = {
backendData.append('cmd', 'UPDATEAFTER') cmd: 'UPDATEAFTER',
backendData.append('id', id) id: id
}
// Formulardaten zu Backend-Feldern mappen // Formulardaten zu Backend-Feldern mappen
// Basis-Status // Basis-Status
if (formData.stattgefunden === 'ja') { if (formData.stattgefunden === 'ja') {
backendData.append('stattgefunden', '1') backendData.stattgefunden = '1'
// Spenden-Informationen // Spenden-Informationen
if (formData.spendenArt) { if (formData.spendenArt) {
switch (formData.spendenArt) { switch (formData.spendenArt) {
case 'bar': case 'bar':
backendData.append('bezahlt', `Kasse ${formData.betrag})`) backendData.bezahlt = `Kasse ${formData.betrag}`
break break
case 'ueber': case 'ueber':
backendData.append('bezahlt', 'Überweisung') backendData.bezahlt = 'Überweisung'
break break
case 'kasse': case 'kasse':
backendData.append('bezahlt', 'Spendenkässle') backendData.bezahlt = 'Spendenkässle'
break break
case 'keine': case 'keine':
backendData.append('bezahlt', 'keine') backendData.bezahlt = 'keine'
break break
} }
} }
} else if (formData.stattgefunden === 'nein') { } else if (formData.stattgefunden === 'nein') {
backendData.append('stattgefunden', '0') backendData.stattgefunden = '0'
backendData.append('bezahlt', 'keine') backendData.bezahlt = 'keine'
// Grund für Ausfall // Grund für Ausfall
if (formData.abgesagt === 'abgesagt') { if (formData.abgesagt === 'abgesagt') {
backendData.append('status', 3) backendData.status = 3
} else if (formData.abgesagt === 'verschoben') { } else if (formData.abgesagt === 'verschoben') {
backendData.append('wtermin', formData.neuesDatum || '1900-01-01 00:00:00') backendData.wtermin = formData.neuesDatum || '1900-01-01 00:00:00'
} }
} }
// Bemerkungen // Bemerkungen
backendData.append('remark', formData.bemerkungen || '') backendData.remark = formData.bemerkungen || ''
// Besucher // Besucher
backendData.append('besucher', formData.besucher || '0') backendData.besucher = formData.besucher || '0'
// // Bearbeitungsdatum setzen // Debug: JSON-Daten loggen
// const now = new Date().toISOString().slice(0, 19).replace('T', ' ') console.log("=== JSON DATA DEBUG ===")
// backendData.append('bearbeitet_am', now)
// Debug: FormData kann nicht direkt geloggt werden, deshalb iterieren
console.log("=== FORM DATA DEBUG ===")
console.log("Original formData aus Context:", formData) console.log("Original formData aus Context:", formData)
console.log("URL ID:", id) console.log("URL ID:", id)
console.log("Backend FormData Inhalt:") console.log("Backend JSON Daten:", JSON.stringify(backendData, null, 2))
for (let [key, value] of backendData.entries()) {
console.log(` ${key}: ${value}`)
}
console.log("========================") console.log("========================")
// HTTP Basic Authentication Header // HTTP Headers mit Basic Authentication und Content-Type
const headers = {} const headers = {
'Content-Type': 'application/json'
}
if (username && password) { if (username && password) {
const credentials = btoa(`${username}:${password}`) const credentials = btoa(`${username}:${password}`)
headers['Authorization'] = `Basic ${credentials}` headers['Authorization'] = `Basic ${credentials}`
} }
// Backend-Aufruf // Backend-Aufruf mit JSON
const response = await fetch(APIURL, { const response = await fetch(APIURL, {
method: 'POST', method: 'POST',
headers: headers, headers: headers,
body: backendData body: JSON.stringify(backendData)
}) })
if (!response.ok) { if (!response.ok) {
@@ -135,9 +135,13 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
responseText.trim() === 'true' responseText.trim() === 'true'
if (isSuccess) { if (isSuccess) {
// E-Mail-Benachrichtigung senden (nicht blockierend)
sendEmailNotification(id, formData, backendData, APIURL, headers)
setModalType('success') setModalType('success')
setModalMessage('✅ Daten erfolgreich gespeichert!') setModalMessage('✅ Daten erfolgreich gespeichert!')
setIsSuccessModal(true) setIsSuccessModal(true)
setIsWideModal(false)
setShowModal(true) setShowModal(true)
} else { } else {
@@ -149,12 +153,93 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
setModalType('error') setModalType('error')
setModalMessage(`❌ Fehler beim Speichern: ${error.message}`) setModalMessage(`❌ Fehler beim Speichern: ${error.message}`)
setIsSuccessModal(false) setIsSuccessModal(false)
setIsWideModal(false)
setShowModal(true) setShowModal(true)
} finally { } finally {
setIsSending(false) setIsSending(false)
} }
} }
// E-Mail-Benachrichtigung senden (asynchron, nicht blockierend)
const sendEmailNotification = async (id, formData, backendData, apiUrl, headers) => {
try {
// Details zur Sonderführung laden (für BEO und Besucher-Name)
let beoName = 'unbekannt'
let visitorName = 'unbekannt'
let termin = ''
try {
const detailsResp = await fetch(apiUrl, {
method: 'POST',
headers,
body: JSON.stringify({ cmd: 'GET_ONE', id })
})
if (detailsResp.ok) {
const details = await detailsResp.json()
// Backend kann Objekt oder Array liefern; robust extrahieren
const d = Array.isArray(details) ? (details[0] || {}) : (details || {})
// Felder: mitarbeiter (BEO), name/vorname (Besucher), wtermin (Termin)
beoName = d.mitarbeiter || beoName
const vn = d.vorname || ''
const nn = d.name || ''
visitorName = (vn + ' ' + nn).trim() || visitorName
termin = d.wtermin || ''
}
} catch (e) {
console.warn('Konnte Details für E-Mail nicht laden:', e)
}
// E-Mail-Betreff und Inhalt erstellen
const stattgefunden = formData.stattgefunden === 'ja' ? 'Ja' : 'Nein'
const besucher = formData.besucher || '0'
const spenden = backendData.bezahlt || 'keine'
const bemerkungen = formData.bemerkungen || 'keine'
const subject = `Sonderführung vom ${termin} - Nachbearbeitung`
const body = `Nachbearbeitung für Sonderführung ID ${id}
BEO: ${beoName}
Besucher: ${visitorName}
Termin: ${termin}
Stattgefunden: ${stattgefunden}
Anzahl Besucher: ${besucher}
Spenden: ${spenden}
Bemerkungen: ${bemerkungen}
Status: ${formData.stattgefunden === 'ja' ? 'Durchgeführt' :
formData.abgesagt === 'abgesagt' ? 'Abgesagt' :
formData.abgesagt === 'verschoben' ? `Verschoben auf ${formData.neuesDatum}` :
'Unbekannt'}
Diese E-Mail wurde automatisch vom Nachbearbeitungs-System generiert.
`
// E-Mail-Command an Backend senden
const emailData = {
cmd: 'SEND_CONFIRMATION',
to: 'rxf@gmx.de',
subject: subject,
body: body
}
const emailResponse = await fetch(apiUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify(emailData)
})
if (emailResponse.ok) {
console.log('✅ E-Mail-Benachrichtigung erfolgreich gesendet')
} else {
console.warn('⚠️ E-Mail-Benachrichtigung konnte nicht gesendet werden:', emailResponse.status)
}
} catch (error) {
// Fehler beim E-Mail-Versand nicht kritisch - nur loggen
console.warn('⚠️ E-Mail-Benachrichtigung fehlgeschlagen:', error)
}
}
const handleAbbruch = () => { const handleAbbruch = () => {
setShowConfirmModal(true) setShowConfirmModal(true)
} }
@@ -178,41 +263,41 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
setShowConfirmModal(false) setShowConfirmModal(false)
} }
const handleAnleitung = () => { const handleAnleitung = async () => {
// Öffne die HTML-Anleitung in einem neuen Fenster/Tab
const anleitungUrl = '/anleitung.html'
const windowFeatures = 'width=800,height=600,scrollbars=yes,resizable=yes,toolbar=no,menubar=no,location=no'
try { try {
// Versuche ein Popup-Fenster zu öffnen // Anleitung soll im großen Modal erscheinen
const anleitungWindow = window.open(anleitungUrl, 'anleitung', windowFeatures) setIsWideModal(true)
// Respect Vite base path in production (vite.config.js base: '/beoanswer/')
// Fallback: Wenn Popup blockiert wird, öffne in neuem Tab const base = (import.meta.env && import.meta.env.BASE_URL) ? import.meta.env.BASE_URL : '/'
if (!anleitungWindow) { const normalizedBase = base.endsWith('/') ? base : base + '/'
window.open(anleitungUrl, '_blank') const url = `${normalizedBase}Anleitung.html?v=${Date.now()}` // cache-bust
// Fetch Anleitung.html relative to app base
let response = await fetch(url)
// Fallback: try relative path without base if first attempt failed
if (!response.ok) {
const fallbackUrl = `Anleitung.html?v=${Date.now()}`
response = await fetch(fallbackUrl)
} }
} catch (error) {
// Letzter Fallback: Als Modal anzeigen if (!response.ok) {
console.warn('Anleitung konnte nicht in neuem Fenster geöffnet werden:', error) throw new Error(`Fehler beim Laden der Anleitung (${response.status}): ${response.url}`)
}
const anleitungContent = await response.text()
// Display the content in the modal
setModalType('info') setModalType('info')
setModalMessage(` setModalMessage(anleitungContent)
📋 Anleitung: setIsModalHtml(true)
setShowModal(true)
1. **Fand statt?** - Wählen Sie "ja" oder "nein" } catch (error) {
console.error('Fehler beim Laden der Anleitung:', error)
2. **Bei "ja":** setModalType('error')
- Anzahl Besucher eingeben setModalMessage('❌ Anleitung konnte nicht geladen werden.')
- Spenden-Art auswählen
- Bei Barspende: Betrag eingeben
- Optional: Bemerkungen
3. **Bei "nein":**
- "abgesagt" oder "verschoben" wählen
- Bei verschoben: neues Datum eingeben
4. **Senden** - Speichert alle Daten im System
`)
setIsModalHtml(false) setIsModalHtml(false)
setIsWideModal(false)
setShowModal(true) setShowModal(true)
} }
} }
@@ -244,6 +329,7 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
setModalMessage('') setModalMessage('')
setIsModalHtml(false) setIsModalHtml(false)
setIsSuccessModal(false) setIsSuccessModal(false)
setIsWideModal(false)
} }
} }
@@ -282,6 +368,7 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
onClose={closeModal} onClose={closeModal}
type={modalType} type={modalType}
isHtml={isModalHtml} isHtml={isModalHtml}
className={isWideModal ? 'custom-modal' : ''}
/> />
)} )}

View File

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

View File

@@ -15,20 +15,28 @@ Dieses Programm kann auch die Überwachung machen, dass Einträge in der DB gel
Versions: Versions:
V 1.0.1 2025-11-17 rxf
- Übergabe der Tage bis zu 'gestern' als CXommandline Parameter: '-d x'. Ohne -d wird 1 angesetzt.
V 1.0.0 2025-11-15 rxf
- Mit Tricks kann das nun DOCH realisiert werden:
Auf externem Rechner (z.Zt. 'strato_1'; Miet-Server von rxf) wird der cron angestoßen,
der das Programm hier (checkfuehrung.js) auf dem Sternwartemserver aufruft.
Das zugehörige 'beoanswer' ist eine Webseute, die auch hier auf dem Sternwarte-Server
gehostet ist.
V 0.0 2019-02-04 rxf V 0.0 2019-02-04 rxf
- Start - Start
*/ */
"use strict" "use strict"
const DEVELOP=0; // 1 -> Entwicklung 0-> Produktion const DEVELOP=0; // 1 -> Entwicklung 0-> Produktion
const DAYS=2;
const nodemailer = require('nodemailer');
const moment = require('moment'); const moment = require('moment');
const axios = require('axios'); const axios = require('axios');
const mysql = require('mysql2/promise'); const mysql = require('mysql2/promise');
const nodemailer = require('nodemailer');
const beo_Url = 'beoanswer/beoanswer.php?id='; const beo_Url = 'beoanswer/?id=';
const Url = DEVELOP ? 'http://localhost:8081/' : 'https://sternwarte-welzheim.de/'; const Url = DEVELOP ? 'http://localhost:8081/' : 'https://sternwarte-welzheim.de/';
const DB_host = DEVELOP ? 'localhost' : 'localhost'; const DB_host = DEVELOP ? 'localhost' : 'localhost';
const DB_port = DEVELOP ? 3306 : 3306; const DB_port = DEVELOP ? 3306 : 3306;
@@ -85,12 +93,12 @@ function send2BEO(info) {
// to: info.email, // to: info.email,
to: 'rexfue@gmail.com', to: 'rexfue@gmail.com',
subject: 'Sonderführung vom '+info.date, subject: 'Sonderführung vom '+info.date,
text: 'Hallo ' + info.name + '(' + info.email + '),\n\n' text: 'Hallo ' + info.name + ',\n\n'
+ 'Du hattest gestern Führung! ' + 'du hattest gestern Führung! '
+ 'Bitte fülle folgendes Webformular aus:\n\n' + 'Bitte fülle folgendes Webformular aus:\n\n'
+ Url + beo_Url + info.id + Url + beo_Url + info.id
+ '\n\nBitte nur über diesen Link zugreifen (oder exakt abschreiben),\n' + '\n\nBitte nur über diesen Link zugreifen (oder exakt abschreiben),\n'
+ 'da sonst die Zuordnung nicht hergestellt werden kann.\n' + 'da sonst die Zuordnung nicht hergestellt werden kann.\n\n'
+ 'Besten Dank.\n\nGrüße vom Sonderführungsteam' + 'Besten Dank.\n\nGrüße vom Sonderführungsteam'
}; };
@@ -121,4 +129,8 @@ async function main() {
console.log("All done"); console.log("All done");
} }
const argv = require('minimist')(process.argv.slice(2));
const DAYS = argv.d || 1;
main().catch(console.error); main().catch(console.error);

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 <?php
$typ=$_GET['typ'] ?? 'regular'; $typ=$_GET['typ'] ?? 'regular';
$tit = "";
if ($typ == 'regular') { if ($typ == 'regular') {
$titel = 'Anmeldungen zur regulären Führung'; $titel = 'Anmeldungen zur regulären Führung';
} elseif ($typ == 'sonnen') { } elseif ($typ == 'sonnen') {
$titel = 'Anmeldungen zur Sonnenführung'; $titel = 'Anmeldungen zur Sonnenführung';
$tit = "-Sonne";
} else { } else {
http_response_code(400); http_response_code(400);
die('Ungültiger Typ.'); die('Ungültiger Typ.');
@@ -16,7 +17,7 @@ if ($typ == 'regular') {
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anmeldungen</title> <title>Anmeldungen<?echo $tit?></title>
<link rel="stylesheet" href="css/anmeld.css"> <!-- Falls du ein Stylesheet hast --> <link rel="stylesheet" href="css/anmeld.css"> <!-- Falls du ein Stylesheet hast -->
</head> </head>
<body> <body>

View File

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

View File

@@ -2,7 +2,7 @@
.storno { .storno {
font-size: 14px; font-size: 14px;
min-height: 500px; min-height: 550px;
position: relative; position: relative;
} }
@@ -131,3 +131,8 @@ h5 {
margin-left: 10px; margin-left: 10px;
font-size: 80%; font-size: 80%;
} }
#abbrechen {
background-color: gray;
margin-top: 40px;
}

View File

@@ -51,7 +51,7 @@
Für diese E-Mail Adresse ist keine Führung angemeldet !<br /> Für diese E-Mail Adresse ist keine Führung angemeldet !<br />
</div> </div>
<div id="anmeldid" class="col-12 col-xm-8 text-center"> <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"> <div id="anmeldung">
2022-07-12 22:00 Uhr 4 Personen 2022-07-12 22:00 Uhr 4 Personen
</div> </div>

View File

@@ -5,8 +5,8 @@
$(document).ready(() => { $(document).ready(() => {
// Globale Konstanten und Variable // Globale Konstanten und Variable
// const ajaxURL="php/anmeldDB.php"; // const ajaxURL="php/anmeldDB.php";
const ajaxURL="../../DB4js.php"; const ajaxURL = "../../DB4js.php";
const maxVisitors = 25 const maxVisitors = 25
const months2add = 3 const months2add = 3
@@ -21,9 +21,9 @@ $(document).ready(() => {
// Return: // Return:
// angeforderte Daten als JSON // angeforderte Daten als JSON
const fetchFromDbase = async (body) => { const fetchFromDbase = async (body) => {
const response = await fetch(ajaxURL,{ const response = await fetch(ajaxURL, {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/js'}, headers: { 'Content-Type': 'application/js' },
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
let rt = await response.json(); let rt = await response.json();
@@ -37,22 +37,41 @@ $(document).ready(() => {
// Return: // Return:
// angeforderte Daten als JSON // angeforderte Daten als JSON
const putToDbase = async (body) => { const putToDbase = async (body) => {
const response = await fetch(ajaxURL,{ const response = await fetch(ajaxURL, {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/js'}, headers: { 'Content-Type': 'application/js' },
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
return await response.json(); 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 // Das Führungsdatum extrahieren
const buildDatum = async (tn, short) => { const buildDatum = async (tn, short) => {
const person = tn.anzahl === '1' ? 'Person' : 'Personen' const person = tn.anzahl === '1' ? 'Person' : 'Personen'
const datum = await fetchFromDbase({cmd: 'GET_ONE_DATE', fid: tn.fid}) const uhrzeit = await getFuhrzeit(tn.fdatum, tn.typ)
if(short) { if (short) {
return `${moment(datum.datum).format('DD.MM.YYYY')}` 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 // Das Führungsdatum anzeigen
@@ -67,13 +86,13 @@ $(document).ready(() => {
} }
const austragen = async (teilnehmer) => { const austragen = async (teilnehmer) => {
// console.log("Austragen von ", teilnehmer) // console.log("Austragen von ", teilnehmer)
// return // return
oldtndata = {...teilnehmer} oldtndata = { ...teilnehmer }
let delstr = {cmd: 'DELETE_ENTRY', id: parseInt(teilnehmer.id)} let delstr = { cmd: 'DELETE_ENTRY', id: parseInt(teilnehmer.id) }
const erg1 = await putToDbase(delstr) const erg1 = await putToDbase(delstr)
console.log("Storno Ergebnis: ",erg1) console.log("Storno Ergebnis: ", erg1)
if(erg1) { if (erg1) {
$('#ausgetragen').show() $('#ausgetragen').show()
$('#lower_text').show() $('#lower_text').show()
sendEmail(teilnehmer, true) sendEmail(teilnehmer, true)
@@ -84,44 +103,48 @@ $(document).ready(() => {
} }
const umbuchen = async (teilnehmer) => { const umbuchen = async (teilnehmer) => {
// console.log('Umbuchen von ',teilnehmer) // console.log('Umbuchen von ',teilnehmer)
// return // return
oldtndata = {...teilnehmer} oldtndata = { ...teilnehmer }
let start = moment() let start = moment()
start = start.add(vorlauf, 'h') start = start.add(vorlauf, 'h')
let end = moment() 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) 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>` 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) { for (let f of fuehrungen) {
if(f.datum == teilnehmer.fdatum) { if (f.datum == teilnehmer.fdatum) {
continue 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 count = count ? parseInt(count) : 0
if(count + anzahl >= maxVisitors) { if (count + anzahl >= maxVisitors) {
continue // wenn der Platz nicht reicht, nicht anzeigen continue // wenn der Platz nicht reicht, nicht anzeigen
} }
r += `<option id=${f.id} value=${f.datum}>` r += `<option id=${f.id} value=${f.datum}>`
r += `${f.wtag.substring(0,2)} , ${moment(f.datum).format('DD.MM.YYYY')} ${f.uhrzeit} ` 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 += `Frei ${(maxVisitors - count) > 0 ? maxVisitors - count : 0}</option>`
} }
r += '</select>' r += '</select>'
$('#umbuchung').html(r) $('#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() $('#umgebucht').show()
// Eventhandler für Auswahl eines Datums über die SelectBox: // Eventhandler für Auswahl eines Datums über die SelectBox:
// Anzeigen der Anmeldungen dazu // Anzeigen der Anmeldungen dazu
$('#ftermin').change( async () => { $('#ftermin').change(async () => {
const x = $('#ftermin').find(':selected'); const x = $('#ftermin').find(':selected');
const date = x[0].value const date = x[0].value
teilnehmer.fdatum = date teilnehmer.fdatum = date
teilnehmer.fid = x[0].id 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) const erg = await putToDbase(update)
if(erg) { if (erg) {
$('#umbuchung').hide() $('#umbuchung').hide()
$('#umgebucht').html(`Sie wurden erfolgreich umgebucht auf <br /><br />${await buildDatum(teilnehmer, false)}`) $('#umgebucht').html(`Sie wurden erfolgreich umgebucht auf <br /><br />${await buildDatum(teilnehmer, false)}`)
$('#umgebucht').show() $('#umgebucht').show()
@@ -137,11 +160,11 @@ $(document).ready(() => {
} }
const aendern = async (teilnehmer) => { const aendern = async (teilnehmer) => {
console.log('Ändern von ',teilnehmer) console.log('Ändern von ', teilnehmer)
let count = await fetchFromDbase({cmd: 'GET_COUNTS_DATE', date: teilnehmer.fdatum}) let count = await fetchFromDbase({ cmd: 'GET_COUNTS_DATE', date: teilnehmer.fdatum })
let anzahl = parseInt(teilnehmer.anzahl) let anzahl = parseInt(teilnehmer.anzahl)
let max = anzahl let max = anzahl
if(count < maxVisitors) { if (count < maxVisitors) {
max = maxVisitors - (count - anzahl) max = maxVisitors - (count - anzahl)
} }
if (max > 10) { if (max > 10) {
@@ -149,24 +172,24 @@ $(document).ready(() => {
} }
console.log("Max: ", max) console.log("Max: ", max)
$('#maxanzahl').text(`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 // Evelthandler Übernehmen geklicked
$('#bsave').click(async () => { $('#bsave').click(async () => {
$('#save').hide() $('#save').hide()
teilnehmer.anzahl = $('#personen').val() 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.`) $('#newperson').html(`Sie sind nun mit <strong>${teilnehmer.anzahl} </strong> Personen angemeldet.`)
$('#bsave').hide() $('#bsave').hide()
$('#newperson').show() $('#newperson').show()
$('#homebutton').show() $('#homebutton').show()
}) })
} }
// Emailadresse eingegeben // Emailadresse eingegeben
const setEvent = (f) => { const setEvent = (f) => {
$(document).on("keypress", "input", async function (e) { $(document).on("keypress", "input", async function (e) {
if (e.which === 13) { if (e.which === 13) {
@@ -196,15 +219,15 @@ $(document).ready(() => {
$('#umbuchen').click(() => { $('#umbuchen').click(() => {
umbuchen(f[idx]) umbuchen(f[idx])
$('#anmeldid').hide() $('#anmeldid').hide()
// $('#email').val("") // $('#email').val("")
}) })
// Ändern geklicked // Ändern geklicked
$('#aendern').click(() => { $('#aendern').click(() => {
aendern(f[idx]) aendern(f[idx])
$('#butgroup').hide() $('#butgroup').hide()
$('#aenderung').show() $('#aenderung').show()
// $('#anmeldid').hide() // $('#anmeldid').hide()
// $('#email').val("") // $('#email').val("")
}) })
} }
}) })
@@ -214,20 +237,18 @@ $(document).ready(() => {
sendEmail = async (tln, storno) => { sendEmail = async (tln, storno) => {
let fdatum = await buildDatum(tln, false) let fdatum = await buildDatum(tln, false)
let oldfdatum = await buildDatum(oldtndata, true) 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 = ` let body_txt = `
Sehr geehrte Dame, sehr geehrter Herr, Sehr geehrte Dame, sehr geehrter Herr,
hiermit bestätigen wir die ${storno ? 'Stornierung' : 'Umbuchung'} Ihrer Führung auf der Sternwarte Welzheim vom` hiermit bestätigen wir die ${storno ? 'Stornierung' : 'Umbuchung'} Ihrer Führung auf der Sternwarte Welzheim vom`
if(!storno) { if (!storno) {
body_txt += ` ${oldfdatum}. body_txt += ` ${oldfdatum}.
Sie wurden umgebucht auf: Sie wurden umgebucht auf:
${fdatum} ${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 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 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. 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 Beobachterteam der Sternwarte Welzheim
www.sternwarte-welzheim.de 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) console.log("Antwort von sendmail_1: ", erg)
body_txt = `Die Führung vom ${oldfdatum} wurde ${storno ? 'storniert' : 'umgebucht'} body_txt = `Die Führung vom ${oldfdatum} wurde ${storno ? 'storniert' : 'umgebucht'}
` `
if(!storno) { if (!storno) {
body_txt += ` body_txt += `
auf ${fdatum}` auf ${fdatum}`
} }
@@ -258,12 +279,12 @@ auf ${fdatum}`
Besucher: ${tln.name} ${tln.vorname}` Besucher: ${tln.name} ${tln.vorname}`
body_html = `Die Führung vom ${oldfdatum} wurde ${storno ? 'storniert' : 'umgebucht'}` body_html = `Die Führung vom ${oldfdatum} wurde ${storno ? 'storniert' : 'umgebucht'}`
if(!storno) { if (!storno) {
body_html += ` auf ${fdatum}<br />` 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) console.log("Antwort von sendmail_2: ", erg)
} }
@@ -277,7 +298,7 @@ Besucher: ${tln.name} ${tln.vorname}`
// und anzeigen // und anzeigen
// Params: // Params:
// n -> Anzahl der zu holnden Daten // n -> Anzahl der zu holnden Daten
async function main(){ async function main() {
console.log("Running...") console.log("Running...")
$('.lastchange').text(`Letzte Änderungen: ${VDATE} rxf`) $('.lastchange').text(`Letzte Änderungen: ${VDATE} rxf`)
$('#anmeldid').hide() $('#anmeldid').hide()
@@ -290,7 +311,7 @@ Besucher: ${tln.name} ${tln.vorname}`
$('#versn').html("Version: " + VERSION + ' vom ' + VDATE); $('#versn').html("Version: " + VERSION + ' vom ' + VDATE);
let fdatum = moment().format('YYYYMMDD') let fdatum = moment().format('YYYYMMDD')
// alle Anmeldungen ab fdatum in ein Array holen // 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) setEvent(fuehrungen)
} }

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