/** * taggcodeApp.js * Dynamische AssetMenu implementatie met TagcodeApp API * Vervangt de statische assetData met calls naar REST-endpoints. */ class TagcodeApp { constructor(baseURL = 'https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api') { this.baseURL = baseURL; } async fetchData(path, params = {}) { const url = new URL(`${this.baseURL}/${path}`); Object.entries(params).forEach(([key, value]) => { url.searchParams.append(key, value); }); const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); const json = await response.json(); if (!json.success) throw new Error(json.error || json.message); return json.data; } // Asset endpoints getAllAssets() { return this.fetchData('asset/get_all_assets.php'); } getAssetDetail(tag_code) { return this.fetchData('asset/get_detail_asset.php', { tag_code }); } getAssetHistory(asset_tag_number) { return this.fetchData('asset/get_history_asset.php', { asset_tag_number }); } getAssetHierarchy(asset_tag_number) { return this.fetchData('asset/get_asset_hierarchy.php', { asset_tag_number }); } createOrUpdateAsset(params) { return this.fetchData('asset/create_asset.php', params); } // Product & vendor endpoints getVendors() { return this.fetchData('vendor/get_vendors.php'); } getSubtypes(vendor_name) { return this.fetchData('product/get_subtypesFromVendor.php', { vendor_name }); } getSubtypesForCategory(vendor_name, category) { return this.fetchData('product/get_subtypesFromVendorAndCategory.php', { vendor_name, category }); } getProductModels(vendor_name, product_subtype_name) { return this.fetchData('product/get_product_models.php', { vendor_name, product_subtype_name }); } getLocations() { return this.fetchData('location/get_locations.php'); } } class DynamicAssetMenu { constructor(nodeName, api = new TagcodeApp()) { this.nodeName = nodeName; this.api = api; //temp translation table for nodeName to API // Mapping van nodeName naar softwareType this.softwareTypeMapping = { 'measurement': 'Sensor', 'rotatingMachine': 'machine', 'valve': 'valve', 'pump': 'machine', 'heatExchanger': 'machine', // Voeg meer mappings toe als nodig }; // Bepaal automatisch de softwareType this.softwareType = this.softwareTypeMapping[nodeName] || nodeName; this.data = { vendors: [], subtypes: {}, models: {} }; } //Added missing getAllMenuData method getAllMenuData() { return { vendors: this.data.vendors || [], locations: this.data.locations || [], htmlTemplate: this.getHtmlTemplate() }; } /** * Initialiseer: haal alleen de vendor-lijst en locaties op */ async init() { try { this.data.suppliers = await this.api.getVendors(); this.data.locations = await this.api.getLocations(); } catch (error) { console.error('Failed to initialize DynamicAssetMenu:', error); this.data.suppliers = []; this.data.locations = []; } } //Complete getClientInitCode method with full TagcodeApp definition getClientInitCode(nodeName) { return ` // --- DynamicAssetMenu voor ${nodeName} --- // ✅ Define COMPLETE TagcodeApp class in browser context window.TagcodeApp = window.TagcodeApp || class { constructor(baseURL = 'https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api') { this.baseURL = baseURL; } async fetchData(path, params = {}) { const url = new URL(this.baseURL + '/' + path); Object.entries(params).forEach(([key, value]) => { url.searchParams.append(key, value); }); const response = await fetch(url); if (!response.ok) throw new Error('HTTP ' + response.status + ': ' + response.statusText); const json = await response.json(); if (!json.success) throw new Error(json.error || json.message); return json.data; } // ✅ ALL API methods defined here getAllAssets() { return this.fetchData('asset/get_all_assets.php'); } getAssetDetail(tag_code) { return this.fetchData('asset/get_detail_asset.php', { tag_code }); } getVendors() { return this.fetchData('vendor/get_vendors.php'); } getSubtypes(vendor_name, category = null) { const params = { vendor_name }; if (category) params.category = category; return this.fetchData('product/get_subtypesFromVendor.php', params); } getProductModels(vendor_name, product_subtype_name) { return this.fetchData('product/get_product_models.php', { vendor_name, product_subtype_name }); } getLocations() { return this.fetchData('location/get_locations.php'); } }; // ✅ Initialize the API instance BEFORE it's needed window.assetAPI = window.assetAPI || new window.TagcodeApp(); // Helper populate function function populate(el, opts, sel) { if (!el) return; const old = el.value; el.innerHTML = ''; (opts||[]).forEach(o=>{ const opt = document.createElement('option'); opt.value = o; opt.textContent = o; el.appendChild(opt); }); el.value = sel || ''; if (el.value !== old) el.dispatchEvent(new Event('change')); } // ✅ Ensure namespace exists and initialize properly if (!window.EVOLV.nodes.${nodeName}.assetMenu) { window.EVOLV.nodes.${nodeName}.assetMenu = {}; } // ✅ Complete initEditor function window.EVOLV.nodes.${nodeName}.assetMenu.initEditor = async function(node) { try { console.log('🚀 Starting asset menu initialization for ${nodeName}'); console.log('🎯 Automatic softwareType: ${this.softwareType}'); // ✅ Verify API is available if (!window.assetAPI) { console.error('❌ window.assetAPI not available'); return; } // ✅ Wait for DOM to be ready and inject HTML with retry const waitForDialogAndInject = () => { return new Promise((resolve) => { let attempts = 0; const maxAttempts = 20; const tryInject = () => { attempts++; console.log('Injection attempt ' + attempts + '/' + maxAttempts); const injectionSuccess = this.injectHtml ? this.injectHtml() : false; if (injectionSuccess) { console.log('✅ HTML injection successful on attempt:', attempts); resolve(true); } else if (attempts < maxAttempts) { setTimeout(tryInject, 100); } else { console.warn('⚠️ HTML injection failed after ' + maxAttempts + ' attempts'); resolve(false); } }; setTimeout(tryInject, 200); }); }; // Wait for HTML injection const htmlReady = await waitForDialogAndInject(); if (!htmlReady) { console.error('❌ Could not inject HTML, continuing without asset menu'); return; } console.log('🔧 Setting up asset menu functionality'); // ✅ Load vendor list with error handling try { console.log('📡 Loading vendors...'); const vendors = await window.assetAPI.getVendors(); console.log('✅ Vendors loaded:', vendors.length); // ✅ Handle both string arrays and object arrays const vendorNames = vendors.map(v => v.name || v); populate(document.getElementById('node-input-supplier'), vendorNames, node.supplier); } catch (vendorError) { console.error('❌ Error loading vendors:', vendorError); } // ✅ Get form elements const elems = { supplier: document.getElementById('node-input-supplier'), category: document.getElementById('node-input-category'), type: document.getElementById('node-input-assetType'), model: document.getElementById('node-input-model'), unit: document.getElementById('node-input-unit') }; // ✅ Set automatic category value if (elems.category) { elems.category.value = '${this.softwareType}'; console.log('✅ Automatic category set to:', elems.category.value); } // ✅ Supplier change: load subtypes for automatic category if (elems.supplier) { elems.supplier.addEventListener('change', async () => { const vendor = elems.supplier.value; const category = '${this.softwareType}'; if (!vendor) { populate(elems.type, [], ''); populate(elems.model, [], ''); populate(elems.unit, [], ''); return; } try { console.log('📡 Loading subtypes for vendor:', vendor, 'category:', category); const subtypes = await window.assetAPI.getSubtypes(vendor, category); console.log('✅ Subtypes loaded:', subtypes.length); const subtypeNames = subtypes.map(s => s.name || s.subtype_name || s); populate(elems.type, subtypeNames, node.assetType); populate(elems.model, [], ''); populate(elems.unit, [], ''); } catch (error) { console.error('❌ Error loading subtypes:', error); populate(elems.type, [], ''); } }); } // ✅ Type change: load models for vendor + selected subtype if (elems.type) { elems.type.addEventListener('change', async () => { const vendor = elems.supplier.value; const selectedSubtype = elems.type.value; if (!vendor || !selectedSubtype) { populate(elems.model, [], ''); populate(elems.unit, [], ''); return; } try { console.log('📡 Loading models for vendor:', vendor, 'subtype:', selectedSubtype); const models = await window.assetAPI.getProductModels(vendor, selectedSubtype); console.log('✅ Models loaded:', models.length); window._currentModels = models; const modelNames = models.map(m => m.name || m.model_name || m); populate(elems.model, modelNames, node.model); populate(elems.unit, [], ''); } catch (error) { console.error('❌ Error loading models:', error); populate(elems.model, [], ''); } }); } // ✅ Model change: show units for selected model if (elems.model) { elems.model.addEventListener('change', () => { const selectedModelName = elems.model.value; const models = window._currentModels || []; const selectedModel = models.find(m => (m.name || m.model_name) === selectedModelName ); const units = selectedModel && selectedModel.product_model_meta ? Object.keys(selectedModel.product_model_meta) : []; populate(elems.unit, units, node.unit); }); } // ✅ Trigger supplier change if there's a saved value if (node.supplier && elems.supplier) { setTimeout(() => { elems.supplier.dispatchEvent(new Event('change')); }, 100); } console.log('✅ Asset menu initialization complete for ${nodeName}'); } catch (error) { console.error('❌ Error in asset menu initialization:', error); } }; `; } getHtmlTemplate() { return `

Asset selection (${this.softwareType})


`; } /** * Fixed getHtmlInjectionCode method */ /** * Fixed getHtmlInjectionCode method with better element detection */ getHtmlInjectionCode(nodeName) { const htmlTemplate = this.getHtmlTemplate().replace(/`/g, '\\`').replace(/\${/g, '\\${'); return ` // Enhanced HTML injection with multiple fallback strategies window.EVOLV.nodes.${nodeName}.assetMenu.injectHtml = function() { try { // Strategy 1: Find the dialog form container let targetContainer = document.querySelector('#red-ui-editor-dialog .red-ui-editDialog-content'); // Strategy 2: Fallback to the main dialog form if (!targetContainer) { targetContainer = document.querySelector('#dialog-form'); } // Strategy 3: Fallback to any form in the editor dialog if (!targetContainer) { targetContainer = document.querySelector('#red-ui-editor-dialog form'); } // Strategy 4: Find by Red UI classes if (!targetContainer) { targetContainer = document.querySelector('.red-ui-editor-dialog .editor-tray-content'); } if (targetContainer) { // Remove any existing asset menu to prevent duplicates const existingAssetMenu = targetContainer.querySelector('.asset-menu-section'); if (existingAssetMenu) { existingAssetMenu.remove(); } // Create container div const assetMenuDiv = document.createElement('div'); assetMenuDiv.className = 'asset-menu-section'; assetMenuDiv.innerHTML = \`${htmlTemplate}\`; // Insert at the beginning of the form targetContainer.insertBefore(assetMenuDiv, targetContainer.firstChild); console.log(' Asset menu HTML injected successfully into:', targetContainer.className || targetContainer.tagName); return true; } else { console.warn('⚠️ Could not find dialog form container. Available elements:'); console.log('Available dialogs:', document.querySelectorAll('[id*="dialog"], [class*="dialog"]')); console.log('Available forms:', document.querySelectorAll('form')); return false; } } catch (error) { console.error('❌ Error injecting HTML:', error); return false; } }; `; } } // Exporteer voor gebruik in Node-RED module.exports = { TagcodeApp, DynamicAssetMenu }; /* // --- Test CLI --- // Voer deze test uit met `node tagcodeApp.js` om de API-client en menu-init logica te controleren if (require.main === module) { (async () => { const api = new TagcodeApp(); console.log('=== Test: getVendors() ==='); let vendors; try { vendors = await api.getVendors(); console.log('Vendors:', vendors); } catch (e) { console.error('getVendors() error:', e.message); return; } console.log('=== Test: getLocations() ==='); try { const locations = await api.getLocations(); console.log('Locations:', locations); } catch (e) { console.error('getLocations() error:', e.message); return; } // ✅ Test verschillende nodeNames met automatische softwareType mapping const testNodes = [ { nodeName: 'measurement', expectedSoftwareType: 'Sensor' }, { nodeName: 'rotatingMachine', expectedSoftwareType: 'machine' }, { nodeName: 'valve', expectedSoftwareType: 'valve' } ]; for (const testNode of testNodes) { console.log(`\n=== Test: ${testNode.nodeName} → ${testNode.expectedSoftwareType} ===`); // Initialize DynamicAssetMenu met automatische softwareType const menu = new DynamicAssetMenu(testNode.nodeName, api); console.log(`✅ Automatic softwareType for ${testNode.nodeName}:`, menu.softwareType); try { await menu.init(); console.log('Preloaded suppliers:', menu.data.suppliers.map(v=>v.name || v)); } catch (e) { console.error(`DynamicAssetMenu.init() error for ${testNode.nodeName}:`, e.message); continue; } console.log(`=== Sequential dropdown simulation for ${testNode.nodeName} ===`); // 1. Select supplier const supplier = menu.data.suppliers[0]; const supplierName = supplier.name || supplier; console.log('Selected supplier:', supplierName); // 2. ✅ Gebruik automatische softwareType in plaats van dropdown const automaticCategory = menu.softwareType; console.log('Automatic category (softwareType):', automaticCategory); // 3. ✅ Direct naar models met supplier + automatische category let models; try { console.log(`📡 Loading models for supplier: "${supplierName}", category: "${automaticCategory}"`); models = await api.getProductModels(supplierName, automaticCategory); console.log('Fetched models:', models.map(m=>m.name || m)); if (models.length === 0) { console.warn(`⚠️ No models found for ${supplierName} + ${automaticCategory}`); continue; } } catch (e) { console.error(`getProductModels error for ${supplierName} + ${automaticCategory}:`, e.message); continue; } // 4. Extract unique types from models const types = Array.from(new Set(models.map(m => m.product_model_type || m.type || 'Unknown'))); console.log('Available types:', types); if (types.length === 0) { console.warn('⚠️ No types found in models'); continue; } // 5. Choose first type const selectedType = types[0]; console.log('Selected type:', selectedType); // 6. Filter models by type const filteredModels = models.filter(m => (m.product_model_type || m.type) === selectedType ); console.log('Models for selected type:', filteredModels.map(m => m.name || m)); if (filteredModels.length === 0) { console.warn('⚠️ No models found for selected type'); continue; } // 7. Choose first model and show units const model = filteredModels[0]; console.log('Selected model:', model.name || model); const units = model.product_model_meta ? Object.keys(model.product_model_meta) : []; console.log('Available units:', units); const unit = units[0] || 'N/A'; console.log('Selected unit:', unit); console.log(`✅ Complete flow for ${testNode.nodeName}:`); console.log(` Supplier: ${supplierName}`); console.log(` Category: ${automaticCategory} (automatic)`); console.log(` Type: ${selectedType}`); console.log(` Model: ${model.name || model}`); console.log(` Unit: ${unit}`); } console.log('\n=== Test verschillende softwareTypes ==='); // Test of de API verschillende categories ondersteunt const testCategories = ['Sensor', 'machine', 'valve', 'pump']; const testSupplier = 'Vega'; // Bijvoorbeeld for (const category of testCategories) { try { console.log(`\n📡 Testing category: ${category} with supplier: ${testSupplier}`); const models = await api.getProductModels(testSupplier, category); console.log(`✅ Found ${models.length} models for ${testSupplier} + ${category}`); if (models.length > 0) { const sampleModel = models[0]; console.log(` Sample model:`, sampleModel.name || sampleModel); const types = Array.from(new Set(models.map(m => m.product_model_type || m.type))); console.log(` Available types:`, types); } } catch (e) { console.warn(`⚠️ No models found for ${testSupplier} + ${category}: ${e.message}`); } } console.log('\n=== Klaar met alle tests ==='); })(); } */