Compare commits

...

3 Commits

Author SHA1 Message Date
DanielS
8fb76690fe feat(ui): Slider mit Vor-/Zurück-Buttons und Touch/Mouse-Drag für Warengruppen
All checks were successful
Build and Deploy / build (push) Successful in 54s
2026-09-22 23:07:58 +02:00
DanielS
f59923fb57 feat: Globale Farbsteuerung & Textsteuerung im Admin Panel und dynamisches Rendering auf der Karte 2026-09-22 23:07:32 +02:00
DanielS
d5d9938077 fix(ui): Login Formular Input-Values geleert 2026-09-22 22:48:51 +02:00
4 changed files with 387 additions and 82 deletions

22
db.js
View File

@@ -42,7 +42,7 @@ db.exec(`
);
`);
// Standard Admin Initialisieren: admin / Kathi2022!
// Standard Admin Initialisieren
const adminUser = db.prepare('SELECT value FROM config WHERE key = ?').get('admin_user');
if (!adminUser) {
const hash = bcrypt.hashSync('Kathi2022!', 10);
@@ -51,4 +51,24 @@ if (!adminUser) {
db.prepare('INSERT INTO config (key, value) VALUES (?, ?)').run('jwt_secret', 'secret_' + Math.random().toString(36).substring(2));
}
// Standard Theme & Texte initialisieren
const initConfig = (k, v) => {
const existing = db.prepare('SELECT value FROM config WHERE key = ?').get(k);
if (!existing) {
db.prepare('INSERT INTO config (key, value) VALUES (?, ?)').run(k, v);
}
};
initConfig('site_title', 'GETRÄNKEKARTE');
initConfig('site_subtitle', 'Frisch zubereitete Drinks, Spezialitäten & Erfrischungen');
initConfig('site_badge', '✦ EXKLUSIVE AUSWAHL ✦');
initConfig('site_footer', '© Bar & Lounge • Live POS Sync');
initConfig('theme_bg', '#090b10');
initConfig('theme_card_bg', 'rgba(22, 27, 34, 0.75)');
initConfig('theme_primary', '#f59e0b');
initConfig('theme_accent', '#ec4899');
initConfig('theme_text', '#f8fafc');
initConfig('theme_text_muted', '#94a3b8');
module.exports = db;

View File

@@ -75,6 +75,20 @@
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;
@@ -115,8 +129,13 @@
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-template-columns: 1fr; }
.grid-2, .grid-3 { grid-template-columns: 1fr; }
}
.header-bar {
display: flex;
@@ -219,11 +238,11 @@
<form id="login-form">
<div class="form-group">
<label>Benutzername</label>
<input type="text" id="login-username" value="admin" required />
<input type="text" id="login-username" placeholder="Benutzername eingeben" required />
</div>
<div class="form-group">
<label>Passwort</label>
<input type="password" id="login-password" value="Kathi2022!" required />
<input type="password" id="login-password" placeholder="Passwort eingeben" required />
</div>
<button type="submit" class="btn" style="width: 100%;">Anmelden</button>
</form>
@@ -244,7 +263,95 @@
<div id="dash-alert" class="notification"></div>
<!-- QR-Code Scanner / Setup -->
<!-- 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>
@@ -280,7 +387,7 @@
</form>
</div>
<!-- POS Cloud Konfiguration -->
<!-- 4. POS Cloud Konfiguration -->
<div class="card">
<h3 style="margin-bottom: 16px; color: var(--primary);">⚙️ Aktive POS Cloud Verbindungsparameter</h3>
<form id="config-form">
@@ -320,7 +427,7 @@
</form>
</div>
<!-- Gespeicherte Artikel & Preise -->
<!-- 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>
@@ -345,7 +452,7 @@
</div>
</div>
<!-- Sync Logs -->
<!-- 6. Sync Logs -->
<div class="card">
<h3 style="margin-bottom: 12px; color: var(--text-muted);">📜 Sync Protokoll</h3>
<table id="logs-table">
@@ -433,7 +540,7 @@
showLogin();
});
// Config laden & speichern
// Config laden
async function loadConfig() {
const res = await fetch('/api/admin/config');
if (res.status === 401) return showLogin();
@@ -443,6 +550,95 @@
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);
}
}
@@ -494,7 +690,6 @@
});
if (code && code.data) {
console.log('QR Code erkannt:', code.data);
document.getElementById('qr-json').value = code.data;
stopCamera();
submitQrPayload(code.data);
@@ -580,31 +775,6 @@
submitQrPayload(qrContent);
});
// Manuelle Config
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)
});
if (res.status === 401) return showLogin();
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})...`);

View File

@@ -3,11 +3,11 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GETRÄNKEKARTE</title>
<title id="html-title">GETRÄNKEKARTE</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@500;700&display=swap" rel="stylesheet">
<style>
<style id="dynamic-theme">
:root {
--bg: #090b10;
--card-bg: rgba(22, 27, 34, 0.75);
@@ -20,7 +20,8 @@
--text-muted: #94a3b8;
--tag-bg: rgba(245, 158, 11, 0.12);
}
</style>
<style>
* {
box-sizing: border-box;
margin: 0;
@@ -38,6 +39,7 @@
padding: 20px 16px 40px;
position: relative;
overflow-x: hidden;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Ambient Glow */
@@ -92,7 +94,7 @@
font-size: 32px;
font-weight: 700;
letter-spacing: -0.5px;
background: linear-gradient(135deg, #fff 30%, #cbd5e1 100%);
background: linear-gradient(135deg, var(--text) 30%, var(--text-muted) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
@@ -113,10 +115,10 @@
.search-input {
width: 100%;
padding: 14px 42px 14px 44px;
background: rgba(22, 27, 34, 0.85);
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 14px;
color: #fff;
color: var(--text);
font-size: 14px;
outline: none;
backdrop-filter: blur(16px);
@@ -126,7 +128,7 @@
.search-input:focus {
border-color: var(--primary);
box-shadow: 0 0 16px rgba(245, 158, 11, 0.2);
box-shadow: 0 0 16px var(--primary-glow);
}
.search-icon {
@@ -152,29 +154,63 @@
display: none;
}
/* Swipebar Kategorien */
.category-swipe-wrapper {
/* Interaktiver Slider / Swipe Container */
.slider-nav-container {
position: relative;
display: flex;
align-items: center;
margin: 0 -16px;
padding: 0 16px;
padding: 0 8px;
}
.slider-arrow {
background: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--text);
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
z-index: 2;
flex-shrink: 0;
backdrop-filter: blur(10px);
transition: all 0.2s;
}
.slider-arrow:hover {
background: var(--primary);
color: var(--bg);
border-color: var(--primary);
}
.category-swipe {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 4px;
scrollbar-width: none; /* Firefox */
scroll-behavior: smooth;
padding: 6px 8px;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
flex: 1;
cursor: grab;
user-select: none;
}
.category-swipe:active {
cursor: grabbing;
}
.category-swipe::-webkit-scrollbar {
display: none; /* Chrome, Safari */
display: none;
}
.category-pill {
flex: 0 0 auto;
padding: 8px 16px;
padding: 8px 18px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
@@ -183,20 +219,24 @@
border: 1px solid var(--card-border);
cursor: pointer;
white-space: nowrap;
transition: all 0.2s ease;
user-select: none;
transition: all 0.25s ease;
display: inline-flex;
align-items: center;
gap: 6px;
}
.category-pill:hover {
color: #fff;
color: var(--text);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.category-pill.active {
background: var(--primary);
color: #090b10;
color: var(--bg);
border-color: var(--primary);
box-shadow: 0 0 12px var(--primary-glow);
box-shadow: 0 0 14px var(--primary-glow);
transform: scale(1.03);
}
.section-title {
@@ -250,7 +290,7 @@
.card-name {
font-size: 16px;
font-weight: 700;
color: #fff;
color: var(--text);
}
.card-price {
@@ -266,13 +306,13 @@
.card-ingredients {
font-size: 12.5px;
color: #cbd5e1;
color: var(--text-muted);
line-height: 1.45;
}
.highlight {
background: rgba(245, 158, 11, 0.3);
color: #fff;
color: var(--text);
padding: 0 2px;
border-radius: 3px;
}
@@ -306,9 +346,9 @@
<div class="container">
<header>
<div class="badge">✦ EXKLUSIVE AUSWAHL ✦</div>
<h1>GETRÄNKEKARTE</h1>
<p>Frisch zubereitete Drinks, Spezialitäten & Erfrischungen</p>
<div id="site-badge" class="badge">✦ EXKLUSIVE AUSWAHL ✦</div>
<h1 id="site-title">GETRÄNKEKARTE</h1>
<p id="site-subtitle">Frisch zubereitete Drinks, Spezialitäten & Erfrischungen</p>
</header>
<!-- Volltextsuche -->
@@ -318,11 +358,13 @@
<button id="clear-search" class="clear-search">✕</button>
</div>
<!-- Swipebar Warengruppen -->
<div class="category-swipe-wrapper">
<!-- Warengruppen Slider mit Pfeilen & Touch Drag -->
<div class="slider-nav-container">
<button id="slide-left" class="slider-arrow" aria-label="Zurück">‹</button>
<div id="category-swipe" class="category-swipe">
<button class="category-pill active" data-id="all">Alle Artikel</button>
</div>
<button id="slide-right" class="slider-arrow" aria-label="Weiter">›</button>
</div>
<!-- Getränkeliste -->
@@ -331,7 +373,7 @@
</div>
<footer>
<p>© Bar & Lounge • Live POS Sync</p>
<p id="site-footer">© Bar & Lounge • Live POS Sync</p>
<p style="margin-top: 6px;"><a href="/admin">⚙️ Admin Bereich</a></p>
</footer>
@@ -349,13 +391,42 @@
const data = await res.json();
allCategories = data.categories || [];
if (data.settings) {
applySettings(data.settings);
}
renderCategoryPills();
initSliderControls();
renderMenu();
} catch (err) {
container.innerHTML = '<div class="card" style="color:#ef4444; text-align:center;">Fehler beim Laden der Speisekarte.</div>';
}
}
function applySettings(s) {
if (s.site_title) {
document.getElementById('site-title').textContent = s.site_title;
document.getElementById('html-title').textContent = s.site_title;
}
if (s.site_subtitle) document.getElementById('site-subtitle').textContent = s.site_subtitle;
if (s.site_badge) document.getElementById('site-badge').textContent = s.site_badge;
if (s.site_footer) document.getElementById('site-footer').textContent = s.site_footer;
const root = document.documentElement;
if (s.theme_bg) root.style.setProperty('--bg', s.theme_bg);
if (s.theme_card_bg) root.style.setProperty('--card-bg', s.theme_card_bg);
if (s.theme_primary) {
root.style.setProperty('--primary', s.theme_primary);
root.style.setProperty('--primary-glow', s.theme_primary + '40');
}
if (s.theme_accent) {
root.style.setProperty('--accent', s.theme_accent);
root.style.setProperty('--accent-glow', s.theme_accent + '40');
}
if (s.theme_text) root.style.setProperty('--text', s.theme_text);
if (s.theme_text_muted) root.style.setProperty('--text-muted', s.theme_text_muted);
}
function renderCategoryPills() {
const swipeEl = document.getElementById('category-swipe');
let pillsHtml = `<button class="category-pill ${activeCategoryId === 'all' ? 'active' : ''}" data-id="all">Alle Artikel</button>`;
@@ -367,7 +438,7 @@
swipeEl.innerHTML = pillsHtml;
swipeEl.querySelectorAll('.category-pill').forEach(btn => {
btn.addEventListener('click', (e) => {
btn.addEventListener('click', () => {
swipeEl.querySelectorAll('.category-pill').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeCategoryId = btn.dataset.id;
@@ -377,17 +448,52 @@
});
}
// Slider Controls (Pfeile + Mouse Drag)
function initSliderControls() {
const slider = document.getElementById('category-swipe');
const btnLeft = document.getElementById('slide-left');
const btnRight = document.getElementById('slide-right');
btnLeft.addEventListener('click', () => {
slider.scrollBy({ left: -160, behavior: 'smooth' });
});
btnRight.addEventListener('click', () => {
slider.scrollBy({ left: 160, behavior: 'smooth' });
});
// Mouse Drag Unterstützung am Desktop
let isDown = false;
let startX;
let scrollLeft;
slider.addEventListener('mousedown', (e) => {
isDown = true;
startX = e.pageX - slider.offsetLeft;
scrollLeft = slider.scrollLeft;
});
slider.addEventListener('mouseleave', () => { isDown = false; });
slider.addEventListener('mouseup', () => { isDown = false; });
slider.addEventListener('mousemove', (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 1.5;
slider.scrollLeft = scrollLeft - walk;
});
}
function renderMenu() {
const container = document.getElementById('menu-container');
const query = searchQuery.trim().toLowerCase();
let filteredCategories = allCategories.map(cat => {
// Kategorie Filter
if (activeCategoryId !== 'all' && cat.id !== activeCategoryId) {
return null;
}
// Suche Filter
const matchingItems = cat.items.filter(item => {
if (!query) return true;
const t1 = (item.text1 || '').toLowerCase();
@@ -481,4 +587,4 @@
</script>
</body>
</html>
</html>

View File

@@ -42,11 +42,20 @@ function authMiddleware(req, res, next) {
// ==================== Öffentliche API ====================
// 1. Produkte & Gruppen für die Speisekarte
// 1. Produkte, Gruppen & Theme Settings für die Speisekarte
app.get('/api/menu', (req, res) => {
const groups = db.prepare('SELECT * FROM product_groups ORDER BY sort_order ASC, name ASC').all();
const products = db.prepare('SELECT id, text1, text2, group_id, preis1, unit FROM products WHERE active = 1 ORDER BY sort_order ASC, text1 ASC').all();
// Globale Einstellungen laden
const configRows = db.prepare('SELECT key, value FROM config').all();
const settings = {};
for (const r of configRows) {
if (r.key.startsWith('site_') || r.key.startsWith('theme_')) {
settings[r.key] = r.value;
}
}
const groupMap = {};
for (const g of groups) {
groupMap[g.id] = {
@@ -74,7 +83,7 @@ app.get('/api/menu', (req, res) => {
});
}
res.json({ categories: result, allProducts: products });
res.json({ categories: result, allProducts: products, settings });
});
// ==================== Admin Auth ====================
@@ -108,7 +117,7 @@ app.get('/api/admin/check', authMiddleware, (req, res) => {
res.json({ authenticated: true, user: req.user.user });
});
// ==================== Admin POS Settings, QR Setup & Sync ====================
// ==================== Admin POS Settings, Theme & Sync ====================
// 5. Config abrufen
app.get('/api/admin/config', authMiddleware, (req, res) => {
@@ -118,23 +127,23 @@ app.get('/api/admin/config', authMiddleware, (req, res) => {
res.json(config);
});
// 6. Config manuell speichern
// 6. Config / Theme / Texte speichern
app.post('/api/admin/config', authMiddleware, (req, res) => {
const { pos_url, pos_pass_id, pos_app_id, pos_cash_register, new_admin_password } = req.body;
const updates = req.body;
const upsert = db.prepare('INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value');
if (pos_url !== undefined) upsert.run('pos_url', pos_url);
if (pos_pass_id !== undefined) upsert.run('pos_pass_id', pos_pass_id);
if (pos_app_id !== undefined) upsert.run('pos_app_id', pos_app_id);
if (pos_cash_register !== undefined) upsert.run('pos_cash_register', pos_cash_register);
if (new_admin_password && new_admin_password.trim().length > 0) {
const newHash = bcrypt.hashSync(new_admin_password.trim(), 10);
upsert.run('admin_password_hash', newHash);
for (const [key, val] of Object.entries(updates)) {
if (key === 'new_admin_password') {
if (val && val.trim().length > 0) {
const newHash = bcrypt.hashSync(val.trim(), 10);
upsert.run('admin_password_hash', newHash);
}
} else if (val !== undefined && val !== null) {
upsert.run(key, String(val));
}
}
res.json({ success: true, message: 'Einstellungen gespeichert' });
res.json({ success: true, message: 'Einstellungen erfolgreich gespeichert' });
});
// 7. QR-Code Setup Handshake