606 lines
21 KiB
JavaScript
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 ===');
|
|
})();
|
|
}
|
|
*/ |