Files
getrankekarte/public/admin.html

487 lines
17 KiB
HTML
Raw 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>POS Cloud Admin & Setup</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #090b10;
--card-bg: rgba(22, 27, 34, 0.85);
--card-border: rgba(255, 255, 255, 0.1);
--primary: #f59e0b;
--primary-hover: #d97706;
--accent: #38bdf8;
--success: #10b981;
--danger: #ef4444;
--text: #f8fafc;
--text-muted: #94a3b8;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
justify-content: center;
padding: 24px 16px;
}
.container {
width: 100%;
max-width: 900px;
}
h1, h2, h3 {
font-family: 'Space Grotesk', sans-serif;
}
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 24px;
backdrop-filter: blur(16px);
margin-bottom: 24px;
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
input, select {
width: 100%;
padding: 12px 14px;
background: rgba(15, 23, 42, 0.8);
border: 1px solid var(--card-border);
border-radius: 10px;
color: #fff;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
input:focus {
border-color: var(--primary);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 20px;
border-radius: 10px;
font-weight: 700;
font-size: 14px;
cursor: pointer;
border: none;
transition: 0.2s;
color: #090b10;
background: var(--primary);
}
.btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
.btn-secondary {
background: rgba(255,255,255,0.08);
color: #fff;
border: 1px solid var(--card-border);
}
.btn-secondary:hover {
background: rgba(255,255,255,0.15);
}
.btn-sync {
background: var(--accent);
color: #090b10;
}
.btn-sync:hover { background: #0284c7; }
.btn-danger { background: var(--danger); color: #fff; }
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 640px) {
.grid-2 { grid-template-columns: 1fr; }
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.table-container {
overflow-x: auto;
margin-top: 16px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th, td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
th {
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
font-size: 11px;
}
.badge {
padding: 3px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 700;
}
.badge-success { background: rgba(16, 185, 129, 0.15); color: var(--success); }
.badge-error { background: rgba(239, 68, 68, 0.15); color: var(--danger); }
.notification {
padding: 12px 16px;
border-radius: 10px;
margin-bottom: 16px;
display: none;
font-size: 14px;
}
.notification.success { display: block; background: rgba(16, 185, 129, 0.2); border: 1px solid var(--success); color: #a7f3d0; }
.notification.error { display: block; background: rgba(239, 68, 68, 0.2); border: 1px solid var(--danger); color: #fecaca; }
</style>
</head>
<body>
<div class="container">
<!-- Login Ansicht -->
<div id="login-section" class="card" style="max-width: 420px; margin: 60px auto;">
<h2 style="margin-bottom: 8px; color: var(--primary);">Admin Login</h2>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 20px;">Melde dich an um POS Cloud Einstellungen zu verwalten.</p>
<div id="login-alert" class="notification"></div>
<form id="login-form">
<div class="form-group">
<label>Benutzername</label>
<input type="text" id="login-username" value="admin" required />
</div>
<div class="form-group">
<label>Passwort</label>
<input type="password" id="login-password" value="Kathi2022!" required />
</div>
<button type="submit" class="btn" style="width: 100%;">Anmelden</button>
</form>
</div>
<!-- Dashboard Ansicht -->
<div id="dashboard-section" style="display: none;">
<div class="header-bar">
<div>
<h2>POS Cloud Administration</h2>
<p style="font-size: 13px; color: var(--text-muted);">Schnittstelle & Speisekarten-Synchronisation</p>
</div>
<div style="display: flex; gap: 10px;">
<a href="/" target="_blank" class="btn btn-secondary">🍽️ Zur Karte</a>
<button id="btn-logout" class="btn btn-secondary">Abmelden</button>
</div>
</div>
<div id="dash-alert" class="notification"></div>
<!-- POS Cloud Konfiguration -->
<div class="card">
<h3 style="margin-bottom: 16px; color: var(--primary);">⚙️ POS Cloud Verbindungsparameter</h3>
<form id="config-form">
<div class="grid-2">
<div class="form-group">
<label>ServicePass URL (CASPOS Endpoint)</label>
<input type="url" id="cfg-url" placeholder="https://poscloud.example.com/api" />
</div>
<div class="form-group">
<label>App-ID (appId)</label>
<input type="text" id="cfg-app-id" placeholder="z.B. APP_SPEISEKARTE_01" />
</div>
</div>
<div class="grid-2">
<div class="form-group">
<label>Passwort / Key (passId)</label>
<input type="password" id="cfg-pass-id" placeholder="Passwort aus ServicePass" />
</div>
<div class="form-group">
<label>Kassennummer (cashRegisterNumber / MDCDEVICEID)</label>
<input type="text" id="cfg-register" placeholder="z.B. 1" />
</div>
</div>
<div class="form-group" style="margin-top: 8px; border-top: 1px solid rgba(255,255,255,0.06); padding-top: 16px;">
<label>Admin-Passwort ändern (Optional)</label>
<input type="password" id="cfg-admin-pass" placeholder="Neues Passwort vergeben..." />
</div>
<div style="display: flex; gap: 12px; margin-top: 20px;">
<button type="submit" class="btn">Speichern</button>
<button type="button" id="btn-sync-full" class="btn btn-sync">🔄 Ersteinrichtung Sync (CASPOS.GET)</button>
<button type="button" id="btn-mock-data" class="btn btn-secondary">📥 Musterdaten laden</button>
</div>
</form>
</div>
<!-- Gespeicherte Artikel & Preise -->
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3 style="color: var(--primary);">📋 Importierte Artikel (Live aus DB)</h3>
<span id="product-count" class="badge badge-success">0 Artikel</span>
</div>
<div class="table-container">
<table id="products-table">
<thead>
<tr>
<th>Art.-Nr</th>
<th>Titel (text1)</th>
<th>Beschreibung (text2)</th>
<th>Gruppe</th>
<th>Preis (preis1)</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr><td colspan="6" style="text-align: center; color: var(--text-muted);">Keine Daten geladen</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Sync Logs -->
<div class="card">
<h3 style="margin-bottom: 12px; color: var(--text-muted);">📜 Sync Protokoll</h3>
<table id="logs-table">
<thead>
<tr>
<th>Zeitstempel</th>
<th>Status</th>
<th>Meldung</th>
</tr>
</thead>
<tbody>
<tr><td colspan="3" style="text-align: center; color: var(--text-muted);">Keine Logs vorhanden</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
// Hilfsfunktionen
function showAlert(id, msg, isError = false) {
const el = document.getElementById(id);
el.textContent = msg;
el.className = 'notification ' + (isError ? 'error' : 'success');
setTimeout(() => { el.className = 'notification'; }, 5000);
}
async function checkAuth() {
try {
const res = await fetch('/api/admin/check');
if (res.ok) {
showDashboard();
} else {
showLogin();
}
} catch(e) {
showLogin();
}
}
function showLogin() {
document.getElementById('login-section').style.display = 'block';
document.getElementById('dashboard-section').style.display = 'none';
}
function showDashboard() {
document.getElementById('login-section').style.display = 'none';
document.getElementById('dashboard-section').style.display = 'block';
loadConfig();
loadData();
}
// Login Form
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('login-username').value;
const password = document.getElementById('login-password').value;
try {
const res = await fetch('/api/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (res.ok) {
showDashboard();
} else {
showAlert('login-alert', data.error || 'Fehler beim Login', true);
}
} catch(e) {
showAlert('login-alert', 'Netzwerkfehler', true);
}
});
// Logout
document.getElementById('btn-logout').addEventListener('click', async () => {
await fetch('/api/admin/logout', { method: 'POST' });
showLogin();
});
// Config laden & speichern
async function loadConfig() {
const res = await fetch('/api/admin/config');
if (res.ok) {
const cfg = await res.json();
document.getElementById('cfg-url').value = cfg.pos_url || '';
document.getElementById('cfg-app-id').value = cfg.pos_app_id || '';
document.getElementById('cfg-pass-id').value = cfg.pos_pass_id || '';
document.getElementById('cfg-register').value = cfg.pos_cash_register || '';
}
}
document.getElementById('config-form').addEventListener('submit', async (e) => {
e.preventDefault();
const body = {
pos_url: document.getElementById('cfg-url').value,
pos_app_id: document.getElementById('cfg-app-id').value,
pos_pass_id: document.getElementById('cfg-pass-id').value,
pos_cash_register: document.getElementById('cfg-register').value,
new_admin_password: document.getElementById('cfg-admin-pass').value
};
const res = await fetch('/api/admin/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
if (res.ok) {
showAlert('dash-alert', 'Einstellungen erfolgreich gespeichert!');
document.getElementById('cfg-admin-pass').value = '';
} else {
showAlert('dash-alert', data.error || 'Fehler', true);
}
});
// POS Sync Button
document.getElementById('btn-sync-full').addEventListener('click', async () => {
showAlert('dash-alert', 'Synchronisiere mit POS Cloud...');
try {
const res = await fetch('/api/admin/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'ERSTEINRICHTUNG' })
});
const data = await res.json();
if (res.ok) {
showAlert('dash-alert', `Sync erfolgreich: ${data.result.productsCount} Artikel importiert.`);
loadData();
} else {
showAlert('dash-alert', `Sync Fehler: ${data.error}`, true);
loadData();
}
} catch(e) {
showAlert('dash-alert', 'Verbindung zur POS Cloud fehlgeschlagen', true);
}
});
// Mock-Daten laden für schnellen Test
document.getElementById('btn-mock-data').addEventListener('click', async () => {
const mockData = {
groups: [
{ id: "1", name: "Cocktails & Signature Drinks", sortOrder: 1 },
{ id: "2", name: "Alkoholfreie Cocktails", sortOrder: 2 },
{ id: "3", name: "Longdrinks & Highballs", sortOrder: 3 },
{ id: "4", name: "Bier & Wein", sortOrder: 4 },
{ id: "5", name: "Softdrinks & Shots", sortOrder: 5 }
],
products: [
{ id: "101", text1: "Wild Berry Lillet", text2: "Lillet Blanc, Schweppes Wild Berry, Beeren", groupId: "1", preis1: 7.50 },
{ id: "102", text1: "Aperol Spritz", text2: "Aperol, Prosecco, Soda, frische Orange", groupId: "1", preis1: 7.50 },
{ id: "103", text1: "Mojito Classic", text2: "Weißer Rum, Minze, Limette, Rohrzucker, Soda", groupId: "1", preis1: 8.50 },
{ id: "104", text1: "Sex on the Beach", text2: "Vodka, Pfirsichlikör, Cranberry, Orangensaft", groupId: "1", preis1: 8.50 },
{ id: "105", text1: "Ipanema (Alkoholfrei)", text2: "Limette, Rohrzucker, Maracujanektar, Ginger Ale", groupId: "2", preis1: 6.50 },
{ id: "106", text1: "Virgin Colada (Alkoholfrei)", text2: "Ananassaft, Kokossirup, Sahne", groupId: "2", preis1: 6.50 },
{ id: "107", text1: "Gin Tonic (Bombay)", text2: "Bombay Sapphire, Tonic Water, Zitrone", groupId: "3", preis1: 8.00 },
{ id: "108", text1: "Vodka Red Bull", text2: "Absolut Vodka, Red Bull Energy", groupId: "3", preis1: 8.00 },
{ id: "109", text1: "Frisches Fassbier 0,33l", text2: "Kalt gezapftes Pils", groupId: "4", preis1: 3.80 },
{ id: "110", text1: "Haus-Shot (Berliner Luft)", text2: "Pfefferminzlikör 2cl", groupId: "5", preis1: 2.50 }
]
};
const res = await fetch('/api/admin/mock-import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mockData)
});
if (res.ok) {
showAlert('dash-alert', 'Musterartikel erfolgreich eingespielt!');
loadData();
}
});
// Daten & Logs laden
async function loadData() {
const res = await fetch('/api/admin/data');
if (res.ok) {
const data = await res.json();
// Count
document.getElementById('product-count').textContent = `${data.products.length} Artikel`;
// Gruppen-Map
const groupMap = {};
for (const g of data.groups) groupMap[g.id] = g.name;
// Produkte Table
const tbody = document.querySelector('#products-table tbody');
if (data.products.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; color:var(--text-muted);">Keine Artikel in DB</td></tr>';
} else {
tbody.innerHTML = data.products.map(p => `
<tr>
<td><code>${p.id}</code></td>
<td><strong>${p.text1}</strong></td>
<td style="color:var(--text-muted);">${p.text2 || '-'}</td>
<td><span class="badge" style="background:rgba(245,158,11,0.15); color:var(--primary);">${groupMap[p.group_id] || 'Gruppe ' + (p.group_id || '–')}</span></td>
<td><strong style="color:#fff;">${parseFloat(p.preis1).toFixed(2).replace('.', ',')} €</strong></td>
<td>${p.active ? '<span class="badge badge-success">Aktiv</span>' : '<span class="badge badge-error">Inaktiv</span>'}</td>
</tr>
`).join('');
}
// Logs Table
const logBody = document.querySelector('#logs-table tbody');
if (data.logs.length === 0) {
logBody.innerHTML = '<tr><td colspan="3" style="text-align:center; color:var(--text-muted);">Keine Logs</td></tr>';
} else {
logBody.innerHTML = data.logs.map(l => `
<tr>
<td style="color:var(--text-muted);">${new Date(l.timestamp).toLocaleString('de-DE')}</td>
<td><span class="badge ${l.status === 'SUCCESS' ? 'badge-success' : 'badge-error'}">${l.status}</span></td>
<td>${l.message}</td>
</tr>
`).join('');
}
}
}
checkAuth();
</script>
</body>
</html>