Compare commits
45 Commits
e87f9da4bf
...
dev-Pieter
| Author | SHA1 | Date | |
|---|---|---|---|
| d5d078413c | |||
| 17662ef7cb | |||
| f653a1e98c | |||
| 3886277616 | |||
| 83018fabe0 | |||
| e72579e5d0 | |||
| 0fb42865ff | |||
| b2b811e802 | |||
| bde2dcf7d8 | |||
| 76570280bc | |||
| d7017b5d33 | |||
| f93603c182 | |||
| c261335df5 | |||
| a41f053d5d | |||
| 8d7d98f126 | |||
| 3f90685834 | |||
| efc97d6cd1 | |||
|
|
d72bfd5560 | ||
| 6d30e25daa | |||
| 16e202e841 | |||
|
|
241ed1d3cb | ||
| 3876f86530 | |||
| 56be0f1840 | |||
|
|
a30f2c90f4 | ||
| 302e122387 | |||
|
|
50f99fa642 | ||
| 494a688583 | |||
| c512c96636 | |||
|
|
eb15da2a63 | ||
| 6dcd3c3d26 | |||
| 958ec2269c | |||
|
|
83ca429bf5 | ||
|
|
222d0f56fc | ||
|
|
e1c6124cf0 | ||
| 0bccad05f8 | |||
| 7191e57aea | |||
| aec2d3692d | |||
| 71643375fc | |||
| f13ee68938 | |||
| 475caa90db | |||
| 9aa38f9000 | |||
| 4a6273b037 | |||
| 8c9301b128 | |||
| 7cdfc87c83 | |||
| 839ae2f3da |
@@ -59,15 +59,20 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Level",
|
"name": "Quantity (oxygen)",
|
||||||
"models": [
|
"models": [
|
||||||
{
|
{
|
||||||
"name": "VegaLevel 10",
|
"name": "VegaOxySense 10",
|
||||||
"units": ["m", "ft", "mm"]
|
"units": ["g/m³", "mol/m³"]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "VegaLevel 20",
|
"name": "Quantity (TSS)",
|
||||||
"units": ["m", "ft", "mm"]
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "VegaSolidsProbe",
|
||||||
|
"units": ["g/m³"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,6 +202,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7",
|
||||||
|
"name": "Vegabar 10",
|
||||||
|
"product_model_subtype_id": "3",
|
||||||
|
"product_model_description": null,
|
||||||
|
"vendor_id": "4",
|
||||||
|
"product_model_status": "Actief",
|
||||||
|
"vendor_name": "vega",
|
||||||
|
"product_subtype_name": "pressure",
|
||||||
|
"product_model_meta": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8",
|
||||||
|
"name": "VegaFlow 10",
|
||||||
|
"product_model_subtype_id": "4",
|
||||||
|
"product_model_description": null,
|
||||||
|
"vendor_id": "4",
|
||||||
|
"product_model_status": "Actief",
|
||||||
|
"vendor_name": "vega",
|
||||||
|
"product_subtype_name": "flow",
|
||||||
|
"product_model_meta": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
4
index.js
4
index.js
@@ -12,11 +12,12 @@ const outputUtils = require('./src/helper/outputUtils.js');
|
|||||||
const logger = require('./src/helper/logger.js');
|
const logger = require('./src/helper/logger.js');
|
||||||
const validation = require('./src/helper/validationUtils.js');
|
const validation = require('./src/helper/validationUtils.js');
|
||||||
const configUtils = require('./src/helper/configUtils.js');
|
const configUtils = require('./src/helper/configUtils.js');
|
||||||
|
const assertions = require('./src/helper/assertionUtils.js')
|
||||||
|
|
||||||
// Domain-specific modules
|
// Domain-specific modules
|
||||||
const { MeasurementContainer } = require('./src/measurements/index.js');
|
const { MeasurementContainer } = require('./src/measurements/index.js');
|
||||||
const configManager = require('./src/configs/index.js');
|
const configManager = require('./src/configs/index.js');
|
||||||
const nrmse = require('./src/nrmse/ErrorMetrics.js');
|
const nrmse = require('./src/nrmse/errorMetrics.js');
|
||||||
const state = require('./src/state/state.js');
|
const state = require('./src/state/state.js');
|
||||||
const convert = require('./src/convert/index.js');
|
const convert = require('./src/convert/index.js');
|
||||||
const MenuManager = require('./src/menu/index.js');
|
const MenuManager = require('./src/menu/index.js');
|
||||||
@@ -34,6 +35,7 @@ module.exports = {
|
|||||||
configUtils,
|
configUtils,
|
||||||
logger,
|
logger,
|
||||||
validation,
|
validation,
|
||||||
|
assertions,
|
||||||
MeasurementContainer,
|
MeasurementContainer,
|
||||||
nrmse,
|
nrmse,
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"general": {
|
"general": {
|
||||||
"name": {
|
"name": {
|
||||||
"default": "Measurement Configuration",
|
"default": "Sensor",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A human-readable name or label for this measurement configuration."
|
"description": "A human-readable name or label for this measurement configuration."
|
||||||
@@ -91,6 +91,13 @@
|
|||||||
],
|
],
|
||||||
"description": "Defines the position of the measurement relative to its parent equipment or system."
|
"description": "Defines the position of the measurement relative to its parent equipment or system."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"distance":{
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Defines the position of the measurement relative to its parent equipment or system."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"asset": {
|
"asset": {
|
||||||
|
|||||||
@@ -412,6 +412,14 @@
|
|||||||
],
|
],
|
||||||
"description": "The frequency at which calculations are performed."
|
"description": "The frequency at which calculations are performed."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"flowNumber": {
|
||||||
|
"default": 1,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": false,
|
||||||
|
"description": "Defines which effluent flow of the parent node to handle."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,8 +11,12 @@ class ChildRegistrationUtils {
|
|||||||
|
|
||||||
this.logger.debug(`Registering child: ${name} (${id}) as ${softwareType} at ${positionVsParent}`);
|
this.logger.debug(`Registering child: ${name} (${id}) as ${softwareType} at ${positionVsParent}`);
|
||||||
|
|
||||||
// Enhanced child setup
|
// Enhanced child setup - multiple parents
|
||||||
child.parent = this.mainClass;
|
if (Array.isArray(child.parent)) {
|
||||||
|
child.parent.push(this.mainClass);
|
||||||
|
} else {
|
||||||
|
child.parent = [this.mainClass];
|
||||||
|
}
|
||||||
child.positionVsParent = positionVsParent;
|
child.positionVsParent = positionVsParent;
|
||||||
|
|
||||||
// Enhanced measurement container with rich context
|
// Enhanced measurement container with rich context
|
||||||
@@ -33,9 +37,9 @@ class ChildRegistrationUtils {
|
|||||||
registeredAt: Date.now()
|
registeredAt: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
// IMPORTANT: Only call parent registration - no automatic handling
|
// IMPORTANT: Only call parent registration - no automatic handling and if parent has this function then try to register this child
|
||||||
if (typeof this.mainClass.registerOnChildEvents === 'function') {
|
if (typeof this.mainClass.registerChild === 'function') {
|
||||||
this.mainClass.registerOnChildEvents();
|
this.mainClass.registerChild(child, softwareType);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`✅ Child ${name} registered successfully`);
|
this.logger.info(`✅ Child ${name} registered successfully`);
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ class Measurement {
|
|||||||
const downIndex = downValues.timestamps.indexOf(upTimestamp);
|
const downIndex = downValues.timestamps.indexOf(upTimestamp);
|
||||||
|
|
||||||
if (downIndex !== -1) {
|
if (downIndex !== -1) {
|
||||||
|
|
||||||
const diff = upValues.values[i] - downValues.values[downIndex];
|
const diff = upValues.values[i] - downValues.values[downIndex];
|
||||||
diffMeasurement.setValue(diff, upTimestamp);
|
diffMeasurement.setValue(diff, upTimestamp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,11 +88,18 @@ class MeasurementContainer {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
position(positionName) {
|
position(positionValue) {
|
||||||
if (!this._currentVariant) {
|
if (!this._currentVariant) {
|
||||||
throw new Error('Variant must be specified before position');
|
throw new Error('Variant must be specified before position');
|
||||||
}
|
}
|
||||||
this._currentPosition = positionName;
|
|
||||||
|
// Turn string positions into numeric values
|
||||||
|
if (typeof positionValue == "string") {
|
||||||
|
positionValue = this._convertPositionStr2Num(positionValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._currentPosition = positionValue;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +137,7 @@ class MeasurementContainer {
|
|||||||
measurement.setUnit(finalUnit);
|
measurement.setUnit(finalUnit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ ENHANCED: Emit event with rich context
|
// ENHANCED: Emit event with rich context
|
||||||
const eventData = {
|
const eventData = {
|
||||||
value: convertedValue,
|
value: convertedValue,
|
||||||
originalValue: val,
|
originalValue: val,
|
||||||
@@ -140,7 +147,7 @@ class MeasurementContainer {
|
|||||||
position: this._currentPosition,
|
position: this._currentPosition,
|
||||||
variant: this._currentVariant,
|
variant: this._currentVariant,
|
||||||
type: this._currentType,
|
type: this._currentType,
|
||||||
// ✅ NEW: Enhanced context
|
// NEW: Enhanced context
|
||||||
childId: this.childId,
|
childId: this.childId,
|
||||||
childName: this.childName,
|
childName: this.childName,
|
||||||
parentRef: this.parentRef
|
parentRef: this.parentRef
|
||||||
@@ -148,6 +155,7 @@ class MeasurementContainer {
|
|||||||
|
|
||||||
// Emit the exact event your parent expects
|
// Emit the exact event your parent expects
|
||||||
this.emitter.emit(`${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
|
this.emitter.emit(`${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
|
||||||
|
//console.log(`Emitted event: ${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -242,10 +250,12 @@ class MeasurementContainer {
|
|||||||
const savedPosition = this._currentPosition;
|
const savedPosition = this._currentPosition;
|
||||||
|
|
||||||
// Get upstream and downstream measurements
|
// Get upstream and downstream measurements
|
||||||
this._currentPosition = 'upstream';
|
const positions = this.getPositions();
|
||||||
|
|
||||||
|
this._currentPosition = Math.min(...positions);
|
||||||
const upstream = this.get();
|
const upstream = this.get();
|
||||||
|
|
||||||
this._currentPosition = 'downstream';
|
this._currentPosition = Math.max(...positions);
|
||||||
const downstream = this.get();
|
const downstream = this.get();
|
||||||
|
|
||||||
this._currentPosition = savedPosition;
|
this._currentPosition = savedPosition;
|
||||||
@@ -318,7 +328,7 @@ class MeasurementContainer {
|
|||||||
Object.keys(this.measurements[this._currentType]) : [];
|
Object.keys(this.measurements[this._currentType]) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getPositions() {
|
getPositions(asNumber = false) {
|
||||||
if (!this._currentType || !this._currentVariant) {
|
if (!this._currentType || !this._currentVariant) {
|
||||||
throw new Error('Type and variant must be specified before listing positions');
|
throw new Error('Type and variant must be specified before listing positions');
|
||||||
}
|
}
|
||||||
@@ -328,9 +338,13 @@ class MeasurementContainer {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (asNumber) {
|
||||||
return Object.keys(this.measurements[this._currentType][this._currentVariant]);
|
return Object.keys(this.measurements[this._currentType][this._currentVariant]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Object.keys(this.measurements[this._currentType][this._currentVariant]).map(this._convertPositionNum2Str);
|
||||||
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.measurements = {};
|
this.measurements = {};
|
||||||
this._currentType = null;
|
this._currentType = null;
|
||||||
@@ -403,6 +417,39 @@ class MeasurementContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_convertPositionStr2Num(positionString) {
|
||||||
|
switch(positionString) {
|
||||||
|
case "atEquipment":
|
||||||
|
return 0;
|
||||||
|
case "upstream":
|
||||||
|
return Number.POSITIVE_INFINITY;
|
||||||
|
case "downstream":
|
||||||
|
return Number.NEGATIVE_INFINITY;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error(`Invalid positionVsParent provided: ${positionString}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_convertPositionNum2Str(positionValue) {
|
||||||
|
if (positionValue === 0) {
|
||||||
|
return "atEquipment";
|
||||||
|
}
|
||||||
|
if (positionValue < 0) {
|
||||||
|
return "upstream";
|
||||||
|
}
|
||||||
|
if (positionValue > 0) {
|
||||||
|
return "downstream";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error(`Invalid position provided: ${positionValue}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = MeasurementContainer;
|
module.exports = MeasurementContainer;
|
||||||
|
|||||||
@@ -11,7 +11,25 @@ class PhysicalPositionMenu {
|
|||||||
{ value: 'downstream', label: '→ Downstream' , icon: '→' }
|
{ value: 'downstream', label: '→ Downstream' , icon: '→' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
// Distance contexts for each position
|
||||||
|
distanceContexts: {
|
||||||
|
upstream: {
|
||||||
|
description: 'Distance from parent inlet',
|
||||||
|
placeholder: 'e.g., 2.5 (meters before parent)',
|
||||||
|
helpText: 'How far upstream from the parent equipment'
|
||||||
|
},
|
||||||
|
downstream: {
|
||||||
|
description: 'Distance from parent outlet',
|
||||||
|
placeholder: 'e.g., 3.0 (meters after parent)',
|
||||||
|
helpText: 'How far downstream from the parent equipment'
|
||||||
|
},
|
||||||
|
atEquipment: {
|
||||||
|
description: 'Distance from parent start',
|
||||||
|
placeholder: 'e.g., 1.2 (meters from start)',
|
||||||
|
helpText: 'Position within the parent equipment boundaries'
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,6 +44,24 @@ class PhysicalPositionMenu {
|
|||||||
<!-- optgroups will be injected -->
|
<!-- optgroups will be injected -->
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Distance section -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label> </label>
|
||||||
|
<input type="checkbox" id="node-input-hasDistance" style="display:inline-block; width:auto; margin-right:5px;">
|
||||||
|
<label for="node-input-hasDistance" style="width:auto;">Specify 1D Distance</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="distance-section" class="form-row" style="display:none;">
|
||||||
|
<label for="node-input-distance"><i class="fa fa-ruler"></i>Distance</label>
|
||||||
|
<div style="display:flex; align-items:center; width:70%;">
|
||||||
|
<input type="number" id="node-input-distance" step="0.1" min="0" style="width:60%;" placeholder="0.0">
|
||||||
|
<span style="margin-left:5px; margin-right:5px;">meters</span>
|
||||||
|
</div>
|
||||||
|
<div id="distance-help" class="form-tips" style="margin-left:105px; font-size:11px; color:#666;">
|
||||||
|
Select a position to see distance context
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -52,7 +88,12 @@ class PhysicalPositionMenu {
|
|||||||
window.EVOLV.nodes.${nodeName}.positionMenu.loadData = function(node) {
|
window.EVOLV.nodes.${nodeName}.positionMenu.loadData = function(node) {
|
||||||
const data = window.EVOLV.nodes.${nodeName}.menuData.position;
|
const data = window.EVOLV.nodes.${nodeName}.menuData.position;
|
||||||
const sel = document.getElementById('node-input-positionVsParent');
|
const sel = document.getElementById('node-input-positionVsParent');
|
||||||
if (!sel) return;
|
const hasDistanceCheck = document.getElementById('node-input-hasDistance');
|
||||||
|
const distanceInput = document.getElementById('node-input-distance');
|
||||||
|
const distanceSection = document.getElementById('distance-section');
|
||||||
|
|
||||||
|
//Load position options
|
||||||
|
if (sel) {
|
||||||
sel.innerHTML = '';
|
sel.innerHTML = '';
|
||||||
(data.positionGroups||[]).forEach(grp => {
|
(data.positionGroups||[]).forEach(grp => {
|
||||||
const optg = document.createElement('optgroup');
|
const optg = document.createElement('optgroup');
|
||||||
@@ -66,8 +107,21 @@ class PhysicalPositionMenu {
|
|||||||
});
|
});
|
||||||
sel.appendChild(optg);
|
sel.appendChild(optg);
|
||||||
});
|
});
|
||||||
// default to “atEquipment” if not set
|
|
||||||
sel.value = node.positionVsParent || 'atEquipment';
|
sel.value = node.positionVsParent || 'atEquipment';
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load distance values
|
||||||
|
if (hasDistanceCheck) {
|
||||||
|
hasDistanceCheck.checked = node.hasDistance || false;
|
||||||
|
distanceSection.style.display = hasDistanceCheck.checked ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distanceInput) {
|
||||||
|
distanceInput.value = node.distance || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update distance context for current position
|
||||||
|
this.updateDistanceContext(node.positionVsParent || 'atEquipment', data.distanceContexts);
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -77,24 +131,86 @@ class PhysicalPositionMenu {
|
|||||||
return `
|
return `
|
||||||
// PhysicalPosition events for ${nodeName}
|
// PhysicalPosition events for ${nodeName}
|
||||||
window.EVOLV.nodes.${nodeName}.positionMenu.wireEvents = function(node) {
|
window.EVOLV.nodes.${nodeName}.positionMenu.wireEvents = function(node) {
|
||||||
// no dynamic behavior
|
const positionSel = document.getElementById('node-input-positionVsParent');
|
||||||
|
const hasDistanceCheck = document.getElementById('node-input-hasDistance');
|
||||||
|
const distanceSection = document.getElementById('distance-section');
|
||||||
|
const data = window.EVOLV.nodes.${nodeName}.menuData.position;
|
||||||
|
|
||||||
|
// Toggle distance section visibility
|
||||||
|
if (hasDistanceCheck && distanceSection) {
|
||||||
|
hasDistanceCheck.addEventListener('change', function() {
|
||||||
|
distanceSection.style.display = this.checked ? 'block' : 'none';
|
||||||
|
|
||||||
|
// Clear distance if unchecked
|
||||||
|
if (!this.checked) {
|
||||||
|
const distanceInput = document.getElementById('node-input-distance');
|
||||||
|
if (distanceInput) {
|
||||||
|
distanceInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update distance context when position changes
|
||||||
|
if (positionSel) {
|
||||||
|
positionSel.addEventListener('change', function() {
|
||||||
|
const position = this.value;
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu.updateDistanceContext(position, data.distanceContexts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to update distance context
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu.updateDistanceContext = function(position, contexts) {
|
||||||
|
const distanceInput = document.getElementById('node-input-distance');
|
||||||
|
const distanceHelp = document.getElementById('distance-help');
|
||||||
|
|
||||||
|
const context = contexts && contexts[position];
|
||||||
|
|
||||||
|
if (context && distanceInput && distanceHelp) {
|
||||||
|
distanceInput.placeholder = context.placeholder || '0.0';
|
||||||
|
distanceHelp.textContent = context.helpText || 'Enter distance in meters';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6) Save-logic injector
|
// 6) Save-logic injector
|
||||||
getSaveInjectionCode(nodeName) {
|
getSaveInjectionCode(nodeName) {
|
||||||
return `
|
return `
|
||||||
// PhysicalPosition Save injection for ${nodeName}
|
// PhysicalPosition Save injection for ${nodeName}
|
||||||
window.EVOLV.nodes.${nodeName}.positionMenu.saveEditor = function(node) {
|
window.EVOLV.nodes.${nodeName}.positionMenu.saveEditor = function(node) {
|
||||||
const sel = document.getElementById('node-input-positionVsParent');
|
const sel = document.getElementById('node-input-positionVsParent');
|
||||||
node.positionVsParent = sel? sel.value : 'atEquipment';
|
const hasDistanceCheck = document.getElementById('node-input-hasDistance');
|
||||||
node.positionLabel = sel? sel.options[sel.selectedIndex].textContent : 'At Equipment';
|
const distanceInput = document.getElementById('node-input-distance');
|
||||||
node.positionIcon = sel? sel.options[sel.selectedIndex].getAttribute('data-icon') : 'fa fa-cog';
|
|
||||||
|
// Save existing position data
|
||||||
|
node.positionVsParent = sel ? sel.value : 'atEquipment';
|
||||||
|
node.positionLabel = sel ? sel.options[sel.selectedIndex].textContent : 'At Equipment';
|
||||||
|
node.positionIcon = sel ? sel.options[sel.selectedIndex].getAttribute('data-icon') : 'fa fa-cog';
|
||||||
|
|
||||||
|
// Save distance data (NEW)
|
||||||
|
node.hasDistance = hasDistanceCheck ? hasDistanceCheck.checked : false;
|
||||||
|
|
||||||
|
if (node.hasDistance && distanceInput && distanceInput.value) {
|
||||||
|
node.distance = parseFloat(distanceInput.value) || 0;
|
||||||
|
node.distanceUnit = 'm'; // Fixed to meters for now
|
||||||
|
|
||||||
|
// Generate distance description based on position
|
||||||
|
const contexts = window.EVOLV.nodes.${nodeName}.menuData.position.distanceContexts;
|
||||||
|
const context = contexts && contexts[node.positionVsParent];
|
||||||
|
node.distanceDescription = context ? context.description : 'Distance from parent';
|
||||||
|
} else {
|
||||||
|
// Clear distance data if not specified
|
||||||
|
delete node.distance;
|
||||||
|
delete node.distanceUnit;
|
||||||
|
delete node.distanceDescription;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7) Compose everything into one client bundle
|
// 7) Compose everything into one client bundle
|
||||||
getClientInitCode(nodeName) {
|
getClientInitCode(nodeName) {
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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