Compare commits
7 Commits
14bc991a7b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7527a189ce | |||
| e7b9d27314 | |||
| b53a5ae80a | |||
| 4aa6ab3eb5 | |||
| 8fb01360be | |||
| dd32ad585e | |||
| 1c153db116 |
14
.env.example
Normal file
14
.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
|
||||||
5
.env.production
Normal file
5
.env.production
Normal 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
6
.gitignore
vendored
@@ -12,6 +12,12 @@ dist
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|
||||||
|
# CORS-Proxy Konfiguration (enthält Credentials)
|
||||||
|
cors-config.php
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|||||||
13
ACHTUNG.md
Normal file
13
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
DEPLOYMENT.md
Normal file
123
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
|
||||||
275
README.md
275
README.md
@@ -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
|
- **Interaktive Formulare** für Führungsnachbearbeitung
|
||||||
- [@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
|
- **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
114
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;
|
||||||
|
?>
|
||||||
1562
package-lock.json
generated
1562
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "beoanswer_react",
|
"name": "beoanswer_react",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint .",
|
"build:prod": "vite build --mode production",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"preview:prod": "vite preview --mode production",
|
||||||
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.22",
|
"eslint-plugin-react-refresh": "^0.4.22",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
|
"terser": "^5.44.0",
|
||||||
"vite": "^7.1.7"
|
"vite": "^7.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
147
public/anleitung.html
Normal file
147
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>
|
||||||
398
sofueDB.php
Executable file
398
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);
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
250
src/App.jsx
250
src/App.jsx
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { FormProvider, useFormData } from './FormContext'
|
import { FormProvider, useFormData } from './FormContext'
|
||||||
|
import packageJson from '../package.json'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import FandStattVer from './components/FandStattVer.jsx'
|
import FandStattVer from './components/FandStattVer.jsx'
|
||||||
import BesucherBar from './components/BesucherBar.jsx'
|
import BesucherBar from './components/BesucherBar.jsx'
|
||||||
@@ -11,40 +12,173 @@ import Verschoben from './components/Verschoben.jsx'
|
|||||||
|
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
const datum = "2025-10-23"
|
// States für Backend-Daten
|
||||||
const name = "Meiehofer"
|
const [datum, setDatum] = useState("")
|
||||||
const version = "1.0.0"
|
const [name, setName] = useState("")
|
||||||
const vdate = "2025-11-23"
|
const [loading, setLoading] = useState(true)
|
||||||
//const isBar = 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
|
// States
|
||||||
const [schritt, setSchritt] = useState(0)
|
const [schritt, setSchritt] = useState(0)
|
||||||
const [pfad, setPfad] = useState('')
|
const [pfad, setPfad] = useState('')
|
||||||
|
|
||||||
// Hole formData aus dem Context
|
// 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:
|
// Callbacks:
|
||||||
const handleFandStattVerNext = (auswahl) => {
|
const handleFandStattVerNext = (auswahl) => {
|
||||||
setPfad(auswahl)
|
auswahl && setPfad(auswahl)
|
||||||
setSchritt(1)
|
handleNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
setSchritt((schritt) => schritt + 1)
|
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:
|
// Welche Komponeneten werden angezeigt:
|
||||||
const renderCoponents = () => {
|
const renderCoponents = () => {
|
||||||
const components = []
|
const components = []
|
||||||
|
|
||||||
// Schritt 0: ja/nein - Auswahl
|
// Schritt 0: ja/nein - Auswahl
|
||||||
components.push(
|
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
|
return components
|
||||||
|
}
|
||||||
|
|
||||||
// JA-Pfad:
|
// JA-Pfad:
|
||||||
if (pfad === 'ja') {
|
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
|
// NEIN - Pfad
|
||||||
if (pfad === 'nein') {
|
if (pfad === 'nein') {
|
||||||
@@ -87,27 +214,96 @@ function AppContent() {
|
|||||||
// Schritt 1: abgesagt / verschoben
|
// Schritt 1: abgesagt / verschoben
|
||||||
if (schritt >= 1) {
|
if (schritt >= 1) {
|
||||||
components.push(
|
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
|
// 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} />
|
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
|
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 (
|
return (
|
||||||
<div className="wrapper">
|
<div className="wrapper">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -6,40 +6,38 @@ import { createContext, useContext, useState } from 'react'
|
|||||||
const FormContext = createContext()
|
const FormContext = createContext()
|
||||||
|
|
||||||
export function FormProvider({ children }) {
|
export function FormProvider({ children }) {
|
||||||
// console.log('🚀 FormProvider initialisiert')
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
stattgefunden: '',
|
stattgefunden: '',
|
||||||
besucherAnzahl: '',
|
besucher: '', // war: besucherAnzahl
|
||||||
spendenArt: '',
|
spendenArt: '',
|
||||||
barspende: '',
|
betrag: '', // war: barspende
|
||||||
bemerkungen: '',
|
bemerkungen: '',
|
||||||
neuertermin: '1900-01-01T00:00',
|
neuesDatum: '', // war: neuertermin
|
||||||
|
abgesagt: '', // für abgesagt/verschoben
|
||||||
// Weitere Felder können hier hinzugefügt werden
|
// Weitere Felder können hier hinzugefügt werden
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateFormData = (field, value) => {
|
const updateFormData = (field, value) => {
|
||||||
//console.log('📝 FormContext UPDATE:', field, '=', value)
|
|
||||||
setFormData(prev => {
|
setFormData(prev => {
|
||||||
const newData = {
|
const newData = {
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value
|
[field]: value
|
||||||
}
|
}
|
||||||
//console.log('📊 FormContext NEU:', newData)
|
|
||||||
return newData
|
return newData
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetFormData = () => {
|
const resetFormData = () => {
|
||||||
//console.log('🔄 FormContext RESET')
|
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
stattgefunden: '',
|
stattgefunden: '',
|
||||||
besucherAnzahl: '',
|
besucher: '',
|
||||||
spendenArt: '',
|
spendenArt: '',
|
||||||
barspende: '',
|
betrag: '',
|
||||||
bemerkungen: '',
|
bemerkungen: '',
|
||||||
neuertermin: '1900-01-01T00:00'
|
neuesDatum: '',
|
||||||
|
abgesagt: ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,32 @@ export default function Bemerkungen({ onNext, isCompleted }) {
|
|||||||
onNext()
|
onNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
// Ctrl+Enter oder Cmd+Enter zum Speichern
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
|
handleOK()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="bemerkungen">
|
<section id="bemerkungen">
|
||||||
<h3>Bemerkungen (optional):</h3>
|
<h3>Bemerkungen (optional):</h3>
|
||||||
<div className="bemerkdiv">
|
<div className="bemerkdiv">
|
||||||
<textarea className="beminfeld" />
|
<textarea
|
||||||
<button className="okbutton" onClick={handleOK}>OK</button>
|
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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ export default function BesucherBar({ title, euro, onNext, isCompleted }) {
|
|||||||
|
|
||||||
const { formData, updateFormData } = useFormData()
|
const { formData, updateFormData } = useFormData()
|
||||||
|
|
||||||
// Bestimme das Feld basierend auf dem Titel
|
// Bestimme Feldname basierend auf dem title
|
||||||
const fieldName = title.includes('Barspende') ? 'barspende' : 'besucherAnzahl'
|
const fieldName = title.includes('Barspende') ? 'betrag' : 'besucher'
|
||||||
|
|
||||||
const [wert, setWert] = useState(formData[fieldName] || '')
|
const [wert, setWert] = useState(formData[fieldName] || '')
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
|
|||||||
65
src/components/ConfirmModal.jsx
Normal file
65
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,50 +2,40 @@ import { useState } from 'react'
|
|||||||
import { useFormData } from '../FormContext'
|
import { useFormData } from '../FormContext'
|
||||||
import Modal from './Modal'
|
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 { formData, updateFormData } = useFormData()
|
||||||
const [auswahl, setAuswahl] = useState(formData.stattgefunden || '')
|
|
||||||
const [showModal, setShowModal] = useState(false)
|
|
||||||
|
|
||||||
const handleOK = () => {
|
// Bestimme das Feld basierend auf radioName
|
||||||
if(!auswahl) {
|
const fieldName = radioName === 'abgesagt' ? 'abgesagt' : 'stattgefunden'
|
||||||
setShowModal(true)
|
const [auswahl, setAuswahl] = useState(formData[fieldName] || '')
|
||||||
return
|
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<section>
|
||||||
<section>
|
<h3>{title}</h3>
|
||||||
<h3>{title}</h3>
|
<div className="fstdiv">
|
||||||
<div className="fstdiv">
|
<label className="fsLabel">
|
||||||
<label className="fsLabel">
|
<input type="radio" name={radioName} value={left} checked={auswahl === left}
|
||||||
<input type="radio" name={radioName} value={left} checked={auswahl === left}
|
onChange={handleRadioChange} />
|
||||||
onChange = {(e) => setAuswahl(e.target.value)} />
|
{left}
|
||||||
{left}
|
</label>
|
||||||
</label>
|
<label className="fsLabel">
|
||||||
<label className="fsLabel">
|
<input type="radio" name={radioName} value={right} checked={auswahl === right}
|
||||||
<input type="radio" name={radioName} value={right} checked={auswahl === right}
|
onChange={handleRadioChange} />
|
||||||
onChange = {(e) => setAuswahl(e.target.value)} />
|
{right}
|
||||||
{right}
|
</label>
|
||||||
</label>
|
</div>
|
||||||
<button className="okbutton" onClick={handleOK}>OK</button>
|
</section>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
isOpen={showModal}
|
|
||||||
onClose={closeModal}
|
|
||||||
title="Auswahl erforderlich"
|
|
||||||
>
|
|
||||||
<p>Bitte eine Option wählen</p>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,301 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { useFormData } from '../FormContext'
|
import { useFormData } from '../FormContext'
|
||||||
|
import Modal from './Modal'
|
||||||
|
import ConfirmModal from './ConfirmModal'
|
||||||
|
|
||||||
export default function LastButtons() {
|
export default function LastButtons({ mitSend, mitBack, handleBack}) {
|
||||||
|
|
||||||
const { formData } = useFormData()
|
const { formData, resetFormData } = useFormData()
|
||||||
|
const [isSending, setIsSending] = useState(false)
|
||||||
const handleSenden = () => {
|
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)
|
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 = () => {
|
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 = () => {
|
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 (
|
const closeModal = () => {
|
||||||
<div className="lastbuttons">
|
if (isSuccessModal) {
|
||||||
<button className="btnabbruch" onClick = {handleAbbruch}>Abbruch</button>
|
// Bei erfolgreichem Speichern: Browser-Fenster schließen
|
||||||
<button className="btnanleit" onClick = {handleAnleitung}>Anleitung</button>
|
console.log('Schließe Browser-Fenster nach erfolgreichem Speichern...')
|
||||||
<button className="btnsend" onClick = {handleSenden}>Senden</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
// 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?"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,61 @@
|
|||||||
animation: modalSlideIn 0.2s ease-out;
|
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 {
|
@keyframes modalSlideIn {
|
||||||
from {
|
from {
|
||||||
transform: scale(0.9) translateY(-10px);
|
transform: scale(0.9) translateY(-10px);
|
||||||
@@ -88,6 +143,20 @@
|
|||||||
border-top: 1px solid #e9ecef;
|
border-top: 1px solid #e9ecef;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
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 {
|
.modal-button {
|
||||||
@@ -112,6 +181,30 @@
|
|||||||
outline-offset: 2px;
|
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 */
|
/* Responsive Design */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.modal-content {
|
.modal-content {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
// Import des CSS direkt hier
|
// Import des CSS direkt hier
|
||||||
import './Modal.css'
|
import './Modal.css'
|
||||||
|
|
||||||
export default function Modal({ isOpen, onClose, title, children }) {
|
export default function Modal({ isOpen = true, onClose, title, children, message, type = 'info', isHtml = false }) {
|
||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
|
|
||||||
const handleOverlayClick = (e) => {
|
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 (
|
return (
|
||||||
<div className="modal-overlay" onClick={handleOverlayClick} onKeyDown={handleKeyDown} tabIndex={0}>
|
<div className="modal-overlay" onClick={handleOverlayClick} onKeyDown={handleKeyDown} tabIndex={0}>
|
||||||
<div className="modal-content">
|
<div className={getModalClass()}>
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h3 className="modal-title">{title}</h3>
|
<h3 className="modal-title">{displayTitle}</h3>
|
||||||
<button className="modal-close" onClick={onClose}>×</button>
|
<button className="modal-close" onClick={onClose}>×</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
{children}
|
{getDisplayContent()}
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button className="modal-button" onClick={onClose} autoFocus>OK</button>
|
<button className="modal-button" onClick={onClose} autoFocus>OK</button>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ export default function Verschoben({onNext, isCompleted}) {
|
|||||||
const { formData, updateFormData } = useFormData()
|
const { formData, updateFormData } = useFormData()
|
||||||
|
|
||||||
// State für das selektierte Datum
|
// State für das selektierte Datum
|
||||||
const [selectedDate, setSelectedDate] = useState(formData.neuertermin || '')
|
const [selectedDate, setSelectedDate] = useState(formData.neuesDatum || '')
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
|
|
||||||
const handleOK = () => {
|
const handleOK = () => {
|
||||||
if (selectedDate) {
|
if (selectedDate) {
|
||||||
updateFormData('neuertermin', selectedDate)
|
updateFormData('neuesDatum', selectedDate)
|
||||||
onNext()
|
onNext()
|
||||||
} else {
|
} else {
|
||||||
setShowModal(true)
|
setShowModal(true)
|
||||||
@@ -40,7 +40,12 @@ export default function Verschoben({onNext, isCompleted}) {
|
|||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
id="datetime"
|
id="datetime"
|
||||||
value={selectedDate}
|
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}
|
onKeyDown={handleKeyDown}
|
||||||
min={now.toISOString().slice(0,16)}
|
min={now.toISOString().slice(0,16)}
|
||||||
disabled={isCompleted}
|
disabled={isCompleted}
|
||||||
|
|||||||
@@ -2,6 +2,30 @@ import { defineConfig } from 'vite'
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
plugins: [react()],
|
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']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user