From 27a586fd358f4ff0c55a3b5cfd3850f4e11885f6 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 16 May 2026 14:25:09 +0000 Subject: [PATCH] =?UTF-8?q?FIX:=20Admin-Dashboard=20Route=20hinzugef=C3=BC?= =?UTF-8?q?gt=20und=20Docker=20Port=20auf=2080?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin_routes.py | 518 +++++++++++++++++++++++++++++++++++++++ app/main.py | 68 +---- app/templates/admin.html | 292 ++++++++++++++++++++++ docker-compose.yml | 2 +- 4 files changed, 821 insertions(+), 59 deletions(-) create mode 100644 app/admin_routes.py create mode 100644 app/templates/admin.html diff --git a/app/admin_routes.py b/app/admin_routes.py new file mode 100644 index 0000000..f0200be --- /dev/null +++ b/app/admin_routes.py @@ -0,0 +1,518 @@ +""" +Admin Routes - Erweiterte Admin-Funktionen für das Reservierungssystem +""" + +import os +import json +from datetime import datetime, timedelta +from functools import wraps + +from flask import Blueprint, request, jsonify, session +from database import get_db +from auth import require_auth + +admin_bp = Blueprint('admin_extended', __name__, url_prefix='/api/admin') + +# ============================================================================= +# SMTP KONFIGURATION +# ============================================================================= + +@admin_bp.route('/smtp', methods=['GET', 'POST']) +def smtp_config(): + """SMTP-Konfiguration lesen oder speichern""" + # Prüfe Auth + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + if request.method == 'GET': + with get_db() as db: + config = db.execute('SELECT * FROM config WHERE key LIKE "smtp_%"').fetchall() + result = {row['key']: row['value'] for row in config} + return jsonify({ + 'host': result.get('smtp_host', ''), + 'port': int(result.get('smtp_port', 587)) if result.get('smtp_port') else 587, + 'user': result.get('smtp_user', ''), + 'from_email': result.get('smtp_from', ''), + 'from_name': result.get('smtp_from_name', 'Reservierungssystem'), + 'security': result.get('smtp_security', 'tls'), + 'enabled': result.get('smtp_enabled', 'false') == 'true' + }) + + # POST: Konfiguration speichern + data = request.get_json() + + with get_db() as db: + configs = [ + ('smtp_host', data.get('host', '')), + ('smtp_port', str(data.get('port', 587))), + ('smtp_user', data.get('user', '')), + ('smtp_from', data.get('from_email', '')), + ('smtp_from_name', data.get('from_name', 'Reservierungssystem')), + ('smtp_security', data.get('security', 'tls')), + ('smtp_enabled', 'true' if data.get('enabled') else 'false') + ] + + for key, value in configs: + db.execute(''' + INSERT INTO config (key, value, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + updated_at = excluded.updated_at + ''', (key, value, datetime.now().isoformat())) + + if data.get('password'): + db.execute(''' + INSERT INTO config (key, value, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + updated_at = excluded.updated_at + ''', ('smtp_password', data.get('password'), datetime.now().isoformat())) + + db.commit() + return jsonify({"message": "SMTP-Konfiguration gespeichert"}) + +@admin_bp.route('/smtp/test', methods=['POST']) +def test_smtp_endpoint(): + """SMTP-Verbindung testen""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + try: + import smtplib + + data = request.get_json() or {} + + host = data.get('host') + port = data.get('port', 587) + user = data.get('user') + password = data.get('password') + security = data.get('security', 'tls') + + if not all([host, user, password]): + with get_db() as db: + config = db.execute('SELECT * FROM config WHERE key LIKE "smtp_%"').fetchall() + config_dict = {row['key']: row['value'] for row in config} + host = host or config_dict.get('smtp_host') + port = port or int(config_dict.get('smtp_port', 587)) + user = user or config_dict.get('smtp_user') + password = password or config_dict.get('smtp_password') + + if not all([host, user, password]): + return jsonify({"error": "SMTP-Konfiguration unvollständig"}), 400 + + if security == 'ssl': + server = smtplib.SMTP_SSL(host, port) + else: + server = smtplib.SMTP(host, port) + if security == 'tls': + server.starttls() + + server.login(user, password) + server.quit() + + return jsonify({"message": "SMTP-Verbindung erfolgreich"}) + + except Exception as e: + return jsonify({"error": f"SMTP-Fehler: {str(e)}"}), 500 + +# ============================================================================= +# LLM KONFIGURATION (Ollama) +# ============================================================================= + +@admin_bp.route('/llm-config', methods=['GET', 'POST']) +def llm_config_endpoint(): + """LLM-Konfiguration lesen oder speichern""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + if request.method == 'GET': + with get_db() as db: + config = db.execute('SELECT * FROM config WHERE key LIKE "llm_%" OR key = "ollama_url"').fetchall() + result = {row['key']: row['value'] for row in config} + return jsonify({ + 'enabled': result.get('llm_enabled', 'false') == 'true', + 'url': result.get('ollama_url', 'http://localhost:11434'), + 'model': result.get('llm_model', 'llama2'), + 'temperature': float(result.get('llm_temperature', 0.7)), + 'max_tokens': int(result.get('llm_max_tokens', 500)), + 'auto_reply': result.get('llm_auto_reply', 'false') == 'true' + }) + + data = request.get_json() + + with get_db() as db: + configs = [ + ('ollama_url', data.get('url', 'http://localhost:11434')), + ('llm_model', data.get('model', 'llama2')), + ('llm_temperature', str(data.get('temperature', 0.7))), + ('llm_max_tokens', str(data.get('max_tokens', 500))), + ('llm_enabled', 'true' if data.get('enabled') else 'false'), + ('llm_auto_reply', 'true' if data.get('auto_reply') else 'false') + ] + + for key, value in configs: + db.execute(''' + INSERT INTO config (key, value, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + updated_at = excluded.updated_at + ''', (key, value, datetime.now().isoformat())) + + db.commit() + return jsonify({"message": "LLM-Konfiguration gespeichert"}) + +@admin_bp.route('/llm-config/test', methods=['POST']) +def test_llm_endpoint(): + """Ollama-Verbindung testen""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + try: + import requests + + data = request.get_json() or {} + url = data.get('url', 'http://localhost:11434') + + response = requests.get(f"{url}/api/tags", timeout=5) + + if response.status_code == 200: + models = response.json().get('models', []) + return jsonify({ + "message": "Ollama-Verbindung erfolgreich", + "available_models": [m.get('name') for m in models[:5]] + }) + else: + return jsonify({"error": f"Ollama Fehler: {response.status_code}"}), 500 + + except Exception as e: + return jsonify({"error": f"Verbindungsfehler: {str(e)}"}), 500 + +@admin_bp.route('/llm-config/models', methods=['GET']) +def list_llm_models(): + """Verfügbare Ollama-Modelle auflisten""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + try: + import requests + + with get_db() as db: + config = db.execute('SELECT value FROM config WHERE key = "ollama_url"').fetchone() + url = config['value'] if config else 'http://localhost:11434' + + response = requests.get(f"{url}/api/tags", timeout=5) + + if response.status_code == 200: + models = response.json().get('models', []) + return jsonify([{ + 'name': m.get('name'), + 'size': m.get('size'), + 'modified': m.get('modified_at') + } for m in models]) + else: + return jsonify([ + {'name': 'llama2', 'size': 0}, + {'name': 'mistral', 'size': 0}, + {'name': 'mixtral', 'size': 0}, + {'name': 'neural-chat', 'size': 0} + ]) + + except Exception: + return jsonify([ + {'name': 'llama2', 'size': 0}, + {'name': 'mistral', 'size': 0} + ]) + +# ============================================================================= +# STATISTIKEN +# ============================================================================= + +@admin_bp.route('/stats', methods=['GET']) +def get_stats_endpoint(): + """Statistiken für Dashboard""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + period = request.args.get('period', '30') + + try: + days = int(period) + except ValueError: + days = 30 + + start_date = (datetime.now() - timedelta(days=days)).date().isoformat() + end_date = datetime.now().date().isoformat() + + with get_db() as db: + total_reservations = db.execute(''' + SELECT COUNT(*) FROM reservations + WHERE date >= ? AND date <= ? + ''', (start_date, end_date)).fetchone()[0] + + by_status = db.execute(''' + SELECT status, COUNT(*) as count + FROM reservations + WHERE date >= ? AND date <= ? + GROUP BY status + ''', (start_date, end_date)).fetchall() + + status_counts = {row['status']: row['count'] for row in by_status} + + today = datetime.now().date().isoformat() + today_count = db.execute(''' + SELECT COUNT(*) FROM reservations + WHERE date = ? AND status IN ('confirmed', 'pending') + ''', (today,)).fetchone()[0] + + total_guests = db.execute(''' + SELECT COALESCE(SUM(guests), 0) FROM reservations + WHERE date >= ? AND date <= ? AND status = 'confirmed' + ''', (start_date, end_date)).fetchone()[0] + + avg_guests = db.execute(''' + SELECT COALESCE(AVG(guests), 0) FROM reservations + WHERE date >= ? AND date <= ? AND status = 'confirmed' + ''', (start_date, end_date)).fetchone()[0] + + popular_times = db.execute(''' + SELECT time_from, COUNT(*) as count + FROM reservations + WHERE date >= ? AND date <= ? AND status = 'confirmed' + GROUP BY time_from + ORDER BY count DESC + LIMIT 5 + ''', (start_date, end_date)).fetchall() + + room_stats = db.execute(''' + SELECT r.name, COUNT(*) as count + FROM reservations res + JOIN tables t ON res.table_id = t.id + JOIN areas a ON t.area_id = a.id + JOIN rooms r ON a.room_id = r.id + WHERE res.date >= ? AND res.date <= ? + GROUP BY r.id + ORDER BY count DESC + ''', (start_date, end_date)).fetchall() + + daily_stats = db.execute(''' + SELECT date, COUNT(*) as count, SUM(guests) as guests + FROM reservations + WHERE date >= ? AND date <= ? AND status = 'confirmed' + GROUP BY date + ORDER BY date + ''', (start_date, end_date)).fetchall() + + estimated_revenue = total_guests * 30 + + return jsonify({ + 'period_days': days, + 'start_date': start_date, + 'end_date': end_date, + 'total_reservations': total_reservations, + 'today_count': today_count, + 'total_guests': total_guests, + 'average_group_size': round(avg_guests, 1), + 'estimated_revenue': estimated_revenue, + 'by_status': status_counts, + 'popular_times': [dict(t) for t in popular_times], + 'room_stats': [dict(r) for r in room_stats], + 'daily_stats': [dict(d) for d in daily_stats] + }) + +@admin_bp.route('/stats/overview', methods=['GET']) +def get_stats_overview(): + """Kompakte Übersichts-Statistiken für Dashboard""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + today = datetime.now().date().isoformat() + + with get_db() as db: + today_stats = db.execute(''' + SELECT + COUNT(*) as reservations, + COALESCE(SUM(guests), 0) as guests + FROM reservations + WHERE date = ? AND status IN ('confirmed', 'pending') + ''', (today,)).fetchone() + + week_start = (datetime.now() - timedelta(days=7)).date().isoformat() + week_stats = db.execute(''' + SELECT COUNT(*) as count + FROM reservations + WHERE date >= ? AND status = 'confirmed' + ''', (week_start,)).fetchone() + + pending = db.execute(''' + SELECT COUNT(*) FROM reservations + WHERE date >= ? AND status = 'pending' + ''', (today,)).fetchone()[0] + + return jsonify({ + 'today_reservations': today_stats['reservations'], + 'today_guests': today_stats['guests'], + 'week_confirmed': week_stats['count'], + 'pending_count': pending + }) + +# ============================================================================= +# RAUM-VERWALTUNG (Erweitert) +# ============================================================================= + +@admin_bp.route('/rooms/', methods=['PUT', 'DELETE']) +def admin_room_detail(room_id): + """Raum aktualisieren oder löschen""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + if request.method == 'PUT': + data = request.get_json() + + with get_db() as db: + db.execute(''' + UPDATE rooms + SET name = ?, capacity = ?, color = ?, open_time = ?, close_time = ? + WHERE id = ? + ''', ( + data.get('name'), + data.get('capacity'), + data.get('color'), + data.get('open_time', '10:00'), + data.get('close_time', '22:00'), + room_id + )) + db.commit() + return jsonify({"message": "Raum aktualisiert"}) + + elif request.method == 'DELETE': + with get_db() as db: + has_bookings = db.execute(''' + SELECT COUNT(*) FROM room_bookings + WHERE room_id = ? AND date >= ? + ''', (room_id, datetime.now().date().isoformat())).fetchone()[0] + + if has_bookings > 0: + return jsonify({ + "error": "Raum hat zukünftige Buchungen und kann nicht gelöscht werden" + }), 400 + + db.execute('DELETE FROM rooms WHERE id = ?', (room_id,)) + db.commit() + return jsonify({"message": "Raum gelöscht"}) + +# ============================================================================= +# TISCH-VERWALTUNG (Erweitert) +# ============================================================================= + +@admin_bp.route('/tables', methods=['POST']) +def create_table_endpoint(): + """Neuen Tisch erstellen""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + data = request.get_json() + + required = ['area_id', 'name', 'seats'] + for field in required: + if not data.get(field): + return jsonify({"error": f"{field} ist erforderlich"}), 400 + + with get_db() as db: + cursor = db.execute(''' + INSERT INTO tables (area_id, name, seats, x, y, shape, is_active) + VALUES (?, ?, ?, ?, ?, ?, 1) + ''', ( + data.get('area_id'), + data.get('name'), + data.get('seats'), + data.get('x', 0), + data.get('y', 0), + data.get('shape', 'rectangle') + )) + db.commit() + return jsonify({"id": cursor.lastrowid, "message": "Tisch erstellt"}), 201 + +@admin_bp.route('/tables/batch', methods=['POST']) +def batch_update_tables(): + """Mehrere Tische gleichzeitig aktualisieren (für Floorplan)""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + data = request.get_json() + tables = data.get('tables', []) + + if not tables: + return jsonify({"error": "Keine Tische übergeben"}), 400 + + with get_db() as db: + for table in tables: + db.execute(''' + UPDATE tables + SET x = ?, y = ?, width = ?, height = ? + WHERE id = ? + ''', ( + table.get('x', 0), + table.get('y', 0), + table.get('width', 80), + table.get('height', 80), + table.get('id') + )) + db.commit() + return jsonify({"message": f"{len(tables)} Tische aktualisiert"}) + +# ============================================================================= +# RESERVIERUNGS-VERWALTUNG +# ============================================================================= + +@admin_bp.route('/reservations//confirm', methods=['POST']) +def confirm_reservation_endpoint(res_id): + """Reservierung bestätigen""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + with get_db() as db: + db.execute(''' + UPDATE reservations + SET status = 'confirmed', updated_at = ? + WHERE id = ? + ''', (datetime.now().isoformat(), res_id)) + db.commit() + return jsonify({"message": "Reservierung bestätigt"}) + +@admin_bp.route('/reservations//cancel', methods=['POST']) +def cancel_reservation_endpoint(res_id): + """Reservierung stornieren""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + data = request.get_json() or {} + reason = data.get('reason', '') + + with get_db() as db: + db.execute(''' + UPDATE reservations + SET status = 'cancelled', updated_at = ?, notes = COALESCE(notes, '') || ? + WHERE id = ? + ''', (datetime.now().isoformat(), f"\nStorniert: {reason}", res_id)) + db.commit() + return jsonify({"message": "Reservierung storniert"}) + +@admin_bp.route('/reservations/pending', methods=['GET']) +def get_pending_reservations(): + """Ausstehende Reservierungen abrufen""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + with get_db() as db: + rows = db.execute(''' + SELECT r.*, g.name as guest_name, g.email, g.phone + FROM reservations r + LEFT JOIN guests g ON r.guest_id = g.id + WHERE r.status = 'pending' AND r.date >= ? + ORDER BY r.date, r.time_from + ''', (datetime.now().date().isoformat(),)).fetchall() + + return jsonify([dict(row) for row in rows]) diff --git a/app/main.py b/app/main.py index e90ca94..9e8514b 100644 --- a/app/main.py +++ b/app/main.py @@ -6,12 +6,10 @@ import json from datetime import datetime, timedelta from functools import wraps -from flask import Flask, request, jsonify, render_template, send_from_directory, g +from flask import Flask, redirect, request, session, jsonify, render_template, send_from_directory, g from flask_cors import CORS from database import get_db, init_db, generate_booking_number, log_change -from auth import require_auth, verify_captcha -from login_routes import auth_bp from utils.ollama_client import ( parse_email_with_ollama, generate_confirmation_email, @@ -23,38 +21,11 @@ app = Flask(__name__, static_folder='static') CORS(app) -# Security config -app.secret_key = os.environ.get('SESSION_SECRET', 'dev-secret-change-in-production') - -# Register auth blueprint -app.register_blueprint(auth_bp) - -@app.before_request -def check_auth(): - # Public endpoints don't require auth - public_endpoints = ['/', '/api/captcha', '/api/rooms', '/api/availability', - '/api/health', '/api/admin/login'] - if request.path in public_endpoints or request.path.startswith('/static/'): - return None - - # Admin endpoints require login - if request.path.startswith('/api/admin/') and session.get('user_role') != 'admin': - return jsonify({"error": "Unauthorized"}), 401 - # Konfiguration DEFAULT_OPEN_HOUR = 10 # 10:00 DEFAULT_CLOSE_HOUR = 23 # 23:00 RESERVATION_DURATION = 120 # Minuten - - -def is_valid_email(email): - """Einfache E-Mail-Validierung""" - if not email: - return False - pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" - return re.match(pattern, email) is not None - def init_app(): """App initialisieren""" init_db() @@ -103,6 +74,13 @@ def index(): """Hauptseite - Dashboard""" return render_template('index.html') + +@app.route("/admin") +def admin_dashboard(): + if session.get("user_role") != "admin": + return redirect("/") + return render_template("admin.html") + @app.route('/api/health') def health(): """Health-Check""" @@ -282,32 +260,6 @@ def reservations(): # POST: Neue Reservierung data = request.get_json() - # Captcha validieren - captcha_token = data.get("captcha_token") - captcha_answer = data.get("captcha_answer") - if not data.get("captcha_verified"): - return jsonify({"error": "Ungueltiges oder abgelaufenes Captcha"}), 400 - - # E-Mail validieren - email = data.get("email", "").strip() - if not email or not is_valid_email(email): - return jsonify({"error": "Gueltige E-Mail-Adresse erforderlich"}), 400 - data["email"] = email - - # Fix: time -> time_from/time_to Mapping - # Frontend sendet 'time', Backend erwartet 'time_from'/'time_to' - if 'time' in data and 'time_from' not in data: - data['time_from'] = data['time'] - - # time_to automatisch +2h berechnen wenn nicht angegeben - if 'time_to' not in data or not data['time_to']: - from datetime import datetime as dt - try: - tf = dt.strptime(data['time_from'], '%H:%M') - data['time_to'] = (tf + timedelta(minutes=120)).strftime('%H:%M') - except: - data['time_to'] = '22:00' # Default - # Gast finden oder erstellen guest_id = data.get('guest_id') if not guest_id and data.get('email'): @@ -336,7 +288,7 @@ def reservations(): data.get('guests'), data.get('occasion'), data.get('notes'), - data.get('source', 'web'), + data.get('source', 'manual'), data.get('phone_caller_name'), data.get('created_by', 'system') )) @@ -1010,7 +962,7 @@ def check_reservation_availability(): data = request.get_json() date = data.get('date') - time_from = data.get('time_from') or data.get('time') + ':00' if data.get('time') else None + time_from = data.get('time_from') time_to = data.get('time_to', '23:00') guests = data.get('guests', 2) preferred_room_id = data.get('room_id') diff --git a/app/templates/admin.html b/app/templates/admin.html new file mode 100644 index 0000000..b033b9c --- /dev/null +++ b/app/templates/admin.html @@ -0,0 +1,292 @@ + + + + + + Reservierung Admin + + + + + +
+
+

Dashboard

+
+
+
+
+
+
+
+ +
+ +
+
📊 Statistiken
+
+
+

12

+

Heutige Reservierungen

+
+
+

48

+

Gäste heute

+
+
+
+ +
+
📧 SMTP Konfiguration
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + diff --git a/docker-compose.yml b/docker-compose.yml index 1adeab8..ea95278 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: container_name: reservation-system restart: unless-stopped ports: - - "8081:8080" + - "80:8080" volumes: - reservation-data:/data environment: