Files
Peter (OpenClaw) daaa9bff5e Fix: Admin-UI funktionsfähig machen
- admin.html: Komplette Tab-Navigation implementiert
- admin.html: Alle Funktionen (Räume, Bereiche, Blockierung, SMTP, LLM)
- admin_routes.py: SQL-Fehler behoben (table_id → table_ids)

Admin-Oberfläche jetzt voll funktionsfähig
2026-05-27 07:44:42 +00:00

737 lines
29 KiB
HTML
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.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reservierung Admin</title>
<style>
:root {
--bg: #0f172a;
--card: #1e293b;
--text: #f1f5f9;
--text-muted: #94a3b8;
--accent: #00d4aa;
--accent-dark: #00b894;
--danger: #ef4444;
--warning: #f59e0b;
--success: #10b981;
}
[data-theme="purple"] { --accent: #8b5cf6; --accent-dark: #7c3aed; }
[data-theme="orange"] { --accent: #f97316; --accent-dark: #ea580c; }
[data-theme="blue"] { --accent: #3b82f6; --accent-dark: #2563eb; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
.sidebar {
width: 260px;
background: var(--card);
position: fixed;
height: 100vh;
border-right: 1px solid #334155;
padding: 1.5rem;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
color: var(--accent);
margin-bottom: 2rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.nav-item {
padding: 0.75rem 1rem;
border-radius: 8px;
cursor: pointer;
margin-bottom: 0.5rem;
color: var(--text-muted);
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.75rem;
}
.nav-item:hover, .nav-item.active {
background: var(--accent);
color: #000;
}
.main {
margin-left: 260px;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.card {
background: var(--card);
border-radius: 16px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid #334155;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--accent);
}
.btn {
padding: 0.625rem 1.25rem;
border-radius: 8px;
border: none;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.btn-primary {
background: var(--accent);
color: #000;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 212, 170, 0.4);
}
.btn-danger {
background: var(--danger);
color: #fff;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-muted);
font-size: 0.875rem;
}
.form-group input, .form-group select, .form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #334155;
border-radius: 8px;
background: var(--bg);
color: var(--text);
}
.theme-selector {
display: flex;
gap: 1rem;
}
.theme-btn {
width: 40px;
height: 40px;
border-radius: 50%;
border: 3px solid transparent;
cursor: pointer;
}
.theme-btn.active { border-color: #fff; }
.theme-green { background: linear-gradient(135deg, #00d4aa, #00b894); }
.theme-purple { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }
.theme-orange { background: linear-gradient(135deg, #f97316, #ea580c); }
.theme-blue { background: linear-gradient(135deg, #3b82f6, #2563eb); }
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
.stat-box {
background: var(--bg);
padding: 1.5rem;
border-radius: 12px;
text-align: center;
}
.stat-box h3 {
font-size: 2.5rem;
color: var(--accent);
margin-bottom: 0.5rem;
}
.stat-box p {
color: var(--text-muted);
}
.tab-content { display: none; }
.tab-content.active { display: block; }
table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #334155;
}
th {
color: var(--text-muted);
font-weight: 600;
}
.hidden { display: none !important; }
@media (max-width: 1024px) {
.sidebar { width: 100%; position: relative; height: auto; }
.main { margin-left: 0; }
.grid-2, .grid-3 { grid-template-columns: 1fr; }
}
</style>
</head>
<body data-theme="green">
<aside class="sidebar">
<div class="logo">🍽️ Admin</div>
<nav>
<div class="nav-item active" onclick="showTab('dashboard', this)">📊 Dashboard</div>
<div class="nav-item" onclick="showTab('rooms', this)">🏛️ Räume & Bereiche</div>
<div class="nav-item" onclick="showTab('smtp', this)">📧 E-Mail (SMTP)</div>
<div class="nav-item" onclick="showTab('llm', this)">🤖 LLM (Ollama)</div>
<div class="nav-item" onclick="showTab('settings', this)">⚙️ Einstellungen</div>
<div class="nav-item" onclick="logout()">🚪 Abmelden</div>
</nav>
</aside>
<main class="main">
<div class="header">
<h1 id="page-title">Dashboard</h1>
<div class="theme-selector">
<div class="theme-btn theme-green active" onclick="setTheme('green')"></div>
<div class="theme-btn theme-purple" onclick="setTheme('purple')"></div>
<div class="theme-btn theme-orange" onclick="setTheme('orange')"></div>
<div class="theme-btn theme-blue" onclick="setTheme('blue')"></div>
</div>
</div>
<div id="content">
<!-- DASHBOARD TAB -->
<div id="tab-dashboard" class="tab-content active">
<div class="card">
<div class="card-title">📊 Heutige Übersicht</div>
<div class="grid-3">
<div class="stat-box">
<h3 id="stat-today-count">-</h3>
<p>Reservierungen heute</p>
</div>
<div class="stat-box">
<h3 id="stat-today-guests">-</h3>
<p>Gäste heute</p>
</div>
<div class="stat-box">
<h3 id="stat-pending">-</h3>
<p>Ausstehende E-Mails</p>
</div>
</div>
</div>
<div class="card">
<div class="card-title">📋 Heutige Reservierungen</div>
<div id="today-reservations">Lädt...</div>
</div>
</div>
<!-- ROOMS TAB -->
<div id="tab-rooms" class="tab-content">
<div class="card">
<div class="card-title"> Neuen Raum erstellen</div>
<div class="grid-2">
<div class="form-group">
<label>Raum Name</label>
<input type="text" id="roomName" placeholder="z.B. Hauptraum">
</div>
<div class="form-group">
<label>Kapazität</label>
<input type="number" id="roomCapacity" value="50">
</div>
<div class="form-group">
<label>Farbe</label>
<input type="color" id="roomColor" value="#3498db">
</div>
</div>
<button class="btn btn-primary" onclick="createRoom()">💾 Raum erstellen</button>
</div>
<div class="card">
<div class="card-title"> Neuen Bereich erstellen</div>
<div class="grid-2">
<div class="form-group">
<label>Bereich Name</label>
<input type="text" id="areaName" placeholder="z.B. Fensterbereich">
</div>
<div class="form-group">
<label>Raum</label>
<select id="areaRoom"></select>
</div>
</div>
<button class="btn btn-primary" onclick="createArea()">💾 Bereich erstellen</button>
</div>
<div class="card">
<div class="card-title">🏛️ Räume & Bereiche</div>
<div id="rooms-list">Lädt...</div>
</div>
<div class="card">
<div class="card-title">🚫 Zeit blockieren</div>
<div class="grid-2">
<div class="form-group">
<label>Datum</label>
<input type="date" id="blockDate">
</div>
<div class="form-group">
<label>Raum</label>
<select id="blockRoom"></select>
</div>
<div class="form-group">
<label>Von</label>
<input type="time" id="blockFrom" value="18:00">
</div>
<div class="form-group">
<label>Bis</label>
<input type="time" id="blockTo" value="23:00">
</div>
<div class="form-group" style="grid-column: span 2;">
<label>Grund</label>
<input type="text" id="blockReason" placeholder="z.B. Private Feier">
</div>
</div>
<button class="btn btn-primary" onclick="blockTime()">🚫 Zeit blockieren</button>
</div>
</div>
<!-- SMTP TAB -->
<div id="tab-smtp" class="tab-content">
<div class="card">
<div class="card-title">📧 SMTP Konfiguration</div>
<div class="grid-2">
<div class="form-group">
<label>SMTP Server</label>
<input type="text" id="smtpHost" placeholder="smtp.gmail.com">
</div>
<div class="form-group">
<label>Port</label>
<input type="number" id="smtpPort" value="587">
</div>
<div class="form-group">
<label>Benutzername</label>
<input type="text" id="smtpUser" placeholder="email@domain.de">
</div>
<div class="form-group">
<label>Passwort</label>
<input type="password" id="smtpPass" placeholder="••••••••">
</div>
<div class="form-group">
<label>Absender E-Mail</label>
<input type="email" id="smtpFrom" placeholder="reservierung@domain.de">
</div>
<div class="form-group">
<label>Absender Name</label>
<input type="text" id="smtpFromName" placeholder="Reservierungssystem">
</div>
<div class="form-group">
<label>Sicherheit</label>
<select id="smtpSecurity">
<option value="tls">TLS</option>
<option value="ssl">SSL</option>
<option value="none">Keine</option>
</select>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 0.5rem;">
<input type="checkbox" id="smtpEnabled"> SMTP aktiviert
</label>
</div>
</div>
<button class="btn btn-primary" onclick="saveSMTP()">💾 Speichern</button>
<button class="btn btn-primary" onclick="testSMTP()" style="margin-left: 0.5rem;">🧪 Testen</button>
</div>
</div>
<!-- LLM TAB -->
<div id="tab-llm" class="tab-content">
<div class="card">
<div class="card-title">🤖 LLM Konfiguration (Ollama)</div>
<div class="grid-2">
<div class="form-group">
<label>Ollama URL</label>
<input type="text" id="llmUrl" value="http://localhost:11434">
</div>
<div class="form-group">
<label>Modell</label>
<select id="llmModel">
<option value="llama2">Llama 2</option>
<option value="mistral">Mistral</option>
<option value="mixtral">Mixtral</option>
</select>
</div>
<div class="form-group">
<label>Temperatur (0.0 - 1.0)</label>
<input type="number" id="llmTemp" value="0.7" min="0" max="1" step="0.1">
</div>
<div class="form-group">
<label>Max Tokens</label>
<input type="number" id="llmTokens" value="500" min="100" max="2000" step="100">
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 0.5rem;">
<input type="checkbox" id="llmEnabled"> LLM aktiviert
</label>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 0.5rem;">
<input type="checkbox" id="llmAutoReply"> Auto-Antworten
</label>
</div>
</div>
<button class="btn btn-primary" onclick="saveLLM()">💾 Speichern</button>
<button class="btn btn-primary" onclick="testLLM()" style="margin-left: 0.5rem;">🧪 Testen</button>
</div>
</div>
<!-- SETTINGS TAB -->
<div id="tab-settings" class="tab-content">
<div class="card">
<div class="card-title">⚙️ Öffnungszeiten</div>
<div class="grid-2">
<div class="form-group">
<label>Öffnet um</label>
<input type="number" id="openHour" value="10" min="0" max="23">
</div>
<div class="form-group">
<label>Schließt um</label>
<input type="number" id="closeHour" value="23" min="0" max="23">
</div>
</div>
<button class="btn btn-primary" onclick="saveHours()">💾 Speichern</button>
</div>
</div>
</div>
</main>
<script>
// === THEME ===
function setTheme(theme) {
document.body.setAttribute('data-theme', theme);
document.querySelectorAll('.theme-btn').forEach(btn => btn.classList.remove('active'));
document.querySelector(`.theme-${theme}`).classList.add('active');
localStorage.setItem('theme', theme);
}
// === TAB SWITCHING ===
function showTab(tabName, element) {
// Nav aktualisieren
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
element.classList.add('active');
// Content aktualisieren
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
document.getElementById(`tab-${tabName}`).classList.add('active');
// Titel aktualisieren
const titles = {
'dashboard': 'Dashboard',
'rooms': 'Räume & Bereiche',
'smtp': 'E-Mail Konfiguration',
'llm': 'LLM Konfiguration',
'settings': 'Einstellungen'
};
document.getElementById('page-title').textContent = titles[tabName] || 'Admin';
// Tab-spezifische Daten laden
if (tabName === 'dashboard') loadDashboard();
if (tabName === 'rooms') loadRooms();
if (tabName === 'smtp') loadSMTP();
if (tabName === 'llm') loadLLM();
}
// === AUTH ===
function logout() {
fetch('/api/admin/logout', {method: 'POST'}).then(() => location.href = '/');
}
// === DASHBOARD ===
async function loadDashboard() {
try {
const response = await fetch('/api/dashboard');
const data = await response.json();
document.getElementById('stat-today-count').textContent = data.today_count || 0;
document.getElementById('stat-today-guests').textContent = data.guests_today || 0;
document.getElementById('stat-pending').textContent = data.pending_emails || 0;
// Reservierungen anzeigen
const container = document.getElementById('today-reservations');
if (data.today_reservations && data.today_reservations.length > 0) {
let html = '<table><tr><th>Zeit</th><th>Name</th><th>Gäste</th><th>Status</th></tr>';
data.today_reservations.forEach(r => {
html += `<tr><td>${r.time_from}</td><td>${r.guest_name || 'Unbekannt'}</td><td>${r.guests}</td><td>${r.status}</td></tr>`;
});
html += '</table>';
container.innerHTML = html;
} else {
container.innerHTML = '<p style="color: var(--text-muted)">Keine Reservierungen heute</p>';
}
} catch (e) {
console.error('Dashboard load error:', e);
}
}
// === ROOMS ===
async function loadRooms() {
try {
const response = await fetch('/api/rooms');
const rooms = await response.json();
// Dropdowns befüllen
const roomSelects = ['areaRoom', 'blockRoom'];
roomSelects.forEach(id => {
const select = document.getElementById(id);
if (select) {
select.innerHTML = rooms.map(r => `<option value="${r.id}">${r.name}</option>`).join('');
}
});
// Räume anzeigen
const container = document.getElementById('rooms-list');
let html = '';
rooms.forEach(room => {
html += `<div style="margin-bottom: 1rem; padding: 1rem; background: var(--bg); border-radius: 8px;">`;
html += `<h4 style="color: var(--accent);">${room.name}</h4>`;
if (room.areas && room.areas.length > 0) {
html += '<ul>';
room.areas.forEach(area => {
html += `<li>${area.name} (${area.tables ? area.tables.length : 0} Tische)</li>`;
});
html += '</ul>';
}
html += '</div>';
});
container.innerHTML = html;
} catch (e) {
console.error('Rooms load error:', e);
}
}
async function createRoom() {
const data = {
name: document.getElementById('roomName').value,
capacity: parseInt(document.getElementById('roomCapacity').value),
color: document.getElementById('roomColor').value
};
try {
const response = await fetch('/api/admin/rooms', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (response.ok) {
alert('Raum erstellt!');
loadRooms();
} else {
alert('Fehler beim Erstellen');
}
} catch (e) {
alert('Fehler: ' + e.message);
}
}
async function createArea() {
const data = {
room_id: parseInt(document.getElementById('areaRoom').value),
name: document.getElementById('areaName').value
};
try {
const response = await fetch('/api/admin/areas', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (response.ok) {
alert('Bereich erstellt!');
loadRooms();
} else {
alert('Fehler beim Erstellen');
}
} catch (e) {
alert('Fehler: ' + e.message);
}
}
async function blockTime() {
const data = {
room_id: parseInt(document.getElementById('blockRoom').value),
date: document.getElementById('blockDate').value,
time_from: document.getElementById('blockFrom').value,
time_to: document.getElementById('blockTo').value,
reason: document.getElementById('blockReason').value
};
try {
const response = await fetch('/api/admin/blocked-times', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (response.ok) {
alert('Zeit blockiert!');
} else {
alert('Fehler beim Blockieren');
}
} catch (e) {
alert('Fehler: ' + e.message);
}
}
// === SMTP ===
async function loadSMTP() {
try {
const response = await fetch('/api/admin/smtp');
const data = await response.json();
document.getElementById('smtpHost').value = data.host || '';
document.getElementById('smtpPort').value = data.port || 587;
document.getElementById('smtpUser').value = data.user || '';
document.getElementById('smtpFrom').value = data.from_email || '';
document.getElementById('smtpFromName').value = data.from_name || '';
document.getElementById('smtpSecurity').value = data.security || 'tls';
document.getElementById('smtpEnabled').checked = data.enabled || false;
} catch (e) {
console.error('SMTP load error:', e);
}
}
async function saveSMTP() {
const data = {
host: document.getElementById('smtpHost').value,
port: parseInt(document.getElementById('smtpPort').value),
user: document.getElementById('smtpUser').value,
password: document.getElementById('smtpPass').value,
from_email: document.getElementById('smtpFrom').value,
from_name: document.getElementById('smtpFromName').value,
security: document.getElementById('smtpSecurity').value,
enabled: document.getElementById('smtpEnabled').checked
};
try {
const response = await fetch('/api/admin/smtp', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (response.ok) {
alert('SMTP gespeichert!');
} else {
alert('Fehler beim Speichern');
}
} catch (e) {
alert('Fehler: ' + e.message);
}
}
async function testSMTP() {
try {
const response = await fetch('/api/admin/smtp/test', {method: 'POST'});
const data = await response.json();
alert(data.message || data.error);
} catch (e) {
alert('Fehler: ' + e.message);
}
}
// === LLM ===
async function loadLLM() {
try {
const response = await fetch('/api/admin/llm-config');
const data = await response.json();
document.getElementById('llmUrl').value = data.url || 'http://localhost:11434';
document.getElementById('llmModel').value = data.model || 'llama2';
document.getElementById('llmTemp').value = data.temperature || 0.7;
document.getElementById('llmTokens').value = data.max_tokens || 500;
document.getElementById('llmEnabled').checked = data.enabled || false;
document.getElementById('llmAutoReply').checked = data.auto_reply || false;
} catch (e) {
console.error('LLM load error:', e);
}
}
async function saveLLM() {
const data = {
url: document.getElementById('llmUrl').value,
model: document.getElementById('llmModel').value,
temperature: parseFloat(document.getElementById('llmTemp').value),
max_tokens: parseInt(document.getElementById('llmTokens').value),
enabled: document.getElementById('llmEnabled').checked,
auto_reply: document.getElementById('llmAutoReply').checked
};
try {
const response = await fetch('/api/admin/llm-config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (response.ok) {
alert('LLM gespeichert!');
} else {
alert('Fehler beim Speichern');
}
} catch (e) {
alert('Fehler: ' + e.message);
}
}
async function testLLM() {
try {
const response = await fetch('/api/admin/llm-config/test', {method: 'POST'});
const data = await response.json();
alert(data.message || data.error);
} catch (e) {
alert('Fehler: ' + e.message);
}
}
// === INIT ===
const savedTheme = localStorage.getItem('theme') || 'green';
setTheme(savedTheme);
loadDashboard();
// Datum auf heute setzen
document.getElementById('blockDate').value = new Date().toISOString().split('T')[0];
</script>
</body>
</html>