v2.2: Frontend-Login, Captcha, professionelles UI, Gast-Ansicht
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('/root/reservation-system/app/main.py', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Einfachere Modifikation - füge Admin-Check hinzu nachdem rooms geladen wurden
|
||||||
|
# Suche nach der rooms Funktion und ergänze sie
|
||||||
|
|
||||||
|
old_code = """@app.route('/api/rooms')
|
||||||
|
def rooms():
|
||||||
|
is_admin = session.get('user_role') == 'admin'"""
|
||||||
|
|
||||||
|
new_code = """@app.route('/api/rooms')
|
||||||
|
def rooms():
|
||||||
|
is_admin = session.get('user_role') == 'admin'
|
||||||
|
|
||||||
|
with get_db() as db:"""
|
||||||
|
|
||||||
|
content = content.replace(old_code, new_code)
|
||||||
|
|
||||||
|
with open('/root/reservation-system/app/main.py', 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print("Rooms endpoint updated")
|
||||||
@@ -32,4 +32,3 @@ def check_session():
|
|||||||
role = session.get('user_role')
|
role = session.get('user_role')
|
||||||
if role:
|
if role:
|
||||||
return jsonify({"role": role, "logged_in": True})
|
return jsonify({"role": role, "logged_in": True})
|
||||||
return jsonify({"role": None, "logged_in": False})
|
|
||||||
|
|||||||
+362
-506
@@ -5,633 +5,489 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Reservierungssystem</title>
|
<title>Reservierungssystem</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
:root {
|
||||||
padding: 0;
|
--primary: #3b82f6;
|
||||||
|
--primary-dark: #2563eb;
|
||||||
|
--secondary: #64748b;
|
||||||
|
--bg: #f8fafc;
|
||||||
|
--card: #ffffff;
|
||||||
|
--text: #1e293b;
|
||||||
|
--text-light: #64748b;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--success: #10b981;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--error: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
background: #1a1a2e;
|
background: var(--bg);
|
||||||
color: #fff;
|
color: var(--text);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
.header {
|
.header {
|
||||||
background: #16213e;
|
background: var(--card);
|
||||||
padding: 15px 30px;
|
padding: 1rem 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid #2d3a5c;
|
border-bottom: 1px solid var(--border);
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: #00d4aa;
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.nav { display: flex; gap: 1rem; align-items: center; }
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid #00d4aa;
|
|
||||||
color: #00d4aa;
|
|
||||||
padding: 8px 20px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn:hover, .nav-btn.active {
|
|
||||||
background: #00d4aa;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
background: #16213e;
|
|
||||||
border: 1px solid #2d3a5c;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card h3 {
|
|
||||||
color: #888;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #00d4aa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-card {
|
|
||||||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-card:hover {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-card h3 {
|
|
||||||
color: rgba(255,255,255,0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-card .stat-value {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
background: #16213e;
|
|
||||||
border: 1px solid #2d3a5c;
|
|
||||||
border-radius: 12px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
padding: 20px 25px;
|
|
||||||
border-bottom: 1px solid #2d3a5c;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header h2 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
background: #00d4aa;
|
padding: 0.5rem 1.25rem;
|
||||||
color: #000;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
font-size: 0.9rem;
|
||||||
}
|
transition: all 0.2s;
|
||||||
|
text-decoration: none;
|
||||||
.btn:hover {
|
|
||||||
background: #00b894;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
padding: 15px;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid #2d3a5c;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
color: #888;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:hover {
|
|
||||||
background: rgba(0,212,170,0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-confirmed {
|
.btn-primary {
|
||||||
background: rgba(0,212,170,0.2);
|
background: var(--primary);
|
||||||
color: #00d4aa;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-pending {
|
.btn-primary:hover { background: var(--primary-dark); }
|
||||||
background: rgba(241,196,15,0.2);
|
|
||||||
color: #f1c40f;
|
.btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-cancelled {
|
.btn-secondary:hover { background: var(--bg); }
|
||||||
background: rgba(231,76,60,0.2);
|
|
||||||
color: #e74c3c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.booking-number {
|
/* Login Modal */
|
||||||
font-family: monospace;
|
.modal {
|
||||||
background: rgba(0,212,170,0.1);
|
display: none;
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #00d4aa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal */
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0; left: 0; width: 100%; height: 100%;
|
||||||
left: 0;
|
background: rgba(0,0,0,0.5);
|
||||||
width: 100%;
|
z-index: 1000;
|
||||||
height: 100%;
|
|
||||||
background: rgba(0,0,0,0.7);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal.active { display: flex; }
|
||||||
background: #16213e;
|
|
||||||
border: 1px solid #2d3a5c;
|
.modal-content {
|
||||||
|
background: var(--card);
|
||||||
|
padding: 2rem;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 30px;
|
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 600px;
|
max-width: 400px;
|
||||||
max-height: 90vh;
|
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1);
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal h2 {
|
.modal-title {
|
||||||
margin-bottom: 20px;
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group { margin-bottom: 1rem; }
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
color: #888;
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input, .form-group select, .form-group textarea {
|
.form-group input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 0.75rem;
|
||||||
border: 1px solid #2d3a5c;
|
border: 1px solid var(--border);
|
||||||
background: #1a1a2e;
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row {
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
color: var(--text-light);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Room Cards */
|
||||||
|
.rooms-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
gap: 15px;
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-actions {
|
.room-card {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-card:hover {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
justify-content: space-between;
|
||||||
justify-content: flex-end;
|
align-items: center;
|
||||||
margin-top: 25px;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.room-name {
|
||||||
background: #333;
|
font-size: 1.25rem;
|
||||||
color: #fff;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.room-badge {
|
||||||
background: #444;
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
.room-capacity {
|
||||||
.dashboard-grid {
|
color: var(--text-light);
|
||||||
grid-template-columns: repeat(2, 1fr);
|
font-size: 0.9rem;
|
||||||
}
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
.room-stats {
|
||||||
.dashboard-grid {
|
display: flex;
|
||||||
grid-template-columns: 1fr;
|
gap: 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
/* Captcha */
|
||||||
flex-direction: column;
|
.captcha-box {
|
||||||
gap: 15px;
|
background: var(--bg);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.captcha-question {
|
||||||
flex-wrap: wrap;
|
font-size: 1.5rem;
|
||||||
justify-content: center;
|
font-weight: 600;
|
||||||
|
color: var(--primary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row {
|
.captcha-input {
|
||||||
grid-template-columns: 1fr;
|
width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Guest View Restrictions */
|
||||||
|
.guest-notice {
|
||||||
|
background: #fef3c7;
|
||||||
|
border: 1px solid #fcd34d;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-only { display: none; }
|
||||||
|
.admin-visible .admin-only { display: block; }
|
||||||
|
|
||||||
|
/* Loading */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
color: var(--error);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
background: #f0fdf4;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
color: var(--success);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<header class="header">
|
||||||
<div class="logo">🍽️ Reservierungssystem</div>
|
<div class="logo">🍽️ Reservierung</div>
|
||||||
<div class="nav">
|
<nav class="nav">
|
||||||
<button class="nav-btn active" onclick="showView('dashboard')">Dashboard</button>
|
<span id="userStatus"></span>
|
||||||
<button class="nav-btn" onclick="showView('reservations')">Reservierungen</button>
|
<button class="btn btn-secondary" id="loginBtn" onclick="showLogin()">Admin Login</button>
|
||||||
<button class="nav-btn" onclick="showView('floorplan')">Tischplan</button>
|
<button class="btn btn-primary admin-only" onclick="showNewBooking()">+ Buchung</button>
|
||||||
<button class="nav-btn" onclick="showView('guests')">Gäste</button>
|
</nav>
|
||||||
<button class="nav-btn" onclick="showView('emails')">E-Mails</button>
|
</header>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
<!-- Login Modal -->
|
||||||
<!-- Dashboard View -->
|
<div class="modal" id="loginModal">
|
||||||
<div id="view-dashboard">
|
<div class="modal-content">
|
||||||
<div class="dashboard-grid">
|
<h2 class="modal-title">Admin Login</h2>
|
||||||
<div class="stat-card">
|
<form id="loginForm" onsubmit="handleLogin(event)">
|
||||||
<h3>Heute Reserviert</h3>
|
|
||||||
<div class="stat-value" id="stat-today">0</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<h3>Gäste Heute</h3>
|
|
||||||
<div class="stat-value" id="stat-guests">0</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<h3>Freie Tische</h3>
|
|
||||||
<div class="stat-value" id="stat-available">0</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card alert-card" onclick="showView('emails')" id="alert-card">
|
|
||||||
<h3>⚠️ Klärung Erforderlich</h3>
|
|
||||||
<div class="stat-value" id="stat-pending">0</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Heutige Reservierungen</h2>
|
|
||||||
<button class="btn" onclick="openNewReservation()">+ Neue Reservierung</button>
|
|
||||||
</div>
|
|
||||||
<div id="today-reservations">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Buchungsnr.</th>
|
|
||||||
<th>Zeit</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Personen</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Aktion</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="today-table-body">
|
|
||||||
<!-- Dynamisch gefüllt -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Weitere Views (hidden by default) -->
|
|
||||||
<div id="view-reservations" class="hidden">
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Alle Reservierungen</h2>
|
|
||||||
<div>
|
|
||||||
<input type="date" id="filter-date" onchange="loadReservations()">
|
|
||||||
<button class="btn" onclick="openNewReservation()">+ Neu</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="all-reservations">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Buchungsnr.</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Zeit</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Pers.</th>
|
|
||||||
<th>Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="reservations-table-body">
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="view-floorplan" class="hidden">
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Tischplan</h2>
|
|
||||||
<select id="floorplan-date" onchange="loadFloorplan()">
|
|
||||||
<option>Heute</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div id="floorplan-container" style="min-height: 500px; position: relative;"
|
|
||||||
003e
|
|
||||||
<!-- Dynamisch rendern -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="view-guests" class="hidden">
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Gäste-Adressbuch</h2>
|
|
||||||
<div>
|
|
||||||
<input type="text" placeholder="Suchen..." id="guest-search" onkeyup="searchGuests()">
|
|
||||||
<button class="btn" onclick="openNewGuest()">+ Neuer Gast</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="guests-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="view-emails" class="hidden">
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>E-Mail-Verarbeitung</h2>
|
|
||||||
<div class="nav">
|
|
||||||
<button class="nav-btn active" onclick="filterEmails('needs_review')">Klärung</button>
|
|
||||||
<button class="nav-btn" onclick="filterEmails('auto_processed')">Automatisch</button>
|
|
||||||
<button class="nav-btn" onclick="filterEmails('confirmed')">Bestätigt</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="emails-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal: Neue Reservierung -->
|
|
||||||
<div id="modal-reservation" class="modal-overlay hidden">
|
|
||||||
<div class="modal">
|
|
||||||
<h2>Neue Reservierung</h2>
|
|
||||||
|
|
||||||
<form id="reservation-form">
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Datum *</label>
|
<label>Passwort</label>
|
||||||
<input type="date" id="res-date" required>
|
<input type="password" id="loginPassword" required placeholder="••••••••">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<button type="submit" class="btn btn-primary" style="width: 100%">Anmelden</button>
|
||||||
<label>Uhrzeit *</label>
|
<button type="button" class="btn btn-secondary" style="width: 100%; margin-top: 0.5rem" onclick="hideLogin()">Abbrechen</button>
|
||||||
<input type="time" id="res-time" required>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<main class="container">
|
||||||
<div class="form-group">
|
<div class="guest-notice" id="guestNotice">
|
||||||
<label>Name *</label>
|
👋 Willkommen! Als Gast können Sie Tische reservieren. Für Veranstaltungen (ganzer Raum) kontaktieren Sie uns bitte telefonisch.
|
||||||
<input type="text" id="res-name" placeholder="Name suchen..." onkeyup="searchGuestForReservation()" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Personen *</label>
|
|
||||||
<input type="number" id="res-guests" min="1" max="50" value="2" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<h1 class="page-title">Unsere Räume</h1>
|
||||||
|
<p class="page-subtitle">Wählen Sie einen Raum für Ihre Reservierung</p>
|
||||||
|
|
||||||
|
<div class="loading" id="loading">Lade Räume...</div>
|
||||||
|
<div class="rooms-grid" id="roomsGrid" style="display: none;"></div>
|
||||||
|
|
||||||
|
<div id="bookingSection" style="display: none;">
|
||||||
|
<h2 style="margin: 2rem 0 1rem;">Tisch reservieren</h2>
|
||||||
|
|
||||||
|
<div class="captcha-box">
|
||||||
|
<div class="captcha-question" id="captchaQuestion">Lade Captcha...</div>
|
||||||
|
<input type="number" class="captcha-input" id="captchaAnswer" placeholder="?" required>
|
||||||
|
<input type="hidden" id="captchaToken">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="bookingForm" onsubmit="handleBooking(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Name</label>
|
||||||
|
<input type="text" id="bookingName" required placeholder="Max Mustermann">
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Telefon</label>
|
<label>Telefon</label>
|
||||||
<input type="tel" id="res-phone">
|
<input type="tel" id="bookingPhone" placeholder="+49 123 456789">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>E-Mail</label>
|
<label>Datum</label>
|
||||||
<input type="email" id="res-email">
|
<input type="date" id="bookingDate" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Quelle</label>
|
<label>Zeit</label>
|
||||||
<select id="res-source">
|
<input type="time" id="bookingTime" required>
|
||||||
<option value="phone">Telefon</option>
|
|
||||||
<option value="email">E-Mail</option>
|
|
||||||
<option value="web">Web</option>
|
|
||||||
<option value="walk-in">Walk-in</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="phone-caller-group">
|
|
||||||
<label>Anrufer (bei Telefon-Buchung)</label>
|
|
||||||
<input type="text" id="res-caller">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Notizen</label>
|
<label>Personen</label>
|
||||||
<textarea id="res-notes" rows="3"></textarea>
|
<input type="number" id="bookingGuests" min="1" max="20" value="2" required>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" style="width: 100%">Reservierung anfragen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="btn btn-secondary" onclick="closeModal()">Abbrechen</button>
|
|
||||||
<button class="btn" onclick="saveReservation()">Speichern</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Global state
|
let currentUser = null;
|
||||||
let currentView = 'dashboard';
|
let selectedRoom = null;
|
||||||
|
let selectedTable = null;
|
||||||
|
|
||||||
// Init
|
// Check session on load
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
async function checkSession() {
|
||||||
loadDashboard();
|
try {
|
||||||
document.getElementById('res-date').valueAsDate = new Date();
|
const res = await fetch('/api/session');
|
||||||
|
const data = await res.json();
|
||||||
// Source change handler
|
if (data.logged_in) {
|
||||||
document.getElementById('res-source').addEventListener('change', (e) => {
|
currentUser = data;
|
||||||
const callerGroup = document.getElementById('phone-caller-group');
|
document.body.classList.add('admin-visible');
|
||||||
callerGroup.style.display = e.target.value === 'phone' ? 'block' : 'none';
|
document.getElementById('loginBtn').textContent = 'Abmelden';
|
||||||
});
|
document.getElementById('loginBtn').onclick = logout;
|
||||||
});
|
document.getElementById('guestNotice').style.display = 'none';
|
||||||
|
}
|
||||||
// View switching
|
} catch (e) {
|
||||||
function showView(view) {
|
console.log('Not logged in');
|
||||||
// Hide all views
|
}
|
||||||
document.querySelectorAll('[id^="view-"]').forEach(el => {
|
loadRooms();
|
||||||
el.classList.add('hidden');
|
loadCaptcha();
|
||||||
});
|
|
||||||
|
|
||||||
// Show selected view
|
|
||||||
document.getElementById(`view-${view}`).classList.remove('hidden');
|
|
||||||
|
|
||||||
// Update nav buttons
|
|
||||||
document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
||||||
btn.classList.remove('active');
|
|
||||||
});
|
|
||||||
event.target.classList.add('active');
|
|
||||||
|
|
||||||
// Load data for view
|
|
||||||
if (view === 'dashboard') loadDashboard();
|
|
||||||
if (view === 'reservations') loadReservations();
|
|
||||||
if (view === 'floorplan') loadFloorplan();
|
|
||||||
if (view === 'guests') loadGuests();
|
|
||||||
if (view === 'emails') loadEmails();
|
|
||||||
|
|
||||||
currentView = view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// API calls
|
// Load rooms
|
||||||
async function api(endpoint, options = {}) {
|
async function loadRooms() {
|
||||||
const res = await fetch(`/api${endpoint}`, {
|
try {
|
||||||
...options,
|
const res = await fetch('/api/rooms');
|
||||||
headers: {
|
const rooms = await res.json();
|
||||||
'Content-Type': 'application/json',
|
displayRooms(rooms);
|
||||||
...options.headers
|
} catch (e) {
|
||||||
|
document.getElementById('loading').textContent = 'Fehler beim Laden der Räume';
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return res.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dashboard
|
function displayRooms(rooms) {
|
||||||
async function loadDashboard() {
|
const grid = document.getElementById('roomsGrid');
|
||||||
const data = await api('/dashboard');
|
const loading = document.getElementById('loading');
|
||||||
|
|
||||||
document.getElementById('stat-today').textContent = data.today_count;
|
loading.style.display = 'none';
|
||||||
document.getElementById('stat-guests').textContent = data.guests_today;
|
grid.style.display = 'grid';
|
||||||
document.getElementById('stat-available').textContent = data.total_tables - data.today_count;
|
|
||||||
document.getElementById('stat-pending').textContent = data.pending_emails;
|
|
||||||
|
|
||||||
// Today's reservations
|
grid.innerHTML = rooms.map(room => `
|
||||||
const tbody = document.getElementById('today-table-body');
|
<div class="room-card" onclick="selectRoom(${room.id}, '${room.name}')" style="border-left: 4px solid ${room.color}">
|
||||||
tbody.innerHTML = data.today_reservations.map(r => `
|
<div class="room-header">
|
||||||
<tr>
|
<span class="room-name">${room.name}</span>
|
||||||
<td><span class="booking-number">${r.booking_number}</span></td>
|
<span class="room-badge" style="background: ${room.color}20; color: ${room.color}">
|
||||||
<td>${r.time_from}</td>
|
${room.capacity} Plätze
|
||||||
<td>${r.guest_name || r.phone || 'Unbekannt'}</td>
|
</span>
|
||||||
<td>${r.guests}</td>
|
</div>
|
||||||
<td><span class="badge badge-${r.status}">${r.status}</span></td>
|
<div class="room-capacity">
|
||||||
<td><button class="btn btn-secondary" onclick="viewReservation(${r.id})">Details</button></td>
|
${room.areas ? room.areas.length : 0} Bereiche ·
|
||||||
</tr>
|
${room.areas ? room.areas.reduce((sum, a) => sum + (a.tables ? a.tables.length : 0), 0) : 0} Tische
|
||||||
|
</div>
|
||||||
|
<div class="room-stats">
|
||||||
|
<span>🕐 ${room.areas && room.areas[0] ? room.areas[0].available_from + '-' + room.areas[0].available_to : '10:00-23:00'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reservations
|
function selectRoom(roomId, roomName) {
|
||||||
async function loadReservations() {
|
selectedRoom = roomId;
|
||||||
const date = document.getElementById('filter-date').value;
|
document.getElementById('bookingSection').style.display = 'block';
|
||||||
const params = date ? `?date=${date}` : '';
|
document.getElementById('bookingSection').scrollIntoView({ behavior: 'smooth' });
|
||||||
const data = await api(`/reservations${params}`);
|
|
||||||
|
|
||||||
const tbody = document.getElementById('reservations-table-body');
|
|
||||||
tbody.innerHTML = data.map(r => `
|
|
||||||
<tr>
|
|
||||||
<td><span class="booking-number">${r.booking_number}</span></td>
|
|
||||||
<td>${r.date}</td>
|
|
||||||
<td>${r.time_from}</td>
|
|
||||||
<td>${r.guest_name || 'Unbekannt'}</td>
|
|
||||||
<td>${r.guests}</td>
|
|
||||||
<td><span class="badge badge-${r.status}">${r.status}</span></td>
|
|
||||||
</tr>
|
|
||||||
`).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal functions
|
// Captcha
|
||||||
function openNewReservation() {
|
async function loadCaptcha() {
|
||||||
document.getElementById('modal-reservation').classList.remove('hidden');
|
try {
|
||||||
|
const res = await fetch('/api/captcha');
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById('captchaQuestion').textContent = data.question;
|
||||||
|
document.getElementById('captchaToken').value = data.token;
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('captchaQuestion').textContent = 'Captcha nicht verfügbar';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
// Login
|
||||||
document.getElementById('modal-reservation').classList.add('hidden');
|
function showLogin() {
|
||||||
document.getElementById('reservation-form').reset();
|
document.getElementById('loginModal').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveReservation() {
|
function hideLogin() {
|
||||||
|
document.getElementById('loginModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogin(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const password = document.getElementById('loginPassword').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ password })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
hideLogin();
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Falsches Passwort');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Fehler beim Login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
await fetch('/api/admin/logout', { method: 'POST' });
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Booking
|
||||||
|
async function handleBooking(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const captchaAnswer = document.getElementById('captchaAnswer').value;
|
||||||
|
const captchaToken = document.getElementById('captchaToken').value;
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
date: document.getElementById('res-date').value,
|
name: document.getElementById('bookingName').value,
|
||||||
time_from: document.getElementById('res-time').value,
|
phone: document.getElementById('bookingPhone').value,
|
||||||
guests: parseInt(document.getElementById('res-guests').value),
|
date: document.getElementById('bookingDate').value,
|
||||||
name: document.getElementById('res-name').value,
|
time_from: document.getElementById('bookingTime').value,
|
||||||
phone: document.getElementById('res-phone').value,
|
guests: parseInt(document.getElementById('bookingGuests').value),
|
||||||
email: document.getElementById('res-email').value,
|
captcha_token: captchaToken,
|
||||||
source: document.getElementById('res-source').value,
|
captcha_answer: captchaAnswer
|
||||||
phone_caller_name: document.getElementById('res-caller').value,
|
|
||||||
notes: document.getElementById('res-notes').value
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await api('/reservations', {
|
try {
|
||||||
|
const res = await fetch('/api/reservations', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.booking_number) {
|
if (res.ok) {
|
||||||
alert(`Reservierung erstellt: ${result.booking_number}`);
|
alert('Reservierung erfolgreich! Wir melden uns bei Ihnen.');
|
||||||
closeModal();
|
document.getElementById('bookingForm').reset();
|
||||||
loadDashboard();
|
loadCaptcha();
|
||||||
} else {
|
} else {
|
||||||
alert('Fehler: ' + (result.error || 'Unbekannter Fehler'));
|
const err = await res.json();
|
||||||
|
alert(err.error || 'Fehler bei der Reservierung');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Netzwerkfehler');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholder functions
|
// Set today's date as default
|
||||||
function loadFloorplan() { console.log('Floorplan loading...'); }
|
document.getElementById('bookingDate').valueAsDate = new Date();
|
||||||
function loadGuests() { console.log('Guests loading...'); }
|
|
||||||
function loadEmails() { console.log('Emails loading...'); }
|
// Initialize
|
||||||
function filterEmails(status) { console.log('Filter:', status); }
|
checkSession();
|
||||||
function searchGuests() { console.log('Search guests...'); }
|
|
||||||
function openNewGuest() { console.log('New guest...'); }
|
|
||||||
function searchGuestForReservation() { console.log('Search guest...'); }
|
|
||||||
function viewReservation(id) { console.log('View:', id); }
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user