FIX: Admin-Dashboard Route hinzugefügt und Docker Port auf 80
This commit is contained in:
@@ -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/<int:room_id>', 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/<int:res_id>/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/<int:res_id>/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])
|
||||
+10
-58
@@ -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')
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reservierung Admin</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f172a;
|
||||
--card: #1e293b;
|
||||
--text: #f1f5f9;
|
||||
--text-muted: #94a3b8;
|
||||
--accent: #00d4aa;
|
||||
--accent-dark: #00b894;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--success: #10b981;
|
||||
}
|
||||
|
||||
[data-theme="purple"] {
|
||||
--accent: #8b5cf6;
|
||||
--accent-dark: #7c3aed;
|
||||
}
|
||||
|
||||
[data-theme="orange"] {
|
||||
--accent: #f97316;
|
||||
--accent-dark: #ea580c;
|
||||
}
|
||||
|
||||
[data-theme="blue"] {
|
||||
--accent: #3b82f6;
|
||||
--accent-dark: #2563eb;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: var(--card);
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
border-right: 1px solid #334155;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.nav-item:hover, .nav-item.active {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.main {
|
||||
margin-left: 260px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 212, 170, 0.4);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group input, .form-group select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.theme-selector {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.theme-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theme-btn.active {
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.theme-green { background: linear-gradient(135deg, #00d4aa, #00b894); }
|
||||
.theme-purple { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }
|
||||
.theme-orange { background: linear-gradient(135deg, #f97316, #ea580c); }
|
||||
.theme-blue { background: linear-gradient(135deg, #3b82f6, #2563eb); }
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar { width: 100%; position: relative; height: auto; }
|
||||
.main { margin-left: 0; }
|
||||
.grid-2 { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-theme="green">
|
||||
<aside class="sidebar">
|
||||
<div class="logo">🍽️ Admin</div>
|
||||
<nav>
|
||||
<div class="nav-item active" onclick="showTab('dashboard')">📊 Dashboard</div>
|
||||
<div class="nav-item" onclick="showTab('rooms')">🏛️ Räume</div>
|
||||
<div class="nav-item" onclick="showTab('smtp')">📧 E-Mail</div>
|
||||
<div class="nav-item" onclick="showTab('llm')">🤖 LLM</div>
|
||||
<div class="nav-item" onclick="showTab('settings')">⚙️ Einstellungen</div>
|
||||
<div class="nav-item" onclick="logout()">🚪 Abmelden</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<div class="header">
|
||||
<h1>Dashboard</h1>
|
||||
<div class="theme-selector">
|
||||
<div class="theme-btn theme-green active" onclick="setTheme('green')"></div>
|
||||
<div class="theme-btn theme-purple" onclick="setTheme('purple')"></div>
|
||||
<div class="theme-btn theme-orange" onclick="setTheme('orange')"></div>
|
||||
<div class="theme-btn theme-blue" onclick="setTheme('blue')"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<!-- Dashboard -->
|
||||
<div class="card">
|
||||
<div class="card-title">📊 Statistiken</div>
|
||||
<div class="grid-2">
|
||||
<div>
|
||||
<h3 style="font-size: 2rem; color: var(--accent)">12</h3>
|
||||
<p style="color: var(--text-muted)">Heutige Reservierungen</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style="font-size: 2rem; color: var(--accent)">48</h3>
|
||||
<p style="color: var(--text-muted)">Gäste heute</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">📧 SMTP Konfiguration</div>
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label>SMTP Server</label>
|
||||
<input type="text" id="smtpHost" placeholder="smtp.gmail.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Port</label>
|
||||
<input type="number" id="smtpPort" value="587">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Benutzername</label>
|
||||
<input type="text" id="smtpUser" placeholder="email@domain.de">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Passwort</label>
|
||||
<input type="password" id="smtpPass" placeholder="••••••••">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="saveSMTP()">💾 Speichern</button>
|
||||
<button class="btn btn-primary" onclick="testSMTP()" style="margin-left: 0.5rem;">🧪 Testen</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function setTheme(theme) {
|
||||
document.body.setAttribute('data-theme', theme);
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelector(`.theme-${theme}`).classList.add('active');
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
|
||||
function showTab(tab) {
|
||||
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
// TODO: Tab content switching
|
||||
}
|
||||
|
||||
function logout() {
|
||||
fetch('/api/admin/logout', {method: 'POST'}).then(() => location.href = '/');
|
||||
}
|
||||
|
||||
function saveSMTP() {
|
||||
const data = {
|
||||
host: document.getElementById('smtpHost').value,
|
||||
port: parseInt(document.getElementById('smtpPort').value),
|
||||
user: document.getElementById('smtpUser').value,
|
||||
password: document.getElementById('smtpPass').value
|
||||
};
|
||||
fetch('/api/admin/smtp', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
}).then(r => r.ok ? alert('Gespeichert!') : alert('Fehler!'));
|
||||
}
|
||||
|
||||
// Theme laden
|
||||
const savedTheme = localStorage.getItem('theme') || 'green';
|
||||
setTheme(savedTheme);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user