Compare commits

...

3 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
10 changed files with 725 additions and 1458 deletions

3
.gitignore vendored
View File

@@ -15,6 +15,9 @@ dist-ssr
# 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

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,7 +1,7 @@
{
"name": "beoanswer_react",
"private": true,
"version": "1.0.0",
"version": "1.0.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -24,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>

View File

@@ -51,7 +51,7 @@ function AppContent() {
console.log('Loading data for ID:', id)
// Backend-Aufruf mit Proxy
// Backend-Aufruf mit HTTP Basic Auth
const formData = new FormData()
formData.append('cmd', 'GET_ONE')
formData.append('id', id)
@@ -235,15 +235,15 @@ function AppContent() {
// LastButtons IMMER anzeigen, aber Senden-Button nur wenn bereit
const sendenBereit = () => {
if (pfad === 'ja') {
// JA-Pfad: vollständig wenn Bemerkungen-Schritt erreicht
// JA-Pfad: vollständig wenn Bemerkungen-Schritt ABGESCHLOSSEN ist
const bemerkungsSchritt = (formData.spendenArt === 'bar') ? 4 : 3
return schritt >= bemerkungsSchritt
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
return schritt >= 2 // Beim Erreichen von Schritt 2 (nach Auswahl abgesagt)
} else if (formData.abgesagt === 'verschoben') {
return schritt >= 3 && formData.neuesDatum
return schritt >= 3 && formData.neuesDatum // Beim Erreichen von Schritt 3 mit Datum
}
}
return false

View File

@@ -10,6 +10,7 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
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)
@@ -19,7 +20,7 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
setIsSending(true)
try {
// API URL und Auth-Daten aus Environment
// 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
@@ -122,7 +123,7 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
// Versuche JSON zu parsen
result = JSON.parse(responseText)
console.log('Backend Response (parsed):', result)
} catch (e) {
} 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 }
@@ -178,6 +179,21 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
}
const handleAnleitung = () => {
// Öffne die HTML-Anleitung in einem neuen Fenster/Tab
const anleitungUrl = '/anleitung.html'
const windowFeatures = 'width=800,height=600,scrollbars=yes,resizable=yes,toolbar=no,menubar=no,location=no'
try {
// Versuche ein Popup-Fenster zu öffnen
const anleitungWindow = window.open(anleitungUrl, 'anleitung', windowFeatures)
// Fallback: Wenn Popup blockiert wird, öffne in neuem Tab
if (!anleitungWindow) {
window.open(anleitungUrl, '_blank')
}
} catch (error) {
// Letzter Fallback: Als Modal anzeigen
console.warn('Anleitung konnte nicht in neuem Fenster geöffnet werden:', error)
setModalType('info')
setModalMessage(`
📋 Anleitung:
@@ -196,8 +212,10 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
4. **Senden** - Speichert alle Daten im System
`)
setIsModalHtml(false)
setShowModal(true)
}
}
const closeModal = () => {
if (isSuccessModal) {
@@ -224,6 +242,7 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
// Normales Modal schließen
setShowModal(false)
setModalMessage('')
setIsModalHtml(false)
setIsSuccessModal(false)
}
}
@@ -262,6 +281,7 @@ export default function LastButtons({ mitSend, mitBack, handleBack}) {
message={modalMessage}
onClose={closeModal}
type={modalType}
isHtml={isModalHtml}
/>
)}

View File

@@ -2,7 +2,7 @@ import React from 'react'
// Import des CSS direkt hier
import './Modal.css'
export default function Modal({ isOpen = true, onClose, title, children, message, type = 'info' }) {
export default function Modal({ isOpen = true, onClose, title, children, message, type = 'info', isHtml = false }) {
if (!isOpen) return null
const handleOverlayClick = (e) => {
@@ -37,7 +37,23 @@ export default function Modal({ isOpen = true, onClose, title, children, message
}
const displayTitle = title || getDefaultTitle()
const displayContent = message ? <p style={{whiteSpace: 'pre-line'}}>{message}</p> : children
// 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}>
@@ -47,7 +63,7 @@ export default function Modal({ isOpen = true, onClose, title, children, message
<button className="modal-close" onClick={onClose}>&times;</button>
</div>
<div className="modal-body">
{displayContent}
{getDisplayContent()}
</div>
<div className="modal-footer">
<button className="modal-button" onClick={onClose} autoFocus>OK</button>