license update and enhancements to measurement functionality + child parent relationship
This commit is contained in:
@@ -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
606
src/menu/tagcodeApp.js
Normal 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
207
src/menu/tagcodeAsset.js
Normal 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 };
|
||||
Reference in New Issue
Block a user