2000 lines
81 KiB
Python
2000 lines
81 KiB
Python
from flask import Flask, request, jsonify, session, render_template_string, redirect
|
||
import os
|
||
import json
|
||
import random
|
||
import string
|
||
import re
|
||
import hashlib
|
||
from datetime import date, datetime, timedelta
|
||
from functools import wraps
|
||
|
||
app = Flask(__name__)
|
||
app.secret_key = os.environ.get('SESSION_SECRET', 'dev-secret')
|
||
|
||
# === BENUTZERVERWALTUNG ===
|
||
default_users = [
|
||
{
|
||
'id': 1,
|
||
'username': 'admin',
|
||
'password_hash': hashlib.sha256('changeme'.encode()).hexdigest(),
|
||
'name': 'Administrator',
|
||
'email': 'admin@restaurant.de',
|
||
'role': 'admin',
|
||
'is_active': True,
|
||
'created_at': datetime.now().isoformat(),
|
||
'last_login': None,
|
||
'force_password_change': False
|
||
}
|
||
]
|
||
|
||
users = default_users.copy()
|
||
|
||
ROLE_PERMISSIONS = {
|
||
'admin': ['dashboard', 'reservations', 'rooms', 'tables', 'templates', 'smtp', 'users', 'reports', 'settings', 'change_password'],
|
||
'manager': ['dashboard', 'reservations', 'rooms', 'tables', 'templates', 'reports', 'change_password'],
|
||
'staff': ['dashboard', 'reservations', 'rooms', 'change_password']
|
||
}
|
||
|
||
def hash_password(password):
|
||
return hashlib.sha256(password.encode()).hexdigest()
|
||
|
||
def check_permission(user_role, permission):
|
||
return permission in ROLE_PERMISSIONS.get(user_role, [])
|
||
|
||
def require_permission(permission):
|
||
def decorator(f):
|
||
@wraps(f)
|
||
def decorated(*args, **kwargs):
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
user = next((u for u in users if u['id'] == session['user_id']), None)
|
||
if not user or not user.get('is_active', False):
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
if not check_permission(user['role'], permission):
|
||
return jsonify({'error': 'Forbidden'}), 403
|
||
|
||
return f(*args, **kwargs)
|
||
return decorated
|
||
return decorator
|
||
|
||
# SMTP-Konfiguration
|
||
smtp_config = {
|
||
'host': os.environ.get('SMTP_HOST', ''),
|
||
'port': int(os.environ.get('SMTP_PORT', 587)),
|
||
'user': os.environ.get('SMTP_USER', ''),
|
||
'password': os.environ.get('SMTP_PASS', ''),
|
||
'from': os.environ.get('SMTP_FROM', 'reservierung@restaurant.de')
|
||
}
|
||
|
||
email_templates = {
|
||
'confirmation': """Sehr geehrte/r {name},
|
||
|
||
vielen Dank für Ihre Reservierung bei uns.
|
||
|
||
Buchungsdetails:
|
||
- Buchungsnummer: {booking_id}
|
||
- Datum: {date}
|
||
- Uhrzeit: {time}
|
||
- Personen: {guests}
|
||
- Raum: {room}
|
||
|
||
Wir freuen uns auf Ihren Besuch!
|
||
|
||
Mit freundlichen Grüßen
|
||
Ihr Restaurant-Team
|
||
""",
|
||
'email_reply': """Sehr geehrte/r {name},
|
||
|
||
vielen Dank für Ihre E-Mail. Wir haben Ihre Reservierungsanfrage erhalten.
|
||
|
||
{parsed_info}
|
||
|
||
Ihre Buchungsnummer lautet: {booking_id}
|
||
|
||
Bei Rückfragen erreichen Sie uns telefonisch oder per E-Mail.
|
||
|
||
Mit freundlichen Grüßen
|
||
Ihr Restaurant-Team
|
||
""",
|
||
'user_welcome': """Sehr geehrte/r {name},
|
||
|
||
Ihr Account für das Reservierungssystem wurde erstellt.
|
||
|
||
Zugangsdaten:
|
||
- Benutzername: {username}
|
||
- Passwort: {password}
|
||
- Rolle: {role}
|
||
|
||
Bitte ändern Sie Ihr Passwort nach dem ersten Login.
|
||
|
||
Mit freundlichen Grüßen
|
||
Ihr Restaurant-Team
|
||
""",
|
||
'password_changed': """Sehr geehrte/r {name},
|
||
|
||
Ihr Passwort wurde erfolgreich geändert.
|
||
|
||
Falls Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie bitte umgehend den Administrator.
|
||
|
||
Mit freundlichen Grüßen
|
||
Ihr Restaurant-Team
|
||
"""
|
||
}
|
||
|
||
# Datenstruktur - Räume mit Kapazität und Block-Status
|
||
rooms = [
|
||
{
|
||
'id': 1,
|
||
'name': 'Hauptraum',
|
||
'description': 'Großer Saal mit Fensterfront',
|
||
'capacity': 40,
|
||
'blocked': False,
|
||
'tables': [
|
||
{'id': 1, 'name': 'Tisch 1', 'seats': 4, 'shape': 'circle'},
|
||
{'id': 2, 'name': 'Tisch 2', 'seats': 2, 'shape': 'circle'},
|
||
{'id': 3, 'name': 'Tisch 3', 'seats': 6, 'shape': 'rect'},
|
||
{'id': 4, 'name': 'Tisch 4', 'seats': 4, 'shape': 'rect'},
|
||
]
|
||
},
|
||
{
|
||
'id': 2,
|
||
'name': 'Nebenraum',
|
||
'description': 'Gemütlicher kleiner Raum',
|
||
'capacity': 20,
|
||
'blocked': False,
|
||
'tables': [
|
||
{'id': 5, 'name': 'Tisch A', 'seats': 4, 'shape': 'circle'},
|
||
{'id': 6, 'name': 'Tisch B', 'seats': 4, 'shape': 'circle'},
|
||
]
|
||
},
|
||
{
|
||
'id': 3,
|
||
'name': 'Terrasse',
|
||
'description': 'Draußen unter dem Vordach',
|
||
'capacity': 15,
|
||
'blocked': False,
|
||
'tables': [
|
||
{'id': 7, 'name': 'Tisch T1', 'seats': 4, 'shape': 'rect'},
|
||
{'id': 8, 'name': 'Tisch T2', 'seats': 4, 'shape': 'rect'},
|
||
{'id': 9, 'name': 'Tisch T3', 'seats': 6, 'shape': 'rect'},
|
||
]
|
||
}
|
||
]
|
||
|
||
reservations = []
|
||
|
||
def generate_booking_id():
|
||
while True:
|
||
booking_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
||
if not any(r.get('booking_id') == booking_id for r in reservations):
|
||
return booking_id
|
||
|
||
def get_room(room_id):
|
||
for room in rooms:
|
||
if room['id'] == room_id:
|
||
return room
|
||
return None
|
||
|
||
def get_table(table_id):
|
||
for room in rooms:
|
||
for table in room['tables']:
|
||
if table['id'] == table_id:
|
||
return table, room
|
||
return None, None
|
||
|
||
def get_available_tables(date_str, time_str):
|
||
available = []
|
||
for room in rooms:
|
||
if room.get('blocked', False):
|
||
continue
|
||
for table in room['tables']:
|
||
table_copy = table.copy()
|
||
table_copy['room_id'] = room['id']
|
||
table_copy['room_name'] = room['name']
|
||
if is_table_available(table['id'], date_str, time_str):
|
||
available.append(table_copy)
|
||
return available
|
||
|
||
def is_table_available(table_id, date_str, time_str):
|
||
for res in reservations:
|
||
if res['date'] == date_str and res['time'] == time_str:
|
||
if table_id in res.get('table_ids', []):
|
||
return False
|
||
return True
|
||
|
||
def auto_assign_tables(room_id, guests, date_str, time_str):
|
||
"""Automatische Tisch-Zuweisung basierend auf Raum und Gästeanzahl"""
|
||
room = get_room(room_id)
|
||
if not room:
|
||
return None, "Raum nicht gefunden"
|
||
|
||
if room.get('blocked', False):
|
||
return None, "Raum ist derzeit nicht verfügbar"
|
||
|
||
# Alle verfügbaren Tische im Raum
|
||
available_tables = []
|
||
for table in room['tables']:
|
||
if is_table_available(table['id'], date_str, time_str):
|
||
available_tables.append(table)
|
||
|
||
if not available_tables:
|
||
return None, "Keine Tische im Raum verfügbar"
|
||
|
||
# Sortiere nach Sitzplätzen (größte zuerst)
|
||
available_tables.sort(key=lambda t: t['seats'], reverse=True)
|
||
|
||
# Finde beste Kombination für Gäste
|
||
total_seats = sum(t['seats'] for t in available_tables)
|
||
if total_seats < guests:
|
||
return None, f"Nicht genügend Platz im Raum. Verfügbar: {total_seats} Plätze"
|
||
|
||
# Wähle Tische aus (greedy - erst große, dann kleine)
|
||
selected_tables = []
|
||
remaining_guests = guests
|
||
|
||
for table in available_tables:
|
||
if remaining_guests <= 0:
|
||
break
|
||
selected_tables.append(table)
|
||
remaining_guests -= table['seats']
|
||
|
||
return [t['id'] for t in selected_tables], None
|
||
|
||
def send_email_smtp(to_email, subject, body):
|
||
try:
|
||
import smtplib
|
||
from email.mime.text import MIMEText
|
||
from email.mime.multipart import MIMEMultipart
|
||
|
||
if not smtp_config['host']:
|
||
print(f"[SMTP] Kein SMTP konfiguriert. E-Mail würde an {to_email}:")
|
||
return True
|
||
|
||
msg = MIMEMultipart()
|
||
msg['From'] = smtp_config['from']
|
||
msg['To'] = to_email
|
||
msg['Subject'] = subject
|
||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||
|
||
server = smtplib.SMTP(smtp_config['host'], smtp_config['port'])
|
||
server.starttls()
|
||
server.login(smtp_config['user'], smtp_config['password'])
|
||
server.send_message(msg)
|
||
server.quit()
|
||
return True
|
||
except Exception as e:
|
||
print(f"[SMTP] Fehler: {e}")
|
||
return False
|
||
|
||
# === HTML TEMPLATES ===
|
||
|
||
PUBLIC_RESERVATION_HTML = '''
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Reservierung - Restaurant</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 2rem 1rem;
|
||
}
|
||
.container { max-width: 800px; margin: 0 auto; }
|
||
.header { text-align: center; margin-bottom: 2rem; color: white; }
|
||
.header h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
|
||
.card {
|
||
background: white;
|
||
border-radius: 20px;
|
||
padding: 2rem;
|
||
margin-bottom: 1.5rem;
|
||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||
}
|
||
.card h2 { color: #333; margin-bottom: 1.5rem; font-size: 1.5rem; }
|
||
.form-group { margin-bottom: 1.25rem; }
|
||
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #555; }
|
||
.form-group label .required { color: #ef4444; }
|
||
.form-group input, .form-group select {
|
||
width: 100%; padding: 1rem; border: 2px solid #e0e0e0;
|
||
border-radius: 10px; font-size: 1rem;
|
||
}
|
||
.form-group input:focus, .form-group select:focus { outline: none; border-color: #667eea; }
|
||
.error-msg { display: none; color: #ef4444; font-size: 0.85rem; margin-top: 0.5rem; }
|
||
.success-message { text-align: center; padding: 3rem 2rem; }
|
||
.btn {
|
||
width: 100%; padding: 1.25rem;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white; border: none; border-radius: 10px;
|
||
font-size: 1.1rem; font-weight: 600; cursor: pointer;
|
||
}
|
||
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||
.booking-id {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white; padding: 1rem 2rem; border-radius: 10px;
|
||
font-size: 1.5rem; font-weight: bold; margin: 1rem 0; letter-spacing: 2px;
|
||
}
|
||
.hidden { display: none !important; }
|
||
.capacity-info { color: #667eea; font-size: 0.9rem; margin-top: 0.5rem; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🍽️ Tischreservierung</h1>
|
||
<p>Reservieren Sie Ihren Tisch einfach online</p>
|
||
</div>
|
||
|
||
<div id="reservation-form">
|
||
<div class="card">
|
||
<h2>📅 Wann möchten Sie reservieren?</h2>
|
||
<div class="form-group">
|
||
<label>Datum <span class="required">*</span></label>
|
||
<input type="date" id="res-date" min="{{ min_date }}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Uhrzeit <span class="required">*</span></label>
|
||
<select id="res-time">
|
||
<option value="">Bitte wählen...</option>
|
||
<option value="11:00">11:00</option>
|
||
<option value="12:00">12:00</option>
|
||
<option value="13:00">13:00</option>
|
||
<option value="18:00">18:00</option>
|
||
<option value="19:00" selected>19:00</option>
|
||
<option value="20:00">20:00</option>
|
||
<option value="21:00">21:00</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>👥 Wie viele Personen?</h2>
|
||
<div class="form-group">
|
||
<label>Anzahl Personen <span class="required">*</span></label>
|
||
<input type="number" id="res-guests" value="2" min="1" max="50" onchange="validateCapacity()">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>🏠 Welcher Raum?</h2>
|
||
<div class="form-group" id="group-room">
|
||
<label>Raum wählen <span class="required">*</span></label>
|
||
<select id="res-room" onchange="updateRoomInfo()">
|
||
<option value="">Bitte wählen...</option>
|
||
</select>
|
||
<div class="error-msg" id="room-error">Bitte wählen Sie einen Raum aus</div>
|
||
<div class="capacity-info" id="room-info"></div>
|
||
<div class="error-msg" id="capacity-error" style="color: #f59e0b;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>👤 Ihre Kontaktdaten</h2>
|
||
<div class="form-group">
|
||
<label>Name <span class="required">*</span></label>
|
||
<input type="text" id="res-name" placeholder="Ihr vollständiger Name">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>E-Mail <span class="required">*</span></label>
|
||
<input type="email" id="res-email" placeholder="ihre@email.de">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Telefon (optional)</label>
|
||
<input type="tel" id="res-phone" placeholder="+49 123 456789">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Sonderwünsche (optional)</label>
|
||
<input type="text" id="res-notes" placeholder="z.B. Hochstuhl, Allergien...">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" style="background: transparent; box-shadow: none; padding: 0;">
|
||
<button class="btn" id="submit-btn" onclick="submitReservation()" disabled>
|
||
📧 Reservierung mit Bestätigungsmail senden
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="success-message" class="card hidden">
|
||
<div class="success-message">
|
||
<div style="font-size: 4rem; margin-bottom: 1rem;">🎉</div>
|
||
<h2>Vielen Dank für Ihre Reservierung!</h2>
|
||
<p>Ihre Buchungsnummer:</p>
|
||
<div class="booking-id" id="success-booking-id">ABC12345</div>
|
||
<p>Eine Bestätigungsmail wurde an <strong id="success-email"></strong> gesendet.</p>
|
||
<button class="btn" style="margin-top: 2rem;" onclick="location.reload()">Neue Reservierung</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let roomsData = [];
|
||
document.getElementById('res-date').valueAsDate = new Date();
|
||
document.getElementById('res-date').min = new Date().toISOString().split('T')[0];
|
||
|
||
// Lade Räume beim Start
|
||
async function loadRooms() {
|
||
const res = await fetch('/api/public/rooms');
|
||
const data = await res.json();
|
||
roomsData = data.rooms.filter(r => !r.blocked);
|
||
|
||
const select = document.getElementById('res-room');
|
||
select.innerHTML = '<option value="">Bitte wählen...</option>' +
|
||
roomsData.map(r => `<option value="${r.id}">${r.name} (bis ${r.capacity} Personen)</option>`).join('');
|
||
}
|
||
|
||
loadRooms();
|
||
|
||
function updateRoomInfo() {
|
||
const roomId = parseInt(document.getElementById('res-room').value);
|
||
const room = roomsData.find(r => r.id === roomId);
|
||
const infoDiv = document.getElementById('room-info');
|
||
const capacityError = document.getElementById('capacity-error');
|
||
|
||
if (room) {
|
||
infoDiv.textContent = `${room.tables.length} Tische verfügbar · Gesamtkapazität: ${room.capacity} Plätze`;
|
||
validateCapacity();
|
||
} else {
|
||
infoDiv.textContent = '';
|
||
capacityError.style.display = 'none';
|
||
}
|
||
checkFormValidity();
|
||
}
|
||
|
||
function validateCapacity() {
|
||
const guests = parseInt(document.getElementById('res-guests').value) || 0;
|
||
const roomId = parseInt(document.getElementById('res-room').value);
|
||
const room = roomsData.find(r => r.id === roomId);
|
||
const capacityError = document.getElementById('capacity-error');
|
||
|
||
if (room && guests > room.capacity) {
|
||
capacityError.textContent = `⚠️ Zu viele Gäste für diesen Raum (max. ${room.capacity})`;
|
||
capacityError.style.display = 'block';
|
||
document.getElementById('submit-btn').disabled = true;
|
||
return false;
|
||
} else {
|
||
capacityError.style.display = 'none';
|
||
return true;
|
||
}
|
||
}
|
||
|
||
function checkFormValidity() {
|
||
const date = document.getElementById('res-date').value;
|
||
const time = document.getElementById('res-time').value;
|
||
const guests = parseInt(document.getElementById('res-guests').value);
|
||
const room = document.getElementById('res-room').value;
|
||
const name = document.getElementById('res-name').value.trim();
|
||
const email = document.getElementById('res-email').value.trim();
|
||
|
||
const roomData = roomsData.find(r => r.id === parseInt(room));
|
||
const capacityOk = !roomData || guests <= roomData.capacity;
|
||
|
||
const valid = date && time && guests > 0 && room && name && email && capacityOk;
|
||
document.getElementById('submit-btn').disabled = !valid;
|
||
}
|
||
|
||
document.getElementById('res-date').addEventListener('change', checkFormValidity);
|
||
document.getElementById('res-time').addEventListener('change', checkFormValidity);
|
||
document.getElementById('res-guests').addEventListener('input', checkFormValidity);
|
||
document.getElementById('res-name').addEventListener('input', checkFormValidity);
|
||
document.getElementById('res-email').addEventListener('input', checkFormValidity);
|
||
|
||
async function submitReservation() {
|
||
const btn = document.getElementById('submit-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Wird gespeichert...';
|
||
|
||
const data = {
|
||
name: document.getElementById('res-name').value,
|
||
email: document.getElementById('res-email').value,
|
||
phone: document.getElementById('res-phone').value,
|
||
date: document.getElementById('res-date').value,
|
||
time: document.getElementById('res-time').value,
|
||
guests: parseInt(document.getElementById('res-guests').value),
|
||
room_id: parseInt(document.getElementById('res-room').value),
|
||
notes: document.getElementById('res-notes').value
|
||
};
|
||
|
||
const res = await fetch('/api/public/reservations', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
const result = await res.json();
|
||
if (result.status === 'ok') {
|
||
document.getElementById('reservation-form').classList.add('hidden');
|
||
document.getElementById('success-message').classList.remove('hidden');
|
||
document.getElementById('success-booking-id').textContent = result.booking_id;
|
||
document.getElementById('success-email').textContent = data.email;
|
||
} else {
|
||
alert('Fehler: ' + result.error);
|
||
btn.disabled = false;
|
||
btn.textContent = '📧 Reservierung mit Bestätigungsmail senden';
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
'''
|
||
|
||
# Admin HTML Templates...
|
||
ADMIN_LOGIN_HTML = '''
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Admin Login - Reservierung</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh; display: flex; justify-content: center; align-items: center; }
|
||
.login-box { background: white; padding: 3rem; border-radius: 20px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3); width: 100%; max-width: 400px; }
|
||
h1 { color: #333; margin-bottom: 0.5rem; text-align: center; }
|
||
.form-group { margin-bottom: 1rem; }
|
||
label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #555; }
|
||
input { width: 100%; padding: 1rem; border: 2px solid #e0e0e0;
|
||
border-radius: 10px; font-size: 1rem; }
|
||
button { width: 100%; padding: 1rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white; border: none; border-radius: 10px; font-size: 1rem; font-weight: 600; cursor: pointer; }
|
||
.error { color: #ef4444; text-align: center; margin-top: 1rem; }
|
||
.warning { background: #fef3c7; border: 1px solid #f59e0b; color: #92400e; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; font-size: 0.9rem; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="login-box">
|
||
<h1>🔐 Admin Login</h1>
|
||
<p style="color: #666; margin-bottom: 2rem; text-align: center;">Reservierungssystem</p>
|
||
|
||
{% if force_password_change %}
|
||
<div class="warning">
|
||
⚠️ <strong>Sicherheitshinweis:</strong> Bitte ändern Sie Ihr Passwort nach dem Login.
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="form-group">
|
||
<label>Benutzername</label>
|
||
<input type="text" id="username" placeholder="Benutzername" value="{{ default_username }}">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Passwort</label>
|
||
<input type="password" id="password" placeholder="Passwort" value="{{ default_password }}">
|
||
</div>
|
||
|
||
<button onclick="login()">Anmelden</button>
|
||
|
||
<div id="error-msg" class="error"></div>
|
||
</div>
|
||
|
||
<script>
|
||
async function login() {
|
||
const res = await fetch('/api/login', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
username: document.getElementById('username').value,
|
||
password: document.getElementById('password').value
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.status === 'ok') {
|
||
if (data.force_password_change) {
|
||
location.href = '/change-password';
|
||
} else {
|
||
location.href = '/admin';
|
||
}
|
||
} else {
|
||
document.getElementById('error-msg').textContent = data.error || 'Login fehlgeschlagen';
|
||
}
|
||
}
|
||
document.getElementById('password').addEventListener('keypress', e => { if (e.key === 'Enter') login(); });
|
||
</script>
|
||
</body>
|
||
</html>
|
||
'''
|
||
|
||
CHANGE_PASSWORD_HTML = '''
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Passwort ändern - Reservierung</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh; display: flex; justify-content: center; align-items: center; }
|
||
.box { background: white; padding: 3rem; border-radius: 20px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3); width: 100%; max-width: 450px; }
|
||
h1 { color: #333; margin-bottom: 0.5rem; text-align: center; }
|
||
.subtitle { color: #666; text-align: center; margin-bottom: 2rem; }
|
||
.form-group { margin-bottom: 1.25rem; }
|
||
label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #555; }
|
||
input { width: 100%; padding: 1rem; border: 2px solid #e0e0e0;
|
||
border-radius: 10px; font-size: 1rem; }
|
||
input:focus { outline: none; border-color: #667eea; }
|
||
button { width: 100%; padding: 1rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white; border: none; border-radius: 10px; font-size: 1rem; font-weight: 600; cursor: pointer; }
|
||
button:disabled { opacity: 0.6; cursor: not-allowed; }
|
||
.error { color: #ef4444; text-align: center; margin-top: 1rem; }
|
||
.success { color: #10b981; text-align: center; margin-top: 1rem; }
|
||
.info { background: #f0f4ff; border: 1px solid #667eea; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; font-size: 0.9rem; color: #4338ca; }
|
||
.requirements { font-size: 0.85rem; color: #666; margin-top: 0.5rem; }
|
||
.requirements li { margin-bottom: 0.25rem; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="box">
|
||
<h1>🔐 Passwort ändern</h1>
|
||
<p class="subtitle">Sichern Sie Ihr Konto</p>
|
||
|
||
{% if force_change %}
|
||
<div class="info">
|
||
⚠️ <strong>Sicherheitshinweis:</strong> Sie müssen Ihr Passwort ändern, bevor Sie fortfahren können.
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="form-group">
|
||
<label>Aktuelles Passwort</label>
|
||
<input type="password" id="current-password" placeholder="Ihr aktuelles Passwort">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Neues Passwort</label>
|
||
<input type="password" id="new-password" placeholder="Mindestens 8 Zeichen">
|
||
</div>
|
||
|
||
<ul class="requirements">
|
||
<li id="req-length">✓ Mindestens 8 Zeichen</li>
|
||
<li id="req-upper">✓ Großbuchstabe</li>
|
||
<li id="req-lower">✓ Kleinbuchstabe</li>
|
||
<li id="req-number">✓ Zahl</li>
|
||
</ul>
|
||
|
||
<div class="form-group" style="margin-top: 1rem;">
|
||
<label>Neues Passwort wiederholen</label>
|
||
<input type="password" id="confirm-password" placeholder="Passwort bestätigen">
|
||
</div>
|
||
|
||
<button id="submit-btn" onclick="changePassword()">Passwort speichern</button>
|
||
|
||
<div id="message"></div>
|
||
</div>
|
||
|
||
<script>
|
||
function validatePassword() {
|
||
const pwd = document.getElementById('new-password').value;
|
||
|
||
document.getElementById('req-length').style.color = pwd.length >= 8 ? '#10b981' : '#ef4444';
|
||
document.getElementById('req-length').textContent = (pwd.length >= 8 ? '✓' : '✗') + ' Mindestens 8 Zeichen';
|
||
|
||
document.getElementById('req-upper').style.color = /[A-Z]/.test(pwd) ? '#10b981' : '#ef4444';
|
||
document.getElementById('req-upper').textContent = (/[A-Z]/.test(pwd) ? '✓' : '✗') + ' Großbuchstabe';
|
||
|
||
document.getElementById('req-lower').style.color = /[a-z]/.test(pwd) ? '#10b981' : '#ef4444';
|
||
document.getElementById('req-lower').textContent = (/[a-z]/.test(pwd) ? '✓' : '✗') + ' Kleinbuchstabe';
|
||
|
||
document.getElementById('req-number').style.color = /[0-9]/.test(pwd) ? '#10b981' : '#ef4444';
|
||
document.getElementById('req-number').textContent = (/[0-9]/.test(pwd) ? '✓' : '✗') + ' Zahl';
|
||
}
|
||
|
||
document.getElementById('new-password').addEventListener('input', validatePassword);
|
||
|
||
async function changePassword() {
|
||
const current = document.getElementById('current-password').value;
|
||
const newPwd = document.getElementById('new-password').value;
|
||
const confirm = document.getElementById('confirm-password').value;
|
||
|
||
if (newPwd !== confirm) {
|
||
document.getElementById('message').innerHTML = '<div class="error">Die Passwörter stimmen nicht überein.</div>';
|
||
return;
|
||
}
|
||
|
||
if (newPwd.length < 8) {
|
||
document.getElementById('message').innerHTML = '<div class="error">Das neue Passwort muss mindestens 8 Zeichen haben.</div>';
|
||
return;
|
||
}
|
||
|
||
const btn = document.getElementById('submit-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Speichere...';
|
||
|
||
const res = await fetch('/api/change-password', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
current_password: current,
|
||
new_password: newPwd
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
|
||
if (data.status === 'ok') {
|
||
document.getElementById('message').innerHTML = '<div class="success">✅ Passwort erfolgreich geändert! Weiterleitung...</div>';
|
||
setTimeout(() => location.href = '/admin', 1500);
|
||
} else {
|
||
document.getElementById('message').innerHTML = '<div class="error">' + (data.error || 'Fehler beim Ändern des Passworts') + '</div>';
|
||
btn.disabled = false;
|
||
btn.textContent = 'Passwort speichern';
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
'''
|
||
|
||
ADMIN_DASHBOARD_HTML = '''
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Admin Dashboard - Reservierung</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #f5f7fa; min-height: 100vh; }
|
||
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;
|
||
padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; }
|
||
.header h1 { font-size: 1.5rem; }
|
||
.user-info { display: flex; align-items: center; gap: 1rem; }
|
||
.role-badge { background: rgba(255,255,255,0.2); padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.8rem; text-transform: uppercase; }
|
||
.nav { background: white; padding: 1rem 2rem; border-bottom: 1px solid #e0e0e0; display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||
.nav-btn { padding: 0.5rem 1rem; border: none; background: #f0f0f0; border-radius: 6px; cursor: pointer; font-size: 0.9rem; }
|
||
.nav-btn.active { background: #667eea; color: white; }
|
||
.nav-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||
.container { max-width: 1400px; margin: 2rem auto; padding: 0 1rem; }
|
||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 1.5rem; }
|
||
.card { background: white; border-radius: 16px; padding: 1.5rem; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
|
||
.card h2 { color: #333; margin-bottom: 1rem; font-size: 1.25rem; }
|
||
.btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none;
|
||
padding: 0.75rem 1.5rem; border-radius: 8px; cursor: pointer; }
|
||
.btn-secondary { background: #6b7280; }
|
||
.btn-danger { background: #ef4444; }
|
||
.btn-success { background: #10b981; }
|
||
.btn-warning { background: #f59e0b; }
|
||
.btn-sm { padding: 0.5rem 1rem; font-size: 0.85rem; }
|
||
.search-box { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
|
||
.search-box input { flex: 1; padding: 0.75rem; border: 1px solid #ddd; border-radius: 8px; }
|
||
.table { width: 100%; border-collapse: collapse; }
|
||
.table th, .table td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #e0e0e0; }
|
||
.table th { font-weight: 600; color: #555; }
|
||
.badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
|
||
.badge-admin { background: #ef4444; color: white; }
|
||
.badge-manager { background: #f59e0b; color: white; }
|
||
.badge-staff { background: #10b981; color: white; }
|
||
.badge-blocked { background: #ef4444; color: white; }
|
||
.badge-active { background: #10b981; color: white; }
|
||
.form-group { margin-bottom: 1rem; }
|
||
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; color: #555; }
|
||
.form-group input, .form-group select { width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 8px; }
|
||
textarea { width: 100%; min-height: 150px; padding: 0.75rem; border: 1px solid #ddd; border-radius: 8px; }
|
||
.modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||
background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center; }
|
||
.modal-overlay.active { display: flex; }
|
||
.modal { background: white; border-radius: 16px; padding: 2rem; width: 90%; max-width: 600px; max-height: 90vh; overflow-y: auto; }
|
||
.modal h2 { margin-bottom: 1.5rem; }
|
||
.modal h3 { margin: 1.5rem 0 0.75rem 0; color: #333; }
|
||
.hidden { display: none !important; }
|
||
.alert { padding: 1rem; border-radius: 8px; margin-bottom: 1rem; }
|
||
.alert.success { background: #d1fae5; color: #065f46; }
|
||
.alert.error { background: #fee2e2; color: #991b1b; }
|
||
.alert.info { background: #dbeafe; color: #1e40af; }
|
||
.permission-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
|
||
.permission-tag { background: #e0e7ff; color: #4338ca; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; }
|
||
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
|
||
.stat-card { background: white; border-radius: 12px; padding: 1.5rem; text-align: center; }
|
||
.stat-value { font-size: 2rem; font-weight: bold; color: #667eea; }
|
||
.stat-label { color: #666; font-size: 0.9rem; margin-top: 0.5rem; }
|
||
.dropdown { position: relative; display: inline-block; }
|
||
.dropdown-content { display: none; position: absolute; right: 0; background: white; min-width: 200px;
|
||
box-shadow: 0 8px 16px rgba(0,0,0,0.1); border-radius: 8px; z-index: 100; }
|
||
.dropdown-content a { color: #333; padding: 0.75rem 1rem; text-decoration: none; display: block; border-bottom: 1px solid #f0f0f0; }
|
||
.dropdown-content a:hover { background: #f8f9fa; }
|
||
.dropdown-content a:last-child { border-bottom: none; }
|
||
.dropdown:hover .dropdown-content { display: block; }
|
||
.room-card { border: 2px solid #e0e0e0; border-radius: 12px; padding: 1.5rem; transition: all 0.2s; }
|
||
.room-card:hover { border-color: #667eea; }
|
||
.room-card.blocked { background: #fef2f2; border-color: #ef4444; }
|
||
.room-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
|
||
.room-title { font-size: 1.25rem; font-weight: 600; }
|
||
.room-capacity { color: #666; font-size: 0.9rem; }
|
||
.tables-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 1rem; }
|
||
.table-item { background: #f3f4f6; padding: 0.5rem 0.75rem; border-radius: 6px; font-size: 0.85rem; }
|
||
.action-btns { display: flex; gap: 0.5rem; margin-top: 1rem; }
|
||
@media (max-width: 768px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<h1>📊 Reservierung Admin</h1>
|
||
<div class="user-info">
|
||
<div class="dropdown">
|
||
<span id="current-user-name" style="cursor: pointer;">User ▼</span>
|
||
<div class="dropdown-content">
|
||
<a href="/change-password">🔐 Passwort ändern</a>
|
||
<a href="#" onclick="logout(); return false;">🚪 Abmelden</a>
|
||
</div>
|
||
</div>
|
||
<span id="current-user-role" class="role-badge">Role</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="nav">
|
||
<button class="nav-btn active" onclick="showTab('dashboard')">📈 Dashboard</button>
|
||
<button class="nav-btn" onclick="showTab('reservations')">📋 Buchungen</button>
|
||
<button class="nav-btn" onclick="showTab('rooms')">🏠 Räume</button>
|
||
<button class="nav-btn" id="btn-users" onclick="showTab('users')">👥 Benutzer</button>
|
||
<button class="nav-btn" id="btn-templates" onclick="showTab('templates')">✉️ Templates</button>
|
||
<button class="nav-btn" id="btn-settings" onclick="showTab('settings')">⚙️ Einstellungen</button>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<!-- Dashboard -->
|
||
<div id="tab-dashboard" class="tab-content">
|
||
<div class="stats-grid" style="margin-bottom: 2rem;">
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="stat-total">0</div>
|
||
<div class="stat-label">Gesamtbuchungen</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="stat-today">0</div>
|
||
<div class="stat-label">Heute</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="stat-week">0</div>
|
||
<div class="stat-label">Diese Woche</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="stat-rooms">0</div>
|
||
<div class="stat-label">Räume</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid">
|
||
<div class="card">
|
||
<h2>🔍 Schnellsuche</h2>
|
||
<div class="search-box">
|
||
<input type="text" id="quick-search" placeholder="Name, E-Mail oder Buchungsnummer...">
|
||
<button class="btn" onclick="doQuickSearch()">Suchen</button>
|
||
</div>
|
||
<div id="quick-results"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Reservations -->
|
||
<div id="tab-reservations" class="tab-content hidden">
|
||
<div class="card">
|
||
<h2>📋 Alle Buchungen</h2>
|
||
<div class="search-box">
|
||
<input type="text" id="res-search" placeholder="Suchen...">
|
||
<button class="btn" onclick="searchReservations()">🔍 Suchen</button>
|
||
<button class="btn btn-secondary" onclick="resetResSearch()">❌ Reset</button>
|
||
</div>
|
||
<div id="reservations-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Rooms -->
|
||
<div id="tab-rooms" class="tab-content hidden">
|
||
<div class="card">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||
<h2>🏠 Raumverwaltung</h2>
|
||
<button class="btn" onclick="showAddRoomModal()">➕ Raum hinzufügen</button>
|
||
</div>
|
||
<div id="rooms-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Users -->
|
||
<div id="tab-users" class="tab-content hidden">
|
||
<div class="card">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||
<h2>👥 Benutzerverwaltung</h2>
|
||
<button class="btn" onclick="showAddUserModal()">➕ Benutzer hinzufügen</button>
|
||
</div>
|
||
<div id="users-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Templates -->
|
||
<div id="tab-templates" class="tab-content hidden">
|
||
<div class="card">
|
||
<h2>✉️ E-Mail Templates</h2>
|
||
<h3 style="margin-top: 1rem;">Buchungsbestätigung</h3>
|
||
<textarea id="template-confirmation">{{ template_confirmation }}</textarea>
|
||
<p style="font-size: 0.85rem; color: #666; margin: 0.5rem 0;">Variablen: {name}, {booking_id}, {date}, {time}, {guests}, {room}</p>
|
||
|
||
<h3 style="margin-top: 1rem;">E-Mail-Antwort</h3>
|
||
<textarea id="template-email-reply">{{ template_email_reply }}</textarea>
|
||
<button class="btn" style="margin-top: 1rem;" onclick="saveTemplates()">💾 Speichern</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Settings -->
|
||
<div id="tab-settings" class="tab-content hidden">
|
||
<div class="card">
|
||
<h2>⚙️ SMTP-Einstellungen</h2>
|
||
<div class="form-group">
|
||
<label>SMTP-Server</label>
|
||
<input type="text" id="smtp-host" value="{{ smtp_host }}" placeholder="smtp.gmail.com">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Port</label>
|
||
<input type="number" id="smtp-port" value="{{ smtp_port }}" placeholder="587">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Benutzername</label>
|
||
<input type="text" id="smtp-user" value="{{ smtp_user }}" placeholder="email@gmail.com">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Passwort</label>
|
||
<input type="password" id="smtp-pass" value="{{ smtp_password }}" placeholder="App-Passwort">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Absender</label>
|
||
<input type="email" id="smtp-from" value="{{ smtp_from }}" placeholder="reservierung@restaurant.de">
|
||
</div>
|
||
<button class="btn" onclick="saveSMTP()">💾 Speichern</button>
|
||
<button class="btn btn-secondary" style="margin-left: 0.5rem;" onclick="testSMTP()">📧 Testmail</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add/Edit Room Modal -->
|
||
<div id="room-modal" class="modal-overlay" onclick="if(event.target===this)closeRoomModal()">
|
||
<div class="modal">
|
||
<h2 id="room-modal-title">🏠 Raum hinzufügen</h2>
|
||
<div class="form-group">
|
||
<label>Raumname *</label>
|
||
<input type="text" id="room-name" placeholder="z.B. Großer Saal">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Beschreibung</label>
|
||
<input type="text" id="room-desc" placeholder="z.B. Mit Fensterfront">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Kapazität (max. Personen) *</label>
|
||
<input type="number" id="room-capacity" placeholder="40" min="1">
|
||
</div>
|
||
<div style="display: flex; gap: 0.5rem;">
|
||
<button class="btn" onclick="saveRoom()">💾 Speichern</button>
|
||
<button class="btn btn-secondary" onclick="closeRoomModal()">❌ Abbrechen</button>
|
||
</div>
|
||
<input type="hidden" id="room-id">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Manage Tables Modal -->
|
||
<div id="tables-modal" class="modal-overlay" onclick="if(event.target===this)closeTablesModal()">
|
||
<div class="modal" style="max-width: 700px;">
|
||
<h2>🪑 Tische verwalten</h2>
|
||
<p id="tables-room-name" style="color: #666; margin-bottom: 1rem;"></p>
|
||
|
||
<h3>Neuer Tisch</h3>
|
||
<div style="display: grid; grid-template-columns: 2fr 1fr 1fr auto; gap: 0.5rem; margin-bottom: 1rem;">
|
||
<input type="text" id="new-table-name" placeholder="Tischname (z.B. Tisch 1)">
|
||
<input type="number" id="new-table-seats" placeholder="Plätze" min="1">
|
||
<select id="new-table-shape">
|
||
<option value="rect">Rechteck</option>
|
||
<option value="circle">Rund</option>
|
||
</select>
|
||
<button class="btn btn-success btn-sm" onclick="addTable()">➕ Hinzufügen</button>
|
||
</div>
|
||
|
||
<h3>Vorhandene Tische</h3>
|
||
<div id="tables-list"></div>
|
||
|
||
<div style="margin-top: 1.5rem;">
|
||
<button class="btn btn-secondary" onclick="closeTablesModal()">Schließen</button>
|
||
</div>
|
||
<input type="hidden" id="tables-room-id">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add User Modal -->
|
||
<div id="user-modal" class="modal-overlay" onclick="if(event.target===this)closeUserModal()">
|
||
<div class="modal">
|
||
<h2>👤 Benutzer hinzufügen</h2>
|
||
<div class="form-group">
|
||
<label>Benutzername *</label>
|
||
<input type="text" id="new-user-username" placeholder="max.mustermann">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Name *</label>
|
||
<input type="text" id="new-user-name" placeholder="Max Mustermann">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>E-Mail *</label>
|
||
<input type="email" id="new-user-email" placeholder="max@restaurant.de">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Rolle *</label>
|
||
<select id="new-user-role">
|
||
<option value="staff">Mitarbeiter (nur Buchungen)</option>
|
||
<option value="manager">Manager (Buchungen + Räume)</option>
|
||
<option value="admin">Administrator (alle Rechte)</option>
|
||
</select>
|
||
</div>
|
||
<div id="user-role-info" style="background: #f0f4ff; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; font-size: 0.9rem;">
|
||
<strong>Berechtigungen:</strong>
|
||
<div class="permission-list" id="role-permissions"></div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>
|
||
<input type="checkbox" id="new-user-send-email" checked>
|
||
Willkommens-E-Mail mit Passwort senden
|
||
</label>
|
||
</div>
|
||
<div style="display: flex; gap: 0.5rem;">
|
||
<button class="btn" onclick="createUser()">💾 Erstellen</button>
|
||
<button class="btn btn-secondary" onclick="closeUserModal()">❌ Abbrechen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentUser = null;
|
||
let allRooms = [];
|
||
let currentReservations = [];
|
||
|
||
loadCurrentUser();
|
||
loadReservations();
|
||
|
||
document.getElementById('new-user-role').addEventListener('change', updateRoleInfo);
|
||
|
||
async function loadCurrentUser() {
|
||
const res = await fetch('/api/me');
|
||
const data = await res.json();
|
||
if (data.user) {
|
||
currentUser = data.user;
|
||
document.getElementById('current-user-name').textContent = data.user.name + ' ▼';
|
||
document.getElementById('current-user-role').textContent = data.user.role;
|
||
|
||
if (!data.permissions.includes('users')) {
|
||
document.getElementById('btn-users').style.display = 'none';
|
||
}
|
||
if (!data.permissions.includes('templates')) {
|
||
document.getElementById('btn-templates').style.display = 'none';
|
||
}
|
||
if (!data.permissions.includes('settings')) {
|
||
document.getElementById('btn-settings').style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
function showTab(tab) {
|
||
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
||
document.getElementById('tab-' + tab).classList.remove('hidden');
|
||
document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active'));
|
||
event.target.classList.add('active');
|
||
|
||
if (tab === 'reservations') loadReservations();
|
||
if (tab === 'rooms') loadRooms();
|
||
if (tab === 'users') loadUsers();
|
||
}
|
||
|
||
async function logout() {
|
||
await fetch('/api/logout', {method: 'POST'});
|
||
location.href = '/';
|
||
}
|
||
|
||
// Dashboard & Reservations
|
||
async function doQuickSearch() {
|
||
const query = document.getElementById('quick-search').value;
|
||
if (!query) return;
|
||
await searchReservations(query);
|
||
showTab('reservations');
|
||
document.querySelectorAll('.nav-btn')[1].classList.add('active');
|
||
document.querySelectorAll('.nav-btn')[0].classList.remove('active');
|
||
}
|
||
|
||
async function searchReservations(query = null) {
|
||
query = query || document.getElementById('res-search').value;
|
||
const res = await fetch('/api/admin/reservations/search?q=' + encodeURIComponent(query));
|
||
const data = await res.json();
|
||
currentReservations = data.reservations;
|
||
renderReservations(currentReservations);
|
||
}
|
||
|
||
async function resetResSearch() {
|
||
document.getElementById('res-search').value = '';
|
||
await loadReservations();
|
||
}
|
||
|
||
async function loadReservations() {
|
||
const res = await fetch('/api/admin/reservations');
|
||
const data = await res.json();
|
||
currentReservations = data.reservations;
|
||
renderReservations(currentReservations);
|
||
updateStats(data.reservations);
|
||
}
|
||
|
||
function renderReservations(reservations) {
|
||
const container = document.getElementById('reservations-list');
|
||
if (reservations.length === 0) {
|
||
container.innerHTML = '<p style="color: #999; text-align: center; padding: 2rem;">Keine Buchungen</p>';
|
||
return;
|
||
}
|
||
container.innerHTML = `
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>Buchungsnr.</th>
|
||
<th>Name</th>
|
||
<th>Datum/Zeit</th>
|
||
<th>Personen</th>
|
||
<th>Raum</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${reservations.map(r => `
|
||
<tr>
|
||
<td><code style="background: #667eea; color: white; padding: 0.2rem 0.5rem; border-radius: 4px;">${r.booking_id}</code></td>
|
||
<td><strong>${r.name}</strong></td>
|
||
<td>${r.date}<br><small>${r.time}</small></td>
|
||
<td>${r.guests}</td>
|
||
<td>${r.room_name || '-'}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function updateStats(reservations) {
|
||
const today = new Date().toISOString().split('T')[0];
|
||
const weekLater = new Date(Date.now() + 7*24*60*60*1000).toISOString().split('T')[0];
|
||
document.getElementById('stat-total').textContent = reservations.length;
|
||
document.getElementById('stat-today').textContent = reservations.filter(r => r.date === today).length;
|
||
document.getElementById('stat-week').textContent = reservations.filter(r => r.date >= today && r.date <= weekLater).length;
|
||
document.getElementById('stat-rooms').textContent = allRooms.length;
|
||
}
|
||
|
||
// ROOMS MANAGEMENT
|
||
async function loadRooms() {
|
||
const res = await fetch('/api/admin/rooms');
|
||
const data = await res.json();
|
||
allRooms = data.rooms;
|
||
renderRooms(allRooms);
|
||
updateStats(currentReservations);
|
||
}
|
||
|
||
function renderRooms(rooms) {
|
||
const container = document.getElementById('rooms-list');
|
||
if (rooms.length === 0) {
|
||
container.innerHTML = '<p style="color: #999; text-align: center; padding: 2rem;">Keine Räume vorhanden</p>';
|
||
return;
|
||
}
|
||
container.innerHTML = rooms.map(room => `
|
||
<div class="room-card ${room.blocked ? 'blocked' : ''}">
|
||
<div class="room-header">
|
||
<div>
|
||
<div class="room-title">${room.name}</div>
|
||
<div class="room-capacity">${room.description || ''} · Kapazität: ${room.capacity} Personen · ${room.tables.length} Tische</div>
|
||
</div>
|
||
<span class="badge ${room.blocked ? 'badge-blocked' : 'badge-active'}">${room.blocked ? '🔒 Blockiert' : '✅ Aktiv'}</span>
|
||
</div>
|
||
<div class="tables-list">
|
||
${room.tables.map(t => `<span class="table-item">${t.name} (${t.seats} P.)</span>`).join('')}
|
||
</div>
|
||
<div class="action-btns">
|
||
<button class="btn btn-sm btn-secondary" onclick="editRoom(${room.id})">✏️ Bearbeiten</button>
|
||
<button class="btn btn-sm ${room.blocked ? 'btn-success' : 'btn-warning'}" onclick="toggleRoomBlock(${room.id}, ${!room.blocked})">${room.blocked ? '🔓 Freigeben' : '🔒 Blockieren'}</button>
|
||
<button class="btn btn-sm" onclick="manageTables(${room.id})">🪑 Tische</button>
|
||
<button class="btn btn-sm btn-danger" onclick="deleteRoom(${room.id})">🗑️ Löschen</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
let editingRoomId = null;
|
||
|
||
function showAddRoomModal() {
|
||
editingRoomId = null;
|
||
document.getElementById('room-modal-title').textContent = '🏠 Raum hinzufügen';
|
||
document.getElementById('room-name').value = '';
|
||
document.getElementById('room-desc').value = '';
|
||
document.getElementById('room-capacity').value = '';
|
||
document.getElementById('room-modal').classList.add('active');
|
||
}
|
||
|
||
function editRoom(roomId) {
|
||
const room = allRooms.find(r => r.id === roomId);
|
||
if (!room) return;
|
||
editingRoomId = roomId;
|
||
document.getElementById('room-modal-title').textContent = '🏠 Raum bearbeiten';
|
||
document.getElementById('room-name').value = room.name;
|
||
document.getElementById('room-desc').value = room.description || '';
|
||
document.getElementById('room-capacity').value = room.capacity;
|
||
document.getElementById('room-modal').classList.add('active');
|
||
}
|
||
|
||
function closeRoomModal() {
|
||
document.getElementById('room-modal').classList.remove('active');
|
||
editingRoomId = null;
|
||
}
|
||
|
||
async function saveRoom() {
|
||
const data = {
|
||
name: document.getElementById('room-name').value,
|
||
description: document.getElementById('room-desc').value,
|
||
capacity: parseInt(document.getElementById('room-capacity').value) || 0
|
||
};
|
||
|
||
if (!data.name || data.capacity < 1) {
|
||
alert('Bitte Name und Kapazität eingeben!');
|
||
return;
|
||
}
|
||
|
||
const url = editingRoomId ? `/api/admin/rooms/${editingRoomId}` : '/api/admin/rooms';
|
||
const method = editingRoomId ? 'PUT' : 'POST';
|
||
|
||
const res = await fetch(url, {
|
||
method: method,
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
const result = await res.json();
|
||
if (result.status === 'ok') {
|
||
closeRoomModal();
|
||
loadRooms();
|
||
} else {
|
||
alert('Fehler: ' + result.error);
|
||
}
|
||
}
|
||
|
||
async function deleteRoom(roomId) {
|
||
if (!confirm('Raum wirklich löschen? Alle zugehörigen Tische werden auch gelöscht.')) return;
|
||
|
||
const res = await fetch(`/api/admin/rooms/${roomId}`, {method: 'DELETE'});
|
||
const result = await res.json();
|
||
if (result.status === 'ok') {
|
||
loadRooms();
|
||
} else {
|
||
alert('Fehler: ' + result.error);
|
||
}
|
||
}
|
||
|
||
async function toggleRoomBlock(roomId, block) {
|
||
const action = block ? 'block' : 'unblock';
|
||
const res = await fetch(`/api/admin/rooms/${roomId}/${action}`, {method: 'POST'});
|
||
const result = await res.json();
|
||
if (result.status === 'ok') {
|
||
loadRooms();
|
||
} else {
|
||
alert('Fehler: ' + result.error);
|
||
}
|
||
}
|
||
|
||
// TABLES MANAGEMENT
|
||
let tablesRoomId = null;
|
||
|
||
function manageTables(roomId) {
|
||
const room = allRooms.find(r => r.id === roomId);
|
||
if (!room) return;
|
||
tablesRoomId = roomId;
|
||
document.getElementById('tables-room-name').textContent = room.name + ' - Tische verwalten';
|
||
document.getElementById('tables-room-id').value = roomId;
|
||
renderTablesList(room.tables);
|
||
document.getElementById('tables-modal').classList.add('active');
|
||
}
|
||
|
||
function closeTablesModal() {
|
||
document.getElementById('tables-modal').classList.remove('active');
|
||
tablesRoomId = null;
|
||
}
|
||
|
||
function renderTablesList(tables) {
|
||
const container = document.getElementById('tables-list');
|
||
if (tables.length === 0) {
|
||
container.innerHTML = '<p style="color: #999; text-align: center; padding: 1rem;">Keine Tische vorhanden</p>';
|
||
return;
|
||
}
|
||
container.innerHTML = `
|
||
<table class="table">
|
||
<thead>
|
||
<tr><th>Name</th><th>Plätze</th><th>Form</th><th>Aktion</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
${tables.map(t => `
|
||
<tr>
|
||
<td>${t.name}</td>
|
||
<td>${t.seats}</td>
|
||
<td>${t.shape === 'circle' ? 'Rund' : 'Rechteck'}</td>
|
||
<td><button class="btn btn-danger btn-sm" onclick="deleteTable(${t.id})">Löschen</button></td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
async function addTable() {
|
||
const name = document.getElementById('new-table-name').value;
|
||
const seats = parseInt(document.getElementById('new-table-seats').value);
|
||
const shape = document.getElementById('new-table-shape').value;
|
||
|
||
if (!name || !seats) {
|
||
alert('Bitte Name und Plätze eingeben!');
|
||
return;
|
||
}
|
||
|
||
const res = await fetch(`/api/admin/rooms/${tablesRoomId}/tables`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name, seats, shape})
|
||
});
|
||
|
||
const result = await res.json();
|
||
if (result.status === 'ok') {
|
||
document.getElementById('new-table-name').value = '';
|
||
document.getElementById('new-table-seats').value = '';
|
||
manageTables(tablesRoomId); // Reload
|
||
} else {
|
||
alert('Fehler: ' + result.error);
|
||
}
|
||
}
|
||
|
||
async function deleteTable(tableId) {
|
||
if (!confirm('Tisch wirklich löschen?')) return;
|
||
|
||
const res = await fetch(`/api/admin/rooms/${tablesRoomId}/tables/${tableId}`, {method: 'DELETE'});
|
||
const result = await res.json();
|
||
if (result.status === 'ok') {
|
||
manageTables(tablesRoomId); // Reload
|
||
} else {
|
||
alert('Fehler: ' + result.error);
|
||
}
|
||
}
|
||
|
||
// USERS
|
||
async function loadUsers() {
|
||
const res = await fetch('/api/admin/users');
|
||
const data = await res.json();
|
||
const container = document.getElementById('users-list');
|
||
container.innerHTML = `
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Benutzername</th>
|
||
<th>Rolle</th>
|
||
<th>Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${data.users.map(u => `
|
||
<tr>
|
||
<td><strong>${u.name}</strong></td>
|
||
<td>${u.username}</td>
|
||
<td><span class="badge badge-${u.role}">${u.role}</span></td>
|
||
<td>${u.is_active ? '✅ Aktiv' : '❌ Inaktiv'}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function showAddUserModal() {
|
||
document.getElementById('user-modal').classList.add('active');
|
||
updateRoleInfo();
|
||
}
|
||
|
||
function closeUserModal() {
|
||
document.getElementById('user-modal').classList.remove('active');
|
||
}
|
||
|
||
function updateRoleInfo() {
|
||
const role = document.getElementById('new-user-role').value;
|
||
const perms = {
|
||
'admin': ['Dashboard', 'Buchungen', 'Räume', 'Benutzer', 'Templates', 'Einstellungen'],
|
||
'manager': ['Dashboard', 'Buchungen', 'Räume', 'Templates'],
|
||
'staff': ['Dashboard', 'Buchungen']
|
||
};
|
||
document.getElementById('role-permissions').innerHTML = perms[role].map(p =>
|
||
`<span class="permission-tag">${p}</span>`
|
||
).join('');
|
||
}
|
||
|
||
async function createUser() {
|
||
const data = {
|
||
username: document.getElementById('new-user-username').value,
|
||
name: document.getElementById('new-user-name').value,
|
||
email: document.getElementById('new-user-email').value,
|
||
role: document.getElementById('new-user-role').value,
|
||
send_email: document.getElementById('new-user-send-email').checked
|
||
};
|
||
if (!data.username || !data.name || !data.email) {
|
||
alert('Bitte alle Pflichtfelder ausfüllen!');
|
||
return;
|
||
}
|
||
const res = await fetch('/api/admin/users', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(data)
|
||
});
|
||
const result = await res.json();
|
||
if (result.status === 'ok') {
|
||
alert(`Benutzer erstellt!${result.temp_password ? ` Temporäres Passwort: ${result.temp_password}` : ''}`);
|
||
closeUserModal();
|
||
loadUsers();
|
||
} else {
|
||
alert('Fehler: ' + result.error);
|
||
}
|
||
}
|
||
|
||
// TEMPLATES & SMTP
|
||
async function saveTemplates() {
|
||
const res = await fetch('/api/admin/templates', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
confirmation: document.getElementById('template-confirmation').value,
|
||
email_reply: document.getElementById('template-email-reply').value
|
||
})
|
||
});
|
||
alert((await res.json()).status === 'ok' ? 'Gespeichert!' : 'Fehler');
|
||
}
|
||
|
||
async function saveSMTP() {
|
||
const res = await fetch('/api/admin/smtp', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
host: document.getElementById('smtp-host').value,
|
||
port: parseInt(document.getElementById('smtp-port').value),
|
||
user: document.getElementById('smtp-user').value,
|
||
password: document.getElementById('smtp-pass').value,
|
||
from: document.getElementById('smtp-from').value
|
||
})
|
||
});
|
||
alert((await res.json()).status === 'ok' ? 'Gespeichert!' : 'Fehler');
|
||
}
|
||
|
||
async function testSMTP() {
|
||
const res = await fetch('/api/admin/smtp/test', {method: 'POST'});
|
||
const data = await res.json();
|
||
alert(data.status === 'ok' ? 'Testmail gesendet!' : 'Fehler: ' + data.error);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
'''
|
||
|
||
# === API ROUTES ===
|
||
|
||
@app.route('/api/login', methods=['POST'])
|
||
def login():
|
||
data = request.get_json() or {}
|
||
username = data.get('username', '').lower()
|
||
password = data.get('password', '')
|
||
|
||
password_hash = hash_password(password)
|
||
|
||
user = next((u for u in users if u['username'].lower() == username and
|
||
u['password_hash'] == password_hash and u.get('is_active', False)), None)
|
||
|
||
if user:
|
||
session['user_id'] = user['id']
|
||
user['last_login'] = datetime.now().isoformat()
|
||
return jsonify({
|
||
'status': 'ok',
|
||
'user': {'id': user['id'], 'name': user['name'], 'role': user['role']},
|
||
'force_password_change': user.get('force_password_change', False)
|
||
})
|
||
|
||
return jsonify({'error': 'Invalid username or password'}), 401
|
||
|
||
@app.route('/api/logout', methods=['POST'])
|
||
def logout():
|
||
session.pop('user_id', None)
|
||
return jsonify({'status': 'ok'})
|
||
|
||
# Passwort ändern
|
||
@app.route('/api/change-password', methods=['GET', 'POST'])
|
||
def change_password():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
user = next((u for u in users if u['id'] == session['user_id']), None)
|
||
if not user:
|
||
return jsonify({'error': 'User not found'}), 404
|
||
|
||
if request.method == 'GET':
|
||
return jsonify({
|
||
'force_change': user.get('force_password_change', False)
|
||
})
|
||
|
||
data = request.get_json() or {}
|
||
current_password = data.get('current_password', '')
|
||
new_password = data.get('new_password', '')
|
||
|
||
if hash_password(current_password) != user['password_hash']:
|
||
return jsonify({'error': 'Aktuelles Passwort ist falsch'}), 400
|
||
|
||
if len(new_password) < 8:
|
||
return jsonify({'error': 'Neues Passwort muss mindestens 8 Zeichen haben'}), 400
|
||
|
||
user['password_hash'] = hash_password(new_password)
|
||
user['force_password_change'] = False
|
||
|
||
try:
|
||
body = email_templates['password_changed'].format(name=user['name'])
|
||
send_email_smtp(user['email'], 'Ihr Passwort wurde geändert', body)
|
||
except Exception as e:
|
||
print(f"Password change email error: {e}")
|
||
|
||
return jsonify({'status': 'ok'})
|
||
|
||
@app.route('/api/me')
|
||
def get_current_user():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Not logged in'}), 401
|
||
|
||
user = next((u for u in users if u['id'] == session['user_id']), None)
|
||
if not user:
|
||
return jsonify({'error': 'User not found'}), 404
|
||
|
||
return jsonify({
|
||
'user': {'id': user['id'], 'name': user['name'], 'role': user['role']},
|
||
'permissions': ROLE_PERMISSIONS.get(user['role'], [])
|
||
})
|
||
|
||
# Public API - Räume
|
||
@app.route('/api/public/rooms')
|
||
def public_get_rooms():
|
||
"""Öffentliche Raum-Liste (nicht blockierte Räume)"""
|
||
available = []
|
||
for room in rooms:
|
||
if not room.get('blocked', False):
|
||
available.append({
|
||
'id': room['id'],
|
||
'name': room['name'],
|
||
'description': room.get('description', ''),
|
||
'capacity': room.get('capacity', 0),
|
||
'tables': room['tables']
|
||
})
|
||
return jsonify({'rooms': available})
|
||
|
||
# Public API - Reservierung
|
||
@app.route('/api/public/reservations', methods=['POST'])
|
||
def public_create_reservation():
|
||
global reservations
|
||
data = request.get_json() or {}
|
||
|
||
# NEU: room_id statt table_ids
|
||
required = ['name', 'email', 'date', 'time', 'guests', 'room_id']
|
||
for field in required:
|
||
if not data.get(field):
|
||
return jsonify({'error': f'{field} is required'}), 400
|
||
|
||
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', data['email']):
|
||
return jsonify({'error': 'Invalid email address'}), 400
|
||
|
||
room_id = int(data['room_id'])
|
||
guests = data['guests']
|
||
date_str = data['date']
|
||
time_str = data['time']
|
||
|
||
# Raum validieren
|
||
room = get_room(room_id)
|
||
if not room:
|
||
return jsonify({'error': 'Ungültiger Raum'}), 400
|
||
|
||
if room.get('blocked', False):
|
||
return jsonify({'error': 'Raum ist derzeit nicht verfügbar'}), 400
|
||
|
||
# Kapazität prüfen
|
||
if guests > room.get('capacity', 0):
|
||
return jsonify({'error': f'Zu viele Gäste für diesen Raum (max. {room["capacity"]})'}), 400
|
||
|
||
# Automatische Tisch-Zuweisung
|
||
table_ids, error = auto_assign_tables(room_id, guests, date_str, time_str)
|
||
if error:
|
||
return jsonify({'error': error}), 400
|
||
|
||
booking_id = generate_booking_id()
|
||
|
||
table_names = []
|
||
for table_id in table_ids:
|
||
table, _ = get_table(table_id)
|
||
if table:
|
||
table_names.append(table['name'])
|
||
|
||
reservation = {
|
||
'id': len(reservations) + 1,
|
||
'booking_id': booking_id,
|
||
'name': data['name'],
|
||
'email': data['email'],
|
||
'phone': data.get('phone', ''),
|
||
'date': date_str,
|
||
'time': time_str,
|
||
'guests': guests,
|
||
'room_id': room_id,
|
||
'room_name': room['name'],
|
||
'table_ids': table_ids,
|
||
'table_names': ', '.join(table_names),
|
||
'notes': data.get('notes', ''),
|
||
'created_at': datetime.now().isoformat()
|
||
}
|
||
reservations.append(reservation)
|
||
|
||
try:
|
||
email_body = email_templates['confirmation'].format(
|
||
name=data['name'],
|
||
booking_id=booking_id,
|
||
date=date_str,
|
||
time=time_str,
|
||
guests=guests,
|
||
room=room['name']
|
||
)
|
||
send_email_smtp(data['email'], f'Ihre Reservierung - {booking_id}', email_body)
|
||
except Exception as e:
|
||
print(f"Email error: {e}")
|
||
|
||
return jsonify({'status': 'ok', 'booking_id': booking_id})
|
||
|
||
# Admin API - Reservierungen
|
||
@app.route('/api/admin/reservations')
|
||
def admin_get_reservations():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
return jsonify({'reservations': reservations})
|
||
|
||
@app.route('/api/admin/reservations/search')
|
||
def admin_search_reservations():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
query = request.args.get('q', '').lower()
|
||
if not query:
|
||
return jsonify({'reservations': reservations})
|
||
|
||
results = [r for r in reservations if query in r.get('name', '').lower() or
|
||
query in r.get('email', '').lower() or query in r.get('booking_id', '').lower()]
|
||
return jsonify({'reservations': results})
|
||
|
||
# Admin API - Users
|
||
@app.route('/api/admin/users')
|
||
def admin_get_users():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
current = next((u for u in users if u['id'] == session['user_id']), None)
|
||
if not current or current['role'] != 'admin':
|
||
return jsonify({'error': 'Forbidden'}), 403
|
||
|
||
safe_users = [{'id': u['id'], 'username': u['username'], 'name': u['name'],
|
||
'email': u['email'], 'role': u['role'], 'is_active': u.get('is_active', True),
|
||
'last_login': u.get('last_login')} for u in users]
|
||
return jsonify({'users': safe_users})
|
||
|
||
@app.route('/api/admin/users', methods=['POST'])
|
||
def admin_create_user():
|
||
global users
|
||
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
current = next((u for u in users if u['id'] == session['user_id']), None)
|
||
if not current or current['role'] != 'admin':
|
||
return jsonify({'error': 'Forbidden'}), 403
|
||
|
||
data = request.get_json() or {}
|
||
|
||
if not all(k in data for k in ['username', 'name', 'email', 'role']):
|
||
return jsonify({'error': 'Missing required fields'}), 400
|
||
|
||
if any(u['username'].lower() == data['username'].lower() for u in users):
|
||
return jsonify({'error': 'Username already exists'}), 400
|
||
|
||
temp_password = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
|
||
|
||
new_user = {
|
||
'id': max([u['id'] for u in users], default=0) + 1,
|
||
'username': data['username'],
|
||
'password_hash': hash_password(temp_password),
|
||
'name': data['name'],
|
||
'email': data['email'],
|
||
'role': data['role'],
|
||
'is_active': True,
|
||
'force_password_change': True,
|
||
'created_at': datetime.now().isoformat(),
|
||
'last_login': None
|
||
}
|
||
users.append(new_user)
|
||
|
||
if data.get('send_email', True):
|
||
try:
|
||
body = email_templates['user_welcome'].format(
|
||
name=data['name'],
|
||
username=data['username'],
|
||
password=temp_password,
|
||
role=data['role']
|
||
)
|
||
send_email_smtp(data['email'], 'Ihr Reservierungssystem-Zugang', body)
|
||
except Exception as e:
|
||
print(f"Welcome email error: {e}")
|
||
|
||
return jsonify({'status': 'ok', 'temp_password': temp_password if not data.get('send_email') else None})
|
||
|
||
# Admin API - Räume (NEU)
|
||
@app.route('/api/admin/rooms')
|
||
def admin_get_rooms():
|
||
"""Liste aller Räume für Admins"""
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
return jsonify({'rooms': rooms})
|
||
|
||
@app.route('/api/admin/rooms', methods=['POST'])
|
||
def admin_create_room():
|
||
global rooms
|
||
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
data = request.get_json() or {}
|
||
|
||
if not data.get('name'):
|
||
return jsonify({'error': 'Name erforderlich'}), 400
|
||
|
||
capacity = data.get('capacity', 0)
|
||
if capacity < 1:
|
||
return jsonify({'error': 'Kapazität muss mindestens 1 sein'}), 400
|
||
|
||
new_room = {
|
||
'id': max([r['id'] for r in rooms], default=0) + 1,
|
||
'name': data['name'],
|
||
'description': data.get('description', ''),
|
||
'capacity': capacity,
|
||
'blocked': False,
|
||
'tables': []
|
||
}
|
||
rooms.append(new_room)
|
||
|
||
return jsonify({'status': 'ok', 'room': new_room})
|
||
|
||
@app.route('/api/admin/rooms/<int:room_id>', methods=['PUT'])
|
||
def admin_update_room(room_id):
|
||
global rooms
|
||
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
room = get_room(room_id)
|
||
if not room:
|
||
return jsonify({'error': 'Raum nicht gefunden'}), 404
|
||
|
||
data = request.get_json() or {}
|
||
|
||
if data.get('name'):
|
||
room['name'] = data['name']
|
||
if 'description' in data:
|
||
room['description'] = data['description']
|
||
if 'capacity' in data:
|
||
capacity = data['capacity']
|
||
if capacity < 1:
|
||
return jsonify({'error': 'Kapazität muss mindestens 1 sein'}), 400
|
||
room['capacity'] = capacity
|
||
|
||
return jsonify({'status': 'ok', 'room': room})
|
||
|
||
@app.route('/api/admin/rooms/<int:room_id>', methods=['DELETE'])
|
||
def admin_delete_room(room_id):
|
||
global rooms, reservations
|
||
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
room = get_room(room_id)
|
||
if not room:
|
||
return jsonify({'error': 'Raum nicht gefunden'}), 404
|
||
|
||
# Prüfe auf aktive Reservierungen für diesen Raum
|
||
active_reservations = [r for r in reservations if r.get('room_id') == room_id]
|
||
if active_reservations:
|
||
return jsonify({'error': f'Raum hat {len(active_reservations)} aktive Reservierungen'}), 400
|
||
|
||
rooms = [r for r in rooms if r['id'] != room_id]
|
||
return jsonify({'status': 'ok'})
|
||
|
||
@app.route('/api/admin/rooms/<int:room_id>/tables', methods=['POST'])
|
||
def admin_add_table(room_id):
|
||
global rooms
|
||
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
room = get_room(room_id)
|
||
if not room:
|
||
return jsonify({'error': 'Raum nicht gefunden'}), 404
|
||
|
||
data = request.get_json() or {}
|
||
|
||
if not data.get('name'):
|
||
return jsonify({'error': 'Tischname erforderlich'}), 400
|
||
|
||
seats = data.get('seats', 0)
|
||
if seats < 1:
|
||
return jsonify({'error': 'Plätze müssen mindestens 1 sein'}), 400
|
||
|
||
# Generiere eindeutige Tisch-ID über alle Räume
|
||
max_table_id = 0
|
||
for r in rooms:
|
||
for t in r['tables']:
|
||
max_table_id = max(max_table_id, t['id'])
|
||
|
||
new_table = {
|
||
'id': max_table_id + 1,
|
||
'name': data['name'],
|
||
'seats': seats,
|
||
'shape': data.get('shape', 'rect')
|
||
}
|
||
room['tables'].append(new_table)
|
||
|
||
return jsonify({'status': 'ok', 'table': new_table})
|
||
|
||
@app.route('/api/admin/rooms/<int:room_id>/tables/<int:table_id>', methods=['DELETE'])
|
||
def admin_delete_table(room_id, table_id):
|
||
global rooms
|
||
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
room = get_room(room_id)
|
||
if not room:
|
||
return jsonify({'error': 'Raum nicht gefunden'}), 404
|
||
|
||
# Prüfe ob Tisch in Reservierungen verwendet wird
|
||
for res in reservations:
|
||
if table_id in res.get('table_ids', []):
|
||
return jsonify({'error': 'Tisch ist in einer Reservierung'}), 400
|
||
|
||
room['tables'] = [t for t in room['tables'] if t['id'] != table_id]
|
||
return jsonify({'status': 'ok'})
|
||
|
||
@app.route('/api/admin/rooms/<int:room_id>/block', methods=['POST'])
|
||
def admin_block_room(room_id):
|
||
global rooms
|
||
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
room = get_room(room_id)
|
||
if not room:
|
||
return jsonify({'error': 'Raum nicht gefunden'}), 404
|
||
|
||
room['blocked'] = True
|
||
return jsonify({'status': 'ok'})
|
||
|
||
@app.route('/api/admin/rooms/<int:room_id>/unblock', methods=['POST'])
|
||
def admin_unblock_room(room_id):
|
||
global rooms
|
||
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
room = get_room(room_id)
|
||
if not room:
|
||
return jsonify({'error': 'Raum nicht gefunden'}), 404
|
||
|
||
room['blocked'] = False
|
||
return jsonify({'status': 'ok'})
|
||
|
||
# Rooms API (für Admin-Dashboard)
|
||
@app.route('/api/rooms')
|
||
def get_rooms():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
return jsonify({'rooms': rooms})
|
||
|
||
# Templates & SMTP
|
||
@app.route('/api/admin/templates', methods=['GET', 'POST'])
|
||
def handle_templates():
|
||
global email_templates
|
||
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
if request.method == 'POST':
|
||
data = request.get_json() or {}
|
||
if 'confirmation' in data:
|
||
email_templates['confirmation'] = data['confirmation']
|
||
if 'email_reply' in data:
|
||
email_templates['email_reply'] = data['email_reply']
|
||
return jsonify({'status': 'ok'})
|
||
|
||
return jsonify({'templates': email_templates})
|
||
|
||
@app.route('/api/admin/smtp', methods=['GET', 'POST'])
|
||
def handle_smtp():
|
||
global smtp_config
|
||
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
if request.method == 'POST':
|
||
data = request.get_json() or {}
|
||
smtp_config.update(data)
|
||
return jsonify({'status': 'ok'})
|
||
|
||
return jsonify({
|
||
'host': smtp_config.get('host', ''),
|
||
'port': smtp_config.get('port', 587),
|
||
'user': smtp_config.get('user', ''),
|
||
'from': smtp_config.get('from', '')
|
||
})
|
||
|
||
@app.route('/api/admin/smtp/test', methods=['POST'])
|
||
def test_smtp():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
|
||
result = send_email_smtp(
|
||
smtp_config.get('user', 'test@example.com'),
|
||
'SMTP Test',
|
||
'Dies ist eine Testnachricht.'
|
||
)
|
||
if result:
|
||
return jsonify({'status': 'ok'})
|
||
return jsonify({'error': 'SMTP test failed'}), 500
|
||
|
||
# Main routes
|
||
@app.route('/')
|
||
def index():
|
||
if 'user_id' in session:
|
||
return redirect('/admin')
|
||
return render_template_string(PUBLIC_RESERVATION_HTML, min_date=str(date.today()))
|
||
|
||
@app.route('/admin')
|
||
def admin():
|
||
if 'user_id' not in session:
|
||
return render_template_string(ADMIN_LOGIN_HTML,
|
||
default_username='admin',
|
||
default_password='changeme',
|
||
force_password_change=False)
|
||
|
||
user = next((u for u in users if u['id'] == session['user_id']), None)
|
||
if not user:
|
||
session.pop('user_id', None)
|
||
return redirect('/admin')
|
||
|
||
return render_template_string(ADMIN_DASHBOARD_HTML,
|
||
template_confirmation=email_templates['confirmation'],
|
||
template_email_reply=email_templates['email_reply'],
|
||
smtp_host=smtp_config.get('host', ''),
|
||
smtp_port=smtp_config.get('port', 587),
|
||
smtp_user=smtp_config.get('user', ''),
|
||
smtp_password='***' if smtp_config.get('password') else '',
|
||
smtp_from=smtp_config.get('from', '')
|
||
)
|
||
|
||
@app.route('/change-password')
|
||
def change_password_page():
|
||
if 'user_id' not in session:
|
||
return redirect('/')
|
||
|
||
user = next((u for u in users if u['id'] == session['user_id']), None)
|
||
if not user:
|
||
session.pop('user_id', None)
|
||
return redirect('/')
|
||
|
||
return render_template_string(CHANGE_PASSWORD_HTML,
|
||
force_change=user.get('force_password_change', False))
|
||
|
||
if __name__ == '__main__':
|
||
app.run(host='0.0.0.0', port=8080)
|