brobada / CPQ Config Validator

// ==UserScript==
// @name         CPQ Config Validator
// @version      0.005
// @description  Validates CPQ configurations in quotes or favorites
// @author       Obada Kadri
// @match        *://*.bigmachines.com/*
// @grant        GM_addStyle
// @license      MIT
// @require      https://code.jquery.com/jquery-3.4.1.min.js
// ==/UserScript==

// @require      https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js

(async function() {
    'use strict';
    const pages = ["commerce_list.jsp", "document.jsp", "model_configs.jsp"];
    let configModels = {};

    const isConfigValid = configState => {
        return configState.configData._state.actions._addToTxn.visible ||
            configState.configData._state.actions._save.visible;
    }

    const saveConfiguration = async (e, callSettings) => {
        if ($(e.currentTarget).hasClass("saved")) return false;
        $(e.currentTarget).text("Saving...");
        const configState = await $.ajax(callSettings)
        .catch(err => alert(`Status: ${err.status} - ${err.statusText}\nResponse: ${err.responseText}`));
        $(e.currentTarget).text("Valid (Saved)");
        $(e.currentTarget).removeClass("unsaved");
        $(e.currentTarget).addClass("saved");
    };

    const doValidation = async (e, modelInfo, callSettings) => {
        if ($(e.currentTarget).hasClass("invalid")) return false;

        $(e.currentTarget).text("Validating...");
        $(e.currentTarget).removeClass("valid invalid unsaved");
        $(e.currentTarget).addClass("in-progress");

        const configState = await $.ajax(callSettings)
        .catch(err => alert(`Status: ${err.status} - ${err.statusText}\nResponse: ${err.responseText}`));
        console.log(configState);
        $(e.currentTarget).removeClass("in-progress");

        if (configState.configData.searchMessages_HWSW && configState.configData.searchMessages_HWSW.length > 50) {
            var modalHtml = `
    <!-- Modal -->
    <div class="modal fade" id="configHealthCheck" role="dialog">
      <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">${modelInfo.config_name} Messages</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <div id="config-hc-body">${configState.configData.searchMessages_HWSW}</div>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
          </div>
        </div>
      </div>
    </div>
  `;

            $("body").append(modalHtml);
            $('#configHealthCheck').modal('show');
        }

        if (isConfigValid(configState)) {
            $(e.currentTarget).text("Valid"); // Valid (unsaved)
            $(e.currentTarget).addClass("valid"); // valid unsaved
            $(e.currentTarget).off("click");
            //const saveAction = configState.configData._state.actions._save.visible ? "_save" : "_addToTxn";
            $(e.currentTarget).click(saveEvent => {
                saveEvent.preventDefault();
                /*const saveCallSettings = {
                    "url": `/rest/v8/config${modelInfo.pf_segment}.${modelInfo.product_line}.${modelInfo.configuration}/actions/${saveAction}`,
                    "method": "POST",
                    "headers": {
                        "Content-Type": "application/json",
                    },
                    "data": JSON.stringify({"cacheInstanceId":configState.cacheInstanceId}),
                };
                saveConfiguration(saveEvent, saveCallSettings);*/
                $('#configHealthCheck').modal('show');
            });
        } else {
            $(e.currentTarget).text("Invalid");
            $(e.currentTarget).addClass("invalid");
            $(e.currentTarget).click(clickEvent => {
                clickEvent.preventDefault();
                $('#configHealthCheck').modal('show');
            });
        }
    };

    const setConfigModels = async () => {
        console.info("CPQ Config Validator - Fetching configModelInfo");
        // Populate the configModels from configModelInfo datatable
        const modelCallSettings = {
            "url": "/rest/v8/customConfigModelInfo",
            "method": "GET",
            "headers": {
                "Content-Type": "application/json",
            }
        };
        configModels = await $.ajax(modelCallSettings);
        console.log(configModels);
    }

    // Only quote and favorites pages
    if (pages.indexOf(document.URL.split("/").pop().split("?").shift()) >= 0) {
        console.info("CPQ Config Validator");
        await setConfigModels();

        $("head").append(
            '<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">'
        );

        // Add validate button next to favorite configs
        $("input[name=ids]:not(:disabled)").each((i, e) => {
            const modelTd = $(e).closest("td");
            const validateBtn = $(`<button class="validate-btn" data-fav-id="${$(e).val()}">Validate</button>`);
            modelTd.append(validateBtn);

            $("button.validate-btn").click(e => {
                e.preventDefault();
                // Scrape the model name from the 5th column
                const favId = $(e.currentTarget).data("favId");
                const modelName = $(e.currentTarget).closest("tr").find("td:nth-child(5)").text().split('/').slice(1).join('/').trim();
                const modelInfo = configModels.items.find(model => model.config_name.replace(/\s/g, " ") === modelName.replace(/\s/g, " "));

                const favCallSettings = {
                    "url": `/rest/v8/config${modelInfo.pf_segment}.${modelInfo.product_line}.${modelInfo.configuration}/actions/_reconfigureFav`,
                    "method": "POST",
                    "headers": {
                        "Content-Type": "application/json"
                    },
                    "data": JSON.stringify({"favoriteId":favId,"criteria":{"state":true}}),
                };

                doValidation(e, modelInfo, favCallSettings);
            });
        });


        // Quote configs
        const modelPnTds = $("#line-item-grid tr.parent-line-item td[id*=__part_number]");

        modelPnTds.each((i, modelTd) => {
            const validateBtn = $(`<button class="validate-btn">Validate</button>`);
            // Scrape the model name from the Item column
            const modelTr = $(modelTd).closest("tr");
            const modelName = modelTr.find("td[id*=_displayedItemName_l]").text();
            const modelInfo = configModels.items.find(model => model.config_name.replace(/\s/g, " ") === modelName.replace(/\s/g, " "));

            const bsid = parseInt($("[name=id]").val());
            const documentNumber = parseInt(modelTr.attr("documentNumber"));
            const documentId = parseInt(modelTr.attr("documentId"));

            if (modelInfo) {
                console.info(`Adding validate button for ${modelName}`);
                $(modelTd).append(validateBtn);

                $("button.validate-btn").click(e => {
                    e.preventDefault();

                    const quoteCallSettings = {
                        "url": `/rest/v8/config${modelInfo.pf_segment}.${modelInfo.product_line}.${modelInfo.configuration}/actions/_reconfigureTxn`,
                        "method": "POST",
                        "headers": {
                            "Content-Type": "application/json"
                        },
                        "data": JSON.stringify({"bsId":bsid,"documentNumber":documentNumber,"documentId":documentId,"legacyMode":true,"mainDocAction":true,"criteria":{"state":true}}),
                    };

                    doValidation(e, modelInfo, quoteCallSettings);
                });
            }
        });

    }

})();

// Styling Stuff
var bm_css_src = `
button.validate-btn {
  width: 8em;
  background-color: #fff;
  border: 1px solid #08c;
  box-shadow: 1px 1px 4px 0px rgba(0, 0, 0, .25);
  color: #08c;
  opacity: 1;
  white-space: nowrap;
  cursor: pointer;
}
button.validate-btn.in-progress {
  border-color: #ffbf00;
  color: #ffbf00
}
button.validate-btn.valid {
  border-color: #008900;
  color: #008900;
}
button.validate-btn.unsaved {
  border-color: #ffbf00;
}
button.validate-btn.invalid {
  border-color: #a00000;
  color: #a00000;
}
ul.ok-tool-bar {
  position: fixed;
  bottom: 0;
  right: 10%;
  margin: 0;
}
ul.ok-tool-bar li {
  display: inline-block;
  padding: 3px 6px;
  margin: 0 3px;
  font-size: 11px;
  white-space: nowrap;
  vertical-align: middle;
  -ms-touch-action: manipulation;
  cursor: pointer;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  background-image: none;
  border: 1px solid transparent;
  border-radius: 5px 5px 0 0;
}
li.btn-config-hc {
  color: #fff;
  background-color: #660000;
  border-color: #660000;
}

`;

// eslint-disable-next-line no-undef
GM_addStyle(bm_css_src);