Compare commits

...

7 Commits

Author SHA1 Message Date
rxf
7527a189ce Rückbau auf lokal
ACHTUNG Text
2025-10-29 17:35:19 +01:00
rxf
e7b9d27314 'Senden'-Button kam zu früh - ist behoben
Anleitung als HTML extern
2025-10-29 15:04:20 +01:00
rxf
b53a5ae80a 1.0.1 2025-10-29 14:02:05 +01:00
rxf
4aa6ab3eb5 V 1.0.0 erste sauber lauffähige Version 2025-10-29 14:01:16 +01:00
rxf
8fb01360be Fast Alles drin außer echtes Abspeicher und Backtickern 2025-10-28 17:52:43 +01:00
rxf
dd32ad585e Radiobuttons OHNE OK 2025-10-27 17:24:48 +01:00
rxf
1c153db116 Default Zeit ist ' ' und nicht 1900-01-01 2025-10-27 17:18:53 +01:00
22 changed files with 2022 additions and 1547 deletions

14
.env.example Normal file
View File

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

5
.env.production Normal file
View File

@@ -0,0 +1,5 @@
# Production Environment Variables
VITE_API_URL=https://dein-produktions-server.com/intern/sofue/php/sofueDB.php
# Optional: Debug-Modus für Production meist ausgeschaltet
# VITE_DEBUG=false

6
.gitignore vendored
View File

@@ -12,6 +12,12 @@ dist
dist-ssr
*.local
# Environment variables
.env
# CORS-Proxy Konfiguration (enthält Credentials)
cors-config.php
# Editor directories and files
.vscode/*
!.vscode/extensions.json

13
ACHTUNG.md Normal file
View File

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

123
DEPLOYMENT.md Normal file
View File

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

275
README.md
View File

@@ -1,16 +1,273 @@
# React + Vite
# BeoAnswer React App
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Eine React-Anwendung zur Nachbearbeitung von Sonderführungen mit Backend-Integration.
Currently, two official plugins are available:
## 📋 Features
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
- **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**
## React Compiler
## 🚀 Quick Start
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
### Voraussetzungen
## Expanding the ESLint configuration
- Node.js (v16 oder höher)
- npm oder yarn
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
### 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
cors-proxy.php Normal file
View File

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

1562
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,15 @@
{
"name": "beoanswer_react",
"private": true,
"version": "0.0.0",
"version": "1.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
"build:prod": "vite build --mode production",
"preview": "vite preview",
"preview:prod": "vite preview --mode production",
"lint": "eslint ."
},
"dependencies": {
"react": "^19.1.1",
@@ -22,6 +24,7 @@
"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
public/anleitung.html Normal file
View File

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

398
sofueDB.php Executable file
View File

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

View File

@@ -1,5 +1,6 @@
import { useState } from 'react'
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'
@@ -11,40 +12,173 @@ import Verschoben from './components/Verschoben.jsx'
function AppContent() {
const datum = "2025-10-23"
const name = "Meiehofer"
const version = "1.0.0"
const vdate = "2025-11-23"
//const isBar = true
// 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 } = useFormData()
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) => {
setPfad(auswahl)
setSchritt(1)
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} iscompleted={schritt > 1} />
<FandStattVer key="fandstatt" left='ja' right='nein' title='Fand die Führung statt?'
onNext={handleFandStattVerNext}
setbackButton={setBackButton}
iscompleted={schritt > 1} />
)
if (schritt === 0)
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') {
@@ -73,13 +207,6 @@ function AppContent() {
)
}
// Schritt 5 (bei Bar-Spende) oder Schritt 4 (bei anderen Spenden): unterste Buttons
const endeSchritt = (formData.spendenArt === 'bar') ? 5 : 4
if (schritt >= endeSchritt) {
components.push(<LastButtons key='lastbutt' />
)
}
}
// NEIN - Pfad
if (pfad === 'nein') {
@@ -87,27 +214,96 @@ function AppContent() {
// 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} iscompleted={schritt > 1} />
<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.stattgefunden === 'verschoben') {
if (schritt >= 2 && formData.abgesagt === 'verschoben') {
components.push(<Verschoben key='verschoben' onNext={handleNext} isCompleted={schritt > 2} />
)
}
// Schritt 4 (bei verschoben) oder Schritt 3 (bei absage): unterste Buttons
const endeNeinSchritt = (formData.stattgefunden === 'verschoben') ? 3 : 2
if (schritt >= endeNeinSchritt) {
components.push(<LastButtons key='lastbutt' />
)
}
}
// 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>

View File

@@ -6,40 +6,38 @@ import { createContext, useContext, useState } from 'react'
const FormContext = createContext()
export function FormProvider({ children }) {
// console.log('🚀 FormProvider initialisiert')
const [formData, setFormData] = useState({
stattgefunden: '',
besucherAnzahl: '',
besucher: '', // war: besucherAnzahl
spendenArt: '',
barspende: '',
betrag: '', // war: barspende
bemerkungen: '',
neuertermin: '1900-01-01T00:00',
neuesDatum: '', // war: neuertermin
abgesagt: '', // für abgesagt/verschoben
// Weitere Felder können hier hinzugefügt werden
})
const updateFormData = (field, value) => {
//console.log('📝 FormContext UPDATE:', field, '=', value)
setFormData(prev => {
const newData = {
...prev,
[field]: value
}
//console.log('📊 FormContext NEU:', newData)
return newData
})
}
const resetFormData = () => {
//console.log('🔄 FormContext RESET')
setFormData({
stattgefunden: '',
besucherAnzahl: '',
besucher: '',
spendenArt: '',
barspende: '',
betrag: '',
bemerkungen: '',
neuertermin: '1900-01-01T00:00'
neuesDatum: '',
abgesagt: ''
})
}

View File

@@ -12,13 +12,32 @@ export default function Bemerkungen({ onNext, isCompleted }) {
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" />
<button className="okbutton" onClick={handleOK}>OK</button>
<textarea
className="beminfeld"
value={wert}
onChange={(e) => setWert(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Hier können Sie optionale Bemerkungen zur Führung eingeben..."
disabled={isCompleted}
/>
<button
className="okbutton"
onClick={handleOK}
disabled={isCompleted}
>
OK
</button>
</div>
</section>
)

View File

@@ -6,8 +6,8 @@ export default function BesucherBar({ title, euro, onNext, isCompleted }) {
const { formData, updateFormData } = useFormData()
// Bestimme das Feld basierend auf dem Titel
const fieldName = title.includes('Barspende') ? 'barspende' : 'besucherAnzahl'
// Bestimme Feldname basierend auf dem title
const fieldName = title.includes('Barspende') ? 'betrag' : 'besucher'
const [wert, setWert] = useState(formData[fieldName] || '')
const [showModal, setShowModal] = useState(false)

View File

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

View File

@@ -2,50 +2,40 @@ import { useState } from 'react'
import { useFormData } from '../FormContext'
import Modal from './Modal'
export default function FandStattVer({left, right, title, onNext, radioName = "fst"}) {
export default function FandStattVer({left, right, title, onNext, radioName = "fst", setbackButton}) {
const { formData, updateFormData } = useFormData()
const [auswahl, setAuswahl] = useState(formData.stattgefunden || '')
const [showModal, setShowModal] = useState(false)
const handleOK = () => {
if(!auswahl) {
setShowModal(true)
return
// 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()
}
updateFormData('stattgefunden', auswahl)
onNext(auswahl)
}
const closeModal = () => {
setShowModal(false)
}
return (
<>
<section>
<h3>{title}</h3>
<div className="fstdiv">
<label className="fsLabel">
<input type="radio" name={radioName} value={left} checked={auswahl === left}
onChange = {(e) => setAuswahl(e.target.value)} />
{left}
</label>
<label className="fsLabel">
<input type="radio" name={radioName} value={right} checked={auswahl === right}
onChange = {(e) => setAuswahl(e.target.value)} />
{right}
</label>
<button className="okbutton" onClick={handleOK}>OK</button>
</div>
</section>
<Modal
isOpen={showModal}
onClose={closeModal}
title="Auswahl erforderlich"
>
<p>Bitte eine Option wählen</p>
</Modal>
</>
<section>
<h3>{title}</h3>
<div className="fstdiv">
<label className="fsLabel">
<input type="radio" name={radioName} value={left} checked={auswahl === left}
onChange={handleRadioChange} />
{left}
</label>
<label className="fsLabel">
<input type="radio" name={radioName} value={right} checked={auswahl === right}
onChange={handleRadioChange} />
{right}
</label>
</div>
</section>
)
}

View File

@@ -1,27 +1,301 @@
import { useState } from 'react'
import { useFormData } from '../FormContext'
import Modal from './Modal'
import ConfirmModal from './ConfirmModal'
export default function LastButtons() {
const { formData } = useFormData()
const handleSenden = () => {
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 = () => {
console.log("Abbruch")
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 = () => {
console.log("Zeige Anleitung")
// Ö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)
}
}
return (
<div className="lastbuttons">
<button className="btnabbruch" onClick = {handleAbbruch}>Abbruch</button>
<button className="btnanleit" onClick = {handleAnleitung}>Anleitung</button>
<button className="btnsend" onClick = {handleSenden}>Senden</button>
</div>
const closeModal = () => {
if (isSuccessModal) {
// Bei erfolgreichem Speichern: Browser-Fenster schließen
console.log('Schließe Browser-Fenster nach erfolgreichem Speichern...')
// Formular zurücksetzen (für den Fall, dass das Schließen nicht funktioniert)
resetFormData()
// Browser-Fenster schließen
window.close()
// Fallback: Falls window.close() nicht funktioniert (z.B. bei direkt aufgerufenen URLs)
// Nach 100ms prüfen, ob das Fenster noch offen ist
setTimeout(() => {
// Wenn das Fenster noch offen ist, zur vorherigen Seite oder Neustart
if (!window.closed) {
console.log('Fenster konnte nicht geschlossen werden, führe Neustart durch...')
window.location.reload()
}
}, 100)
} else {
// Normales Modal schließen
setShowModal(false)
setModalMessage('')
setIsModalHtml(false)
setIsSuccessModal(false)
}
}
const sendeButton = mitSend ?
<button
className="btnsend"
onClick={handleSenden}
disabled={isSending}
>
{isSending ? 'Speichert...' : 'Senden'}
</button>
: null
const backButton = mitBack ?
<button className="btnback" onClick={handleBack}>
Zurück
</button>
: null
return (
<>
<div className="lastbuttons">
<button className="btnabbruch" onClick={handleAbbruch}>
Abbruch
</button>
<button className="btnanleit" onClick={handleAnleitung}>
Anleitung
</button>
{backButton}
{sendeButton}
</div>
{showModal && (
<Modal
message={modalMessage}
onClose={closeModal}
type={modalType}
isHtml={isModalHtml}
/>
)}
{showConfirmModal && (
<ConfirmModal
message={`Möchten Sie wirklich abbrechen?
Alle eingegebenen Daten gehen verloren und werden nicht gespeichert.`}
onClose={cancelAbbruch}
onConfirm={confirmAbbruch}
type="warning"
title="Vorgang abbrechen?"
/>
)}
</>
)
}

View File

@@ -25,6 +25,61 @@
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);
@@ -88,6 +143,20 @@
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 {
@@ -112,6 +181,30 @@
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 {

View File

@@ -2,7 +2,7 @@ import React from 'react'
// Import des CSS direkt hier
import './Modal.css'
export default function Modal({ isOpen, onClose, title, children }) {
export default function Modal({ isOpen = true, onClose, title, children, message, type = 'info', isHtml = false }) {
if (!isOpen) return null
const handleOverlayClick = (e) => {
@@ -20,15 +20,50 @@ export default function Modal({ isOpen, onClose, title, children }) {
}
}
// 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="modal-content">
<div className={getModalClass()}>
<div className="modal-header">
<h3 className="modal-title">{title}</h3>
<h3 className="modal-title">{displayTitle}</h3>
<button className="modal-close" onClick={onClose}>&times;</button>
</div>
<div className="modal-body">
{children}
{getDisplayContent()}
</div>
<div className="modal-footer">
<button className="modal-button" onClick={onClose} autoFocus>OK</button>

View File

@@ -6,12 +6,12 @@ export default function Verschoben({onNext, isCompleted}) {
const { formData, updateFormData } = useFormData()
// State für das selektierte Datum
const [selectedDate, setSelectedDate] = useState(formData.neuertermin || '')
const [selectedDate, setSelectedDate] = useState(formData.neuesDatum || '')
const [showModal, setShowModal] = useState(false)
const handleOK = () => {
if (selectedDate) {
updateFormData('neuertermin', selectedDate)
updateFormData('neuesDatum', selectedDate)
onNext()
} else {
setShowModal(true)
@@ -40,7 +40,12 @@ export default function Verschoben({onNext, isCompleted}) {
type="datetime-local"
id="datetime"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
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}

View File

@@ -2,6 +2,30 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
export default defineConfig(({ mode }) => {
return {
plugins: [react()],
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']
}
}
}
}
}
})