diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2b9c3cb --- /dev/null +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..639a861 --- /dev/null +++ b/.env.production @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index a547bf3..a3fe099 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ dist dist-ssr *.local +# Environment variables +.env + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..d07ef9c --- /dev/null +++ b/DEPLOYMENT.md @@ -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 + +``` + +## 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 \ No newline at end of file diff --git a/package.json b/package.json index 27815ef..087151d 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,10 @@ "scripts": { "dev": "vite", "build": "vite build", - "lint": "eslint .", - "preview": "vite preview" + "build:prod": "vite build --mode production", + "preview": "vite preview", + "preview:prod": "vite preview --mode production", + "lint": "eslint ." }, "dependencies": { "react": "^19.1.1", diff --git a/sofueDB.php b/sofueDB.php new file mode 100755 index 0000000..7842b0c --- /dev/null +++ b/sofueDB.php @@ -0,0 +1,398 @@ +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); + +?> + diff --git a/src/App.jsx b/src/App.jsx index 65a01cd..3fa29d0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { FormProvider, useFormData } from './FormContext' import './App.css' import FandStattVer from './components/FandStattVer.jsx' @@ -11,11 +11,14 @@ import Verschoben from './components/Verschoben.jsx' function AppContent() { - const datum = "2025-10-23" - const name = "Meiehofer" + // States für Backend-Daten + const [datum, setDatum] = useState("") + const [name, setName] = useState("") + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const version = "1.0.0" const vdate = "2025-11-23" - //const isBar = true // States const [schritt, setSchritt] = useState(0) @@ -24,6 +27,82 @@ function AppContent() { // Hole formData aus dem Context const { formData } = 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 Proxy + const formData = new FormData() + formData.append('cmd', 'GET_ONE') + formData.append('id', id) + + // HTTP Basic Authentication Header erstellen + const username = import.meta.env.VITE_API_USERNAME + const password = import.meta.env.VITE_API_PASSWORD + const headers = {} + + if (username && password) { + const credentials = btoa(`${username}:${password}`) + headers['Authorization'] = `Basic ${credentials}` + } + + const response = await fetch(APIURL, { + method: 'POST', + headers: headers, + body: formData + }) + + if (!response.ok) { + throw new Error(`Daten konnten nicht geladen werden. Server-Fehler: ${response.status}`) + } + + const data = await response.json() + + console.log('Received data:', data) // Debug-Ausgabe + + // Anpassung an die Datenbankfelder der SoFue2 Tabelle + if (!data.wtermin || !data.name) { + throw new Error('Unvollständige Daten vom Server erhalten.') + } + + // Daten aus Backend setzen + // wtermin ist vermutlich ein datetime, also nur das Datum extrahieren + const terminDate = new Date(data.wtermin) + const formatiertesDatum = terminDate.toLocaleDateString('de-DE') + + setDatum(formatiertesDatum) + setName(data.name + (data.vorname ? ' ' + data.vorname : '')) + + console.log('Data loaded:', data) + setLoading(false) + + } catch (err) { + console.error('Error loading data:', err) + setError(err.message) + setLoading(false) + } + } + + fetchData() + }, []) // Leere Dependency-Array = nur beim ersten Laden ausführen + // Callbacks: const handleFandStattVerNext = (auswahl) => { @@ -92,13 +171,13 @@ function AppContent() { } // Schritt 2: Ende wenn abgesagt bzw. neues Datum bei verschoben - if (schritt >= 2 && formData.stattgefunden === 'verschoben') { + if (schritt >= 2 && formData.abgesagt === 'verschoben') { components.push( 2} /> ) } // Schritt 4 (bei verschoben) oder Schritt 3 (bei absage): unterste Buttons - const endeNeinSchritt = (formData.stattgefunden === 'verschoben') ? 3 : 2 + const endeNeinSchritt = (formData.abgesagt === 'verschoben') ? 3 : 2 if (schritt >= endeNeinSchritt) { components.push( ) @@ -108,6 +187,55 @@ function AppContent() { return components } + // Loading und Error States + if (loading) { + return ( +
+
+

Lade Daten...

+

Bitte warten Sie, während die Führungsdaten geladen werden.

+
+
+ ) + } + + if (error) { + return ( +
+
+

+ Fehler beim Laden der Daten +

+
+

❌ Die Anwendung kann nicht gestartet werden

+

Grund: {error}

+
+

Mögliche Lösungen:

+
    +
  • Überprüfen Sie die URL - sie sollte eine ID enthalten (z.B. ?id=123)
  • +
  • Stellen Sie sicher, dass das Backend erreichbar ist
  • +
  • Kontaktieren Sie den Administrator
  • +
+ +
+
+
+ ) + } + return (
diff --git a/src/FormContext.jsx b/src/FormContext.jsx index 5a2cf0a..b88f337 100644 --- a/src/FormContext.jsx +++ b/src/FormContext.jsx @@ -6,40 +6,42 @@ import { createContext, useContext, useState } from 'react' const FormContext = createContext() export function FormProvider({ children }) { -// console.log('🚀 FormProvider initialisiert') + console.log('🚀 FormProvider initialisiert') const [formData, setFormData] = useState({ stattgefunden: '', - besucherAnzahl: '', + besucher: '', // war: besucherAnzahl spendenArt: '', - barspende: '', + betrag: '', // war: barspende bemerkungen: '', - neuertermin: '', + neuesDatum: '', // war: neuertermin + abgesagt: '', // für abgesagt/verschoben // Weitere Felder können hier hinzugefügt werden }) const updateFormData = (field, value) => { - //console.log('📝 FormContext UPDATE:', field, '=', value) + console.log('📝 FormContext UPDATE:', field, '=', value) setFormData(prev => { const newData = { ...prev, [field]: value } - //console.log('📊 FormContext NEU:', newData) + console.log('📊 FormContext NEU:', newData) return newData }) } const resetFormData = () => { - //console.log('🔄 FormContext RESET') + console.log('🔄 FormContext RESET') setFormData({ stattgefunden: '', - besucherAnzahl: '', + besucher: '', spendenArt: '', - barspende: '', + betrag: '', bemerkungen: '', - neuertermin: '' + neuesDatum: '', + abgesagt: '' }) } diff --git a/src/components/Bemerkungen.jsx b/src/components/Bemerkungen.jsx index 021051f..dc28ed7 100644 --- a/src/components/Bemerkungen.jsx +++ b/src/components/Bemerkungen.jsx @@ -12,13 +12,32 @@ export default function Bemerkungen({ onNext, isCompleted }) { onNext() } + const handleKeyDown = (e) => { + // Ctrl+Enter oder Cmd+Enter zum Speichern + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + handleOK() + } + } return (

Bemerkungen (optional):

-