diff --git a/.gitignore b/.gitignore index b6ecba1..6f03d41 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ download *.log webseiten +sternwarte/beoanswer/.env.production diff --git a/sternwarte/DB4js.php b/sternwarte/DB4js.php index 8368e1f..1baa466 100644 --- a/sternwarte/DB4js.php +++ b/sternwarte/DB4js.php @@ -5,6 +5,35 @@ include 'config_stern.php'; include 'phpmailer/dosendmail.php'; +// ===== Request-Daten verarbeiten ===== +// Unterstützt sowohl JSON als auch FormData +if ($_SERVER['REQUEST_METHOD'] == 'POST') { + // Prüfe Content-Type + $contentType = $_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? ''; + + if (strpos($contentType, 'application/json') !== false) { + // JSON-Daten + $jsonData = file_get_contents('php://input'); + $_POST = json_decode($jsonData, true); + + // Debug-Logging + error_log("=== PHP JSON DEBUG ==="); + error_log("Content-Type: " . $contentType); + error_log("Raw JSON: " . $jsonData); + error_log("Decoded POST: " . print_r($_POST, true)); + error_log("====================="); + } + // Bei FormData ist $_POST bereits automatisch gefüllt + else { + error_log("=== PHP FormData DEBUG ==="); + error_log("Content-Type: " . $contentType); + error_log("POST data: " . print_r($_POST, true)); + error_log("=========================="); + } +} + +// Ab hier bleibt alles gleich +$cmd = $_POST["cmd"] ?? ''; // Holen der Einträge in der anmelde-Datenbank für den selektierten Tag // Parameter @@ -435,14 +464,15 @@ function getOneRecordTermin($termin) { return $erg; } - +/* $_POST = json_decode(file_get_contents('php://input'), true); $erg = ""; if ($_SERVER['REQUEST_METHOD'] == 'POST') { $cmd = $_POST["cmd"]; -/* +*/ + $x = "["; foreach ($_POST as $key => $value) { if(gettype($value) == "array") { @@ -451,7 +481,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { $x = $x . $key . " => " . $value . ","; } $x = $x . "]"; -*/ + switch ($cmd) { case 'GET_ANMELD': $erg = getAnmeldungen($_POST['id']); @@ -567,7 +597,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { default: $erg = ['error' => 'Unknown POST-Command', 'cmd' => $cmd, 'params' => $x]; } -} else { /* $x = "["; foreach ($_GET as $key => $value) { @@ -575,6 +604,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { } $x = $x . "]"; */ +if ($_SERVER['REQUEST_METHOD'] == 'GET') { $cmd = $_GET['cmd']; switch ($cmd) { case 'GET_FDATES': diff --git a/sternwarte/DB4js_all.php b/sternwarte/DB4js_all.php new file mode 100644 index 0000000..2d6398a --- /dev/null +++ b/sternwarte/DB4js_all.php @@ -0,0 +1,725 @@ + 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); +} diff --git a/sternwarte/beoanswer/package.json b/sternwarte/beoanswer/package.json index a81c402..454c25f 100644 --- a/sternwarte/beoanswer/package.json +++ b/sternwarte/beoanswer/package.json @@ -1,7 +1,7 @@ { "name": "beoanswer_react", "private": true, - "version": "1.0.2", + "version": "1.0.3", "type": "module", "scripts": { "dev": "vite", diff --git a/sternwarte/beoanswer/src/App.jsx b/sternwarte/beoanswer/src/App.jsx index ff57000..2324002 100644 --- a/sternwarte/beoanswer/src/App.jsx +++ b/sternwarte/beoanswer/src/App.jsx @@ -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: diff --git a/sternwarte/beoanswer/src/components/LastButtons.css b/sternwarte/beoanswer/src/components/LastButtons.css new file mode 100644 index 0000000..fdae593 --- /dev/null +++ b/sternwarte/beoanswer/src/components/LastButtons.css @@ -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 */ +} \ No newline at end of file diff --git a/sternwarte/beoanswer/src/components/LastButtons.jsx b/sternwarte/beoanswer/src/components/LastButtons.jsx index 9d4d564..6ab9a53 100644 --- a/sternwarte/beoanswer/src/components/LastButtons.jsx +++ b/sternwarte/beoanswer/src/components/LastButtons.jsx @@ -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' : ''} /> )} diff --git a/sternwarte/beoanswer/src/components/Modal.jsx b/sternwarte/beoanswer/src/components/Modal.jsx index d5c9ec9..9417b14 100644 --- a/sternwarte/beoanswer/src/components/Modal.jsx +++ b/sternwarte/beoanswer/src/components/Modal.jsx @@ -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 (