license update and enhancements to measurement functionality + child parent relationship

This commit is contained in:
znetsixe
2025-08-07 13:52:29 +02:00
parent 7061d6a539
commit e87f9da4bf
9 changed files with 1862 additions and 363 deletions

View File

@@ -1,23 +1,26 @@
const AssetMenu = require('./asset.js');
const { TagcodeApp, DynamicAssetMenu } = require('./tagcodeApp.js');
const LoggerMenu = require('./logger.js');
const PhysicalPositionMenu = require('./physicalPosition.js');
class MenuManager {
constructor() {
this.registeredMenus = new Map(); // Store menu type instances
this.registerMenu('asset', new AssetMenu()); // Register asset menu by default
this.registerMenu('logger', new LoggerMenu()); // Register logger menu by default
this.registerMenu('position', new PhysicalPositionMenu()); // Register position menu by default
this.registeredMenus = new Map();
// Register factory functions
this.registerMenu('asset', () => new AssetMenu()); // static menu to be replaced by dynamic one but later
//this.registerMenu('asset', (nodeName) => new DynamicAssetMenu(nodeName, new TagcodeApp()));
this.registerMenu('logger', () => new LoggerMenu());
this.registerMenu('position', () => new PhysicalPositionMenu());
}
/**
* Register a menu type with its handler instance
* Register a menu type with its handler factory function
* @param {string} menuType - The type of menu (e.g., 'asset', 'logging')
* @param {object} menuHandler - The menu handler instance
* @param {function} menuFactory - The menu factory function
*/
registerMenu(menuType, menuHandler) {
this.registeredMenus.set(menuType, menuHandler);
registerMenu(menuType, menuFactory) {
this.registeredMenus.set(menuType, menuFactory);
}
/**
@@ -27,58 +30,145 @@ class MenuManager {
* @returns {string} Complete JavaScript code to serve
*/
createEndpoint(nodeName, menuTypes) {
// 1. Collect all menu data
const menuData = {};
menuTypes.forEach(menuType => {
const handler = this.registeredMenus.get(menuType);
if (handler && typeof handler.getAllMenuData === 'function') {
menuData[menuType] = handler.getAllMenuData();
try {
// ✅ Create instances using factory functions with proper error handling
const instantiatedMenus = new Map();
menuTypes.forEach(menuType => {
try {
const factory = this.registeredMenus.get(menuType);
if (typeof factory === 'function') {
const instance = factory(nodeName);
instantiatedMenus.set(menuType, instance);
} else {
console.warn(`No factory function found for menu type: ${menuType}`);
}
} catch (error) {
console.error(`Error creating instance for ${menuType}:`, error);
}
});
// ✅ Collect all menu data with error handling
const menuData = {};
menuTypes.forEach(menuType => {
try {
const handler = instantiatedMenus.get(menuType);
if (handler && typeof handler.getAllMenuData === 'function') {
menuData[menuType] = handler.getAllMenuData();
} else {
// Provide default empty data if method doesn't exist
menuData[menuType] = {};
}
} catch (error) {
console.error(`Error getting menu data for ${menuType}:`, error);
menuData[menuType] = {};
}
});
// ✅ Generate HTML injection code with error handling
const htmlInjections = menuTypes.map(type => {
try {
const menu = instantiatedMenus.get(type);
if (menu && typeof menu.getHtmlInjectionCode === 'function') {
return menu.getHtmlInjectionCode(nodeName);
}
return '';
} catch (error) {
console.error(`Error generating HTML injection for ${type}:`, error);
return `// Error generating HTML injection for ${type}: ${error.message}`;
}
}).join('\n');
// ✅ Collect all client initialization code with error handling
const initFunctions = [];
menuTypes.forEach(menuType => {
try {
const handler = instantiatedMenus.get(menuType);
if (handler && typeof handler.getClientInitCode === 'function') {
initFunctions.push(handler.getClientInitCode(nodeName));
}
} catch (error) {
console.error(`Error generating init code for ${menuType}:`, error);
initFunctions.push(`// Error in ${menuType} initialization: ${error.message}`);
}
});
// Convert menu data to JSON
const menuDataJSON = JSON.stringify(menuData, null, 2);
// ✅ Assemble the complete script with comprehensive error handling
return `
try {
// Create the namespace structure with safety checks
window.EVOLV = window.EVOLV || {};
window.EVOLV.nodes = window.EVOLV.nodes || {};
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
// Initialize menu namespaces
${menuTypes.map(type => `window.EVOLV.nodes.${nodeName}.${type}Menu = window.EVOLV.nodes.${nodeName}.${type}Menu || {};`).join('\n ')}
// Inject the pre-loaded menu data directly into the namespace
window.EVOLV.nodes.${nodeName}.menuData = ${menuDataJSON};
// HTML injections with error handling
try {
${htmlInjections}
} catch (htmlError) {
console.error('Error in HTML injections for ${nodeName}:', htmlError);
}
// Initialize functions with error handling
try {
${initFunctions.join('\n\n ')}
} catch (initError) {
console.error('Error in initialization functions for ${nodeName}:', initError);
}
// Main initialization function that calls all menu initializers
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
try {
${menuTypes.map(type => `
try {
if (window.EVOLV.nodes.${nodeName}.${type}Menu && window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor) {
window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor(node);
}
} catch (${type}Error) {
console.error('Error initializing ${type} menu for ${nodeName}:', ${type}Error);
}`).join('')}
} catch (editorError) {
console.error('Error in main editor initialization for ${nodeName}:', editorError);
}
};
console.log('${nodeName} menu data and initializers loaded for: ${menuTypes.join(', ')}');
} catch (globalError) {
console.error('Critical error in ${nodeName} menu initialization:', globalError);
// Fallback initialization
window.EVOLV = window.EVOLV || {};
window.EVOLV.nodes = window.EVOLV.nodes || {};
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
console.warn('Using fallback editor initialization for ${nodeName}');
};
}
});
`;
// Generate HTML injection code
const htmlInjections = menuTypes.map(type => {
const menu = this.registeredMenus.get(type);
if (menu && menu.getHtmlInjectionCode) {
return menu.getHtmlInjectionCode(nodeName);
}
return '';
}).join('\n');
// 2. Collect all client initialization code
const initFunctions = [];
menuTypes.forEach(menuType => {
const handler = this.registeredMenus.get(menuType);
if (handler && typeof handler.getClientInitCode === 'function') {
initFunctions.push(handler.getClientInitCode(nodeName));
}
});
// 3. Convert menu data to JSON
const menuDataJSON = JSON.stringify(menuData, null, 2);
// 4. Assemble the complete script
return `
// Create the namespace structure
window.EVOLV = window.EVOLV || {};
window.EVOLV.nodes = window.EVOLV.nodes || {};
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
// Inject the pre-loaded menu data directly into the namespace
window.EVOLV.nodes.${nodeName}.menuData = ${menuDataJSON};
${initFunctions.join('\n\n')}
// Main initialization function that calls all menu initializers
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
${menuTypes.map(type => `
if (window.EVOLV.nodes.${nodeName}.${type}Menu && window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor) {
window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor(node);
}`).join('')}
};
console.log('${nodeName} menu data and initializers loaded for: ${menuTypes.join(', ')}');
`;
} catch (error) {
console.error(`Critical error creating endpoint for ${nodeName}:`, error);
// Return minimal fallback script
return `
window.EVOLV = window.EVOLV || {};
window.EVOLV.nodes = window.EVOLV.nodes || {};
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
console.error('Menu system failed to initialize for ${nodeName}');
};
console.error('Menu system failed for ${nodeName}:', '${error.message}');
`;
}
}
}

606
src/menu/tagcodeApp.js Normal file
View File

@@ -0,0 +1,606 @@
/**
* 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 = '<option value="">Select…</option>';
(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 Properties -->
<hr />
<h3>Asset selection (${this.softwareType})</h3>
<div class="form-row">
<label for="node-input-supplier"><i class="fa fa-industry"></i> Supplier</label>
<select id="node-input-supplier" style="width:70%;"></select>
</div>
<!-- ✅ Toon softwareType als readonly info -->
<div class="form-row">
<label><i class="fa fa-sitemap"></i> Category</label>
<input type="text" value="${this.softwareType}" readonly style="width:70%; background-color: #f5f5f5;" />
<input type="hidden" id="node-input-category" value="${this.softwareType}" />
</div>
<div class="form-row">
<label for="node-input-assetType"><i class="fa fa-puzzle-piece"></i> Type</label>
<select id="node-input-assetType" style="width:70%;"></select>
</div>
<div class="form-row">
<label for="node-input-model"><i class="fa fa-wrench"></i> Model</label>
<select id="node-input-model" style="width:70%;"></select>
</div>
<div class="form-row">
<label for="node-input-unit"><i class="fa fa-balance-scale"></i> Unit</label>
<select id="node-input-unit" style="width:70%;"></select>
</div>
<hr />
`;
}
/**
* 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 ===');
})();
}
*/

207
src/menu/tagcodeAsset.js Normal file
View File

@@ -0,0 +1,207 @@
/**
* 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) {
// Bij create/update worden alle velden via query params meegegeven
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 });
}
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;
this.data = {
vendors: [],
subtypes: {}, // per vendor
models: {} // per vendor+subtype
};
}
/**
* Initialiseer: haal vendors en locaties eenmalig op
*/
async init() {
this.data.vendors = await this.api.getVendors();
this.data.locations = await this.api.getLocations();
}
/**
* Injecteer HTML, data en events
*/
getClientInitCode() {
const node = this.nodeName;
return `
// --- DynamicAssetMenu voor ${node} ---
window.TagcodeApp = window.TagcodeApp || ${TagcodeApp.toString()};
window.assetAPI = window.assetAPI || new TagcodeApp();
// Helper populate
function populate(el, opts, sel) {
const old = el.value;
el.innerHTML = '<option value="">Select…</option>';
(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'));
}
// InitEditor
window.EVOLV.nodes.${node}.assetMenu.initEditor = async function(node) {
this.injectHtml();
// eerst: laad vendor-lijst
const vendors = await window.assetAPI.getVendors();
const vendorNames = vendors.map(v=>v.name);
populate(document.getElementById('node-input-supplier'), vendorNames, node.supplier);
// wire events
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')
};
elems.supplier.addEventListener('change', async ()=>{
const v = elems.supplier.value;
if (!v) return populate(elems.category, [], '');
const subs = await window.assetAPI.getSubtypes(v);
const names = subs.map(s=>s.name);
populate(elems.category, names, node.category);
});
elems.category.addEventListener('change', async ()=>{
const v = elems.supplier.value, c = elems.category.value;
if (!v||!c) return populate(elems.type, [], '');
const models = await window.assetAPI.getProductModels(v, c);
window._currentModels = models; // tijdelijk cachen
const types = Array.from(new Set(models.map(m=>m.product_model_type)));
populate(elems.type, types, node.assetType);
});
elems.type.addEventListener('change', ()=>{
const t = elems.type.value;
const models = window._currentModels || [];
const filtered = models.filter(m=>m.product_model_type===t);
const names = filtered.map(m=>m.name);
window._filteredModels = filtered;
populate(elems.model, names, node.model);
});
elems.model.addEventListener('change', ()=>{
const m = elems.model.value;
const models = window._filteredModels || [];
const entry = models.find(x=>x.name===m);
const units = entry && entry.product_model_meta ? Object.keys(entry.product_model_meta) : [];
populate(elems.unit, units, node.unit);
});
// laadt opgeslagen waarden
if (node.supplier) elems.supplier.dispatchEvent(new Event('change'));
};
`;
}
getHtmlTemplate() {
return `
<!-- Asset Properties -->
<hr />
<h3>Asset selection</h3>
<div class="form-row">
<label for="node-input-supplier"><i class="fa fa-industry"></i> Supplier</label>
<select id="node-input-supplier" style="width:70%;"></select>
</div>
<div class="form-row">
<label for="node-input-category"><i class="fa fa-sitemap"></i> Category</label>
<select id="node-input-category" style="width:70%;"></select>
</div>
<div class="form-row">
<label for="node-input-assetType"><i class="fa fa-puzzle-piece"></i> Type</label>
<select id="node-input-assetType" style="width:70%;"></select>
</div>
<div class="form-row">
<label for="node-input-model"><i class="fa fa-wrench"></i> Model</label>
<select id="node-input-model" style="width:70%;"></select>
</div>
<div class="form-row">
<label for="node-input-unit"><i class="fa fa-balance-scale"></i> Unit</label>
<select id="node-input-unit" style="width:70%;"></select>
</div>
<hr />
`;
}
getHtmlInjectionCode() {
const tmpl = this.getHtmlTemplate().replace(/`/g, '\\`').replace(/\$/g, '\\\$');
return `
// Asset HTML injection voor ${this.nodeName}
window.EVOLV.nodes.${this.nodeName}.assetMenu.injectHtml = function() {
const placeholder = document.getElementById('asset-fields-placeholder');
if (placeholder && !placeholder.hasChildNodes()) {
placeholder.innerHTML = \`${tmpl}\`;
}
};
`;
}
}
// Exporteer voor gebruik in Node-RED
module.exports = { TagcodeApp, DynamicAssetMenu };