From 1b0726deef76be2932c96add3ef2357a5a67c559 Mon Sep 17 00:00:00 2001 From: DanielS Date: Tue, 22 Sep 2026 23:36:12 +0200 Subject: [PATCH] feat: Text 2 als feste Inhaltsangabe und Langtext 1-4 als aufklappbare Details (Hover/Touch) --- db.js | 14 +++++- posClient.js | 53 +++++++++++++++++--- public/index.html | 120 ++++++++++++++++++++++++++++++++++++++++------ server.js | 2 +- 4 files changed, 165 insertions(+), 24 deletions(-) diff --git a/db.js b/db.js index b853624..61563d9 100644 --- a/db.js +++ b/db.js @@ -3,7 +3,6 @@ const path = require('path'); const Database = require('better-sqlite3'); const bcrypt = require('bcryptjs'); -// Verzeichnis für Persistenz sicherstellen (/data im Docker Container oder local dir) const dataDir = process.env.DATA_DIR || (fs.existsSync('/data') ? '/data' : __dirname); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); @@ -12,7 +11,6 @@ if (!fs.existsSync(dataDir)) { const dbPath = path.join(dataDir, 'data.db'); const db = new Database(dbPath); -// Standard Journal Mode db.pragma('journal_mode = WAL'); // Tabellen anlegen @@ -32,6 +30,10 @@ db.exec(` id TEXT PRIMARY KEY, text1 TEXT NOT NULL, text2 TEXT, + langtext1 TEXT, + langtext2 TEXT, + langtext3 TEXT, + langtext4 TEXT, group_id TEXT, preis1 REAL DEFAULT 0, unit TEXT, @@ -49,6 +51,14 @@ db.exec(` ); `); +// Migration für bestehende DBs falls Spalten fehlen +const tableInfo = db.prepare("PRAGMA table_info(products)").all(); +const cols = tableInfo.map(c => c.name); +if (!cols.includes('langtext1')) db.exec("ALTER TABLE products ADD COLUMN langtext1 TEXT;"); +if (!cols.includes('langtext2')) db.exec("ALTER TABLE products ADD COLUMN langtext2 TEXT;"); +if (!cols.includes('langtext3')) db.exec("ALTER TABLE products ADD COLUMN langtext3 TEXT;"); +if (!cols.includes('langtext4')) db.exec("ALTER TABLE products ADD COLUMN langtext4 TEXT;"); + // Standard Admin Initialisieren const adminUser = db.prepare('SELECT value FROM config WHERE key = ?').get('admin_user'); if (!adminUser) { diff --git a/posClient.js b/posClient.js index 12b0188..40d0333 100644 --- a/posClient.js +++ b/posClient.js @@ -182,11 +182,15 @@ async function parseAndStoreZip(buffer) { `); const insertProduct = db.prepare(` - INSERT INTO products (id, text1, text2, group_id, preis1, unit, active) - VALUES (?, ?, ?, ?, ?, ?, 1) + INSERT INTO products (id, text1, text2, langtext1, langtext2, langtext3, langtext4, group_id, preis1, unit, active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1) ON CONFLICT(id) DO UPDATE SET text1=excluded.text1, text2=excluded.text2, + langtext1=excluded.langtext1, + langtext2=excluded.langtext2, + langtext3=excluded.langtext3, + langtext4=excluded.langtext4, group_id=excluded.group_id, preis1=excluded.preis1, unit=excluded.unit, @@ -228,6 +232,31 @@ async function parseAndStoreZip(buffer) { const text2 = item.text2 || item.description || item.additionalText || ''; const groupId = String(item.groupId || item.warengruppe || ''); + // Langtexte aus Attributen / Descriptions / Feldern extrahieren + let langtext1 = item.langtext1 || item.Langtext1 || item.longText1 || item.LongText1 || ''; + let langtext2 = item.langtext2 || item.Langtext2 || item.longText2 || item.LongText2 || ''; + let langtext3 = item.langtext3 || item.Langtext3 || item.longText3 || item.LongText3 || ''; + let langtext4 = item.langtext4 || item.Langtext4 || item.longText4 || item.LongText4 || ''; + + // Falls in descriptions Array oder attributes Array abgelegt + if (Array.isArray(item.descriptions)) { + if (item.descriptions[0]?.text) langtext1 = item.descriptions[0].text; + if (item.descriptions[1]?.text) langtext2 = item.descriptions[1].text; + if (item.descriptions[2]?.text) langtext3 = item.descriptions[2].text; + if (item.descriptions[3]?.text) langtext4 = item.descriptions[3].text; + } + + if (Array.isArray(item.attributes)) { + for (const attr of item.attributes) { + const attrName = (attr.name || attr.title || '').toLowerCase(); + const attrVal = attr.value || attr.text || ''; + if (attrName.includes('langtext 1') || attrName.includes('langtext1')) langtext1 = attrVal; + if (attrName.includes('langtext 2') || attrName.includes('langtext2')) langtext2 = attrVal; + if (attrName.includes('langtext 3') || attrName.includes('langtext3')) langtext3 = attrVal; + if (attrName.includes('langtext 4') || attrName.includes('langtext4')) langtext4 = attrVal; + } + } + // Preis 1 ermitteln (VK1) let preis1 = 0; if (Array.isArray(item.prices) && item.prices.length > 0) { @@ -246,7 +275,7 @@ async function parseAndStoreZip(buffer) { const unit = item.contentUnit || item.salesUnit || item.unit || ''; if (id) { - insertProduct.run(id, text1, text2, groupId, isNaN(preis1) ? 0 : preis1, unit); + insertProduct.run(id, text1, text2, langtext1, langtext2, langtext3, langtext4, groupId, isNaN(preis1) ? 0 : preis1, unit); productsCount++; } } @@ -282,9 +311,13 @@ async function parseAndStoreZip(buffer) { const id = String(item.id || item.articleNo || ''); const text1 = item.text1 || item.name || 'Unbekannt'; const text2 = item.text2 || ''; + const langtext1 = item.langtext1 || item.Langtext1 || ''; + const langtext2 = item.langtext2 || item.Langtext2 || ''; + const langtext3 = item.langtext3 || item.Langtext3 || ''; + const langtext4 = item.langtext4 || item.Langtext4 || ''; const groupId = String(item.groupId || ''); let preis1 = parseFloat(String(item.preis1 || item.price || 0).replace(',', '.')); - if (id) { insertProduct.run(id, text1, text2, groupId, isNaN(preis1) ? 0 : preis1, ''); productsCount++; } + if (id) { insertProduct.run(id, text1, text2, langtext1, langtext2, langtext3, langtext4, groupId, isNaN(preis1) ? 0 : preis1, ''); productsCount++; } } } } @@ -298,11 +331,15 @@ async function parseAndStoreZip(buffer) { function importJsonData(data) { const insertProduct = db.prepare(` - INSERT INTO products (id, text1, text2, group_id, preis1, unit, active) - VALUES (?, ?, ?, ?, ?, ?, 1) + INSERT INTO products (id, text1, text2, langtext1, langtext2, langtext3, langtext4, group_id, preis1, unit, active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1) ON CONFLICT(id) DO UPDATE SET text1=excluded.text1, text2=excluded.text2, + langtext1=excluded.langtext1, + langtext2=excluded.langtext2, + langtext3=excluded.langtext3, + langtext4=excluded.langtext4, group_id=excluded.group_id, preis1=excluded.preis1, unit=excluded.unit, @@ -329,6 +366,10 @@ function importJsonData(data) { String(p.id || Math.random().toString(36).substring(2)), p.text1 || 'Unbekannt', p.text2 || '', + p.langtext1 || '', + p.langtext2 || '', + p.langtext3 || '', + p.langtext4 || '', String(p.groupId || ''), parseFloat(String(p.preis1 || p.price || 0).replace(',', '.')), p.unit || '' diff --git a/public/index.html b/public/index.html index 72830ee..5f4ee2c 100644 --- a/public/index.html +++ b/public/index.html @@ -274,6 +274,11 @@ transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; position: relative; overflow: hidden; + cursor: pointer; + } + + .card:hover { + border-color: rgba(255, 255, 255, 0.18); } .card:active { @@ -284,13 +289,38 @@ display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: 6px; + margin-bottom: 4px; + } + + .card-name-row { + display: flex; + align-items: center; + gap: 6px; } .card-name { font-size: 16px; font-weight: 700; color: var(--text); + transition: color 0.2s; + } + + .card:hover .card-name { + color: var(--primary); + } + + .expand-indicator { + font-size: 12px; + color: var(--text-muted); + transition: transform 0.3s ease, color 0.2s ease; + } + + .card:hover .expand-indicator, .card.expanded .expand-indicator { + color: var(--primary); + } + + .card.expanded .expand-indicator { + transform: rotate(180deg); } .card-price { @@ -304,10 +334,50 @@ border: 1px solid rgba(245, 158, 11, 0.2); } - .card-ingredients { - font-size: 12.5px; - color: var(--text-muted); + /* Text 2 - Feste Inhaltsangabe direkt unter dem Namen */ + .card-content-desc { + font-size: 13px; + color: var(--text); + opacity: 0.9; line-height: 1.45; + margin-top: 2px; + } + + /* Aufklappbarer Bereich (Langtext 1 bis 4) */ + .card-extra-details { + max-height: 0; + opacity: 0; + overflow: hidden; + transition: max-height 0.35s ease, opacity 0.3s ease, margin-top 0.3s ease; + display: flex; + flex-direction: column; + gap: 6px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + margin-top: 0; + padding-top: 0; + } + + /* Bei Hover am Desktop oder Klick/Tap am Handy aufklappen */ + .card:hover .card-extra-details, .card.expanded .card-extra-details { + max-height: 350px; + opacity: 1; + margin-top: 10px; + padding-top: 8px; + } + + .detail-item { + font-size: 12px; + color: var(--text-muted); + line-height: 1.4; + display: flex; + gap: 6px; + } + + .detail-item strong { + color: var(--primary); + font-weight: 600; + min-width: 70px; + flex-shrink: 0; } .highlight { @@ -448,7 +518,6 @@ }); } - // Slider Controls (Pfeile + Mouse Drag) function initSliderControls() { const slider = document.getElementById('category-swipe'); const btnLeft = document.getElementById('slide-left'); @@ -462,7 +531,6 @@ slider.scrollBy({ left: 160, behavior: 'smooth' }); }); - // Mouse Drag Unterstützung am Desktop let isDown = false; let startX; let scrollLeft; @@ -498,7 +566,11 @@ if (!query) return true; const t1 = (item.text1 || '').toLowerCase(); const t2 = (item.text2 || '').toLowerCase(); - return t1.includes(query) || t2.includes(query); + const l1 = (item.langtext1 || '').toLowerCase(); + const l2 = (item.langtext2 || '').toLowerCase(); + const l3 = (item.langtext3 || '').toLowerCase(); + const l4 = (item.langtext4 || '').toLowerCase(); + return t1.includes(query) || t2.includes(query) || l1.includes(query) || l2.includes(query) || l3.includes(query) || l4.includes(query); }); if (matchingItems.length === 0) return null; @@ -525,15 +597,33 @@
${escapeHtml(cat.name)} (${cat.items.length})
`; diff --git a/server.js b/server.js index 3ba4cb1..687fd99 100644 --- a/server.js +++ b/server.js @@ -45,7 +45,7 @@ function authMiddleware(req, res, next) { // 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(); + const products = db.prepare('SELECT id, text1, text2, langtext1, langtext2, langtext3, langtext4, 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();