Files
getrankekarte/public/admin.html
DanielS 21cb5c929a
All checks were successful
Build and Deploy / build (push) Successful in 59s
feat(qr): automatischer QR-Code Handshake (@LNK & CASPOS.PUT Aktivierung)
2026-09-22 22:08:53 +02:00

530 lines
19 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, textarea, 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;
font-family: inherit;
}
input:focus, textarea: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-qr {
background: #8b5cf6;
color: #fff;
}
.btn-qr:hover { background: #7c3aed; }
.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, QR-Setup & 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>
<!-- QR-Code Scanner / Setup -->
<div class="card" style="border-left: 4px solid #8b5cf6;">
<h3 style="margin-bottom: 8px; color: #a78bfa;">📷 Automatisches QR-Code Setup (ww-setup)</h3>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 16px;">
Füge hier den gescannten QR-Code Inhalt (JSON) aus der POS Cloud ein, um den automatischen Handshake (@LNK) und die Aktivierung (CASPOS.PUT) auszuführen.
</p>
<form id="qr-form">
<div class="form-group">
<textarea id="qr-json" rows="3" placeholder='{"$type": "ww-setup", "url": "https://...", "servicepasslink": "...", "mdcDeviceId": "1"}' required></textarea>
</div>
<button type="submit" class="btn btn-qr">🚀 QR-Code Handshake ausführen</button>
</form>
</div>
<!-- POS Cloud Konfiguration -->
<div class="card">
<h3 style="margin-bottom: 16px; color: var(--primary);">⚙️ Manuelle 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; flex-wrap: wrap;">
<button type="submit" class="btn">Speichern</button>
<button type="button" id="btn-sync-full" class="btn btn-sync">🔄 Voll-Sync (ERSTEINRICHTUNG)</button>
<button type="button" id="btn-sync-diff" class="btn btn-secondary">⚡ Diff-Sync (AENDERUNGEN)</button>
<button type="button" id="btn-mock-data" class="btn btn-secondary">📥 Musterdaten</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>
function showAlert(id, msg, isError = false) {
const el = document.getElementById(id);
el.textContent = msg;
el.className = 'notification ' + (isError ? 'error' : 'success');
setTimeout(() => { el.className = 'notification'; }, 6000);
}
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 || '';
}
}
// QR Handshake Form
document.getElementById('qr-form').addEventListener('submit', async (e) => {
e.preventDefault();
const qrContent = document.getElementById('qr-json').value;
showAlert('dash-alert', 'QR-Code Handshake läuft...');
try {
const res = await fetch('/api/admin/qr-setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ qrContent })
});
const data = await res.json();
if (res.ok) {
showAlert('dash-alert', 'QR-Code Handshake erfolgreich! Parameter übernommen & aktiviert.');
document.getElementById('qr-json').value = '';
loadConfig();
loadData();
} else {
showAlert('dash-alert', 'QR-Code Fehler: ' + data.error, true);
}
} catch (err) {
showAlert('dash-alert', 'Netzwerkfehler beim QR-Handshake', true);
}
});
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
async function triggerSync(type) {
showAlert('dash-alert', `Synchronisiere mit POS Cloud (${type})...`);
try {
const res = await fetch('/api/admin/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type })
});
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);
}
}
document.getElementById('btn-sync-full').addEventListener('click', () => triggerSync('ERSTEINRICHTUNG'));
document.getElementById('btn-sync-diff').addEventListener('click', () => triggerSync('AENDERUNGEN'));
// 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();
document.getElementById('product-count').textContent = `${data.products.length} Artikel`;
const groupMap = {};
for (const g of data.groups) groupMap[g.id] = g.name;
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('');
}
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>