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; } } } module.exports = MenuUtils;