Compare commits
2 Commits
75a6988248
...
e3bb5d36b9
| Author | SHA1 | Date | |
|---|---|---|---|
| e3bb5d36b9 | |||
| 9071f96f0d |
26
docs/Server-Ideen.md
Normal file
26
docs/Server-Ideen.md
Normal 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
BIN
docs/Server-Ideen.pdf
Normal file
Binary file not shown.
8
sternwarte/.vscode/launch.json
vendored
8
sternwarte/.vscode/launch.json
vendored
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
BIN
sternwarte/Anleitungen/Anleitung für Wireguard (VPN).pdf
Normal file
BIN
sternwarte/Anleitungen/Anleitung für Wireguard (VPN).pdf
Normal file
Binary file not shown.
@@ -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;
|
||||
}
|
||||
|
||||
14
sternwarte/beoanswer/.env.example
Normal file
14
sternwarte/beoanswer/.env.example
Normal 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
|
||||
9
sternwarte/beoanswer/.env.production
Normal file
9
sternwarte/beoanswer/.env.production
Normal 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
30
sternwarte/beoanswer/.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Environment variables
|
||||
.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?
|
||||
13
sternwarte/beoanswer/ACHTUNG.md
Normal file
13
sternwarte/beoanswer/ACHTUNG.md
Normal 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
|
||||
123
sternwarte/beoanswer/DEPLOYMENT.md
Normal file
123
sternwarte/beoanswer/DEPLOYMENT.md
Normal 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
|
||||
12
sternwarte/beoanswer/Dockerfile
Normal file
12
sternwarte/beoanswer/Dockerfile
Normal 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"]
|
||||
273
sternwarte/beoanswer/README.md
Normal file
273
sternwarte/beoanswer/README.md
Normal 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
|
||||
114
sternwarte/beoanswer/cors-proxy.php
Normal file
114
sternwarte/beoanswer/cors-proxy.php
Normal 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;
|
||||
?>
|
||||
12
sternwarte/beoanswer/docker-compose.yml
Normal file
12
sternwarte/beoanswer/docker-compose.yml
Normal 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"]
|
||||
|
||||
29
sternwarte/beoanswer/eslint.config.js
Normal file
29
sternwarte/beoanswer/eslint.config.js
Normal 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_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
13
sternwarte/beoanswer/index.html
Normal file
13
sternwarte/beoanswer/index.html
Normal 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
2900
sternwarte/beoanswer/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
sternwarte/beoanswer/package.json
Normal file
30
sternwarte/beoanswer/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
147
sternwarte/beoanswer/public/anleitung.html
Normal file
147
sternwarte/beoanswer/public/anleitung.html
Normal 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>
|
||||
1
sternwarte/beoanswer/public/vite.svg
Normal file
1
sternwarte/beoanswer/public/vite.svg
Normal 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
398
sternwarte/beoanswer/sofueDB.php
Executable 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 . "< Dbase: >" . $dbase . "< 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);
|
||||
|
||||
?>
|
||||
|
||||
122
sternwarte/beoanswer/src/App.css
Normal file
122
sternwarte/beoanswer/src/App.css
Normal 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;
|
||||
}
|
||||
330
sternwarte/beoanswer/src/App.jsx
Normal file
330
sternwarte/beoanswer/src/App.jsx
Normal 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
|
||||
57
sternwarte/beoanswer/src/FormContext.jsx
Normal file
57
sternwarte/beoanswer/src/FormContext.jsx
Normal 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
|
||||
}
|
||||
1
sternwarte/beoanswer/src/assets/react.svg
Normal file
1
sternwarte/beoanswer/src/assets/react.svg
Normal 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 |
44
sternwarte/beoanswer/src/components/Bemerkungen.jsx
Normal file
44
sternwarte/beoanswer/src/components/Bemerkungen.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
57
sternwarte/beoanswer/src/components/BesucherBar.jsx
Normal file
57
sternwarte/beoanswer/src/components/BesucherBar.jsx
Normal 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}
|
||||
/>
|
||||
{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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
65
sternwarte/beoanswer/src/components/ConfirmModal.jsx
Normal file
65
sternwarte/beoanswer/src/components/ConfirmModal.jsx
Normal 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}>×</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>
|
||||
)
|
||||
}
|
||||
41
sternwarte/beoanswer/src/components/FandStattVer.jsx
Normal file
41
sternwarte/beoanswer/src/components/FandStattVer.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
301
sternwarte/beoanswer/src/components/LastButtons.jsx
Normal file
301
sternwarte/beoanswer/src/components/LastButtons.jsx
Normal 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?"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
13
sternwarte/beoanswer/src/components/LastLine.jsx
Normal file
13
sternwarte/beoanswer/src/components/LastLine.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
220
sternwarte/beoanswer/src/components/Modal.css
Normal file
220
sternwarte/beoanswer/src/components/Modal.css
Normal 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;
|
||||
}
|
||||
}
|
||||
74
sternwarte/beoanswer/src/components/Modal.jsx
Normal file
74
sternwarte/beoanswer/src/components/Modal.jsx
Normal 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}>×</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{getDisplayContent()}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="modal-button" onClick={onClose} autoFocus>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
sternwarte/beoanswer/src/components/Spende.jsx
Normal file
48
sternwarte/beoanswer/src/components/Spende.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
67
sternwarte/beoanswer/src/components/Verschoben.jsx
Normal file
67
sternwarte/beoanswer/src/components/Verschoben.jsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
69
sternwarte/beoanswer/src/index.css
Normal file
69
sternwarte/beoanswer/src/index.css
Normal 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;
|
||||
}
|
||||
}
|
||||
*/
|
||||
10
sternwarte/beoanswer/src/main.jsx
Normal file
10
sternwarte/beoanswer/src/main.jsx
Normal 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>,
|
||||
)
|
||||
32
sternwarte/beoanswer/vite.config.js
Normal file
32
sternwarte/beoanswer/vite.config.js
Normal 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']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
0
sternwarte/checkfuehrung/Entwicklung
Normal file
0
sternwarte/checkfuehrung/Entwicklung
Normal file
0
sternwarte/checkfuehrung/Produktion
Normal file
0
sternwarte/checkfuehrung/Produktion
Normal 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");
|
||||
}
|
||||
|
||||
@@ -225,3 +225,7 @@ th, td {
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.absdat {
|
||||
font-size: 60%;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user