feat: Text 2 als feste Inhaltsangabe und Langtext 1-4 als aufklappbare Details (Hover/Touch)
All checks were successful
Build and Deploy / build (push) Successful in 54s
All checks were successful
Build and Deploy / build (push) Successful in 54s
This commit is contained in:
14
db.js
14
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) {
|
||||
|
||||
53
posClient.js
53
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 || ''
|
||||
|
||||
@@ -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 @@
|
||||
<section>
|
||||
<div class="section-title">${escapeHtml(cat.name)} (${cat.items.length})</div>
|
||||
<div class="menu-grid">
|
||||
${cat.items.map(item => `
|
||||
<div class="card">
|
||||
<div class="card-top">
|
||||
<div class="card-name">${highlightText(item.text1, query)}</div>
|
||||
<div class="card-price">${parseFloat(item.preis1).toFixed(2).replace('.', ',')} €</div>
|
||||
${cat.items.map(item => {
|
||||
const hasExtras = item.langtext1 || item.langtext2 || item.langtext3 || item.langtext4;
|
||||
return `
|
||||
<div class="card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="card-top">
|
||||
<div class="card-name-row">
|
||||
<div class="card-name">${highlightText(item.text1, query)}</div>
|
||||
${hasExtras ? '<span class="expand-indicator">▼</span>' : ''}
|
||||
</div>
|
||||
<div class="card-price">${parseFloat(item.preis1).toFixed(2).replace('.', ',')} €</div>
|
||||
</div>
|
||||
|
||||
<!-- Text 2: Feste Inhaltsangabe direkt unter Name -->
|
||||
${item.text2 ? `<div class="card-content-desc">${highlightText(item.text2, query)}</div>` : ''}
|
||||
|
||||
<!-- Aufklappbare Details bei Hover / Klick -->
|
||||
${hasExtras ? `
|
||||
<div class="card-extra-details">
|
||||
${item.langtext1 ? `<div class="detail-item"><strong>Details:</strong> <span>${highlightText(item.langtext1, query)}</span></div>` : ''}
|
||||
${item.langtext2 ? `<div class="detail-item"><strong>Zutaten:</strong> <span>${highlightText(item.langtext2, query)}</span></div>` : ''}
|
||||
${item.langtext3 ? `<div class="detail-item"><strong>Hinweise:</strong> <span>${highlightText(item.langtext3, query)}</span></div>` : ''}
|
||||
${item.langtext4 ? `<div class="detail-item"><strong>Allergene:</strong> <span>${highlightText(item.langtext4, query)}</span></div>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
${item.text2 ? `<div class="card-ingredients">${highlightText(item.text2, query)}</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user