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 Database = require('better-sqlite3');
|
||||||
const bcrypt = require('bcryptjs');
|
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);
|
const dataDir = process.env.DATA_DIR || (fs.existsSync('/data') ? '/data' : __dirname);
|
||||||
if (!fs.existsSync(dataDir)) {
|
if (!fs.existsSync(dataDir)) {
|
||||||
fs.mkdirSync(dataDir, { recursive: true });
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
@@ -12,7 +11,6 @@ if (!fs.existsSync(dataDir)) {
|
|||||||
const dbPath = path.join(dataDir, 'data.db');
|
const dbPath = path.join(dataDir, 'data.db');
|
||||||
const db = new Database(dbPath);
|
const db = new Database(dbPath);
|
||||||
|
|
||||||
// Standard Journal Mode
|
|
||||||
db.pragma('journal_mode = WAL');
|
db.pragma('journal_mode = WAL');
|
||||||
|
|
||||||
// Tabellen anlegen
|
// Tabellen anlegen
|
||||||
@@ -32,6 +30,10 @@ db.exec(`
|
|||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
text1 TEXT NOT NULL,
|
text1 TEXT NOT NULL,
|
||||||
text2 TEXT,
|
text2 TEXT,
|
||||||
|
langtext1 TEXT,
|
||||||
|
langtext2 TEXT,
|
||||||
|
langtext3 TEXT,
|
||||||
|
langtext4 TEXT,
|
||||||
group_id TEXT,
|
group_id TEXT,
|
||||||
preis1 REAL DEFAULT 0,
|
preis1 REAL DEFAULT 0,
|
||||||
unit TEXT,
|
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
|
// Standard Admin Initialisieren
|
||||||
const adminUser = db.prepare('SELECT value FROM config WHERE key = ?').get('admin_user');
|
const adminUser = db.prepare('SELECT value FROM config WHERE key = ?').get('admin_user');
|
||||||
if (!adminUser) {
|
if (!adminUser) {
|
||||||
|
|||||||
53
posClient.js
53
posClient.js
@@ -182,11 +182,15 @@ async function parseAndStoreZip(buffer) {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
const insertProduct = db.prepare(`
|
const insertProduct = db.prepare(`
|
||||||
INSERT INTO products (id, text1, text2, group_id, preis1, unit, active)
|
INSERT INTO products (id, text1, text2, langtext1, langtext2, langtext3, langtext4, group_id, preis1, unit, active)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, 1)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
text1=excluded.text1,
|
text1=excluded.text1,
|
||||||
text2=excluded.text2,
|
text2=excluded.text2,
|
||||||
|
langtext1=excluded.langtext1,
|
||||||
|
langtext2=excluded.langtext2,
|
||||||
|
langtext3=excluded.langtext3,
|
||||||
|
langtext4=excluded.langtext4,
|
||||||
group_id=excluded.group_id,
|
group_id=excluded.group_id,
|
||||||
preis1=excluded.preis1,
|
preis1=excluded.preis1,
|
||||||
unit=excluded.unit,
|
unit=excluded.unit,
|
||||||
@@ -228,6 +232,31 @@ async function parseAndStoreZip(buffer) {
|
|||||||
const text2 = item.text2 || item.description || item.additionalText || '';
|
const text2 = item.text2 || item.description || item.additionalText || '';
|
||||||
const groupId = String(item.groupId || item.warengruppe || '');
|
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)
|
// Preis 1 ermitteln (VK1)
|
||||||
let preis1 = 0;
|
let preis1 = 0;
|
||||||
if (Array.isArray(item.prices) && item.prices.length > 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 || '';
|
const unit = item.contentUnit || item.salesUnit || item.unit || '';
|
||||||
|
|
||||||
if (id) {
|
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++;
|
productsCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,9 +311,13 @@ async function parseAndStoreZip(buffer) {
|
|||||||
const id = String(item.id || item.articleNo || '');
|
const id = String(item.id || item.articleNo || '');
|
||||||
const text1 = item.text1 || item.name || 'Unbekannt';
|
const text1 = item.text1 || item.name || 'Unbekannt';
|
||||||
const text2 = item.text2 || '';
|
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 || '');
|
const groupId = String(item.groupId || '');
|
||||||
let preis1 = parseFloat(String(item.preis1 || item.price || 0).replace(',', '.'));
|
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) {
|
function importJsonData(data) {
|
||||||
const insertProduct = db.prepare(`
|
const insertProduct = db.prepare(`
|
||||||
INSERT INTO products (id, text1, text2, group_id, preis1, unit, active)
|
INSERT INTO products (id, text1, text2, langtext1, langtext2, langtext3, langtext4, group_id, preis1, unit, active)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, 1)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
text1=excluded.text1,
|
text1=excluded.text1,
|
||||||
text2=excluded.text2,
|
text2=excluded.text2,
|
||||||
|
langtext1=excluded.langtext1,
|
||||||
|
langtext2=excluded.langtext2,
|
||||||
|
langtext3=excluded.langtext3,
|
||||||
|
langtext4=excluded.langtext4,
|
||||||
group_id=excluded.group_id,
|
group_id=excluded.group_id,
|
||||||
preis1=excluded.preis1,
|
preis1=excluded.preis1,
|
||||||
unit=excluded.unit,
|
unit=excluded.unit,
|
||||||
@@ -329,6 +366,10 @@ function importJsonData(data) {
|
|||||||
String(p.id || Math.random().toString(36).substring(2)),
|
String(p.id || Math.random().toString(36).substring(2)),
|
||||||
p.text1 || 'Unbekannt',
|
p.text1 || 'Unbekannt',
|
||||||
p.text2 || '',
|
p.text2 || '',
|
||||||
|
p.langtext1 || '',
|
||||||
|
p.langtext2 || '',
|
||||||
|
p.langtext3 || '',
|
||||||
|
p.langtext4 || '',
|
||||||
String(p.groupId || ''),
|
String(p.groupId || ''),
|
||||||
parseFloat(String(p.preis1 || p.price || 0).replace(',', '.')),
|
parseFloat(String(p.preis1 || p.price || 0).replace(',', '.')),
|
||||||
p.unit || ''
|
p.unit || ''
|
||||||
|
|||||||
@@ -274,6 +274,11 @@
|
|||||||
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:active {
|
.card:active {
|
||||||
@@ -284,13 +289,38 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-name {
|
.card-name {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text);
|
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 {
|
.card-price {
|
||||||
@@ -304,10 +334,50 @@
|
|||||||
border: 1px solid rgba(245, 158, 11, 0.2);
|
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-ingredients {
|
/* Text 2 - Feste Inhaltsangabe direkt unter dem Namen */
|
||||||
font-size: 12.5px;
|
.card-content-desc {
|
||||||
color: var(--text-muted);
|
font-size: 13px;
|
||||||
|
color: var(--text);
|
||||||
|
opacity: 0.9;
|
||||||
line-height: 1.45;
|
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 {
|
.highlight {
|
||||||
@@ -448,7 +518,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slider Controls (Pfeile + Mouse Drag)
|
|
||||||
function initSliderControls() {
|
function initSliderControls() {
|
||||||
const slider = document.getElementById('category-swipe');
|
const slider = document.getElementById('category-swipe');
|
||||||
const btnLeft = document.getElementById('slide-left');
|
const btnLeft = document.getElementById('slide-left');
|
||||||
@@ -462,7 +531,6 @@
|
|||||||
slider.scrollBy({ left: 160, behavior: 'smooth' });
|
slider.scrollBy({ left: 160, behavior: 'smooth' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mouse Drag Unterstützung am Desktop
|
|
||||||
let isDown = false;
|
let isDown = false;
|
||||||
let startX;
|
let startX;
|
||||||
let scrollLeft;
|
let scrollLeft;
|
||||||
@@ -498,7 +566,11 @@
|
|||||||
if (!query) return true;
|
if (!query) return true;
|
||||||
const t1 = (item.text1 || '').toLowerCase();
|
const t1 = (item.text1 || '').toLowerCase();
|
||||||
const t2 = (item.text2 || '').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;
|
if (matchingItems.length === 0) return null;
|
||||||
@@ -525,15 +597,33 @@
|
|||||||
<section>
|
<section>
|
||||||
<div class="section-title">${escapeHtml(cat.name)} (${cat.items.length})</div>
|
<div class="section-title">${escapeHtml(cat.name)} (${cat.items.length})</div>
|
||||||
<div class="menu-grid">
|
<div class="menu-grid">
|
||||||
${cat.items.map(item => `
|
${cat.items.map(item => {
|
||||||
<div class="card">
|
const hasExtras = item.langtext1 || item.langtext2 || item.langtext3 || item.langtext4;
|
||||||
<div class="card-top">
|
return `
|
||||||
<div class="card-name">${highlightText(item.text1, query)}</div>
|
<div class="card" onclick="this.classList.toggle('expanded')">
|
||||||
<div class="card-price">${parseFloat(item.preis1).toFixed(2).replace('.', ',')} €</div>
|
<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>
|
</div>
|
||||||
${item.text2 ? `<div class="card-ingredients">${highlightText(item.text2, query)}</div>` : ''}
|
`;
|
||||||
</div>
|
}).join('')}
|
||||||
`).join('')}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function authMiddleware(req, res, next) {
|
|||||||
// 1. Produkte, Gruppen & Theme Settings für die Speisekarte
|
// 1. Produkte, Gruppen & Theme Settings für die Speisekarte
|
||||||
app.get('/api/menu', (req, res) => {
|
app.get('/api/menu', (req, res) => {
|
||||||
const groups = db.prepare('SELECT * FROM product_groups ORDER BY sort_order ASC, name ASC').all();
|
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
|
// Globale Einstellungen laden
|
||||||
const configRows = db.prepare('SELECT key, value FROM config').all();
|
const configRows = db.prepare('SELECT key, value FROM config').all();
|
||||||
|
|||||||
Reference in New Issue
Block a user