From 71910f84a2bdade0b1c0395754fec1f1670b33b9 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Sun, 23 Nov 2025 11:40:42 +0100 Subject: [PATCH] feat: Enhance product management by adding WebSocket support for real-time updates, implement product loading in categories, and improve caching for category products. --- category-products-syncer.js | 174 ++++++++++++++++++++++++++++++++++++ index.html | 96 ++++++++++++++++++++ index.js | 19 +++- server.js | 79 +++++++++++++++- 4 files changed, 361 insertions(+), 7 deletions(-) create mode 100644 category-products-syncer.js diff --git a/category-products-syncer.js b/category-products-syncer.js new file mode 100644 index 0000000..89db5be --- /dev/null +++ b/category-products-syncer.js @@ -0,0 +1,174 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { EventEmitter } from 'events'; +import { createConnection } from './database.js'; + +class CategoryProductsSyncer extends EventEmitter { + constructor() { + super(); + if (CategoryProductsSyncer.instance) { + return CategoryProductsSyncer.instance; + } + this.cacheBaseDir = process.env.CACHE_LOCATION || '.'; + + // Track syncing state + this.isSyncing = false; + this.queuedCategoryIds = null; + + CategoryProductsSyncer.instance = this; + } + + async syncProducts(categoryIds) { + // Check if already syncing + if (this.isSyncing) { + console.log('⏳ CategoryProductsSyncer is busy. Queuing sync...'); + this.queuedCategoryIds = categoryIds; + return; + } + + this.isSyncing = true; + try { + await this._performSync(categoryIds); + } catch (err) { + console.error('❌ Error syncing products:', err); + } finally { + this.isSyncing = false; + // Process queued sync if exists + if (this.queuedCategoryIds) { + const nextIds = this.queuedCategoryIds; + this.queuedCategoryIds = null; + // Use setTimeout to allow event loop to breathe + setTimeout(() => this.syncProducts(nextIds), 0); + } + } + } + + async _performSync(categoryIds) { + const startTime = Date.now(); + const productsDir = path.join(this.cacheBaseDir, 'products'); + + // Ensure directory exists + await fs.mkdir(productsDir, { recursive: true }); + + // Get existing files + let existingFiles = []; + try { + existingFiles = await fs.readdir(productsDir); + } catch (err) { + // Directory might be empty or new + } + + // Filter for category json files (assuming we save as category_{id}.json) + const existingIds = existingFiles + .filter(f => f.startsWith('category_') && f.endsWith('.json')) + .map(f => parseInt(f.replace('category_', '').replace('.json', ''))); + + const validIds = new Set(categoryIds.filter(id => id !== null && id !== undefined)); + + // 1. Delete obsolete category files + const toDelete = existingIds.filter(id => !validIds.has(id)); + for (const id of toDelete) { + const filePath = path.join(productsDir, `category_${id}.json`); + await fs.unlink(filePath); + } + if (toDelete.length > 0) { + console.log(`🗑️ Deleted ${toDelete.length} obsolete product lists.`); + } + + // 2. Update/Create product lists for all valid categories + // We update all because product assignments might have changed even if category exists + if (validIds.size > 0) { + console.log(`📦 Syncing products for ${validIds.size} categories...`); + await this._fetchAndWriteProducts([...validIds], productsDir); + } else { + console.log(`✅ No categories to sync products for.`); + } + + const duration = Date.now() - startTime; + console.log(`✅ Product sync completed in ${duration}ms.`); + } + + async _fetchAndWriteProducts(ids, dir) { + let pool; + try { + pool = await createConnection(); + + // Process in chunks to avoid huge queries + const chunkSize = 50; + for (let i = 0; i < ids.length; i += chunkSize) { + const chunk = ids.slice(i, i + chunkSize); + const list = chunk.join(','); + + // Fetch products for this chunk of categories + // We need kArtikel and cName, ordered by bRowversion descending + const result = await pool.request().query(` + SELECT + ka.kKategorie, + ka.kArtikel, + ab.cName + FROM tkategorieartikel ka + JOIN tArtikelBeschreibung ab ON ka.kArtikel = ab.kArtikel + JOIN tArtikel a ON ka.kArtikel = a.kArtikel + WHERE ab.kSprache = ${process.env.JTL_SPRACHE_ID} + AND ab.kPlattform = ${process.env.JTL_PLATTFORM_ID} + AND ab.kShop = ${process.env.JTL_SHOP_ID} + AND ka.kKategorie IN (${list}) + ORDER BY a.bRowversion DESC, ab.bRowversion DESC + `); + + // Group results by kKategorie + const productsByCategory = {}; + + // Initialize arrays for all requested IDs (so we create empty files for empty categories) + chunk.forEach(id => { + productsByCategory[id] = []; + }); + + for (const record of result.recordset) { + if (productsByCategory[record.kKategorie]) { + productsByCategory[record.kKategorie].push({ + kArtikel: record.kArtikel, + cName: record.cName + }); + } + } + + // Write files + for (const catId of chunk) { + const filePath = path.join(dir, `category_${catId}.json`); + const products = productsByCategory[catId] || []; + const newContent = JSON.stringify(products, null, 2); + + // Check for changes + let oldContent = ''; + try { + oldContent = await fs.readFile(filePath, 'utf-8'); + } catch (e) { + // File doesn't exist yet + } + + if (oldContent !== newContent) { + await fs.writeFile(filePath, newContent); + this.emit('categoryUpdated', { id: catId, products }); + } + } + + const processed = Math.min(i + chunkSize, ids.length); + if (processed === ids.length) { + console.log(`✅ Processed products for ${processed}/${ids.length} categories.`); + } else { + console.log(`⏳ Processed products for ${processed}/${ids.length} categories...`); + } + } + } catch (err) { + console.error('❌ Error fetching products:', err); + } finally { + if (pool) { + await pool.close(); + } + } + } +} + +const instance = new CategoryProductsSyncer(); +export default instance; diff --git a/index.html b/index.html index 3b27159..50057ea 100644 --- a/index.html +++ b/index.html @@ -117,6 +117,21 @@ border-radius: 8px; text-align: center; } + .category-products { + margin-top: 0.5rem; + padding-left: 4rem; + font-size: 0.9rem; + color: #555; + } + + .product-item { + padding: 0.25rem 0; + border-bottom: 1px solid #eee; + } + + .product-item:last-child { + border-bottom: none; + } @@ -127,7 +142,32 @@ +