diff --git a/src/helper/menuUtils_DEPRECATED.js b/src/helper/menuUtils_DEPRECATED.js new file mode 100644 index 0000000..ebed6b8 --- /dev/null +++ b/src/helper/menuUtils_DEPRECATED.js @@ -0,0 +1,543 @@ +class MenuUtils { + + +initBasicToggles(elements) { + // 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 +initMeasurementToggles(elements) { + // 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"; +} + +initTensionToggles(elements, node) { + 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 +populateSmoothingMethods(configUrls, elements, node) { + this.fetchData(configUrls.cloud.config, configUrls.local.config) + .then((configData) => { + const smoothingMethods = + configData.smoothing?.smoothMethod?.rules?.values?.map( + (o) => o.value + ) || []; + this.populateDropdown( + elements.smoothMethod, + smoothingMethods, + node, + "smooth_method" + ); + }) + .catch((err) => { + console.error("Error loading smoothing methods", err); + }); +} + +populateInterpolationMethods(configUrls, elements, node) { + this.fetchData(configUrls.cloud.config, configUrls.local.config) + .then((configData) => { + const interpolationMethods = + configData?.interpolation?.type?.rules?.values.map((m) => m.value) || + []; + this.populateDropdown( + 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); + this.initTensionToggles(elements, node); + }) + .catch((err) => { + console.error("Error loading interpolation methods", err); + }); +} + +populateLogLevelOptions(logLevelSelect, configData, node) { + // 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 + this.populateDropdown(logLevelSelect, logLevels, node.logLevel); +} + +//cascade dropdowns for asset type, supplier, subType, model, unit +fetchAndPopulateDropdowns(configUrls, elements, node) { + this.fetchData(configUrls.cloud.config, configUrls.local.config) + .then((configData) => { + const assetType = configData.asset?.type?.default; + const localSuppliersUrl = this.constructUrl(configUrls.local.taggcodeAPI,`${assetType}s`,"suppliers.json"); + const cloudSuppliersUrl = this.constructCloudURL(configUrls.cloud.taggcodeAPI, "/vendor/get_vendors.php"); + + return this.fetchData(cloudSuppliersUrl, localSuppliersUrl) + .then((supplierData) => { + + const suppliers = supplierData.map((supplier) => supplier.name); + + // Populate suppliers dropdown and set up its change handler + return this.populateDropdown( + elements.supplier, + suppliers, + node, + "supplier", + function (selectedSupplier) { + if (selectedSupplier) { + this.populateSubTypes(configUrls, elements, node, selectedSupplier); + } + } + ); + }) + .then(() => { + // If we have a saved supplier, trigger subTypes population + if (node.supplier) { + this.populateSubTypes(configUrls, elements, node, node.supplier); + } + }); + }) + .catch((error) => { + console.error("Error in initial dropdown population:", error); + }); +} + +getSpecificConfigUrl(nodeName,cloudAPI) { + + const cloudConfigURL = cloudAPI + "/config/" + nodeName + ".json"; + const localConfigURL = "http://localhost:1880/"+ nodeName + "/dependencies/"+ nodeName + "/" + nodeName + "Config.json"; + + return { cloudConfigURL, localConfigURL }; + +} + +// Save changes to API +async apiCall(node) { + try{ + // OLFIANT when a browser refreshes the tag code is lost!!! fix this later!!!!! + // FIX UUID ALSO LATER + + if(node.assetTagCode !== "" || node.assetTagCode !== null){ } + // 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); + } +} + + +async fetchData(url, fallbackUrl) { + 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; + /* .map(item => { + 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 []; + } + } +} + +async fetchProjectData(url) { + 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) { + } +} + +async populateDropdown( + htmlElement, + options, + node, + property, + callback +) { + this.generateHtml(htmlElement, options, node[property]); + + 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 +constructUrl(base, ...paths) { + + // 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 +constructCloudURL(base, ...paths) { + // 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; +} + +populateSubTypes(configUrls, elements, node, selectedSupplier) { + + this.fetchData(configUrls.cloud.config, configUrls.local.config) + .then((configData) => { + const assetType = configData.asset?.type?.default; + const supplierFolder = this.constructUrl( configUrls.local.taggcodeAPI, `${assetType}s`, selectedSupplier ); + + const localSubTypesUrl = this.constructUrl(supplierFolder, "subtypes.json"); + const cloudSubTypesUrl = this.constructCloudURL(configUrls.cloud.taggcodeAPI, "/product/get_subtypesFromVendor.php?vendor_name=" + selectedSupplier); + + return this.fetchData(cloudSubTypesUrl, localSubTypesUrl) + .then((subTypeData) => { + const subTypes = subTypeData.map((subType) => subType.name); + + return this.populateDropdown( + elements.subType, + subTypes, + node, + "subType", + function (selectedSubType) { + if (selectedSubType) { + // When subType changes, update both models and units + this.populateModels( + configUrls, + elements, + node, + selectedSupplier, + selectedSubType + ); + this.populateUnitsForSubType( + configUrls, + elements, + node, + selectedSubType + ); + } + } + ); + }) + .then(() => { + // If we have a saved subType, trigger both models and units population + if (node.subType) { + this.populateModels( + configUrls, + elements, + node, + selectedSupplier, + node.subType + ); + this.populateUnitsForSubType(configUrls, elements, node, node.subType); + } + //console.log("In fetch part of subtypes "); + // Store all data from selected model +/* node["modelMetadata"] = modelData.find( + (model) => model.name === node.model + ); + console.log("Model Metadata: ", node["modelMetadata"]); */ + }); + }) + .catch((error) => { + console.error("Error populating subtypes:", error); + }); +} + +populateUnitsForSubType(configUrls, elements, node, selectedSubType) { + // Fetch the units data + this.fetchData(configUrls.cloud.units, configUrls.local.units) + .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 + this.populateDropdown( + 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" }]; + this.populateDropdown( + 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); + }); +} + +populateModels( + configUrls, + elements, + node, + selectedSupplier, + selectedSubType +) { + + this.fetchData(configUrls.cloud.config, configUrls.local.config) + .then((configData) => { + const assetType = configData.asset?.type?.default; + // save assetType to fetch later + node.assetType = assetType; + + 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); + + return this.fetchData(cloudModelsUrl, localModelsUrl).then((modelData) => { + 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); + } + + this.populateDropdown(elements.model, models, node, "model", (selectedModel) => { + // 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); + }); +} + +generateHtml(htmlElement, options, savedValue) { + htmlElement.innerHTML = options.length + ? `${options + .map((opt) => ``) + .join("")}` + : ""; + + if (savedValue && options.includes(savedValue)) { + htmlElement.value = savedValue; + } +} + +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; \ No newline at end of file diff --git a/src/menu/physicalPosition.js b/src/menu/physicalPosition.js index fa1cab8..82b8a6f 100644 --- a/src/menu/physicalPosition.js +++ b/src/menu/physicalPosition.js @@ -21,8 +21,8 @@ class PhysicalPositionMenu {

Physical Position vs parent

- -
@@ -51,7 +51,7 @@ class PhysicalPositionMenu { // PhysicalPosition data loader for ${nodeName} window.EVOLV.nodes.${nodeName}.positionMenu.loadData = function(node) { const data = window.EVOLV.nodes.${nodeName}.menuData.position; - const sel = document.getElementById('node-input-physicalAspect'); + const sel = document.getElementById('node-input-positionVsParent'); if (!sel) return; sel.innerHTML = ''; (data.positionGroups||[]).forEach(grp => { @@ -66,7 +66,7 @@ class PhysicalPositionMenu { sel.appendChild(optg); }); // default to “atEquipment” if not set - sel.value = node.physicalAspect || 'atEquipment'; + sel.value = node.positionVsParent || 'atEquipment'; }; `; } @@ -86,8 +86,8 @@ class PhysicalPositionMenu { return ` // PhysicalPosition Save injection for ${nodeName} window.EVOLV.nodes.${nodeName}.positionMenu.saveEditor = function(node) { - const sel = document.getElementById('node-input-physicalAspect'); - node.physicalAspect = sel? sel.value : 'atEquipment'; + const sel = document.getElementById('node-input-positionVsParent'); + node.positionVsParent = sel? sel.value : 'atEquipment'; return true; }; `;