2025-06-12 17:04:02 +02:00
class MenuUtils {
2025-06-25 10:55:50 +02:00
2025-06-12 17:04:02 +02:00
initBasicToggles ( elements ) {
2025-06-10 12:36:39 +02:00
// Toggle visibility for log level
elements . logCheckbox . addEventListener ( "change" , function ( ) {
elements . rowLogLevel . style . display = this . checked ? "block" : "none" ;
} ) ;
elements . rowLogLevel . style . display = elements . logCheckbox . checked
? "block"
: "none" ;
}
// Define the initialize toggles function within scope
2025-06-12 17:04:02 +02:00
initMeasurementToggles ( elements ) {
2025-06-10 12:36:39 +02:00
// Toggle visibility for scaling inputs
elements . scalingCheckbox . addEventListener ( "change" , function ( ) {
elements . rowInputMin . style . display = this . checked ? "block" : "none" ;
elements . rowInputMax . style . display = this . checked ? "block" : "none" ;
} ) ;
// Set initial states
elements . rowInputMin . style . display = elements . scalingCheckbox . checked
? "block"
: "none" ;
elements . rowInputMax . style . display = elements . scalingCheckbox . checked
? "block"
: "none" ;
}
2025-06-12 17:04:02 +02:00
initTensionToggles ( elements , node ) {
2025-06-10 12:36:39 +02:00
const currentMethod = node . interpolationMethod ;
elements . rowTension . style . display =
currentMethod === "monotone_cubic_spline" ? "block" : "none" ;
console . log (
"Initial tension row display: " ,
elements . rowTension . style . display
) ;
elements . interpolationMethodInput . addEventListener ( "change" , function ( ) {
const selectedMethod = this . value ;
console . log ( ` Interpolation method changed: ${ selectedMethod } ` ) ;
node . interpolationMethod = selectedMethod ;
// Toggle visibility for tension input
elements . rowTension . style . display =
selectedMethod === "monotone_cubic_spline" ? "block" : "none" ;
console . log ( "Tension row display: " , elements . rowTension . style . display ) ;
} ) ;
}
// Define the smoothing methods population function within scope
2025-06-12 17:04:02 +02:00
populateSmoothingMethods ( configUrls , elements , node ) {
this . fetchData ( configUrls . cloud . config , configUrls . local . config )
2025-06-10 12:36:39 +02:00
. then ( ( configData ) => {
const smoothingMethods =
configData . smoothing ? . smoothMethod ? . rules ? . values ? . map (
( o ) => o . value
) || [ ] ;
2025-06-12 17:04:02 +02:00
this . populateDropdown (
2025-06-10 12:36:39 +02:00
elements . smoothMethod ,
smoothingMethods ,
node ,
"smooth_method"
) ;
} )
. catch ( ( err ) => {
console . error ( "Error loading smoothing methods" , err ) ;
} ) ;
}
2025-06-12 17:04:02 +02:00
populateInterpolationMethods ( configUrls , elements , node ) {
this . fetchData ( configUrls . cloud . config , configUrls . local . config )
2025-06-10 12:36:39 +02:00
. then ( ( configData ) => {
const interpolationMethods =
configData ? . interpolation ? . type ? . rules ? . values . map ( ( m ) => m . value ) ||
[ ] ;
2025-06-12 17:04:02 +02:00
this . populateDropdown (
2025-06-10 12:36:39 +02:00
elements . interpolationMethodInput ,
interpolationMethods ,
node ,
"interpolationMethod"
) ;
// Find the selected method and use it to spawn 1 more field to fill in tension
//const selectedMethod = interpolationMethods.find(m => m === node.interpolationMethod);
2025-06-12 17:04:02 +02:00
this . initTensionToggles ( elements , node ) ;
2025-06-10 12:36:39 +02:00
} )
. catch ( ( err ) => {
console . error ( "Error loading interpolation methods" , err ) ;
} ) ;
}
2025-06-12 17:04:02 +02:00
populateLogLevelOptions ( logLevelSelect , configData , node ) {
2025-06-10 12:36:39 +02:00
// debug log level
//console.log("Displaying configData => ", configData) ;
const logLevels =
configData ? . general ? . logging ? . logLevel ? . rules ? . values ? . map (
( l ) => l . value
) || [ ] ;
//console.log("Displaying logLevels => ", logLevels);
// Reuse your existing generic populateDropdown helper
2025-06-12 17:04:02 +02:00
this . populateDropdown ( logLevelSelect , logLevels , node . logLevel ) ;
2025-06-10 12:36:39 +02:00
}
//cascade dropdowns for asset type, supplier, subType, model, unit
2025-06-12 17:04:02 +02:00
fetchAndPopulateDropdowns ( configUrls , elements , node ) {
this . fetchData ( configUrls . cloud . config , configUrls . local . config )
2025-06-10 12:36:39 +02:00
. then ( ( configData ) => {
const assetType = configData . asset ? . type ? . default ;
2025-06-12 17:04:02 +02:00
const localSuppliersUrl = this . constructUrl ( configUrls . local . taggcodeAPI , ` ${ assetType } s ` , "suppliers.json" ) ;
const cloudSuppliersUrl = this . constructCloudURL ( configUrls . cloud . taggcodeAPI , "/vendor/get_vendors.php" ) ;
2025-06-10 12:36:39 +02:00
2025-06-12 17:04:02 +02:00
return this . fetchData ( cloudSuppliersUrl , localSuppliersUrl )
2025-06-10 12:36:39 +02:00
. then ( ( supplierData ) => {
const suppliers = supplierData . map ( ( supplier ) => supplier . name ) ;
// Populate suppliers dropdown and set up its change handler
2025-06-12 17:04:02 +02:00
return this . populateDropdown (
2025-06-10 12:36:39 +02:00
elements . supplier ,
suppliers ,
node ,
"supplier" ,
function ( selectedSupplier ) {
if ( selectedSupplier ) {
2025-06-12 17:04:02 +02:00
this . populateSubTypes ( configUrls , elements , node , selectedSupplier ) ;
2025-06-10 12:36:39 +02:00
}
}
) ;
} )
. then ( ( ) => {
// If we have a saved supplier, trigger subTypes population
if ( node . supplier ) {
2025-06-12 17:04:02 +02:00
this . populateSubTypes ( configUrls , elements , node , node . supplier ) ;
2025-06-10 12:36:39 +02:00
}
} ) ;
} )
. catch ( ( error ) => {
console . error ( "Error in initial dropdown population:" , error ) ;
} ) ;
}
2025-06-12 17:04:02 +02:00
getSpecificConfigUrl ( nodeName , cloudAPI ) {
2025-06-10 12:36:39 +02:00
const cloudConfigURL = cloudAPI + "/config/" + nodeName + ".json" ;
const localConfigURL = "http://localhost:1880/" + nodeName + "/dependencies/" + nodeName + "/" + nodeName + "Config.json" ;
return { cloudConfigURL , localConfigURL } ;
2025-06-12 17:04:02 +02:00
}
2025-06-10 12:36:39 +02:00
// Save changes to API
2025-06-12 17:04:02 +02:00
async apiCall ( node ) {
2025-06-10 12:36:39 +02:00
try {
// OLFIANT when a browser refreshes the tag code is lost!!! fix this later!!!!!
// FIX UUID ALSO LATER
2026-03-11 13:39:40 +01:00
if ( node . assetTagCode !== "" || node . assetTagCode !== null ) { /* intentionally empty */ }
2025-06-10 12:36:39 +02:00
// API call to register or check asset in central database
let assetregisterAPI = node . configUrls . cloud . taggcodeAPI + "/asset/create_asset.php" ;
const assetModelId = node . modelMetadata . id ; //asset_product_model_id
const uuid = node . uuid ; //asset_product_model_uuid
const assetName = node . assetType ; //asset_name / type?
const description = node . name ; // asset_description
const assetStatus = "actief" ; //asset_status -> koppel aan enable / disable node ? or make dropdown ?
const assetProfileId = 1 ; //asset_profile_id these are the rules to check if the childs are valid under this node (parent / child id?)
const child _assets = [ "63247" ] ; //child_assets tagnummer of id?
const assetProcessId = node . processId ; //asset_process_id
const assetLocationId = node . locationId ; //asset_location_id
const tagCode = node . assetTagCode ; // if already exists in the node information use it to tell the api it exists and it will update else we will get it from the api call
//console.log(`this is my tagCode: ${tagCode}`);
// Build base URL with required parameters
let apiUrl = ` ?asset_product_model_id= ${ assetModelId } &asset_product_model_uuid= ${ uuid } &asset_name= ${ assetName } &asset_description= ${ description } &asset_status= ${ assetStatus } &asset_profile_id= ${ assetProfileId } &asset_location_id= ${ assetLocationId } &asset_process_id= ${ assetProcessId } &child_assets= ${ child _assets } ` ;
// Only add tagCode to URL if it exists
if ( tagCode ) {
apiUrl += ` &asset_tag_number= ${ tagCode } ` ;
console . log ( 'hello there' ) ;
}
assetregisterAPI += apiUrl ;
console . log ( "API call to register asset in central database" , assetregisterAPI ) ;
const response = await fetch ( assetregisterAPI , {
method : "POST"
} ) ;
// Get the response text first
const responseText = await response . text ( ) ;
console . log ( "Raw API response:" , responseText ) ;
// Try to parse the JSON, handling potential parsing errors
let jsonResponse ;
try {
jsonResponse = JSON . parse ( responseText ) ;
} catch ( parseError ) {
console . error ( "JSON Parsing Error:" , parseError ) ;
console . error ( "Response that could not be parsed:" , responseText ) ;
throw new Error ( "Failed to parse API response" ) ;
}
console . log ( jsonResponse ) ;
if ( jsonResponse . success ) {
console . log ( ` ${ jsonResponse . message } , tag number: ${ jsonResponse . asset _tag _number } , asset id: ${ jsonResponse . asset _id } ` ) ;
// Save the asset tag number and id to the node
} else {
console . log ( "Asset not registered in central database" ) ;
}
return jsonResponse ;
} catch ( error ) {
console . log ( "Error saving changes to asset register API" , error ) ;
}
}
2025-06-12 17:04:02 +02:00
async fetchData ( url , fallbackUrl ) {
2025-06-10 12:36:39 +02:00
try {
const response = await fetch ( url ) ;
if ( ! response . ok ) throw new Error ( ` HTTP error! status: ${ response . status } ` ) ;
const responsData = await response . json ( ) ;
//responsData
const data = responsData . data ;
/ * . m a p ( i t e m = > {
const { vendor _name , ... rest } = item ;
return {
name : vendor _name ,
... rest
} ;
} ) ; * /
console . log ( url ) ;
console . log ( "Response Data: " , data ) ;
return data ;
} catch ( err ) {
console . warn (
` Primary URL failed: ${ url } . Trying fallback URL: ${ fallbackUrl } ` ,
err
) ;
try {
const response = await fetch ( fallbackUrl ) ;
if ( ! response . ok )
throw new Error ( ` HTTP error! status: ${ response . status } ` ) ;
return await response . json ( ) ;
} catch ( fallbackErr ) {
console . error ( "Both primary and fallback URLs failed:" , fallbackErr ) ;
return [ ] ;
}
}
}
2025-06-12 17:04:02 +02:00
async fetchProjectData ( url ) {
2025-06-10 12:36:39 +02:00
try {
const response = await fetch ( url ) ;
if ( ! response . ok ) throw new Error ( ` HTTP error! status: ${ response . status } ` ) ;
const responsData = await response . json ( ) ;
console . log ( "Response Data: " , responsData ) ;
return responsData ;
} catch ( err ) {
2026-03-11 13:39:40 +01:00
/* intentionally empty */
2025-06-10 12:36:39 +02:00
}
}
2025-06-12 17:04:02 +02:00
async populateDropdown (
2025-06-10 12:36:39 +02:00
htmlElement ,
options ,
node ,
property ,
callback
) {
2025-06-12 17:04:02 +02:00
this . generateHtml ( htmlElement , options , node [ property ] ) ;
2025-06-10 12:36:39 +02:00
htmlElement . addEventListener ( "change" , async ( e ) => {
const newValue = e . target . value ;
console . log ( ` Dropdown changed: ${ property } = ${ newValue } ` ) ;
node [ property ] = newValue ;
RED . nodes . dirty ( true ) ;
if ( callback ) await callback ( newValue ) ; // Ensure async callback completion
} ) ;
}
// Helper function to construct a URL from a base and path internal
2025-06-12 17:04:02 +02:00
constructUrl ( base , ... paths ) {
2025-06-10 12:36:39 +02:00
// Remove trailing slash from base and leading slashes from paths
const sanitizedBase = ( base || "" ) . replace ( /\/+$/ , "" ) ;
const sanitizedPaths = paths . map ( ( path ) => path . replace ( /^\/+|\/+$/g , "" ) ) ;
// Join sanitized base and paths
const url = ` ${ sanitizedBase } / ${ sanitizedPaths . join ( "/" ) } ` ;
console . log ( "Base:" , sanitizedBase ) ;
console . log ( "Paths:" , sanitizedPaths ) ;
console . log ( "Constructed URL:" , url ) ;
return url ;
}
//Adjust for API Gateway
2025-06-12 17:04:02 +02:00
constructCloudURL ( base , ... paths ) {
2025-06-10 12:36:39 +02:00
// Remove trailing slash from base and leading slashes from paths
const sanitizedBase = base . replace ( /\/+$/ , "" ) ;
const sanitizedPaths = paths . map ( ( path ) => path . replace ( /^\/+|\/+$/g , "" ) ) ;
// Join sanitized base and paths
const url = ` ${ sanitizedBase } / ${ sanitizedPaths . join ( "/" ) } ` ;
return url ;
}
2025-06-12 17:04:02 +02:00
populateSubTypes ( configUrls , elements , node , selectedSupplier ) {
2025-06-10 12:36:39 +02:00
2025-06-12 17:04:02 +02:00
this . fetchData ( configUrls . cloud . config , configUrls . local . config )
2025-06-10 12:36:39 +02:00
. then ( ( configData ) => {
const assetType = configData . asset ? . type ? . default ;
2025-06-12 17:04:02 +02:00
const supplierFolder = this . constructUrl ( configUrls . local . taggcodeAPI , ` ${ assetType } s ` , selectedSupplier ) ;
2025-06-10 12:36:39 +02:00
2025-06-12 17:04:02 +02:00
const localSubTypesUrl = this . constructUrl ( supplierFolder , "subtypes.json" ) ;
const cloudSubTypesUrl = this . constructCloudURL ( configUrls . cloud . taggcodeAPI , "/product/get_subtypesFromVendor.php?vendor_name=" + selectedSupplier ) ;
2025-06-10 12:36:39 +02:00
2025-06-12 17:04:02 +02:00
return this . fetchData ( cloudSubTypesUrl , localSubTypesUrl )
2025-06-10 12:36:39 +02:00
. then ( ( subTypeData ) => {
const subTypes = subTypeData . map ( ( subType ) => subType . name ) ;
2025-06-12 17:04:02 +02:00
return this . populateDropdown (
2025-06-10 12:36:39 +02:00
elements . subType ,
subTypes ,
node ,
"subType" ,
function ( selectedSubType ) {
if ( selectedSubType ) {
// When subType changes, update both models and units
2025-06-12 17:04:02 +02:00
this . populateModels (
2025-06-10 12:36:39 +02:00
configUrls ,
elements ,
node ,
selectedSupplier ,
selectedSubType
) ;
2025-06-12 17:04:02 +02:00
this . populateUnitsForSubType (
2025-06-10 12:36:39 +02:00
configUrls ,
elements ,
node ,
selectedSubType
) ;
}
}
) ;
} )
. then ( ( ) => {
// If we have a saved subType, trigger both models and units population
if ( node . subType ) {
2025-06-12 17:04:02 +02:00
this . populateModels (
2025-06-10 12:36:39 +02:00
configUrls ,
elements ,
node ,
selectedSupplier ,
node . subType
) ;
2025-06-12 17:04:02 +02:00
this . populateUnitsForSubType ( configUrls , elements , node , node . subType ) ;
2025-06-10 12:36:39 +02:00
}
//console.log("In fetch part of subtypes ");
// Store all data from selected model
/ * n o d e [ " m o d e l M e t a d a t a " ] = m o d e l D a t a . f i n d (
( model ) => model . name === node . model
) ;
console . log ( "Model Metadata: " , node [ "modelMetadata" ] ) ; * /
} ) ;
} )
. catch ( ( error ) => {
console . error ( "Error populating subtypes:" , error ) ;
} ) ;
}
2025-06-12 17:04:02 +02:00
populateUnitsForSubType ( configUrls , elements , node , selectedSubType ) {
2025-06-10 12:36:39 +02:00
// Fetch the units data
2025-06-12 17:04:02 +02:00
this . fetchData ( configUrls . cloud . units , configUrls . local . units )
2025-06-10 12:36:39 +02:00
. then ( ( unitsData ) => {
// Find the category that matches the subType name
const categoryData = unitsData . units . find (
( category ) =>
category . category . toLowerCase ( ) === selectedSubType . toLowerCase ( )
) ;
if ( categoryData ) {
// Extract just the unit values and descriptions
const units = categoryData . values . map ( ( unit ) => ( {
value : unit . value ,
description : unit . description ,
} ) ) ;
// Create the options array with descriptions as labels
const options = units . map ( ( unit ) => ( {
value : unit . value ,
label : ` ${ unit . value } - ${ unit . description } ` ,
} ) ) ;
// Populate the units dropdown
2025-06-12 17:04:02 +02:00
this . populateDropdown (
2025-06-10 12:36:39 +02:00
elements . unit ,
options . map ( ( opt ) => opt . value ) ,
node ,
"unit"
) ;
// If there's no currently selected unit but we have options, select the first one
if ( ! node . unit && options . length > 0 ) {
node . unit = options [ 0 ] . value ;
elements . unit . value = options [ 0 ] . value ;
}
} else {
// If no matching category is found, provide a default % option
const defaultUnits = [ { value : "%" , description : "Percentage" } ] ;
2025-06-12 17:04:02 +02:00
this . populateDropdown (
2025-06-10 12:36:39 +02:00
elements . unit ,
defaultUnits . map ( ( unit ) => unit . value ) ,
node ,
"unit"
) ;
console . warn (
` No matching unit category found for subType: ${ selectedSubType } `
) ;
}
} )
. catch ( ( error ) => {
console . error ( "Error fetching units:" , error ) ;
} ) ;
}
2025-06-12 17:04:02 +02:00
populateModels (
2025-06-10 12:36:39 +02:00
configUrls ,
elements ,
node ,
selectedSupplier ,
selectedSubType
) {
2025-06-12 17:04:02 +02:00
this . fetchData ( configUrls . cloud . config , configUrls . local . config )
2025-06-10 12:36:39 +02:00
. then ( ( configData ) => {
const assetType = configData . asset ? . type ? . default ;
// save assetType to fetch later
node . assetType = assetType ;
2025-06-12 17:04:02 +02:00
const supplierFolder = this . constructUrl ( configUrls . local . taggcodeAPI , ` ${ assetType } s ` , selectedSupplier ) ;
const subTypeFolder = this . constructUrl ( supplierFolder , selectedSubType ) ;
const localModelsUrl = this . constructUrl ( subTypeFolder , "models.json" ) ;
const cloudModelsUrl = this . constructCloudURL ( configUrls . cloud . taggcodeAPI , "/product/get_product_models.php?vendor_name=" + selectedSupplier + "&product_subtype_name=" + selectedSubType ) ;
2025-06-10 12:36:39 +02:00
2025-06-12 17:04:02 +02:00
return this . fetchData ( cloudModelsUrl , localModelsUrl ) . then ( ( modelData ) => {
2025-06-10 12:36:39 +02:00
const models = modelData . map ( ( model ) => model . name ) ; // use this to populate the dropdown
// If a model is already selected, store its metadata immediately
if ( node . model ) {
node [ "modelMetadata" ] = modelData . find ( ( model ) => model . name === node . model ) ;
}
2025-06-12 17:04:02 +02:00
this . populateDropdown ( elements . model , models , node , "model" , ( selectedModel ) => {
2025-06-10 12:36:39 +02:00
// Store only the metadata for the selected model
node [ "modelMetadata" ] = modelData . find ( ( model ) => model . name === selectedModel ) ;
} ) ;
/ *
console . log ( 'hello here I am:' ) ;
console . log ( node [ "modelMetadata" ] ) ;
* /
} ) ;
} )
. catch ( ( error ) => {
console . error ( "Error populating models:" , error ) ;
} ) ;
}
2025-06-12 17:04:02 +02:00
generateHtml ( htmlElement , options , savedValue ) {
2025-06-10 12:36:39 +02:00
htmlElement . innerHTML = options . length
? ` <option value="">Select...</option> ${ options
. map ( ( opt ) => ` <option value=" ${ opt } "> ${ opt } </option> ` )
. join ( "" ) } `
: "<option value=''>No options available</option>" ;
if ( savedValue && options . includes ( savedValue ) ) {
htmlElement . value = savedValue ;
}
}
2025-06-12 17:04:02 +02:00
createMenuUtilsEndpoint ( RED , nodeName , customHelpers = { } ) {
RED . httpAdmin . get ( ` / ${ nodeName } /resources/menuUtils.js ` , function ( req , res ) {
console . log ( ` Serving menuUtils.js for ${ nodeName } node ` ) ;
res . set ( 'Content-Type' , 'application/javascript' ) ;
const browserCode = this . generateMenuUtilsCode ( nodeName , customHelpers ) ;
res . send ( browserCode ) ;
} . bind ( this ) ) ;
}
generateMenuUtilsCode ( nodeName , customHelpers = { } ) {
const defaultHelpers = {
validateRequired : ` function(value) {
return value && value . toString ( ) . trim ( ) !== '' ;
} ` ,
formatDisplayValue : ` function(value, unit) {
return \ ` \$ {value} \$ {unit || ''} \` .trim();
} `
} ;
const allHelpers = { ... defaultHelpers , ... customHelpers } ;
const helpersCode = Object . entries ( allHelpers )
. map ( ( [ name , func ] ) => ` ${ name } : ${ func } ` )
. join ( ',\n' ) ;
const classCode = MenuUtils . toString ( ) ; // <-- this gives full class MenuUtils {...}
return `
// Create EVOLV namespace structure
window . EVOLV = window . EVOLV || { } ;
window . EVOLV . nodes = window . EVOLV . nodes || { } ;
window . EVOLV . nodes . $ { nodeName } = window . EVOLV . nodes . $ { nodeName } || { } ;
// Inject MenuUtils class
$ { classCode }
// Expose MenuUtils instance to namespace
window . EVOLV . nodes . $ { nodeName } . utils = {
menuUtils : new MenuUtils ( ) ,
helpers : {
$ { helpersCode }
}
} ;
// Optionally expose globally
window . MenuUtils = MenuUtils ;
console . log ( '${nodeName} utilities loaded in namespace' ) ;
` ;
}
}
module . exports = MenuUtils ;