Files
generalFunctions/src/menu/tagcodeApp.js

606 lines
21 KiB
JavaScript

/**
* 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 ===');
})();
}
*/