daaa9bff5e
- admin.html: Komplette Tab-Navigation implementiert - admin.html: Alle Funktionen (Räume, Bereiche, Blockierung, SMTP, LLM) - admin_routes.py: SQL-Fehler behoben (table_id → table_ids) Admin-Oberfläche jetzt voll funktionsfähig
510 lines
19 KiB
Python
510 lines
19 KiB
Python
"""
|
|
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 = [] # 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/<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])
|