Files
getrankekarte/public/admin.html

887 lines
32 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>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">
<!-- jsQR für maximale Kompatibilität -->
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
<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);
}
input[type="color"] {
height: 44px;
padding: 4px 8px;
cursor: pointer;
}
.color-picker-row {
display: flex;
gap: 10px;
align-items: center;
}
.color-picker-row input[type="color"] {
width: 50px;
flex-shrink: 0;
}
.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;
}
.grid-3 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
}
@media (max-width: 640px) {
.grid-2, .grid-3 { 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; }
.scanner-box {
width: 100%;
max-width: 500px;
margin: 16px auto;
border-radius: 14px;
overflow: hidden;
display: none;
position: relative;
background: #000;
border: 2px solid #8b5cf6;
}
#video {
width: 100%;
height: auto;
display: block;
}
.scan-guide {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 220px;
height: 220px;
border: 2px dashed #8b5cf6;
border-radius: 12px;
pointer-events: none;
box-shadow: 0 0 0 4000px rgba(0, 0, 0, 0.45);
}
.scan-line {
width: 100%;
height: 2px;
background: #ec4899;
position: absolute;
top: 0;
box-shadow: 0 0 8px #ec4899;
animation: scanAnim 2s infinite linear;
}
@keyframes scanAnim {
0% { top: 0; }
50% { top: 100%; }
100% { top: 0; }
}
</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" placeholder="Benutzername eingeben" required />
</div>
<div class="form-group">
<label>Passwort</label>
<input type="password" id="login-password" placeholder="Passwort eingeben" 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, Live QR-Scanner & 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>
<!-- 1. Globale Farbsteuerung & Design -->
<div class="card" style="border-left: 4px solid var(--primary);">
<h3 style="margin-bottom: 16px; color: var(--primary);">🎨 Globale Farb- & Designsteuerung</h3>
<form id="theme-form">
<div class="grid-3">
<div class="form-group">
<label>Hintergrundfarbe (Body)</label>
<div class="color-picker-row">
<input type="color" id="theme-bg" value="#090b10" />
<input type="text" id="theme-bg-text" value="#090b10" />
</div>
</div>
<div class="form-group">
<label>Kartenfarbe (Cards)</label>
<div class="color-picker-row">
<input type="color" id="theme-card-bg" value="#161b22" />
<input type="text" id="theme-card-bg-text" value="rgba(22, 27, 34, 0.75)" />
</div>
</div>
<div class="form-group">
<label>Primärfarbe (Akzente / Preise)</label>
<div class="color-picker-row">
<input type="color" id="theme-primary" value="#f59e0b" />
<input type="text" id="theme-primary-text" value="#f59e0b" />
</div>
</div>
</div>
<div class="grid-3">
<div class="form-group">
<label>Glow / Sekundärfarbe</label>
<div class="color-picker-row">
<input type="color" id="theme-accent" value="#ec4899" />
<input type="text" id="theme-accent-text" value="#ec4899" />
</div>
</div>
<div class="form-group">
<label>Textfarbe Haupt</label>
<div class="color-picker-row">
<input type="color" id="theme-text" value="#f8fafc" />
<input type="text" id="theme-text-text" value="#f8fafc" />
</div>
</div>
<div class="form-group">
<label>Textfarbe Gedämpft</label>
<div class="color-picker-row">
<input type="color" id="theme-text-muted" value="#94a3b8" />
<input type="text" id="theme-text-muted-text" value="#94a3b8" />
</div>
</div>
</div>
<div style="margin-top: 8px;">
<button type="submit" class="btn">🎨 Farben speichern</button>
</div>
</form>
</div>
<!-- 2. Globale Text- & Titelsteuerung -->
<div class="card" style="border-left: 4px solid var(--accent);">
<h3 style="margin-bottom: 16px; color: var(--accent);">✍️ Globale Titel & Texte</h3>
<form id="texts-form">
<div class="grid-2">
<div class="form-group">
<label>Haupttitel (z.B. GETRÄNKEKARTE)</label>
<input type="text" id="site-title" placeholder="GETRÄNKEKARTE" />
</div>
<div class="form-group">
<label>Badge / Oberzeile (z.B. EXKLUSIVE AUSWAHL)</label>
<input type="text" id="site-badge" placeholder="✦ EXKLUSIVE AUSWAHL ✦" />
</div>
</div>
<div class="grid-2">
<div class="form-group">
<label>Untertitel / Beschreibung</label>
<input type="text" id="site-subtitle" placeholder="Frisch zubereitete Drinks..." />
</div>
<div class="form-group">
<label>Footer Text</label>
<input type="text" id="site-footer" placeholder="© Bar & Lounge • Live POS Sync" />
</div>
</div>
<div style="margin-top: 8px;">
<button type="submit" class="btn btn-sync">✍️ Texte speichern</button>
</div>
</form>
</div>
<!-- 3. QR-Code Scanner / Setup -->
<div class="card" style="border-left: 4px solid #8b5cf6;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; flex-wrap: wrap; gap: 10px;">
<h3 style="color: #a78bfa;">📷 QR-Code Setup (Kamera / Bild / JSON)</h3>
<div style="display: flex; gap: 8px;">
<label class="btn btn-secondary" style="margin: 0; padding: 8px 14px; font-size: 13px; cursor: pointer;">
📁 Foto hochladen <input type="file" id="qr-file-input" accept="image/*" style="display: none;" />
</label>
<button type="button" id="btn-toggle-camera" class="btn btn-qr" style="padding: 8px 14px; font-size: 13px;">
🎥 Kamera starten
</button>
</div>
</div>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 12px;">
QR-Code vor die Kamera halten, als Screenshot hochladen oder JSON direkt einfügen.
</p>
<!-- Kamera Viewport -->
<div id="scanner-box" class="scanner-box">
<video id="video" playsinline></video>
<canvas id="canvas" style="display: none;"></canvas>
<div class="scan-guide">
<div class="scan-line"></div>
</div>
</div>
<form id="qr-form" style="margin-top: 12px;">
<div class="form-group">
<textarea id="qr-json" rows="3" placeholder='{"$type": "ww-setup", "url": "https://...", "servicepasslink": "...", "mdcDeviceId": "1"}' required></textarea>
</div>
<div style="display: flex; gap: 10px;">
<button type="submit" class="btn btn-qr">🚀 QR-Handshake ausführen</button>
</div>
</form>
</div>
<!-- 4. POS Cloud Konfiguration -->
<div class="card">
<h3 style="margin-bottom: 16px; color: var(--primary);">⚙️ Aktive 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 / cr)</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,200,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>
<!-- 5. 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>
<!-- 6. 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>
let videoStream = null;
let scanAnimationId = null;
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';
stopCamera();
}
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 () => {
stopCamera();
await fetch('/api/admin/logout', { method: 'POST' });
showLogin();
});
// Config laden
async function loadConfig() {
const res = await fetch('/api/admin/config');
if (res.status === 401) return showLogin();
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 || '';
// Texte
document.getElementById('site-title').value = cfg.site_title || 'GETRÄNKEKARTE';
document.getElementById('site-subtitle').value = cfg.site_subtitle || '';
document.getElementById('site-badge').value = cfg.site_badge || '';
document.getElementById('site-footer').value = cfg.site_footer || '';
// Theme
syncColorField('theme-bg', cfg.theme_bg || '#090b10');
syncColorField('theme-card-bg', cfg.theme_card_bg || 'rgba(22, 27, 34, 0.75)');
syncColorField('theme-primary', cfg.theme_primary || '#f59e0b');
syncColorField('theme-accent', cfg.theme_accent || '#ec4899');
syncColorField('theme-text', cfg.theme_text || '#f8fafc');
syncColorField('theme-text-muted', cfg.theme_text_muted || '#94a3b8');
}
}
function syncColorField(fieldId, val) {
const input = document.getElementById(fieldId);
const text = document.getElementById(fieldId + '-text');
if (text) text.value = val;
if (input && val.startsWith('#') && val.length === 7) {
input.value = val;
}
}
function setupColorSync(fieldId) {
const picker = document.getElementById(fieldId);
const text = document.getElementById(fieldId + '-text');
picker.addEventListener('input', () => { text.value = picker.value; });
text.addEventListener('input', () => {
if (text.value.startsWith('#') && text.value.length === 7) picker.value = text.value;
});
}
['theme-bg', 'theme-card-bg', 'theme-primary', 'theme-accent', 'theme-text', 'theme-text-muted'].forEach(setupColorSync);
// Theme Speichern
document.getElementById('theme-form').addEventListener('submit', async (e) => {
e.preventDefault();
const body = {
theme_bg: document.getElementById('theme-bg-text').value,
theme_card_bg: document.getElementById('theme-card-bg-text').value,
theme_primary: document.getElementById('theme-primary-text').value,
theme_accent: document.getElementById('theme-accent-text').value,
theme_text: document.getElementById('theme-text-text').value,
theme_text_muted: document.getElementById('theme-text-muted-text').value
};
await saveSettings(body, 'Farbdesign erfolgreich gespeichert!');
});
// Texte Speichern
document.getElementById('texts-form').addEventListener('submit', async (e) => {
e.preventDefault();
const body = {
site_title: document.getElementById('site-title').value,
site_subtitle: document.getElementById('site-subtitle').value,
site_badge: document.getElementById('site-badge').value,
site_footer: document.getElementById('site-footer').value
};
await saveSettings(body, 'Texte & Titel erfolgreich gespeichert!');
});
// POS Config Speichern
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
};
await saveSettings(body, 'POS-Parameter erfolgreich gespeichert!');
document.getElementById('cfg-admin-pass').value = '';
});
async function saveSettings(body, successMsg) {
const res = await fetch('/api/admin/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.status === 401) return showLogin();
const data = await res.json();
if (res.ok) {
showAlert('dash-alert', successMsg);
} else {
showAlert('dash-alert', data.error || 'Fehler beim Speichern', true);
}
}
// Kamera Scanner
document.getElementById('btn-toggle-camera').addEventListener('click', () => {
if (videoStream) {
stopCamera();
} else {
startCamera();
}
});
async function startCamera() {
const scannerBox = document.getElementById('scanner-box');
const video = document.getElementById('video');
const btn = document.getElementById('btn-toggle-camera');
scannerBox.style.display = 'block';
btn.textContent = '⏹️ Kamera stoppen';
btn.style.background = '#ef4444';
try {
videoStream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } }
});
video.srcObject = videoStream;
video.setAttribute('playsinline', true);
await video.play();
scanAnimationId = requestAnimationFrame(scanTick);
} catch (err) {
showAlert('dash-alert', 'Kamerazugriff verweigert oder HTTPS fehlt: ' + err.message, true);
stopCamera();
}
}
function scanTick() {
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (video.readyState === video.HAVE_ENOUGH_DATA) {
canvas.height = video.videoHeight;
canvas.width = video.videoWidth;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'attemptBoth'
});
if (code && code.data) {
document.getElementById('qr-json').value = code.data;
stopCamera();
submitQrPayload(code.data);
return;
}
}
if (videoStream) {
scanAnimationId = requestAnimationFrame(scanTick);
}
}
function stopCamera() {
if (scanAnimationId) cancelAnimationFrame(scanAnimationId);
if (videoStream) {
videoStream.getTracks().forEach(track => track.stop());
videoStream = null;
}
document.getElementById('scanner-box').style.display = 'none';
const btn = document.getElementById('btn-toggle-camera');
if (btn) {
btn.textContent = '🎥 Kamera starten';
btn.style.background = '#8b5cf6';
}
}
// QR Foto Upload
document.getElementById('qr-file-input').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const img = new Image();
img.onload = () => {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'attemptBoth'
});
if (code && code.data) {
document.getElementById('qr-json').value = code.data;
submitQrPayload(code.data);
} else {
showAlert('dash-alert', 'Kein QR-Code im Bild gefunden. Bitte JSON manuell einfügen.', true);
}
};
img.src = URL.createObjectURL(file);
});
async function submitQrPayload(qrContent) {
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 })
});
if (res.status === 401) {
showAlert('login-alert', 'Sitzung abgelaufen. Bitte neu anmelden.', true);
return showLogin();
}
const data = await res.json();
if (res.ok) {
showAlert('dash-alert', 'QR-Code Handshake erfolgreich! Parameter übernommen & aktiviert.');
loadConfig();
loadData();
} else {
showAlert('dash-alert', 'QR-Code Fehler: ' + data.error, true);
}
} catch (err) {
showAlert('dash-alert', 'Netzwerkfehler beim QR-Handshake', true);
}
}
document.getElementById('qr-form').addEventListener('submit', (e) => {
e.preventDefault();
const qrContent = document.getElementById('qr-json').value;
submitQrPayload(qrContent);
});
// 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 })
});
if (res.status === 401) return showLogin();
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.status === 401) return showLogin();
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.status === 401) return showLogin();
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>