Files
2026-05-17 03:55:55 +00:00

2000 lines
81 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)