diff --git a/app/admin_routes.py b/app/admin_routes.py index 2e166f7..966e4fb 100644 --- a/app/admin_routes.py +++ b/app/admin_routes.py @@ -13,6 +13,28 @@ from auth import require_auth admin_bp = Blueprint('admin_extended', __name__, url_prefix='/api/admin') +def get_config_safe(db, key, default=''): + """Config-Wert sicher abrufen""" + try: + result = db.execute('SELECT value FROM config WHERE key = ?', (key,)).fetchone() + return result['value'] if result else default + except: + return default + +def set_config_safe(db, key, value): + """Config-Wert sicher speichern""" + try: + 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())) + return True + except: + return False + # ============================================================================= # SMTP KONFIGURATION # ============================================================================= @@ -20,58 +42,47 @@ admin_bp = Blueprint('admin_extended', __name__, url_prefix='/api/admin') @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} + try: + with get_db() as db: + return jsonify({ + 'host': get_config_safe(db, 'smtp_host', ''), + 'port': int(get_config_safe(db, 'smtp_port', '587') or 587), + 'user': get_config_safe(db, 'smtp_user', ''), + 'password': '', # Nie zurückgeben + 'from_email': get_config_safe(db, 'smtp_from', ''), + 'from_name': get_config_safe(db, 'smtp_from_name', 'Reservierungssystem'), + 'security': get_config_safe(db, 'smtp_security', 'tls'), + 'enabled': get_config_safe(db, 'smtp_enabled', 'false') == 'true' + }) + except Exception as e: 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' + 'host': '', 'port': 587, 'user': '', 'password': '', + 'from_email': '', 'from_name': 'Reservierungssystem', + 'security': 'tls', 'enabled': False }) - # POST: Konfiguration speichern + # POST 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"}) + try: + with get_db() as db: + set_config_safe(db, 'smtp_host', data.get('host', '')) + set_config_safe(db, 'smtp_port', str(data.get('port', 587))) + set_config_safe(db, 'smtp_user', data.get('user', '')) + set_config_safe(db, 'smtp_from', data.get('from_email', '')) + set_config_safe(db, 'smtp_from_name', data.get('from_name', 'Reservierungssystem')) + set_config_safe(db, 'smtp_security', data.get('security', 'tls')) + set_config_safe(db, 'smtp_enabled', 'true' if data.get('enabled') else 'false') + if data.get('password'): + set_config_safe(db, 'smtp_password', data.get('password')) + db.commit() + return jsonify({"message": "SMTP-Konfiguration gespeichert"}) + except Exception as e: + return jsonify({"error": f"Fehler: {str(e)}"}), 500 @admin_bp.route('/smtp/test', methods=['POST']) def test_smtp_endpoint(): @@ -82,22 +93,12 @@ def test_smtp_endpoint(): 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') + with get_db() as db: + host = get_config_safe(db, 'smtp_host', '') + port = int(get_config_safe(db, 'smtp_port', '587') or 587) + user = get_config_safe(db, 'smtp_user', '') + password = get_config_safe(db, 'smtp_password', '') + security = get_config_safe(db, 'smtp_security', 'tls') if not all([host, user, password]): return jsonify({"error": "SMTP-Konfiguration unvollständig"}), 400 @@ -111,7 +112,6 @@ def test_smtp_endpoint(): server.login(user, password) server.quit() - return jsonify({"message": "SMTP-Verbindung erfolgreich"}) except Exception as e: @@ -128,41 +128,40 @@ def llm_config_endpoint(): 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} + try: + with get_db() as db: + return jsonify({ + 'enabled': get_config_safe(db, 'llm_enabled', 'false') == 'true', + 'url': get_config_safe(db, 'ollama_url', 'http://localhost:11434'), + 'model': get_config_safe(db, 'llm_model', 'llama2'), + 'temperature': float(get_config_safe(db, 'llm_temperature', '0.7') or 0.7), + 'max_tokens': int(get_config_safe(db, 'llm_max_tokens', '500') or 500), + 'auto_reply': get_config_safe(db, 'llm_auto_reply', 'false') == 'true' + }) + except: 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' + 'enabled': False, + 'url': 'http://localhost:11434', + 'model': 'llama2', + 'temperature': 0.7, + 'max_tokens': 500, + 'auto_reply': False }) 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"}) + try: + with get_db() as db: + set_config_safe(db, 'ollama_url', data.get('url', 'http://localhost:11434')) + set_config_safe(db, 'llm_model', data.get('model', 'llama2')) + set_config_safe(db, 'llm_temperature', str(data.get('temperature', 0.7))) + set_config_safe(db, 'llm_max_tokens', str(data.get('max_tokens', 500))) + set_config_safe(db, 'llm_enabled', 'true' if data.get('enabled') else 'false') + set_config_safe(db, 'llm_auto_reply', 'true' if data.get('auto_reply') else 'false') + db.commit() + return jsonify({"message": "LLM-Konfiguration gespeichert"}) + except Exception as e: + return jsonify({"error": f"Fehler: {str(e)}"}), 500 @admin_bp.route('/llm-config/test', methods=['POST']) def test_llm_endpoint(): @@ -173,8 +172,8 @@ def test_llm_endpoint(): try: import requests - data = request.get_json() or {} - url = data.get('url', 'http://localhost:11434') + with get_db() as db: + url = get_config_safe(db, 'ollama_url', 'http://localhost:11434') response = requests.get(f"{url}/api/tags", timeout=5) @@ -190,320 +189,132 @@ def test_llm_endpoint(): 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""" +# ============================================================================= +# RÄUME UND BEREICHE +# ============================================================================= + +@admin_bp.route('/rooms', methods=['GET', 'POST']) +def manage_rooms(): + """Räume verwalten""" if not session.get('user_role') == 'admin': return jsonify({"error": "Unauthorized"}), 401 + if request.method == 'GET': + try: + with get_db() as db: + rooms = db.execute('SELECT * FROM rooms').fetchall() + result = [] + for room in rooms: + room_dict = dict(room) + areas = db.execute('SELECT * FROM areas WHERE room_id = ?', (room['id'],)).fetchall() + room_dict['areas'] = [dict(a) for a in areas] + result.append(room_dict) + return jsonify(result) + except Exception as e: + return jsonify([]) + + # POST + data = request.get_json() 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} - ]) + cursor = db.execute( + 'INSERT INTO rooms (name, capacity, color) VALUES (?, ?, ?)', + (data.get('name'), data.get('capacity', 50), data.get('color', '#3498db')) + ) + db.commit() + return jsonify({"id": cursor.lastrowid, "message": "Raum erstellt"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 -# ============================================================================= -# STATISTIKEN -# ============================================================================= - -@admin_bp.route('/stats', methods=['GET']) -def get_stats_endpoint(): - """Statistiken für Dashboard""" +@admin_bp.route('/areas', methods=['POST']) +def create_area(): + """Bereich erstellen""" if not session.get('user_role') == 'admin': return jsonify({"error": "Unauthorized"}), 401 - period = request.args.get('period', '30') - + data = request.get_json() 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 = [] # Deaktiviert - table_ids ist JSON Array, nicht Fremdschlüssel - - 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 = ? + cursor = db.execute( + 'INSERT INTO areas (room_id, name) VALUES (?, ?)', + (data.get('room_id'), data.get('name')) + ) + db.commit() + return jsonify({"id": cursor.lastrowid, "message": "Bereich erstellt"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +# ============================================================================= +# BLOCKIERTE ZEITEN +# ============================================================================= + +@admin_bp.route('/blocked-times', methods=['GET', 'POST']) +def manage_blocked_times(): + """Blockierte Zeiten verwalten""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 + + if request.method == 'GET': + try: + with get_db() as db: + rows = db.execute(''' + SELECT b.*, r.name as room_name + FROM blocked_times b + JOIN rooms r ON b.room_id = r.id + WHERE b.date >= ? + ORDER BY b.date, b.time_from + ''', (datetime.now().date().isoformat(),)).fetchall() + return jsonify([dict(r) for r in rows]) + except: + return jsonify([]) + + # POST + data = request.get_json() + try: + with get_db() as db: + cursor = db.execute(''' + INSERT INTO blocked_times (room_id, date, time_from, time_to, reason, created_by) + VALUES (?, ?, ?, ?, ?, ?) ''', ( - data.get('name'), - data.get('capacity'), - data.get('color'), - data.get('open_time', '10:00'), - data.get('close_time', '22:00'), - room_id + data.get('room_id'), + data.get('date'), + data.get('time_from'), + data.get('time_to'), + data.get('reason', ''), + 'admin' )) db.commit() - return jsonify({"message": "Raum aktualisiert"}) + return jsonify({"id": cursor.lastrowid, "message": "Zeit blockiert"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +# ============================================================================= +# ÖFFNUNGSZEITEN +# ============================================================================= + +@admin_bp.route('/hours', methods=['GET', 'POST']) +def opening_hours(): + """Öffnungszeiten verwalten""" + if not session.get('user_role') == 'admin': + return jsonify({"error": "Unauthorized"}), 401 - 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: + if request.method == 'GET': + try: + with get_db() as db: return jsonify({ - "error": "Raum hat zukünftige Buchungen und kann nicht gelöscht werden" - }), 400 - - db.execute('DELETE FROM rooms WHERE id = ?', (room_id,)) + 'open_hour': int(get_config_safe(db, 'open_hour', '10') or 10), + 'close_hour': int(get_config_safe(db, 'close_hour', '23') or 23) + }) + except: + return jsonify({'open_hour': 10, 'close_hour': 23}) + + # POST + data = request.get_json() + try: + with get_db() as db: + set_config_safe(db, 'open_hour', str(data.get('open_hour', 10))) + set_config_safe(db, 'close_hour', str(data.get('close_hour', 23))) 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]) + return jsonify({"message": "Öffnungszeiten gespeichert"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 \ No newline at end of file diff --git a/app/login_routes.py b/app/login_routes.py index f3c0ffa..a39ccb7 100644 --- a/app/login_routes.py +++ b/app/login_routes.py @@ -1,5 +1,5 @@ """Login und Captcha Routes""" -from flask import Blueprint, request, jsonify, session +from flask import Blueprint, request, jsonify, session, render_template, redirect from auth import generate_captcha, verify_captcha, check_admin_password auth_bp = Blueprint('auth', __name__) @@ -7,7 +7,6 @@ auth_bp = Blueprint('auth', __name__) @auth_bp.route('/api/captcha', methods=['GET']) def get_captcha(): captcha = generate_captcha() - # captcha enthaelt jetzt 'image' und 'token' (kein 'answer' mehr) return jsonify({"image": captcha["image"], "token": captcha["token"]}) @auth_bp.route('/api/admin/login', methods=['POST']) @@ -33,3 +32,9 @@ def check_session(): if role: return jsonify({"role": role, "logged_in": True}) return jsonify({"logged_in": False}) + +@auth_bp.route('/admin') +def admin_dashboard(): + if session.get('user_role') != 'admin': + return redirect('/') + return render_template('admin.html') \ No newline at end of file