From da9783e7b38506d940dfe4b2b2dc6ddf45312a89 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Mon, 24 Nov 2025 15:34:19 +0100 Subject: [PATCH] feat: Add product article number, barcode, and calculated gross price display with updated data fetching and tax zone configuration. --- .env.example | 1 + index.html | 110 ++++++++++++++++-- .../category-products-data-fetcher.js | 10 +- src/syncers/category-products-syncer.js | 4 + 4 files changed, 117 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index e0b7b6e..289fbd0 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,7 @@ ROOT_CATEGORY_ID=0 JTL_SHOP_ID=0 JTL_SPRACHE_ID=1 JTL_PLATTFORM_ID=1 +JTL_STEUERZONE_ID=1 SERVER_PORT=3991 SERVER_HOST=127.0.0.1 SYNC_INTERVAL_MS=600000 diff --git a/index.html b/index.html index c3905c1..44d1ff3 100644 --- a/index.html +++ b/index.html @@ -136,13 +136,33 @@ } .product-item { - padding: 0.25rem 0; + padding: 0.5rem 0; border-bottom: 1px solid #eee; display: flex; align-items: center; + gap: 0.75rem; + } + + .product-details { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.1rem; + } + + .product-meta { + font-size: 0.8rem; + color: #888; + display: flex; gap: 0.5rem; } + .product-price { + font-weight: bold; + color: #2d3748; + font-size: 0.95rem; + } + .product-image { width: 32px; height: 32px; @@ -771,6 +791,49 @@ }); } + function renderProducts(products) { + if (!products) return '
Loading products...
'; + if (products.length === 0) return '
No products found.
'; + + const items = products.map(p => { + const image = p.images && p.images.length > 0 + ? `` + : '
'; + + // Calculate Gross Price + let priceDisplay = ''; + if (p.fVKNetto !== undefined && p.fVKNetto !== null) { + const net = parseFloat(p.fVKNetto); + const rate = p.fSteuersatz ? parseFloat(p.fSteuersatz) : 19.0; // Default to 19% if missing + const gross = net * (1 + rate / 100); + priceDisplay = `
${gross.toFixed(2)} €
`; + console.log('Product:', p.cName, 'Net:', net, 'Rate:', rate, 'Gross:', gross.toFixed(2)); + } else { + console.log('Product missing price:', p.cName, 'fVKNetto:', p.fVKNetto); + } + + // Meta info + const metaParts = []; + if (p.cArtNr) metaParts.push(`Art: ${p.cArtNr}`); + if (p.cBarcode) metaParts.push(`EAN: ${p.cBarcode}`); + const metaHtml = metaParts.length > 0 ? `
${metaParts.join(' • ')}
` : ''; + + return ` +
+ ${image} +
+
+ ${p.cName} +
+ ${metaHtml} +
+ ${priceDisplay} +
+ `; + }).join(''); + + return `
${items}
`; + } function updateCategoryProducts(id) { const findAndReload = (nodes) => { for (const node of nodes) { @@ -971,6 +1034,7 @@ const li = document.createElement('li'); li.className = 'product-item'; + // Image if (p.images && p.images.length > 0) { const img = document.createElement('img'); img.className = 'product-image'; @@ -981,14 +1045,46 @@ li.appendChild(img); } - const span = document.createElement('span'); - span.className = 'product-name-link'; - span.textContent = p.cName; - span.onclick = (e) => { + // Product details container + const detailsDiv = document.createElement('div'); + detailsDiv.className = 'product-details'; + + // Product name + const nameDiv = document.createElement('div'); + const nameLink = document.createElement('span'); + nameLink.className = 'product-name-link'; + nameLink.textContent = p.cName; + nameLink.onclick = (e) => { e.stopPropagation(); - showProductDetails(p.kArtikel); + showProductDetails(p.kArtikel, p.cName); }; - li.appendChild(span); + nameDiv.appendChild(nameLink); + detailsDiv.appendChild(nameDiv); + + // Meta info (Article number and barcode) + const metaParts = []; + if (p.cArtNr) metaParts.push(`Art: ${p.cArtNr}`); + if (p.cBarcode) metaParts.push(`EAN: ${p.cBarcode}`); + if (metaParts.length > 0) { + const metaDiv = document.createElement('div'); + metaDiv.className = 'product-meta'; + metaDiv.textContent = metaParts.join(' • '); + detailsDiv.appendChild(metaDiv); + } + + li.appendChild(detailsDiv); + + // Price + if (p.fVKNetto !== undefined && p.fVKNetto !== null) { + const net = parseFloat(p.fVKNetto); + const rate = p.fSteuersatz ? parseFloat(p.fSteuersatz) : 19.0; + const gross = net * (1 + rate / 100); + const priceDiv = document.createElement('div'); + priceDiv.className = 'product-price'; + priceDiv.textContent = `${gross.toFixed(2)} €`; + li.appendChild(priceDiv); + } + ul.appendChild(li); }); diff --git a/src/services/category-products-data-fetcher.js b/src/services/category-products-data-fetcher.js index 7c38a1c..15b9a5c 100644 --- a/src/services/category-products-data-fetcher.js +++ b/src/services/category-products-data-fetcher.js @@ -18,10 +18,18 @@ export class ProductDataFetcher { SELECT ka.kKategorie, ka.kArtikel, - ab.cName + ab.cName, + a.cArtNr, + a.fVKNetto, + a.cBarcode, + a.kSteuerklasse, + sk.cName AS cSteuerklasseName, + ss.fSteuersatz FROM tkategorieartikel ka JOIN tArtikelBeschreibung ab ON ka.kArtikel = ab.kArtikel JOIN tArtikel a ON ka.kArtikel = a.kArtikel + LEFT JOIN tSteuerklasse sk ON a.kSteuerklasse = sk.kSteuerklasse + LEFT JOIN tSteuersatz ss ON sk.kSteuerklasse = ss.kSteuerklasse AND ss.kSteuerzone = ${process.env.JTL_STEUERZONE_ID} WHERE ab.kSprache = ${process.env.JTL_SPRACHE_ID} AND a.cAktiv = 'Y' AND ab.kPlattform = ${process.env.JTL_PLATTFORM_ID} diff --git a/src/syncers/category-products-syncer.js b/src/syncers/category-products-syncer.js index 8a648eb..c0a654f 100644 --- a/src/syncers/category-products-syncer.js +++ b/src/syncers/category-products-syncer.js @@ -85,6 +85,10 @@ class CategoryProductsSyncer extends EventEmitter { productsByCategory[record.kKategorie].push({ kArtikel: record.kArtikel, cName: record.cName, + cArtNr: record.cArtNr, + fVKNetto: record.fVKNetto, + cBarcode: record.cBarcode, + fSteuersatz: record.fSteuersatz, images: images }); }