Compare commits

..

2 Commits

Author SHA1 Message Date
rxf
e3bb5d36b9 nochmal 2025-11-10 16:10:33 +01:00
rxf
9071f96f0d beoanswe-react und sonst ein paar dinge 2025-11-10 16:10:09 +01:00
54 changed files with 5746 additions and 95 deletions

26
docs/Server-Ideen.md Normal file
View File

@@ -0,0 +1,26 @@
# Neuer Server f. Sternwarte
## Mailserver
* Start0
* IONOS -> billigster ist 1.50/m incl Domain
## Domain
Am einfachsten die **sternwarte-welzheim.de** zu dem Mailserver umziehen.
Die ist da **kostenlos**! Und damit sehen die Mails dann wieder gut aus (z.B. info@sternwarte-welzheim.de).
## Server
* keiner Linux-Server bei z.B.:
* Starto
* IONOS
## Mailingliste
Auf dem Linux-Server mit Mailman selber aufsetzen
### Version
Version | Datum | Beschreibung
--------|-------|-------------
1.0.0 | 2025-11-02 | Beginn der Ideen

BIN
docs/Server-Ideen.pdf Normal file

Binary file not shown.

View File

@@ -4,14 +4,13 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/var/www/html": "${workspaceRoot}"
"/var/www/html": "${workspaceFolder}"
}
},
{
@@ -81,11 +80,6 @@
},
"profile": true,
"openProfile": true
},
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch"
}
]
}

View File

@@ -3,7 +3,7 @@
Um Zugriff auf das Netzwerk der Sternwarte zu erlangen, wurde dort auf der Fritzbox die Software **Wireguard** aktiviert. Diese erlaubt es, über einen sicheren Tunnel von dem Heim-PC (oder auch vom Tablet oder Smartphone) auf das Netzwerk zuzugreifen. Dazu muss auf dem lokalen Gerät (also PC etc). ebenfalls die Software **Wireguard** installiert und aktiviert werden.
###Installation von Wireguard
### Installation von Wireguard
Unabh. vom Betriebssystem muss zuerst die Konfigurationsdatei (*Sternwarte.conf*) auf das Gerät geladen werden. Diese befindet sich auf dem Sternwartenserver im internen Bereich unter *Anleitungen*.
@@ -56,12 +56,12 @@ Dazu auf der Webseite <https://www.wireguard.com/install/> das Programm für das
* den Schalter bei *Sternwarte* einschalten
* Verbindung wird aufgebaut
###Zugriff auf Sternwarten-Netz
Nun kann über die IP-Adresse oder den Namen im Sternwarten-Netz auf die diversen Geräte zugegriffen werden. z.B. kann zum Testen
über **192.168.1.95** der Zugriff auf die Testseite des Wetterserver-Rechners erfolgen.
### Zugriff auf Sternwarten-Netz
Nun kann über die IP-Adresse oder den Namen im Sternwarten-Netz auf die diversen Geräte zugegriffen werden. z.B. kann zum Testen über **192.168.1.26** (oder **http://wetterserver**) der Zugriff auf die Testseite des Wetterserver-Rechners erfolgen. Hier darf **kein** http**s** verwendet werden.
###Zugriff auf das NAS-Laufwerk
Die Web-Oberfläche des NAS kann über die Adresse **192.168.1.250** direkt im Browser aufgerufen werden. Der Username und das Passwort sind: 'Sternwarte', 'Welzheim92'. Natürlich geht die Verbindun nur dann, wenn Wireguard eingeschaltet ist !!
### Zugriff auf das NAS-Laufwerk
Die Web-Oberfläche des NAS kann über die Adresse **https://192.168.1.250** (oder über **https://Goldgrube**) direkt im Browser aufgerufen werden. **ACHTUNG**: Bitte unbedingt **https://** verwenden. Zwar mault dann der Browser, aber es geht leider nicht anders. Je nach Browser sieht die Warnmeldung unterschiedlich aus, aber prinzipiell muss man auf *Erweitert* oder so gehen, dann sagen dass man das Risiko akzeptiert und trotzdem die Webseite besuchen will. Da wir ja über VPN verbunden sind, ist das keinerlei Risiko. In der Regel merkt sich der Browser das und bein nächsten mal gibt es keine Warnung mehr.
Der Username und das Passwort sind: 'Sternwarte', 'Welzheim92'. Natürlich geht die Verbindung nur dann, wenn Wireguard eingeschaltet ist !!
Außerdem kann das Laufwerk direkt eingebunden werden:
@@ -86,14 +86,15 @@ Ab sofort braucht nur Wireguard eingeschaltet sein (oder werden) und der Zugriff
Hier läuft dann der Zugriff direkt über *Goldgrube*.
###VPN (Wireguard) verlassen
### VPN (Wireguard) verlassen
**\*\*\* NICHT vergessen das VPN wieder ausschalten \*\*\***
Denn sonst läuft **jeder** Internetverkehr über die Fritzbox der Sternwarte !
Je nach Betriebssystem die APP **Wireguard** wieder aufrufen und den entsprechenden Schalter wieder ausschalten (bzw. auf *Deaktivieren* klicken.
####Versionen
#### Versionen
Datum | Version | Author | Bemerkung
------|---------|--------|--------
2024-05-15 | 0.1 | rxf | erster Entwurf
2024-06-17 | 1.0 | rxf | vollständige Verison
2025-11-07 | 1.1.0 | rxf | https Notwendigkeit
2024-06-17 | 1.0.0 | rxf | vollständige Verison
2024-05-15 | 0.1.0 | rxf | erster Entwurf

View File

@@ -118,7 +118,7 @@ function getTeilnehmer($seed,$isid,$withdate)
// Daten aller Teilnehmer ab eines Führungsdatumns abholen
// Parameter:
// $fid: Führungsdatum, ab dem die Info geholt wirdTeilnehmer - ID
// $fid: Führungsdatum, ab dem die Info geholt wird
// Return:
// Dict mit allen Daten des Teilnehmers
function getAllTeilnehmer($fdatum)
@@ -189,7 +189,7 @@ function getNextFuehrungen($soviel, $fid) {
function updateTeilnehmer_fdate($id, $fdatum, $fid) {
global $db;
$sql_stmt = "UPDATE anmeldungen SET fdatum=$fdatum,fid=$fid where id=$id";
$sql_stmt = "UPDATE anmeldungen SET fdatum=$fdatum,fid=$fid, abgesagt=NULL where id=$id";
$result = mysqli_query($db, $sql_stmt) or die(mysqli_error($db));
return $result;
}

View File

@@ -0,0 +1,14 @@
# Kopiere diese Datei zu .env und passe die Werte an
# Backend API Configuration
#VITE_API_URL=/api/intern/sofue/php/sofueDB.php
# Für Production könntest du auch direkte URLs verwenden:
VITE_API_URL=https://sternwarte-welzheim.de/intern/sofue/php/sofueDB.php
# HTTP Basic Authentication für geschütztes Backend
VITE_API_USERNAME=dein_username
VITE_API_PASSWORD=dein_passwort
# Debug-Modus (optional)
# VITE_DEBUG=true

View File

@@ -0,0 +1,9 @@
# Production Environment Variables
VITE_API_URL=/intern/sofue/php/sofueDB.php
# HTTP Basic Authentication
VITE_API_USERNAME=beogruppe
VITE_API_PASSWORD=ArktUhr
# Optional: Debug-Modus für Production meist ausgeschaltet
# VITE_DEBUG=false

30
sternwarte/beoanswer/.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
.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

@@ -0,0 +1,13 @@
# ACHTUNG
2025-10-29
Es gibt extreme Probleme mit CORS beim Zugriff auf die Sternwarte. Lokal auf localhost funtioniert die Anwendung jetzt richtig gut.
M.E. macht es keinen Sinn, so weiter zu machen. Sinnvoll ist es, einen neuen Server für die Sternwarte aufzusetzen, auf dem man dann problemlos das Alles laufen lassen kann (da ja dann Alles 'localhost' ist.)
Vorschlag: IONOS oder STRATO oder HETZNER oder ....
Vergleich in einer Numbers-Tabelle (in der iCloud unter Numbers/Vergleich_Server)
rxf

View File

@@ -0,0 +1,123 @@
# Production Deployment Guide
## Schritt 1: Environment Variable setzen
1. Erstelle `.env.production` Datei:
```env
VITE_API_URL=https://dein-produktions-server.com/intern/sofue/php/sofueDB.php
```
2. Oder setze die Environment Variable direkt beim Build:
```bash
VITE_API_URL=https://dein-server.com/api npm run build:prod
```
## Schritt 2: Production Build erstellen
```bash
# Mit .env.production Datei
npm run build:prod
# Oder mit direkter Environment Variable
VITE_API_URL=https://dein-server.com/api npm run build
```
## Schritt 3: Build-Output deployen
Die generierten Dateien im `dist/` Ordner auf deinen Webserver kopieren:
```bash
# Lokaler Build
npm run build:prod
# Upload auf Server (Beispiel mit rsync)
rsync -avz dist/ user@dein-server.com:/var/www/html/beoanswer/
# Oder mit scp
scp -r dist/* user@dein-server.com:/var/www/html/beoanswer/
```
## Schritt 4: Webserver Konfiguration
### Apache (.htaccess)
```apache
RewriteEngine On
RewriteBase /beoanswer/
# Handle Angular and other front-end routes
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /beoanswer/index.html [L]
# CORS Headers (falls nötig)
Header set Access-Control-Allow-Origin "*"
Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type"
```
### Nginx
```nginx
location /beoanswer/ {
try_files $uri $uri/ /beoanswer/index.html;
# CORS Headers (falls nötig)
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'Content-Type';
}
```
## Schritt 5: PHP Backend CORS
Falls nötig, füge in `sofueDB.php` hinzu:
```php
<?php
// CORS Headers für Production und Development
if (isset($_SERVER['HTTP_ORIGIN'])) {
$allowed_origins = [
'https://deine-frontend-domain.com', // Production
'http://localhost:5173', // Vite Development
'http://localhost:3000', // Alternative Port
'http://127.0.0.1:5173' // Localhost als IP
];
if (in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']);
}
} else {
// Fallback für direkte Server-zu-Server Anfragen
header("Access-Control-Allow-Origin: *");
}
header("Access-Control-Allow-Methods: POST, GET, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
header("Access-Control-Allow-Credentials: true");
// Handle preflight requests
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
http_response_code(200);
exit();
}
// Rest des PHP Codes...
?>
```
## Schritt 6: Testen
1. Lokaler Test des Production Builds:
```bash
npm run preview:prod
```
2. Live-Test mit echter URL:
```
https://dein-server.com/beoanswer/?id=123
```
## Troubleshooting
- **CORS Fehler**: Backend CORS Headers prüfen
- **404 Fehler**: Webserver Routing konfigurieren
- **API Fehler**: Environment Variable und Backend-URL prüfen
- **Asset Loading**: Base URL in Vite Config setzen falls nötig

View File

@@ -0,0 +1,12 @@
FROM node:25-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"]

View File

@@ -0,0 +1,273 @@
# BeoAnswer React App
Eine React-Anwendung zur Nachbearbeitung von Sonderführungen mit Backend-Integration.
## 📋 Features
- **Interaktive Formulare** für Führungsnachbearbeitung
- **Backend-Integration** mit PHP über FormData
- **HTTP Basic Authentication** Support
- **Professionelle Modal-Dialoge** anstatt Browser-Alerts
- **Intelligente Navigation** mit Zurück-Button
- **Automatisches Fenster schließen** nach Aktionen
- **Environment-Variable Konfiguration**
- **Responsive Design**
## 🚀 Quick Start
### Voraussetzungen
- Node.js (v16 oder höher)
- npm oder yarn
### Installation
```bash
# Repository klonen
git clone <repository-url>
cd beoanswer_react
# Dependencies installieren
npm install
# Terser für Production Builds installieren
npm install --save-dev terser
# Environment-Datei erstellen
cp .env.example .env
```
### Konfiguration
Erstelle eine `.env` Datei und passe die Werte an:
```env
# Backend API Configuration
VITE_API_URL=/api/intern/sofue/php/sofueDB.php
# HTTP Basic Authentication für geschütztes Backend
VITE_API_USERNAME=dein_username
VITE_API_PASSWORD=dein_passwort
```
### Development
```bash
# Development Server starten
npm run dev
# App öffnet sich auf http://localhost:5173
# Mit URL-Parameter: http://localhost:5173/?id=123
```
### Production
```bash
# Production Build erstellen
npm run build:prod
# Production Preview
npm run preview:prod
# Build-Dateien befinden sich in ./dist/
```
## 📦 Versionsverwaltung
Die App zeigt automatisch die Version aus der `package.json` und das aktuelle Build-Datum an.
### Version erhöhen
```bash
# Patch-Version erhöhen (1.0.0 → 1.0.1)
npm version patch
# Minor-Version erhöhen (1.0.0 → 1.1.0)
npm version minor
# Major-Version erhöhen (1.0.0 → 2.0.0)
npm version major
```
### Manuell in package.json
```json
{
"version": "1.2.3"
}
```
**Wichtig:** Nach Versionänderungen den Development Server neu starten:
```bash
npm run dev
```
## 🛠 Scripts
```bash
npm run dev # Development Server
npm run build # Standard Build
npm run build:prod # Production Build
npm run preview # Preview des Builds
npm run preview:prod # Preview des Production Builds
npm run lint # Code Linting
```
## 🌐 Deployment
### 1. Environment Variables setzen
Für Production eine `.env.production` erstellen:
```env
VITE_API_URL=https://dein-server.com/intern/sofue/php/sofueDB.php
VITE_API_USERNAME=production_user
VITE_API_PASSWORD=production_password
```
### 2. Build erstellen
```bash
npm run build:prod
```
### 3. Dateien auf Server kopieren
```bash
# Beispiel mit rsync
rsync -avz dist/ user@server:/var/www/html/beoanswer/
# Oder mit scp
scp -r dist/* user@server:/var/www/html/beoanswer/
```
### 4. Webserver konfigurieren
Siehe [DEPLOYMENT.md](./DEPLOYMENT.md) für detaillierte Anweisungen.
## 📱 Verwendung
### URL-Parameter
Die App erwartet einen `id` URL-Parameter:
```
https://dein-server.com/beoanswer/?id=123
```
### Workflow
1. **Link aus E-Mail** öffnen
2. **Formular ausfüllen**:
- Ja/Nein ob Führung stattfand
- Bei "Ja": Besucherzahl, Spenden-Art, etc.
- Bei "Nein": Absage oder Verschiebung
3. **Daten speichern** - Fenster schließt automatisch
### Navigation
- **Zurück-Button**: Schrittweise Navigation rückwärts
- **Abbruch**: Bestätigung mit Modal, dann Fenster schließen
- **Anleitung**: Hilfe-Modal mit Workflow-Beschreibung
## 🔧 Entwicklung
### Projektstruktur
```
src/
├── App.jsx # Hauptkomponente mit Routing-Logik
├── FormContext.jsx # Globaler State für Formulardaten
├── main.jsx # React App Entry Point
├── App.css # Haupt-Styling
├── components/
│ ├── BesucherBar.jsx # Eingabe für Besucher/Betrag
│ ├── Bemerkungen.jsx # Textarea für Bemerkungen
│ ├── FandStattVer.jsx # Radio-Button Komponente
│ ├── LastButtons.jsx # Abbruch/Anleitung/Senden Buttons
│ ├── LastLine.jsx # Version/Datum Anzeige
│ ├── Modal.jsx # Standard Modal Dialog
│ ├── ConfirmModal.jsx # Bestätigungs Modal
│ ├── Spende.jsx # Spenden-Art Auswahl
│ └── Verschoben.jsx # Datum-Eingabe für Verschiebung
```
### FormContext
Zentrale State-Verwaltung für alle Formulardaten:
```javascript
const { formData, updateFormData, resetFormData } = useFormData()
// Daten setzen
updateFormData('besucher', '15')
// Daten lesen
console.log(formData.besucher)
// Formular zurücksetzen
resetFormData()
```
### Backend Integration
Die App sendet Daten via FormData an das PHP Backend:
```javascript
const backendData = new FormData()
backendData.append('cmd', 'UPDATEAFTER')
backendData.append('id', id)
backendData.append('besucher', formData.besucher)
// ...weitere Felder
```
## 🐛 Troubleshooting
### Build-Fehler: "terser not found"
```bash
npm install --save-dev terser
```
### CORS-Fehler im Development
Vite Proxy ist konfiguriert für `localhost:8080`. Anpassen in `vite.config.js`:
```javascript
proxy: {
'/api': 'http://localhost:8080'
}
```
### Environment Variables werden nicht geladen
1. Development Server neu starten
2. Variablen müssen mit `VITE_` beginnen
3. `.env` Datei im Projektroot erstellen
### Modal-Buttons haben keinen Abstand
Browser-Cache leeren oder Hard-Refresh (`Ctrl+F5`).
## 📄 Weitere Dokumentation
- [DEPLOYMENT.md](./DEPLOYMENT.md) - Detaillierte Deployment-Anweisungen
- [.env.example](./.env.example) - Environment Variable Template
## 🤝 Beitragen
1. Feature Branch erstellen
2. Änderungen committen
3. Version mit `npm version` erhöhen
4. Build testen: `npm run build:prod`
5. Pull Request erstellen
## 📝 Lizenz
Private Projekt - Alle Rechte vorbehalten.
---
**Version:** Automatisch aus package.json
**Build-Datum:** Automatisch generiert
**Letztes Update:** Oktober 2025

View File

@@ -0,0 +1,114 @@
<?php
/**
* CORS Proxy für sofueDB.php
* Diese Datei muss in einem öffentlich zugänglichen Verzeichnis der Website liegen
*/
// CORS Headers für Frontend-Zugriff
$allowedOrigins = [
'http://localhost:5173',
'https://ihre-produktions-domain.de' // Ersetzen Sie durch Ihre echte Domain
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
header("Access-Control-Allow-Origin: $origin");
} else {
// Für Development: localhost mit beliebigen Ports erlauben
if (preg_match('/^http:\/\/localhost:\d+$/', $origin)) {
header("Access-Control-Allow-Origin: $origin");
}
}
header("Access-Control-Allow-Methods: POST, GET, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
header("Access-Control-Allow-Credentials: true");
// Preflight-Request abfangen
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
// Nur POST-Requests erlauben
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo 'Method Not Allowed';
exit();
}
// Backend-URL und Credentials aus Environment oder Config
$backendUrl = 'https://sternwarte-welzheim.de/intern/sofue/php/sofueDB.php';
// Credentials sicher laden - verschiedene Optionen:
// Option 1: Aus Environment Variables (empfohlen)
$username = getenv('SOFUE_USERNAME') ?: $_ENV['SOFUE_USERNAME'] ?? null;
$password = getenv('SOFUE_PASSWORD') ?: $_ENV['SOFUE_PASSWORD'] ?? null;
// Option 2: Aus separater Config-Datei (Fallback)
if (!$username || !$password) {
$configFile = __DIR__ . '/cors-config.php';
if (file_exists($configFile)) {
include $configFile;
// cors-config.php sollte enthalten:
// <?php $username = 'beogruppe'; $password = 'ArktUhr'; ?>
}
}
// Option 3: Letzter Fallback - aber sicherer als Klartext
if (!$username || !$password) {
// Base64-kodiert (minimal obfuskiert, aber nicht wirklich sicher)
$encoded = 'YmVvZ3J1cHBlOkFya3RVaHI='; // beogruppe:ArktUhr
$decoded = base64_decode($encoded);
list($username, $password) = explode(':', $decoded, 2);
}
// Sicherheitscheck
if (!$username || !$password) {
http_response_code(500);
echo 'Server configuration error';
exit();
}
// POST-Daten aus dem Frontend übernehmen
$postData = $_POST;
// Debug-Log (optional, für Entwicklung)
error_log("CORS-Proxy: Weiterleitung an Backend mit " . count($postData) . " Parametern");
// cURL-Request an das geschützte Backend
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $backendUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
// Response vom Backend holen
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// Fehlerbehandlung
if ($response === false) {
http_response_code(500);
echo "Backend-Verbindungsfehler: " . $error;
exit();
}
// HTTP-Status vom Backend übernehmen
http_response_code($httpCode);
// Content-Type vom Backend übernehmen (falls JSON)
if (strpos($response, '{') === 0 || strpos($response, '[') === 0) {
header('Content-Type: application/json');
} else {
header('Content-Type: text/plain');
}
// Response vom Backend weiterleiten
echo $response;
?>

View File

@@ -0,0 +1,12 @@
services:
beoanswer_rect:
build: .
ports:
- "5173:5173"
volumes:
- .:/app # Source-Code in Container mounten
- /app/node_modules # node_modules im Container behalten
environment:
- NODE_ENV=development
command: ["npm", "run", "dev", "--", "--host"]

View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./src/assets/react.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>beoanswer</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

2900
sternwarte/beoanswer/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
{
"name": "beoanswer_react",
"private": true,
"version": "1.0.2",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:prod": "vite build --mode production",
"preview": "vite preview",
"preview:prod": "vite preview --mode production",
"lint": "eslint ."
},
"dependencies": {
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"terser": "^5.44.0",
"vite": "^7.1.7"
}
}

View File

@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BeoAnswer - Anleitung</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background: white;
}
h1 {
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
}
h2 {
color: #555;
margin-top: 30px;
}
.step {
background: #f8f9fa;
border-left: 4px solid #007bff;
padding: 15px;
margin: 15px 0;
border-radius: 0 8px 8px 0;
}
.step h3 {
margin-top: 0;
color: #007bff;
}
.highlight {
background: #fff3cd;
border: 1px solid #ffeaa7;
padding: 10px;
border-radius: 6px;
margin: 10px 0;
}
ul {
padding-left: 20px;
}
li {
margin: 8px 0;
}
.tip {
background: #d1ecf1;
border: 1px solid #bee5eb;
padding: 10px;
border-radius: 6px;
margin: 15px 0;
}
.tip::before {
content: "💡 ";
font-weight: bold;
}
</style>
</head>
<body>
<h1>📋 BeoAnswer - Anleitung</h1>
<div class="highlight">
<strong>Willkommen!</strong> Diese Anleitung hilft Ihnen bei der Nachbearbeitung von Sonderführungen.
</div>
<h2>🚀 Schnellstart</h2>
<p>Die Anwendung führt Sie Schritt für Schritt durch die Nachbearbeitung. Folgen Sie einfach den Anweisungen auf dem Bildschirm.</p>
<h2>📝 Workflow</h2>
<div class="step">
<h3>1. Grundfrage beantworten</h3>
<p><strong>"Fand die Führung statt?"</strong></p>
<ul>
<li><strong>Ja:</strong> Weiter zu Schritt 2</li>
<li><strong>Nein:</strong> Weiter zu Schritt 5</li>
</ul>
</div>
<div class="step">
<h3>2. Besucherzahl eingeben (nur bei "Ja")</h3>
<p>Geben Sie die Anzahl der Teilnehmer ein und klicken Sie auf "OK".</p>
<div class="tip">Sie können auch die Enter-Taste drücken.</div>
</div>
<div class="step">
<h3>3. Spenden-Art auswählen (nur bei "Ja")</h3>
<p>Wählen Sie aus:</p>
<ul>
<li><strong>Barspende:</strong> Weiter zu Schritt 4</li>
<li><strong>Wird überwiesen:</strong> Weiter zu Schritt 5</li>
<li><strong>Spendenkässle:</strong> Weiter zu Schritt 5</li>
<li><strong>Keine Spende:</strong> Weiter zu Schritt 5</li>
</ul>
</div>
<div class="step">
<h3>4. Spendenbetrag eingeben (nur bei Barspende)</h3>
<p>Geben Sie den Betrag der Barspende in Euro ein.</p>
</div>
<div class="step">
<h3>5. Bemerkungen hinzufügen (optional)</h3>
<p>Hier können Sie zusätzliche Informationen zur Führung eingeben:</p>
<ul>
<li>Besonderheiten</li>
<li>Probleme</li>
<li>Feedback der Teilnehmer</li>
<li>Sonstige Anmerkungen</li>
</ul>
<div class="tip">Verwenden Sie Strg+Enter (oder Cmd+Enter) zum schnellen Speichern.</div>
</div>
<h2>❌ Bei abgesagten/verschobenen Führungen</h2>
<div class="step">
<h3>1. Grund auswählen</h3>
<p><strong>"Die Führung wurde"</strong></p>
<ul>
<li><strong>Abgesagt:</strong> Direkt zum Senden</li>
<li><strong>Verschoben:</strong> Weiter zu Schritt 2</li>
</ul>
</div>
<div class="step">
<h3>2. Neues Datum eingeben (nur bei "Verschoben")</h3>
<p>Wählen Sie das neue Datum und die Uhrzeit für die verschobene Führung.</p>
</div>
<h2>🔄 Navigation</h2>
<ul>
<li><strong>Zurück-Button:</strong> Geht einen Schritt zurück und löscht die entsprechenden Eingaben</li>
<li><strong>Abbruch:</strong> Bricht den Vorgang ab (mit Sicherheitsabfrage)</li>
<li><strong>Anleitung:</strong> Zeigt diese Hilfe an</li>
<li><strong>Senden:</strong> Speichert alle Daten und schließt das Fenster</li>
</ul>
<h2>✅ Abschluss</h2>
<p>Nach dem Klick auf "Senden" werden die Daten gespeichert und das Fenster schließt sich automatisch. Sie kehren zur ursprünglichen Anwendung zurück.</p>
<div class="highlight">
<strong>Fragen oder Probleme?</strong> Wenden Sie sich an den Administrator.
</div>
</body>
</html>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

398
sternwarte/beoanswer/sofueDB.php Executable file
View File

@@ -0,0 +1,398 @@
<?php
# Hier werden die Anfragen vom Javascript verarbeitet und die
# Datenbank bedient
$db = null;
//include '../../dbaseconf.php';
include '../../../config_stern.php';
include '../../../phpmailer/dosendmail.php';
$table = 'SoFue2';
function getFromDbase($db, $query, $single) {
$result = mysqli_query($db,$query) or die (mysqli_error($db));
$erg = array();
if(mysqli_num_rows($result)) {
while($row = mysqli_fetch_assoc($result)) {
$erg[] = $row;
}
}
if($single == true) {
return ($erg[0]);
} else {
return($erg);
}
}
function cudDbase($db, $query) {
return(mysqli_query($db,$query) or die (mysqli_error($db)));
}
// Ein Record holen mit der ID $id holen nd kompleet übermitteln
function getOneRecord($db,$id) {
global $table;
$query = "select * from $table where id = $id";
return(getFromDbase($db,$query,true));
}
// Ein Record holen mit dem Wunschtermin als Auswahl
function getOneRecordTermin($db,$termin) {
global $table;
$query = "select * from $table where DATE(wtermin) = '$termin' and status = 2";
return(getFromDbase($db,$query,true));
}
// Alle Records mit übergebener WHERE-clause übergeben, sortiert in aufsteigender
// Zeitfolge (Führungstermine als Zeitfolge)
// Es werden maximal $cout Records übergeben
function getRecords($db, $st, $termin, $anz, $pagnbr) {
global $table;
$response = new stdClass();
$ergs = array();
$lastdate = new DateTime();
$lastdate = $lastdate->sub(new DateInterval('P9M'));
$lastdate = $lastdate->format('Y-m-d');
if($st == 4) {
$where ="where stattgefunden = 1 and deleted = 0 ";
} else {
$where = "where status = '$st' and deleted = 0";
if ($termin == 'neu') {
$where = $where . " and wtermin >= now()";
}
}
// Anzahl der Records holen
$query = "select count(*) as count from $table $where ";
$row = getFromDbase($db,$query,true);
$count = $row['count'];
// Anzahl der Seiten ausrechnen
$totalpages = ceil($count/$anz);
// Falls die angeforderte Seit > als die ANzahl der Seiten ist, die letzte Seite übergeben
if($pagnbr > $totalpages) $pagnbr = $totalpages;
// Start-Record berechnen
$start = $anz * ($pagnbr-1);
if($start <0) {
$start = 0;
}
$where = $where . " and DATE(wtermin) >= '$lastdate'";
$query = "select * from $table $where order by wtermin desc limit $start,$anz";
$rows = getFromDbase($db, $query, false);
$response->page = $pagnbr;
$response->total = $totalpages; // Es wird immer 1 Page übergeben
$cnt = 0;
foreach($rows as $row) {
$response->rows[$cnt]['id'] = $row['id'];
$response->rows[$cnt]['cell'] = $row;
$cnt++;
}
$response->records = $count;
return ($response);
}
# string substr ( string $string , int $start [, int $length ] )
# Beo-Daten holen
function getBeos($db, $what, $cond) {
$retur = array();
if($cond == "") {
$query = "select $what from beos order by $what";
} else {
$a = strpos($cond,'empty');
if ( $a !== false) {
$b = substr($cond,0,$a);
$query = "select $what from beos where $b '' order by $what";
} else {
$query = "select $what from beos where $cond order by $what";
}
}
# echo $query;
$rows = getFromDbase($db, $query, false);
foreach($rows as $row) {
$retur[] = $row[$what];
}
return ($retur);
}
# Statistikdaten für das laufende (oder ein altes) Jahr holen und übergeben
# Ausgaben: JSON:
# { year: 2018,
# data:[
# { month: 1, angefragt: 10, zugesagt: 7, abgesagt: 3, stattgefunden: 6 },
# { month: 2, angefragt: 8, zugesagt: 6, abgesagt: 2, stattgefunden: 5 },
# { month: 3, angefragt: 23, zugesagt: 20, abgesagt: 3, stattgefunden: 15 },
# ...
# { month: 12, angefragt: 34, zugesagt: 22, abgesagt: 12, stattgefunden: 10 },
function getStatistik($db, $year) {
}
# Daten eines BEO holen, mit Name als Suchkriterium
function getOneBEO($db, $name) {
$query = "select * from beos where name = '$name'";
return getFromDbase($db, $query, true);
}
function updateEntry($db, $post) {
global $table;
$oldinhalt = getOneRecord($db, $post['id']);
$data = "mitarbeiter='" . $post['mitarbeiter'] .
"', status='" . $post['status'] .
"', bemerkung='" . $post['bemerkung'] .
"', wtermin='" . $post['wtermin'] .
"', atermin='" . $post['atermin'] .
"', allwett=''" .
", erledigt_datum='" . $post['erledigt_datum'] . "'";
$id = $post['id'];
$query = "update $table set $data where id='$id'";
$ret = cudDbase($db, $query);
$newinhalt = getOneRecord($db, $post['id']);
$ma = $post['mitarbeiter'];
$oldTermin = $oldinhalt['wtermin'];
return $ret;
}
function updateAfter($db,$post) {
global $table;
$oldinhalt = getOneRecord($db, $post['id']);
$data = "stattgefunden='" . $post['stattgefunden'] .
"', anzahl_echt='" . $post['besucher'] .
"', remarks='" . $post['remark'] .
"', bezahlt='" . $post['bezahlt'] . "'";
// if (!empty($post['wtermin'])) {
// $data .= ", wtermin='" . $post['wtermin'] . "'";
// $ma = $oldInhalt['mitarbeiter'];
// sendMailTo($ma, $oldinhalt, $post['wtermin'], "Wunsch");
// }
if (!empty($post['status'])) {
$data .= ", status='" . $post['status'] . "'";
}
$id = $post['id'];
$query = "update $table set $data where id='$id'";
$ret = cudDbase($db, $query);
return($ret);
}
function deleteEntry($db, $id) {
global $table;
$query = "update $table set deleted=true where id='$id'";
return cudDbase($db, $query);
}
function getDBdata() {
global $host, $dbase, $user, $pass;
$erg = "HOST: >" . $host . "<&nbsp;&nbsp;&nbsp;Dbase: >" . $dbase . "<&nbsp;&nbsp;&nbsp;user/pass: >" . $user . "/" . $pass . "<";
return $erg;
}
function findBeoVorname($who) {
global $db;
$names = explode(",",$who);
$erg = getbeos($db,'vorname',"name='".$names[0]."'");
return ($erg[0]);
}
function findBeoEmail($who) {
global $db;
$names = explode(",",$who);
$erg = getbeos($db,'email_1',"name='".$names[0]."'");
return ($erg[0]);
}
function wterminstr($t) {
$tage = array(
"So",
"Mo",
"Di",
"Mi",
"Do",
"Fr",
"Sa"
);
$dati = strtotime($t);
$dt = $tage[date("w",$dati)] . ", " . date('d.m.Y H:i',$dati);
return $dt;
}
function sendMail2Beo($ma, $termin) {
$dt = wterminstr($termin);
$body = "Hallo " . findBeoVorname($ma) .",
vielen Dank für die Bereitschaft, die Sonderführung am {$dt} zu übernehmen.
Bitte den Termin nicht vergessen und bitte ggf. auch das Teammitglied, das die
Führung mitmacht, informieren.
Der Termin wurde in den Sternwartenkalender eingetragen.
Die Kontaktdaten sind auf der Sonderführungsseite ( https://sternwarte-welzheim.de/intern/sofue/sofue.php ) zu finden.
Viele Grüße
Reinhard
Diese Meldung wurde automatisch erzeugt. Es kann nicht geantwortet werden.";
$betreff = "Vereinbarte Sonderführung am " .$dt;
$absender = "noreply@sternwarte-welzheim.de";
sendmail($betreff, $absender, $body, [], ['rexfue@gmail.com'], [findBeoEmail($ma)]);
}
function sendMailZusage($to, $mitarbeiter, $termin) {
$dt = wterminstr($termin);
$betreff = "ZUSAGE - Sternwartenführung am {$dt} Uhr";
$absender = "anmeldung@sternwarte-welzheim.de";
$ge1 = ($mitarbeiter['gender'] == 'm') ? "unser ehrenamtlicher Mitarbeiter, Herr" : "unsere ehrenamtliche Mitarbeiterin, Frau";
$ge2 = ($mitarbeiter['gender'] == 'm') ? "ihn" : "sie";
$ge3 = ($mitarbeiter['gender'] == 'm') ? "Herrn" : "Frau";
$body = "
Guten Tag,
für Ihren Wunschtermin, {$dt} Uhr, hat sich {$ge1} {$mitarbeiter['vorname']} {$mitarbeiter['name']} bereit erklärt,
die Sonderführung zu übernehmen. Sie erreichen {$ge2} über die e-mail-Adresse: {$mitarbeiter['email_1']}
Um nähere Besuchsmodalitäten zu klären, bitten wir Sie, mit {$ge3} {$mitarbeiter['name']} Kontakt aufzunehmen.
Wir bitten Sie, die Spende in Höhe von €50.00 auf unten aufgeführtes Konto zu überweisen oder in bar zur Führung mitzubringen.
Gesellschaft zur Förderung des Planetariums Stuttgart und der Sternwarte Welzheim e.V.
BANKVERBINDUNG: Deutsche Bank AG Stuttgart
IBAN DE18 6007 0070 0122 0383 00
BIC: DEUTDESSXXX
Mit sternfreundlichen Grüßen
Reinhard X. Fürst
Sternwarte Welzheim
";
sendmail($betreff, $absender, $body, [$mitarbeiter['email_1']], ['rexfue@gmail.com'], [$to]);
}
function sendMail2Liste($to, $erg) {
$betreff = "Anfrage Sonderführung am {$erg['wtermin']}";
$absender = "sonderfuehrung@sternwarte-welzheim.de";
$body = "
Liebe BEOs,
wer kann folgende Sonderführung übernehmen?
Viele Grüße
Reinhard
---------------------------------------------------------------------------------------------------";
$body = $body . "
Name, Vorname: " . $erg['name'] . " " . $erg['vorname'] . "
Verein / Organisation : " . $erg['verein'] . "
Wunsch - Termin: " . $erg['wtermin'] . "
Teilnehmerzahl ca.: " . $erg['anzahl'] . "
Weitere Fragen oder Mitteilungen: " . $erg['mitteilung'] . "
Spendenbescheinigung: " . $erg['spende'] . "
---------------------------------------------------------------------------------------------------";
sendmail($betreff, $absender, $body, [], [], [$to]);
}
// Sonderführung in den Kalender eintragen
function put2kalender($db, $data, $termin, $ma) {
$start = substr($termin,0,16);
$title = "WK, SF " . $data['name'] . ", " . $ma;
$sql = "INSERT into kalender (start, end, title, description) VALUES ('" . $start . "', DATE_ADD('" . $start . "',INTERVAL 2 HOUR), '" . $title . "', '')";
$erg = cudDbase($db, $sql);
$mist = 23;
}
// Hier gehts dann los:
// Alle Paramater aus dem Ajax-Call auslesen
// Mögliche Aufrufe:
/*
* cmd=GET param=ID id=5 bringt das eine Record mit ID=5
* cmd=GET param=STATUS staus=offen bringt ALLE records mit stautus='offen' in zeitlich absteigender Reihenfoleg
*/
$erg = "";
$cmd = $_POST["cmd"];
/*
$x = "# ";
foreach ($_POST as $key => $value) {
$x = $x . $key . " " . $value . "\n";
}
$x = $x . '# ';
echo $x;
*/
switch ($cmd) {
case 'GET_ONE':
$erg = getOneRecord($db, $_POST["id"]);
break;
case 'GET_ONETERMIN':
$erg = getOneRecordTermin($db, $_POST["termin"]);
break;
case 'GET_MANY':
$st = $_POST['status'];
$anzahl = $_POST['rows'];
$page = $_POST['page'];
$termin = $_POST['termin'];
$erg = getRecords($db,$st, $termin ,$anzahl,$page);
break;
case 'GET_BEOS':
$erg = getBeos($db,$_POST['what'],$_POST['cond']);
// echo '#' . $erg ;
break;
case 'GET_ONEBEO':
$erg = getOneBEO($db,$_POST['name']);
// echo '#' . $erg ;
break;
# case GET_FUEH:
# $erg = getFuehrung_findet_statt($db);
# break;
case 'GET_STAT':
# $erg = getStatistik($db,$_POST['year']);
break;
case 'UPDATE':
$erg = updateEntry($db, $_POST);
break;
case 'UPDATEAFTER':
$erg = updateAfter($db, $_POST);
break;
case 'DELETE':
$erg = deleteEntry($db, $_POST['id']);
break;
case 'SENDMAILZUSAGE':
// function sendMailZusage($to, $mitarbeiter, $termin) {
$erg = getOneRecord($db, $_POST["id"]);
$names = explode(",",$_POST['mitarbeiter']);
$ma = getOneBEO($db, $names[0]);
sendMailZusage($erg['email'], $ma, $_POST['termin']);
break;
case 'SENDMAIL2BEO':
// function sendMail2Beo($ma, $termin) {
sendMail2beo($_POST['ma'], $_POST['termin']);
break;
case 'SENDMAIL2LISTE':
$erg = getOneRecord($db, $_POST["id"]);
sendMail2Liste($_POST['to'],$erg);
break;
case 'PUT2KALENDER':
$erg = getOneRecord($db, $_POST["id"]);
put2kalender($db, $erg, $_POST['termin'], $_POST['mitarbeiter']);
break;
case 'SHOWDB':
$erg = getDBdata();
break;
}
header("Content-type: text/json;charset=utf-8");
echo json_encode($erg);
?>

View File

@@ -0,0 +1,122 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.wrapper {
max-width: 600px;
margin: auto;
border: 1px solid blue;
background: lightgray;
border-radius: 10px;
}
button {
background-color: lightskyblue;
font-weight: bold;
}
.nachbearbeitung {
background-color: yellow;
height: 50px;
padding-top: 12px;
}
section {
border-bottom : 1px solid rgb(187, 185, 185);
text-align: left;
margin: 0 auto 20px auto;
padding: 0 0em 1em 2em;
font-weight: bold;
}
.infeldsm {
width: 12em;
}
.fstdiv {
margin-bottom: 1em;
}
.fsLabel {
margin-right: 20px;
margin-bottom: 3rem;
}
.okbutton {
margin-left: 2em;
}
.radiogroup {
display: flex;
flex-direction: column;
gap: 10px;
}
.selspende {
display: flex;
align-items: center;
}
.spendeok {
margin-left: 2em;
}
.bemerkdiv {
display: flex;
align-items: center;
}
.beminfeld {
width: 14em;
}
.lastline {
border-top: 1px solid blue;
display: flex;
justify-content: space-between;
margin: auto;
padding: 15px;
font-size: 80%;
}
.lastbuttons {
display: flex;
justify-content: space-around;
align-content: space-around;
flex-wrap: wrap;
width: 80%;
margin: auto;
margin-bottom: 20px;
gap: 15px;
}
.umbruch {
display: none;
}
.btnsend {
background-color: blue;
color: white;
}
.btnsend :hover {
border-color: #646cff;
}
@media (max-width: 480px) {
.umbruch {
display: block;
}
.lastline {
font-size: 50%;
}
}
input[type="radio"] {
margin-right: 10px;
}

View File

@@ -0,0 +1,330 @@
import { useState, useEffect } from 'react'
import { FormProvider, useFormData } from './FormContext'
import packageJson from '../package.json'
import './App.css'
import FandStattVer from './components/FandStattVer.jsx'
import BesucherBar from './components/BesucherBar.jsx'
import Spende from './components/Spende.jsx'
import LastLine from './components/LastLine.jsx'
import Bemerkungen from './components/Bemerkungen.jsx'
import LastButtons from './components/LastButtons.jsx'
import Verschoben from './components/Verschoben.jsx'
function AppContent() {
// States für Backend-Daten
const [datum, setDatum] = useState("")
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 version = packageJson.version
const vdate = new Date().toLocaleDateString('de-DE')
// States
const [schritt, setSchritt] = useState(0)
const [pfad, setPfad] = useState('')
// Hole formData aus dem Context
const { formData, updateFormData } = useFormData()
// URL-Parameter und Backend-Aufruf
useEffect(() => {
const fetchData = async () => {
// API URL aus Environment Variable laden
const APIURL = import.meta.env.VITE_API_URL
if (!APIURL) {
throw new Error('API URL nicht konfiguriert. Bitte VITE_API_URL in .env Datei setzen.')
}
try {
// URL-Parameter auslesen
const urlParams = new URLSearchParams(window.location.search)
const id = urlParams.get('id')
if (!id) {
throw new Error('Keine ID in der URL gefunden. Bitte rufen Sie die Seite mit ?id=123 auf.')
}
console.log('Loading data for ID:', id)
// Backend-Aufruf mit HTTP Basic Auth
const formData = new FormData()
formData.append('cmd', 'GET_ONE')
formData.append('id', id)
// HTTP Basic Authentication Header erstellen
const username = import.meta.env.VITE_API_USERNAME
const password = import.meta.env.VITE_API_PASSWORD
const headers = {}
if (username && password) {
const credentials = btoa(`${username}:${password}`)
headers['Authorization'] = `Basic ${credentials}`
}
const response = await fetch(APIURL, {
method: 'POST',
headers: headers,
body: formData
})
if (!response.ok) {
throw new Error(`Daten konnten nicht geladen werden. Server-Fehler: ${response.status}`)
}
const data = await response.json()
console.log('Received data:', data) // Debug-Ausgabe
// Anpassung an die Datenbankfelder der SoFue2 Tabelle
if (!data.wtermin || !data.name) {
throw new Error('Unvollständige Daten vom Server erhalten.')
}
// Daten aus Backend setzen
// wtermin ist vermutlich ein datetime, also nur das Datum extrahieren
const terminDate = new Date(data.wtermin)
const formatiertesDatum = terminDate.toLocaleDateString('de-DE')
setDatum(formatiertesDatum)
setName(data.name + (data.vorname ? ' ' + data.vorname : ''))
console.log('Data loaded:', data)
setLoading(false)
} catch (err) {
console.error('Error loading data:', err)
setError(err.message)
setLoading(false)
}
}
fetchData()
}, []) // Leere Dependency-Array = nur beim ersten Laden ausführen
// Callbacks:
const handleFandStattVerNext = (auswahl) => {
auswahl && setPfad(auswahl)
handleNext()
}
const handleNext = () => {
setSchritt((schritt) => schritt + 1)
}
const handleBack = () => {
if (schritt > 0) {
const neuerSchritt = schritt - 1
setSchritt(neuerSchritt)
// Entsprechende FormData-Felder zurücksetzen je nach Schritt und Pfad
if (pfad === 'ja') {
// JA-Pfad rückwärts
if (schritt === 1) {
// Von Besucher zurück zu ja/nein → Pfad löschen
setPfad('')
updateFormData('stattgefunden', '')
} else if (schritt === 2) {
// Von Spende zurück zu Besucher → Besucher löschen
updateFormData('besucher', '')
} else if (schritt === 3) {
// Von Betrag/Bemerkungen zurück zu Spende → Spende löschen
updateFormData('spendenArt', '')
} else if (schritt === 4) {
// Von Bemerkungen zurück → Betrag löschen (bei Bar-Spende)
updateFormData('betrag', '')
} else if (schritt === 5) {
// Von Senden zurück → Bemerkungen löschen
updateFormData('bemerkungen', '')
}
} else if (pfad === 'nein') {
// NEIN-Pfad rückwärts
if (schritt === 1) {
// Von abgesagt/verschoben zurück zu ja/nein → Pfad löschen
setPfad('')
updateFormData('stattgefunden', '')
} else if (schritt === 2) {
// Von Datum/Senden zurück zu abgesagt/verschoben → abgesagt löschen
updateFormData('abgesagt', '')
} else if (schritt === 3) {
// Von Senden zurück → neues Datum löschen (bei verschoben)
updateFormData('neuesDatum', '')
}
}
}
}
const setBackButton = () => {
setMitback(true)
}
// Welche Komponeneten werden angezeigt:
const renderCoponents = () => {
const components = []
// Schritt 0: ja/nein - Auswahl
components.push(
<FandStattVer key="fandstatt" left='ja' right='nein' title='Fand die Führung statt?'
onNext={handleFandStattVerNext}
setbackButton={setBackButton}
iscompleted={schritt > 1} />
)
if (schritt === 0) {
// Bei ja/nein Auswahl: Kein Zurück-Button, kein Senden-Button
components.push(<LastButtons key='lastbutt' mitSend={false} mitBack={false} handleBack={handleBack}/>)
return components
}
// JA-Pfad:
if (pfad === 'ja') {
// Schritt 1: Besucher-Anzahl
if (schritt >= 1) {
components.push(<BesucherBar key='besucher' title='Besucher-Anzahl' euro='' onNext={handleNext} isCompleted={schritt > 1} />
)
}
// Schritt 2: Spende
if (schritt >= 2) {
components.push(<Spende key='spende' onNext={handleNext} isComplete={schritt > 2} />
)
}
// Schritt 3: Betrag der Spende (nur bei Bar-Spende)
if ((schritt >= 3) && (formData.spendenArt === 'bar')) {
components.push(<BesucherBar key='betrag' title='Höhe der Barspende' euro='€' onNext={handleNext} isCompleted={schritt > 3} />
)
}
// Schritt 4 (bei Bar-Spende) oder Schritt 3 (bei anderen Spenden): Bemerkungen
const bemerkungsSchritt = (formData.spendenArt === 'bar') ? 4 : 3
if (schritt >= bemerkungsSchritt) {
components.push(<Bemerkungen key='bemerkungen' onNext={handleNext} isCompleted={schritt > bemerkungsSchritt} />
)
}
}
// NEIN - Pfad
if (pfad === 'nein') {
// Schritt 1: abgesagt / verschoben
if (schritt >= 1) {
components.push(
<FandStattVer key="abgesagt" left='abgesagt' right='verschoben' title='Die Führung wurde' radioName='abgesagt'
onNext={handleNext}
setbackButton={setBackButton}
iscompleted={schritt > 1} />
)
}
// Schritt 2: Ende wenn abgesagt bzw. neues Datum bei verschoben
if (schritt >= 2 && formData.abgesagt === 'verschoben') {
components.push(<Verschoben key='verschoben' onNext={handleNext} isCompleted={schritt > 2} />
)
}
}
// Zurück-Button nur anzeigen wenn nicht bei ja/nein Auswahl
const backVerfuegbar = schritt > 0
// LastButtons IMMER anzeigen, aber Senden-Button nur wenn bereit
const sendenBereit = () => {
if (pfad === 'ja') {
// JA-Pfad: vollständig wenn Bemerkungen-Schritt ABGESCHLOSSEN ist
const bemerkungsSchritt = (formData.spendenArt === 'bar') ? 4 : 3
return schritt > bemerkungsSchritt // NACH dem Bemerkungsschritt, nicht beim Erreichen
} else if (pfad === 'nein') {
// NEIN-Pfad: vollständig wenn abgesagt ODER verschoben mit Datum
if (formData.abgesagt === 'abgesagt') {
return schritt >= 2 // Beim Erreichen von Schritt 2 (nach Auswahl abgesagt)
} else if (formData.abgesagt === 'verschoben') {
return schritt >= 3 && formData.neuesDatum // Beim Erreichen von Schritt 3 mit Datum
}
}
return false
}
// LastButtons immer anzeigen
components.push(<LastButtons key='lastbutt' mitSend={sendenBereit()} mitBack={backVerfuegbar} handleBack={handleBack} />)
return components
}
// Loading und Error States
if (loading) {
return (
<div className="wrapper">
<div>
<h2 className="topline">Lade Daten...</h2>
<p>Bitte warten Sie, während die Führungsdaten geladen werden.</p>
</div>
</div>
)
}
if (error) {
return (
<div className="wrapper">
<div>
<h2 className="nachbearbeitung" style={{backgroundColor: '#ff6b6b', color: 'white'}}>
Fehler beim Laden der Daten
</h2>
<div style={{padding: '20px', textAlign: 'left'}}>
<h3> Die Anwendung kann nicht gestartet werden</h3>
<p><strong>Grund:</strong> {error}</p>
<hr />
<h4>Mögliche Lösungen:</h4>
<ul>
<li>Überprüfen Sie die URL - sie sollte eine ID enthalten (z.B. ?id=123)</li>
<li>Stellen Sie sicher, dass das Backend erreichbar ist</li>
<li>Kontaktieren Sie den Administrator</li>
</ul>
<button
onClick={() => window.location.reload()}
style={{
marginTop: '20px',
padding: '10px 20px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}
>
Seite neu laden
</button>
</div>
</div>
</div>
)
}
return (
<div className="wrapper">
<div>
<h2 className="topline">
Sonderführung vom <br className="umbruch" />{datum}
</h2>
<h4>für {name}</h4>
<h2 className="nachbearbeitung">Nachbearbeitung</h2>
</div>
{renderCoponents().map(component => component)}
<LastLine version={version} vdate={vdate} />
</div>
)
}
function App() {
return (
<FormProvider>
<AppContent />
</FormProvider>
)
}
export default App

View File

@@ -0,0 +1,57 @@
// ========================================
// FormContext.jsx - Globaler State für alle Formulardaten
// ========================================
import { createContext, useContext, useState } from 'react'
const FormContext = createContext()
export function FormProvider({ children }) {
const [formData, setFormData] = useState({
stattgefunden: '',
besucher: '', // war: besucherAnzahl
spendenArt: '',
betrag: '', // war: barspende
bemerkungen: '',
neuesDatum: '', // war: neuertermin
abgesagt: '', // für abgesagt/verschoben
// Weitere Felder können hier hinzugefügt werden
})
const updateFormData = (field, value) => {
setFormData(prev => {
const newData = {
...prev,
[field]: value
}
return newData
})
}
const resetFormData = () => {
setFormData({
stattgefunden: '',
besucher: '',
spendenArt: '',
betrag: '',
bemerkungen: '',
neuesDatum: '',
abgesagt: ''
})
}
return (
<FormContext.Provider value={{ formData, updateFormData, resetFormData }}>
{children}
</FormContext.Provider>
)
}
export function useFormData() {
const context = useContext(FormContext)
if (!context) {
throw new Error('useFormData muss innerhalb von FormProvider verwendet werden. Stelle sicher, dass deine Komponente von <FormProvider> umschlossen ist.')
}
return context
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,44 @@
import { useState } from 'react'
import { useFormData } from '../FormContext'
export default function Bemerkungen({ onNext, isCompleted }) {
const { formData, updateFormData } = useFormData()
const [wert, setWert] = useState(formData.bemerkungen || '')
const handleOK = () => {
updateFormData('bemerkungen', wert)
onNext()
}
const handleKeyDown = (e) => {
// Ctrl+Enter oder Cmd+Enter zum Speichern
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
handleOK()
}
}
return (
<section id="bemerkungen">
<h3>Bemerkungen (optional):</h3>
<div className="bemerkdiv">
<textarea
className="beminfeld"
value={wert}
onChange={(e) => setWert(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Hier können Sie optionale Bemerkungen zur Führung eingeben..."
disabled={isCompleted}
/>
<button
className="okbutton"
onClick={handleOK}
disabled={isCompleted}
>
OK
</button>
</div>
</section>
)
}

View File

@@ -0,0 +1,57 @@
import { useState } from 'react'
import { useFormData } from '../FormContext'
import Modal from './Modal'
export default function BesucherBar({ title, euro, onNext, isCompleted }) {
const { formData, updateFormData } = useFormData()
// Bestimme Feldname basierend auf dem title
const fieldName = title.includes('Barspende') ? 'betrag' : 'besucher'
const [wert, setWert] = useState(formData[fieldName] || '')
const [showModal, setShowModal] = useState(false)
const handleOK = () => {
if (wert) {
updateFormData(fieldName, wert)
onNext()
} else {
setShowModal(true)
}
}
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
handleOK()
}
}
const closeModal = () => {
setShowModal(false)
}
return (
<>
<section id="besucherbar">
<h3>{title}:</h3>
<div className="besadiv">
<input type='number' value={wert} onChange={(e) => setWert(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={euro ? 'Betrag in Euro' : 'Anzahl'} disabled={isCompleted}
/>
&nbsp;&nbsp;{euro}
<button className="okbutton" onClick={handleOK}>OK</button>
</div>
</section>
<Modal
isOpen={showModal}
onClose={closeModal}
title="Eingabe erforderlich"
>
<p>Bitte einen Wert eingeben</p>
</Modal>
</>
)
}

View File

@@ -0,0 +1,65 @@
import React from 'react'
import './Modal.css'
export default function ConfirmModal({ isOpen = true, onClose, onConfirm, title, message, type = 'warning' }) {
if (!isOpen) return null
const handleOverlayClick = (e) => {
if (e.target === e.currentTarget) {
onClose()
}
}
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose()
}
if (e.key === 'Enter') {
onConfirm()
}
}
const getDefaultTitle = () => {
switch(type) {
case 'danger': return 'Achtung'
case 'warning': return 'Bestätigung erforderlich'
case 'info': return 'Information'
default: return 'Bestätigung'
}
}
const getModalClass = () => {
return `modal-content modal-${type}`
}
const displayTitle = title || getDefaultTitle()
return (
<div className="modal-overlay" onClick={handleOverlayClick} onKeyDown={handleKeyDown} tabIndex={0}>
<div className={getModalClass()}>
<div className="modal-header">
<h3 className="modal-title">{displayTitle}</h3>
<button className="modal-close" onClick={onClose}>&times;</button>
</div>
<div className="modal-body">
<p style={{whiteSpace: 'pre-line'}}>{message}</p>
</div>
<div className="modal-footer confirm-buttons">
<button
className="modal-button modal-button-secondary"
onClick={onClose}
>
Nein
</button>
<button
className="modal-button modal-button-danger"
onClick={onConfirm}
autoFocus
>
OK Abbrechen
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,41 @@
import { useState } from 'react'
import { useFormData } from '../FormContext'
import Modal from './Modal'
export default function FandStattVer({left, right, title, onNext, radioName = "fst", setbackButton}) {
const { formData, updateFormData } = useFormData()
// Bestimme das Feld basierend auf radioName
const fieldName = radioName === 'abgesagt' ? 'abgesagt' : 'stattgefunden'
const [auswahl, setAuswahl] = useState(formData[fieldName] || '')
const handleRadioChange = (e) => {
const value = e.target.value
updateFormData(fieldName, value)
setbackButton(true)
if(radioName !== 'abgesagt') {
setAuswahl(value)
onNext(value)
} else {
onNext()
}
}
return (
<section>
<h3>{title}</h3>
<div className="fstdiv">
<label className="fsLabel">
<input type="radio" name={radioName} value={left} checked={auswahl === left}
onChange={handleRadioChange} />
{left}
</label>
<label className="fsLabel">
<input type="radio" name={radioName} value={right} checked={auswahl === right}
onChange={handleRadioChange} />
{right}
</label>
</div>
</section>
)
}

View File

@@ -0,0 +1,301 @@
import { useState } from 'react'
import { useFormData } from '../FormContext'
import Modal from './Modal'
import ConfirmModal from './ConfirmModal'
export default function LastButtons({ mitSend, mitBack, handleBack}) {
const { formData, resetFormData } = useFormData()
const [isSending, setIsSending] = useState(false)
const [showModal, setShowModal] = useState(false)
const [modalMessage, setModalMessage] = useState('')
const [modalType, setModalType] = useState('error') // 'error' oder 'success'
const [isModalHtml, setIsModalHtml] = useState(false)
const [showConfirmModal, setShowConfirmModal] = useState(false)
const [isSuccessModal, setIsSuccessModal] = useState(false)
const handleSenden = async () => {
console.log("Alle Formulardaten: ", formData)
setIsSending(true)
try {
// API URL aus Environment Variable
const APIURL = import.meta.env.VITE_API_URL
const username = import.meta.env.VITE_API_USERNAME
const password = import.meta.env.VITE_API_PASSWORD
if (!APIURL) {
throw new Error('API URL nicht konfiguriert.')
}
// URL-Parameter für ID auslesen
const urlParams = new URLSearchParams(window.location.search)
const id = urlParams.get('id')
if (!id) {
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)
// Formulardaten zu Backend-Feldern mappen
// Basis-Status
if (formData.stattgefunden === 'ja') {
backendData.append('stattgefunden', '1')
// Spenden-Informationen
if (formData.spendenArt) {
switch (formData.spendenArt) {
case 'bar':
backendData.append('bezahlt', `Kasse ${formData.betrag}€)`)
break
case 'ueber':
backendData.append('bezahlt', 'Überweisung')
break
case 'kasse':
backendData.append('bezahlt', 'Spendenkässle')
break
case 'keine':
backendData.append('bezahlt', 'keine')
break
}
}
} else if (formData.stattgefunden === 'nein') {
backendData.append('stattgefunden', '0')
backendData.append('bezahlt', 'keine')
// Grund für Ausfall
if (formData.abgesagt === 'abgesagt') {
backendData.append('status', 3)
} else if (formData.abgesagt === 'verschoben') {
backendData.append('wtermin', formData.neuesDatum || '1900-01-01 00:00:00')
}
}
// Bemerkungen
backendData.append('remark', formData.bemerkungen || '')
// Besucher
backendData.append('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 ===")
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("========================")
// HTTP Basic Authentication Header
const headers = {}
if (username && password) {
const credentials = btoa(`${username}:${password}`)
headers['Authorization'] = `Basic ${credentials}`
}
// Backend-Aufruf
const response = await fetch(APIURL, {
method: 'POST',
headers: headers,
body: backendData
})
if (!response.ok) {
throw new Error(`Server-Fehler: ${response.status}`)
}
// Backend Response auslesen
const responseText = await response.text()
console.log('Backend Response (raw):', responseText)
let result
try {
// Versuche JSON zu parsen
result = JSON.parse(responseText)
console.log('Backend Response (parsed):', result)
} catch {
// Falls kein JSON, behandle als einfachen Text
console.log('Backend Response ist kein JSON, behandle als Text')
result = { success: responseText.trim() === 'true', raw: responseText }
}
// Erfolg prüfen - sowohl JSON als auch Text-Format unterstützen
const isSuccess = result.success === true ||
result.success === 'true' ||
responseText.trim() === 'true'
if (isSuccess) {
setModalType('success')
setModalMessage('✅ Daten erfolgreich gespeichert!')
setIsSuccessModal(true)
setShowModal(true)
} else {
throw new Error(result.message || result.error || 'Unbekannter Fehler beim Speichern')
}
} catch (error) {
console.error('Fehler beim Speichern:', error)
setModalType('error')
setModalMessage(`❌ Fehler beim Speichern: ${error.message}`)
setIsSuccessModal(false)
setShowModal(true)
} finally {
setIsSending(false)
}
}
const handleAbbruch = () => {
setShowConfirmModal(true)
}
const confirmAbbruch = () => {
setShowConfirmModal(false)
// Versuche das Browser-Fenster zu schließen (wie beim Speichern)
window.close()
// Fallback: Falls window.close() nicht funktioniert
setTimeout(() => {
if (!window.closed) {
// Wenn das Fenster nicht geschlossen werden kann, zur vorherigen Seite
window.location.reload()
}
}, 100)
}
const cancelAbbruch = () => {
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'
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')
}
} catch (error) {
// Letzter Fallback: Als Modal anzeigen
console.warn('Anleitung konnte nicht in neuem Fenster geöffnet werden:', error)
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
`)
setIsModalHtml(false)
setShowModal(true)
}
}
const closeModal = () => {
if (isSuccessModal) {
// Bei erfolgreichem Speichern: Browser-Fenster schließen
console.log('Schließe Browser-Fenster nach erfolgreichem Speichern...')
// Formular zurücksetzen (für den Fall, dass das Schließen nicht funktioniert)
resetFormData()
// Browser-Fenster schließen
window.close()
// Fallback: Falls window.close() nicht funktioniert (z.B. bei direkt aufgerufenen URLs)
// Nach 100ms prüfen, ob das Fenster noch offen ist
setTimeout(() => {
// Wenn das Fenster noch offen ist, zur vorherigen Seite oder Neustart
if (!window.closed) {
console.log('Fenster konnte nicht geschlossen werden, führe Neustart durch...')
window.location.reload()
}
}, 100)
} else {
// Normales Modal schließen
setShowModal(false)
setModalMessage('')
setIsModalHtml(false)
setIsSuccessModal(false)
}
}
const sendeButton = mitSend ?
<button
className="btnsend"
onClick={handleSenden}
disabled={isSending}
>
{isSending ? 'Speichert...' : 'Senden'}
</button>
: null
const backButton = mitBack ?
<button className="btnback" onClick={handleBack}>
Zurück
</button>
: null
return (
<>
<div className="lastbuttons">
<button className="btnabbruch" onClick={handleAbbruch}>
Abbruch
</button>
<button className="btnanleit" onClick={handleAnleitung}>
Anleitung
</button>
{backButton}
{sendeButton}
</div>
{showModal && (
<Modal
message={modalMessage}
onClose={closeModal}
type={modalType}
isHtml={isModalHtml}
/>
)}
{showConfirmModal && (
<ConfirmModal
message={`Möchten Sie wirklich abbrechen?
Alle eingegebenen Daten gehen verloren und werden nicht gespeichert.`}
onClose={cancelAbbruch}
onConfirm={confirmAbbruch}
type="warning"
title="Vorgang abbrechen?"
/>
)}
</>
)
}

View File

@@ -0,0 +1,13 @@
export default function LastLine({version, vdate}) {
return(
<div className = "lastline">
<div className = "mailto">
<a href="mailto:rexfue@gmail.com">mailto:rexfue@gmail.com</a>
</div>
<div className = "versn">
Version: {version} vom {vdate}
</div>
</div>
)
}

View File

@@ -0,0 +1,220 @@
/* Modal Overlay - covers the entire screen */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(2px);
}
/* Modal Content Box */
.modal-content {
background: white;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
max-width: 400px;
width: 90%;
max-height: 90vh;
overflow: hidden;
animation: modalSlideIn 0.2s ease-out;
}
/* Modal Type Variants */
.modal-success {
border-left: 5px solid #28a745;
}
.modal-error {
border-left: 5px solid #dc3545;
}
.modal-warning {
border-left: 5px solid #ffc107;
}
.modal-info {
border-left: 5px solid #17a2b8;
}
/* Type-specific header colors */
.modal-success .modal-header {
background: #d4edda;
border-bottom-color: #c3e6cb;
}
.modal-error .modal-header {
background: #f8d7da;
border-bottom-color: #f5c6cb;
}
.modal-warning .modal-header {
background: #fff3cd;
border-bottom-color: #ffeaa7;
}
.modal-info .modal-header {
background: #d1ecf1;
border-bottom-color: #bee5eb;
}
/* Type-specific title colors */
.modal-success .modal-title {
color: #155724;
}
.modal-error .modal-title {
color: #721c24;
}
.modal-warning .modal-title {
color: #856404;
}
.modal-info .modal-title {
color: #0c5460;
}
@keyframes modalSlideIn {
from {
transform: scale(0.9) translateY(-10px);
opacity: 0;
}
to {
transform: scale(1) translateY(0);
opacity: 1;
}
}
/* Modal Header */
.modal-header {
background: #f8f9fa;
padding: 16px 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
margin: 0;
color: #333;
font-size: 1.2rem;
font-weight: 600;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
}
.modal-close:hover {
background: #e9ecef;
color: #333;
}
/* Modal Body */
.modal-body {
padding: 20px;
color: #333;
line-height: 1.5;
text-align: center;
}
/* Modal Footer */
.modal-footer {
background: #f8f9fa;
padding: 16px 20px;
border-top: 1px solid #e9ecef;
display: flex;
justify-content: center;
gap: 12px;
}
/* Confirm Modal - Multiple Buttons */
.modal-footer.confirm-buttons {
justify-content: center;
}
.modal-footer.confirm-buttons .modal-button:first-child {
margin-right: 20px;
}
.modal-footer.confirm-buttons .modal-button {
margin: 0;
}
.modal-button {
background: #007bff;
color: white;
border: none;
padding: 8px 24px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
min-width: 80px;
}
.modal-button:hover {
background: #0056b3;
transform: translateY(-1px);
}
.modal-button:focus {
outline: 2px solid #80bdff;
outline-offset: 2px;
}
.modal-button-secondary {
background: #6c757d;
color: white;
}
.modal-button-secondary:hover {
background: #545862;
}
.modal-button-danger {
background: #dc3545;
color: white;
}
.modal-button-danger:hover {
background: #c82333;
}
/* Type-specific button for single button modals */
.modal-content:not(.modal-confirm) .modal-footer {
justify-content: center;
gap: 0;
}
/* Responsive Design */
@media (max-width: 480px) {
.modal-content {
margin: 20px;
width: calc(100% - 40px);
}
.modal-header,
.modal-body,
.modal-footer {
padding: 12px 16px;
}
}

View File

@@ -0,0 +1,74 @@
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 }) {
if (!isOpen) return null
const handleOverlayClick = (e) => {
if (e.target === e.currentTarget) {
onClose()
}
}
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose()
}
if (e.key === 'Enter') {
onClose()
}
}
// Automatischer Titel basierend auf type
const getDefaultTitle = () => {
switch(type) {
case 'success': return 'Erfolg'
case 'error': return 'Fehler'
case 'warning': return 'Warnung'
case 'info': return 'Information'
default: return 'Meldung'
}
}
// CSS-Klasse basierend auf type
const getModalClass = () => {
return `modal-content modal-${type}`
}
const displayTitle = title || getDefaultTitle()
// Content-Behandlung: HTML oder normaler Text
const getDisplayContent = () => {
if (children) return children
if (message) {
if (isHtml) {
// HTML-Inhalt sicher anzeigen
return <div dangerouslySetInnerHTML={{ __html: message }} />
} else {
// Normaler Text mit Zeilenumbrüchen
return <p style={{whiteSpace: 'pre-line'}}>{message}</p>
}
}
return null
}
return (
<div className="modal-overlay" onClick={handleOverlayClick} onKeyDown={handleKeyDown} tabIndex={0}>
<div className={getModalClass()}>
<div className="modal-header">
<h3 className="modal-title">{displayTitle}</h3>
<button className="modal-close" onClick={onClose}>&times;</button>
</div>
<div className="modal-body">
{getDisplayContent()}
</div>
<div className="modal-footer">
<button className="modal-button" onClick={onClose} autoFocus>OK</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,48 @@
import { useState } from 'react'
import { useFormData } from '../FormContext'
export default function Spende({ onNext, isCompleted }) {
const { formData, updateFormData } = useFormData()
// Initialisiere State mit dem Wert aus formData (falls vorhanden)
const [spendenArt, setSpendenArt] = useState(formData.spendenArt || '')
const handleRadioChange = (e) => {
const art = e.target.value
setSpendenArt(art)
updateFormData('spendenArt', art)
onNext()
}
return (
<section>
<h3>Eine Spende</h3>
<div className="radiogroup">
<label>
<input type="radio" name="spende" value="bar"
checked={spendenArt === 'bar'} onChange={handleRadioChange} disabled={isCompleted}
/>
ist in bar eingegangen
</label>
<label>
<input type="radio" name="spende" value="ueber"
checked={spendenArt === 'ueber'} onChange={handleRadioChange} disabled={isCompleted}
/>
wird überwiesen
</label>
<label>
<input type="radio" name="spende" value="kasse"
checked={spendenArt === 'kasse'} onChange={handleRadioChange} disabled={isCompleted}
/>
ist in der Spendenkasse
</label>
<label>
<input type="radio" name="spende" value="not"
checked={spendenArt === 'not'} onChange={handleRadioChange} disabled={isCompleted}
/>
ist nicht vorgesehen
</label>
</div>
</section>
)
}

View File

@@ -0,0 +1,67 @@
import { useState } from 'react'
import { useFormData } from '../FormContext/'
import Modal from './Modal'
export default function Verschoben({onNext, isCompleted}) {
const { formData, updateFormData } = useFormData()
// State für das selektierte Datum
const [selectedDate, setSelectedDate] = useState(formData.neuesDatum || '')
const [showModal, setShowModal] = useState(false)
const handleOK = () => {
if (selectedDate) {
updateFormData('neuesDatum', selectedDate)
onNext()
} else {
setShowModal(true)
}
}
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
handleOK()
}
}
const closeModal = () => {
setShowModal(false)
}
let now = new Date()
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
return (
<>
<section>
<h3>Verschoben auf:</h3>
<div className="verschoben">
<input
type="datetime-local"
id="datetime"
value={selectedDate}
onChange={(e) => {
let selD = e.target.value
selD = selD + ':00'
selD = selD.replace('T',' ')
setSelectedDate(selD)
}}
onKeyDown={handleKeyDown}
min={now.toISOString().slice(0,16)}
disabled={isCompleted}
/>
<button className="okbutton" onClick={handleOK}>OK</button>
</div>
</section>
<Modal
isOpen={showModal}
onClose={closeModal}
title="Datum erforderlich"
>
<p>Bitte ein Datum auswählen</p>
</Modal>
</>
)
}

View File

@@ -0,0 +1,69 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
/* line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87); */
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
/*
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
*/
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
/*
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
*/

View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
// <StrictMode>
<App />
// </StrictMode>,
)

View File

@@ -0,0 +1,32 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
return {
plugins: [react()],
base: mode === 'production' ? '/beoanswer/' : '/', // Nur in Production Unterverzeichnis
server: {
// Proxy nur für Development
proxy: mode === 'development' ? {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
} : {}
},
build: {
// Production Build Optimierungen
sourcemap: false,
minify: 'terser',
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom']
}
}
}
}
}
})

View File

View File

View File

@@ -5,12 +5,6 @@ Checked per cron jeden Tag die SOFUE - Datenbank. Prüft, ob 'gestern' eine
Führung hätte stattfinden sollen. Wenn ja, wird der BEO der Führung per mail
benachrichtigt mit der Bitte, die Nachbearbeitungs-Webseite auszufüllen.
*******************
2025-06-16:
Da leider kein CRON auf dem Webserver läuft, wird dieses Prgramm bis auf
Weiteres nicht weiter gepflegt, d.h. es wird nicht ausgeführt! Ebenso kann
'beoanswer' nicht ausgeführt werden.
*******************
TODO
@@ -26,20 +20,21 @@ V 0.0 2019-02-04 rxf
*/
"use strict"
const DEVELOP=1; // 1 -> Entwicklung 0-> Produktion
const DAYS=9;
const DEVELOP=0; // 1 -> Entwicklung 0-> Produktion
const DAYS=2;
const nodemailer = require('nodemailer');
const moment = require('moment');
const axios = require('axios');
const mysql = require('mysql2/promise');
const beo_Url = 'beoanswer/beoanswer.php?fdate=';
const beo_Url = 'beoanswer/beoanswer.php?id=';
const Url = DEVELOP ? 'http://localhost:8081/' : 'https://sternwarte-welzheim.de/';
const DB_host = process.env.DB_HOST || 'localhost';
const DB_port = process.env.DB_PORT || 3306;
const DB_user = process.env.DB_USER || 'root';
const DB_pass = process.env.DB_PASS || 'SFluorit';
const DB_dbase = process.env.DB_NAME || 'sternwarte';
const DB_host = DEVELOP ? 'localhost' : 'localhost';
const DB_port = DEVELOP ? 3306 : 3306;
const DB_user = DEVELOP ? 'root' : 'admin_310927';
const DB_pass = DEVELOP ? 'SFluorit' : '5D5u49cKNFqf';
const DB_dbase = DEVELOP ? 'sternwarte' : 'db310927';
const transporter = DEVELOP ? nodemailer.createTransport({
host: 'localhost',
@@ -90,10 +85,10 @@ function send2BEO(info) {
// to: info.email,
to: 'rexfue@gmail.com',
subject: 'Sonderführung vom '+info.date,
text: 'Hallo ' + info.name +',\n\n'
text: 'Hallo ' + info.name + '(' + info.email + '),\n\n'
+ 'Du hattest gestern Führung! '
+ 'Bitte fülle folgendes Webformular aus:\n\n'
+ Url + beo_Url + info.date + '&id=' + info.id
+ Url + beo_Url + info.id
+ '\n\nBitte nur über diesen Link zugreifen (oder exakt abschreiben),\n'
+ 'da sonst die Zuordnung nicht hergestellt werden kann.\n'
+ 'Besten Dank.\n\nGrüße vom Sonderführungsteam'
@@ -110,16 +105,18 @@ function send2BEO(info) {
}
async function main() {
console.log(DB_host, DB_port, DB_user, DB_pass, DB_dbase);
const yesterday = moment().subtract(DAYS, 'd').format('YYYY-MM-DD');
console.log('Yesterday:', yesterday)
// console.log(DB_host, DB_port, DB_user, DB_pass, DB_dbase);
console.log('Start: ' + moment().format('YYYY-MM-DD HH:mm'))
const connection = await mysql.createConnection({
host: DB_host,
port: DB_port,
// host: DB_host,
// port: DB_port,
user: DB_user,
password: DB_pass,
database: DB_dbase,
socketPath: '/var/lib/mysql/mysql.sock'
});
const yesterday = moment().subtract(DAYS, 'd').format('YYYY-MM-DD');
console.log('Yesterday:', yesterday)
await fetchDatafromDB(connection, yesterday);
console.log("All done");
}

View File

@@ -225,3 +225,7 @@ th, td {
justify-content: space-between;
margin-top: 20px;
}
.absdat {
font-size: 60%;
}

View File

@@ -30,6 +30,7 @@ document.addEventListener('DOMContentLoaded', async () => {
bodytext = ""
betreff = ""
const TEXTE = {
absagebutton: (abg) => `Absage ${abg ? `wurde gesendet am ${abg}` : `senden`}`,
absagetext: "Absage an alle angemeldeten Besucher senden.",
bittegrund: "Die Führung wird abgesagt wegen:",
schonabgesagt: "Absage schon gesendet. Nochmal senden?",
@@ -50,7 +51,7 @@ Beobachtergruppe Sternwarte Welzheim`
ids: []
};
let abgesagt = false;
let abgesagt = null
let actualdate;
let isSmallScreen = false
let DateTime = luxon.DateTime
@@ -97,9 +98,11 @@ Beobachtergruppe Sternwarte Welzheim`
}
async function storeAbsage(ids) {
const update = { cmd: 'UPDATE', field: 'abgesagt', ids: ids, values: [1] };
const dt = DateTime.now()
const jetzt = dt.toFormat('yyyy-LL-dd HH:mm')
const update = { cmd: 'UPDATE', field: 'abgesagt', ids: ids, values: [`"${jetzt}"`] };
await putToDbase(update);
abgesagt = true;
abgesagt = jetzt
}
async function getDetailText(id) {
@@ -132,7 +135,6 @@ Beobachtergruppe Sternwarte Welzheim`
actualdate = date;
liste.emails = [];
liste.ids = [];
abgesagt = true;
let column = query.storno ? "col-2" : "col-3";
const anmeldungen = await fetchFromDbase({cmd:'GET_ANMELD', id:date});
let besucher = 0;
@@ -143,9 +145,7 @@ Beobachtergruppe Sternwarte Welzheim`
besucher += parseInt(e.anzahl);
liste.emails.push(e.email);
liste.ids.push(e.id);
if (e.abgesagt !== '1') {
abgesagt = false;
}
abgesagt = e.abgesagt ? e.abgesagt.slice(0,16) : null
// const selected = e.teilgenommen === "1" ? "checked" : "";
const row = document.createElement('tr');
@@ -174,11 +174,7 @@ Beobachtergruppe Sternwarte Welzheim`
$('#tabAnmeld tbody').appendChild(row);
}
if (abgesagt) {
$('#absagen').innerHTML = 'Absage<br />wurde gesendet';
} else {
$('#absagen').innerHTML = 'Absage senden';
}
$('#absagen').innerHTML = TEXTE.absagebutton(abgesagt)
if (besucher !== 0) {
const sumRow = document.createElement('tr');
@@ -288,7 +284,7 @@ Beobachtergruppe Sternwarte Welzheim`
}
console.log("Mailret: ", mailRet, "Gesendet an: ", liste.emails)
$('#absagen').innerHTML = 'Absage<br />wurde gesendet';
$('#absagen').innerHTML = TEXTE.absagebutton(abgesagt)
$('#absagedialog').close();
});

View File

@@ -1,12 +1,15 @@
// VersiosNummern und -Geschichte
const VERSION="1.8.1";
const VDATE="2025-10-20";
const VERSION="1.9.0";
const VDATE="2025-11-07";
/* History
Rev. Datum Entwickler
1.9.0 2025-11-07 rxf
- Datum der Absge mit in der DB (abgesagt). Wird angezeigt, wenn abgesagt wurde.
1.8.1 2025-10-19 rxf
- Errormeldung, wenn bei 'anmeld.js' die Abmeldung nicht rausgeht

View File

@@ -45,7 +45,7 @@ $(document).ready(() => {
return await response.json();
}
// Dat Führungsdatum extrahieren
// Das Führungsdatum extrahieren
const buildDatum = async (tn, short) => {
const person = tn.anzahl === '1' ? 'Person' : 'Personen'
const datum = await fetchFromDbase({cmd: 'GET_ONE_DATE', fid: tn.fid})
@@ -244,23 +244,6 @@ Mit freundlichen Grüßen
Beobachterteam der Sternwarte Welzheim
www.sternwarte-welzheim.de
`
/* let body_html = `Sehr geehrte Dame, sehr geehrter Herr,<br /><br />`
if(!storno) {
body_html += `hiermit bestätigen wir die <strong>Umbuchung</strong> Ihrer Führung auf der Sternwarte Welzheim.<br />
<br />Sie wurden umgebucht auf:<br /><br />${fdatum}<br /><br />
Bitte bringen Sie diese Bestätigung als Ausdruck oder digital zur Führung mit.<br /><br />
Die Führung findet NUR bei sternklarem Himmel statt. Falls der Himmel bedeckt ist
und die Führung ausfällt, erhalten Sie bis spätestens eine Stunde vor Führungsbeginn
eine Email. Sie können sich dann gerne zu einer anderen Führung neu anmelden.<br /><br />
Allen Teilnehmern/-innen wird dringend empfohlen, eine FFP2-Maske, die Mund und Nase
bedeckt, zu tragen.<br />
Sollten Sie Fragen haben senden Sie bitte eine Email an <a href="mailto:anmeldung@sternwarte-welzheim.de">anmeldung@sternwarte-welzheim.de</a>`
} else {
body_html += `hiermit bestätigen wir die <strong>Stornierung</strong> Ihrer Führung auf der Sternwarte Welzheim vom<br />`
body_html += `${fdatum}.`
}
body_html += `<br /><br />Mit freundlichen Grüßen<br />Beobachterteam der Sternwarte Welzheim<br /><a href="https://www.sternwarte-welzheim.de">www.sternwarte-welzheim.de</a>`
*/
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)
@@ -309,28 +292,6 @@ Besucher: ${tln.name} ${tln.vorname}`
// alle Anmeldungen ab fdatum in ein Array holen
let fuehrungen = await fetchFromDbase({cmd: 'GET_ALLTEILN', fdatum: fdatum})
setEvent(fuehrungen)
// // Media Query einbauen:
// let x = window.matchMedia("(max-width: 800px)");
// switchText(x.matches);
// x.addEventListener("change", async (e) => {
// switchText(e.matches);
// // await showAktAnmeldungen(actualdate);
// });
// let curtime = moment().subtract(14,'days').format("YYYYMMDD");
// // let curtime = moment().format("YYYYMMDD");
// console.log(curtime)
// const y = await fetchFromDbase({cmd:'GET_DATES', anzahl:n, date: curtime});
// const last = await fetchFromDbase({cmd:'GET_LASTANMELDUNG', date: curtime});
// const sel = await buildFuehrungsDates(y, last);
// await showAktAnmeldungen(y[sel].datum);
// console.log(y);
// if(params.name != 'Null') {
// await findName(params.name)
// }
// if(params.double == 'true') {
// await showDoubles(curtime);
// }
}