brobada / ATO Model Helper

// ==UserScript==
// @name         ATO Model Helper
// @version      0.20
// @description  Helper tool to assist in ATO models NPI and EOL process in CPQ
// @author       Obada Kadri
// @match       *://*.bigmachines.com/commerce/display_company_profile.jsp*
// @require     https://code.jquery.com/jquery-3.4.1.min.js
// @require     https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

let nextPosition = {};
let fromHwAtoConfigModel = {};
let fromHwSMHierarchy = {};
let fromExtCfgDetails = {};
const colorDict = {};

const PRODUCT_FAMILY = 'SWCPQ';
const PF_SEGMENT = 'hwato';
const HW_SYSTEM_TYPE = "HW ATO Config"

const tileColors = [
  'echoSignBackground',
  'greenBackground',
  'pinkBackground',
  'blueBackground',
  'orangeBackground',
  'burgundyBackground',
  'darkblueBackground',
  'darkBurgundyBackground',
  'grayBackground',
];

const productLines = [
  {
    variable: "networkingProducts",
    genericModel: 'networkingProductsGeneric',
    product_line_label: "Networking Products",
    icon: "quoteCrossProductServices.png",
    smhCode: "SMHW100100",
    color: 0,
  },
  {
    variable: "engineeredSystems",
    genericModel: 'engineeredSystemsGeneric',
    product_line_label: "Oracle Engineered Systems",
    icon: "quoteCrossProductServices.png",
    smhCode: "SMHW100031",
    color: 0,
  },
  {
    variable: "sPARCServers",
    genericModel: 'sPARCServersGeneric',
    product_line_label: "Sparc Servers",
    icon: "quoteCrossProductServices.png",
    smhCode: "SMHW100001",
    color: 0,
  },
  {
    variable: "tapeStorage",
    genericModel: 'tapeStorageGeneric',
    product_line_label: "Tape Storage",
    icon: "quoteCrossProductServices.png",
    smhCode: "SMHW100061",
    color: 0,
  },
  {
    variable: "cGBUProducts",
    genericModel: 'cGBUGeneric',
    product_line_label: "CGBU Products",
    icon: "quoteCrossProductServices.png",
    smhCode: "SMHW100082",
    color: 0,
  },
  {
    variable: "fujitsuSPARCServers",
    genericModel: 'fujitsuSPARCServersGeneric',
    product_line_label: "Fujitsu SPARC Servers",
    icon: "quoteCrossProductServices.png",
    smhCode: "SMHW100130",
    color: 0,
  },
  {
    variable: "nASStorage",
    genericModel: 'nASStorageGeneric',
    product_line_label: "NAS Storage",
    icon: "quoteCrossProductServices.png",
    smhCode: "SMHW100051",
    color: 0,
  },
  {
    variable: "netraSystems",
    genericModel: 'netraSystemsGeneric',
    product_line_label: "Netra Systems",
    icon: "quoteCrossProductServices.png",
    smhCode: "SMHW100079",
    color: 0,
  },
  {
    variable: "x86Servers",
    genericModel: 'x86ServersGeneric',
    product_line_label: "x86 Servers",
    icon: "quoteCrossProductServices.png",
    smhCode: "SMHW100131",
    color: 0,
  },
  {
    variable: "internalCommercial",
    genericModel: 'internalCommercialGeneric',
    product_line_label: "Oracle\nEng Systems Exadata Upgrades",
    icon: "quoteCrossProductServices.png",
    smhCode: "SMHW100291",
    color: 0,
  },
  {
    variable: "sANStorage",
    genericModel: 'sANStorageGeneric',
    product_line_label: 'SAN Storage',
    icon: "quoteCrossProductServices.png",
    smhCode: "",
    color: 0,
  },
  {
    variable: "oracleCloudAtCustomer",
    genericModel: 'oracleCloudAtCustomerGeneric',
    product_line_label: 'Oracle Cloud at Customer',
    icon: "quoteCrossProductServices.png",
    smhCode: "SMHW100323",
    color: 0,
  },
  {
    variable: "genericF",
    genericModel: 'generic',
    product_line_label: 'CHANGE_ME',
    icon: "quoteCrossProductServices.png",
  },
];

const capitalizeFirstLetter = (string) => {
  return string.charAt(0).toUpperCase() + string.slice(1);
};

// Datatable API
const getApiTableName = (tableName) => {
  let result = tableName;
  if (tableName === "_parts") {
    result = "parts";
  } else if (tableName.length) {
    result = "custom" + tableName[0].toUpperCase() + tableName.slice(1);
  }
  return result;
};

const getTableMetaData = async (tableName) => {
  const metaData = {};
  const apiTableName = getApiTableName(tableName);
  const base_url = `${location.protocol}//${location.hostname}`;
  const url = `${base_url}/rest/v8/metadata-catalog/${apiTableName}`;
  const data = await fetch(url)
    .then(r => r.json(), console.log)
    .catch(console.log);
  metaData.columns = Object.keys(data.definitions[apiTableName].properties)
    .filter(c => c !== "id")
    .map(col => data.definitions[apiTableName].properties[col]);
  return metaData;
};

const getTableData = (tableName, query, offset, allData, deferred) => {
  const dTableData = Object.is(deferred, undefined)
    ? new $.Deferred()
    : deferred;
  offset = offset || 0;

  query = query || "";
  const apiTableName = getApiTableName(tableName);
  const url = `/rest/v8/${apiTableName}?q=${query}&totalResults=true&offset=${offset}`;

  allData = allData || [];

  $.get(url, (data) => {
    if (data.count && data.hasMore) {
      let nextOffset = offset + 1000;
      allData = allData.concat(data.items);

      getTableData(tableName, query, nextOffset, allData, dTableData);
    } else {
      dTableData.resolve(allData.concat(data.items));
    }
  }).fail(() => {
    dTableData.reject();
  });

  return dTableData.promise();
};

const getTableInfo = async (tableName, query) => {
  const tableInfo = {};
  tableInfo.isValid = false;
  if (tableName) {
    tableInfo.isValid = true;
    tableInfo.name = tableName;
    tableInfo.metaData = await getTableMetaData(tableName);
    tableInfo.rawData = await getTableData(tableName, query);
  }
  return tableInfo;
};

const sanitizeCell = (desc) => {
  let itemDesc;
  if (desc) {
    if (isNaN(desc)) {
      itemDesc = (desc + '').replace(/(\r\n|\n|\r|\s+|\t| )/gm,' ');
      itemDesc = itemDesc.replace(/"/g, '""');
      itemDesc = itemDesc.replace(/ +(?= )/g,'');
      if (itemDesc.indexOf('"') >= 0 || itemDesc.indexOf(',') >= 0) {
        itemDesc = '"' + itemDesc + '"';
      }
    } else {
      itemDesc = desc;
    }
  } else {
    itemDesc = '';
  }
  return itemDesc;
};

const dataToCsv = (tableMetaData, tableData, includeUpdate = true) => {
  let output = '_start meta data' + tableMetaData.columns.map(col => '').join(',') + '\n';
  output += (includeUpdate ? '_update_action,' : '') + tableMetaData.columns.map(col => col.title).join(',') + '\n';
  output += (includeUpdate ? 'String,' : '') + tableMetaData.columns.map(col => capitalizeFirstLetter(col.type)).join(',') + '\n';
  output += (includeUpdate ? ',' : '') + tableMetaData.columns.map(col => sanitizeCell(col.description)).join(',') + '\n';
  output += '_end meta data' + tableMetaData.columns.map(col => '').join(',') + '\n';
  output += tableData.map(row =>
    (includeUpdate ? 'modify,' : '') +
    tableMetaData.columns.map(col =>
      sanitizeCell(row[col.title.toLowerCase()])
    ).join(',')).join('\n');
  return output + '\n';
};

const render = (parentId, title, body, erase = false) => {
  if (erase) $(`#${parentId}`).empty();

  let renderEl = $('<div></div>');
  // Title
  renderEl.append(`<div class="section-title"><strong>${title}</strong></div>`);
  // Content
  renderEl.append(body);
  $(`#${parentId}`).append(renderEl);
};

const renderAtoSelectOperation = async () => {
  if (!fromHwAtoConfigModel.isValid) {
    fromHwAtoConfigModel = await getTableInfo("hwATOconfigModel");
    console.info('fromHwAtoConfigModel', fromHwAtoConfigModel);
    fromHwSMHierarchy = await getTableInfo("hwSMHierarchy");
    console.info('fromHwSMHierarchy', fromHwSMHierarchy);
  }

  const content = `
  <div class="list-group">
    <a href="#" class="list-group-item list-group-item-action list-group-item-success operation-type" data-action="add">Add a Model</a>
    <a href="#" class="list-group-item list-group-item-action list-group-item-danger operation-type" data-action="remove">Remove a Model</a>
  </div>
  `;
  render("ato-helper-body", `Select Operation`, content, true);

  $(".operation-type").on("click", (e) => {
    const selectedAction = $(e.currentTarget).data('action');

    if (selectedAction == 'add') {
      npiCollectRequiredInfo();
    } else if (selectedAction == 'remove') {
      eolCollectRequiredInfo();
    }
  });
};

const setColorDict = () => {
    const sortedL1Arr = fromHwSMHierarchy.rawData
    .filter(obj => !obj.smh_code_parent)
    .sort((obj1, obj2) => obj1.smh_desc_child > obj2.smh_desc_child ? 1 : obj1.smh_desc_child < obj2.smh_desc_child ? -1 : 0);

    sortedL1Arr.forEach(l1 => {
        const plObj = productLines.find(pl => pl.smhCode?.toLowerCase() == l1.smh_code_child.toLowerCase());
        if (plObj) {
            let colorIdx = plObj.color;
            colorDict[l1.smh_code_child] = {color: tileColors[colorIdx], desc: l1.smh_desc_child, level: 1};

            const sortedL2Arr = fromHwSMHierarchy.rawData
            .filter(obj => obj.smh_code_parent == l1.smh_code_child && obj.smh_code_intermediat) // Only children of L1 that have intermediate level
            .filter((obj, index, arr) => arr.map(mapObj => mapObj.smh_code_intermediat).indexOf(obj.smh_code_intermediat) === index) // Remove duplicate intermediate levels
            .sort((obj1, obj2) => obj1.smh_code_intermediat > obj2.smh_code_intermediat ? 1 : obj1.smh_code_intermediat < obj2.smh_code_intermediat ? -1 : 0);
            console.log(l1.smh_desc_child, sortedL2Arr);

            sortedL2Arr.forEach(l2 => {
                colorDict[l2.smh_code_intermediat] = {color: tileColors[colorIdx], level: 2, parent: l2.smh_code_parent};
                colorIdx = (colorIdx + 1) % tileColors.length;
            });
        } else {
            console.warn(`Could not find a matching product line for ${l1.smh_desc_child}`);
        }
    });
};

const npiCollectRequiredInfo = () => {
  const content = `
    <form>
      <div class="form-group">
        <label for="part-numbers">Part Number(s)</label>
        <input type="text" class="form-control ato-info-input required" id="part-numbers" placeholder="Example: 7123456, 7123457, 7123458">
      </div>
      <div class="form-group">
        <div class="form-check">
          <input class="form-check-input ato-info-input" type="checkbox" value="" id="new-table-mode">
          <label class="form-check-label" for="new-table-mode">
            New Table Mode (Ignore existing data)
          </label>
        </div>
      </div>
      <button id="submit-ato-info" class="btn btn-primary" disabled>Submit</button>
    </form>
  `;
  render("ato-helper-body", "New ATO Model", content, true);
  $("#start-over").removeClass('d-none');

  $(".ato-info-input").on("change keyup", (e) => {
    const emptyInputs = $(".ato-info-input.required").filter((i, e) => $(e).val().trim() === "");
    if (emptyInputs.length === 0) {
      $('#submit-ato-info').prop('disabled', false);
    }
  });

  $("#submit-ato-info").on("click", (e) => {
    e.preventDefault();
    $("#submit-ato-info").addClass('d-none'); // Removes submit button
    $(".ato-info-input").prop('disabled', true); // Disable inputs
    $("#new-table-mode").prop('disabled', true); // Disable inputs

    const partNumbers = $("#part-numbers").val();
    const newTableMode = $("#new-table-mode").prop('checked');
    npiCalculateChanges(partNumbers, newTableMode);
  });
};

const getHwAtoEntry = (partNumber, fromItemMaster, fromHwPartAttributes, fromPartToSMH, newTableMode) => {
  const hwAtoEntry = {};
  let productLine = 'UNKNOWN';
  let hwSmhHierarchyRecord, plObj;
  const hwAtoFields = fromHwAtoConfigModel.metaData.columns.map(col => col.title.toLowerCase());
  const itemMasterRecord = fromItemMaster.rawData.find(record => record.partNumber === partNumber);
  const hwPaRecord = fromHwPartAttributes.rawData.find(record => record.part_number === partNumber);
  const partToSmhRecord = fromPartToSMH.rawData.find(record => record.part_number === partNumber);
  if (partToSmhRecord) {
      hwSmhHierarchyRecord = fromHwSMHierarchy.rawData.find(record => record.smh_code_child === partToSmhRecord.smh_code_child);
      plObj = productLines.find(pl => pl.smhCode?.toLowerCase() == hwSmhHierarchyRecord.smh_code_parent.toLowerCase()) || {};
      productLine = plObj?.variable;

      const lastPlPosition = fromHwAtoConfigModel.rawData
          .filter(cm => cm.product_line === productLine)
          .map(cm => cm.hw_config_position)
          .sort((n1, n2) => n1 > n2 ? 1 : n1 < n2 ? -1 : 0).pop() || 0;

      nextPosition[productLine] = nextPosition[productLine] || ( newTableMode ? 1 : lastPlPosition + 1 );
  } else {
      console.warn(`Could not find product line for ${partNumber}`);
      nextPosition[productLine] = 'UNKNOWN';
  }


  hwAtoFields.forEach(field => {
    switch(field) {
      case 'configuration':
        hwAtoEntry[field] = partNumber;
        break;
      case 'config_name':
      case 'alt_display_name':
        hwAtoEntry[field] = itemMasterRecord.description;
        break;
      case 'product_line':
      case 'product_link':
        hwAtoEntry[field] = productLine;
        break;
      case 'product_line_label':
      case 'icon':
        hwAtoEntry[field] = plObj ? plObj[field] : 'UNKNOWN';
        break;
      case 'color':
        hwAtoEntry[field] = hwSmhHierarchyRecord ? (hwSmhHierarchyRecord.smh_code_intermediat ?
            colorDict[hwSmhHierarchyRecord.smh_code_intermediat]?.color : colorDict[hwSmhHierarchyRecord.smh_code_parent]?.color)
            : 'UNKNOWN'
        break;
      case 'product_family':
        hwAtoEntry[field] = PRODUCT_FAMILY;
        break;
      case 'pf_segment':
        hwAtoEntry[field] = PF_SEGMENT;
        break;
      case 'hw_system_type':
        hwAtoEntry[field] = HW_SYSTEM_TYPE;
        break;
      case 'segplinemodelvarname':
        hwAtoEntry[field] = plObj ? `${PF_SEGMENT}:${productLine}:${plObj.genericModel}` : 'UNKNOWN';
        break;
      case 'hw_config_position':
        hwAtoEntry[field] = nextPosition[productLine]++;
        break;
      case 'is_active':
        hwAtoEntry[field] = 'Y';
        break;
      case 'is_redundant':
        hwAtoEntry[field] = 'N';
        break;
      case 'model_type':
        hwAtoEntry[field] = hwPaRecord.support_exc_cat === 'SE1027' ? 'CLM' : '';
        break;
      case 'is_restricted':
        hwAtoEntry[field] = itemMasterRecord._part_custom_field12.toLowerCase().indexOf('active') >= 0 || hwPaRecord.support_exc_cat === 'SE1027' ? 'N' : 'Y';
        break;
      case 'inventory_item_id':
        hwAtoEntry[field] = itemMasterRecord._part_custom_field1;
        break;
      default:
        hwAtoEntry[field] = 'UNKNOWN'
        console.warn(`Unknown field "${field}"`);
    }
  });

  return hwAtoEntry;
};

const npiCalculateChanges = async (partNumbers, newTableMode) => {
  const partNumArr = partNumbers.split(',').map(pn => pn.trim());
  let fromHwPartAttributes = await getTableInfo("hwPartAttributes", `{$or:[${partNumArr.map(pn => `{part_number:{$eq: '${pn}'}}`).join(',')}]}`);
  let fromItemMaster = await getTableInfo("_parts", `{$or:[${partNumArr.map(pn => `{partNumber:{$eq: '${pn}'}}`).join(',')}]}`);
  let fromPartToSMH = await getTableInfo("partToSMH", `{$or:[${partNumArr.map(pn => `{part_number:{$eq: '${pn}'}}`).join(',')}]}`);

  console.info('fromHwPartAttributes',fromHwPartAttributes);
  console.info('fromItemMaster', fromItemMaster);
  console.info('fromPartToSMH',fromPartToSMH);

  if (!newTableMode && fromHwAtoConfigModel.rawData.some(cm => partNumArr.indexOf(cm.configuration) >= 0)) {
    render("ato-helper-body", "A Model Already exists in hwATOconfigModel");
  } else if (fromHwPartAttributes.rawData.length === partNumArr.length && fromItemMaster.rawData.length === partNumArr.length) {
    // Construct the hwPartAttributes row
    setColorDict();
    console.log('colorDict', colorDict);
    const hwAtoEntries = partNumArr.map(pn => getHwAtoEntry(pn, fromItemMaster, fromHwPartAttributes, fromPartToSMH, newTableMode));
    render(
      "ato-helper-body",
      "Next Steps:",
      '<table class="table table-striped table-sm table-limited-width"><tr>' +
      Object
        .keys(hwAtoEntries[0])
        .map(key =>
          `<th>${key}</th>${hwAtoEntries.map(e => `<td class='${e[key] == 'UNKNOWN' ? 'danger' : ''}'>${e[key]}</td>`).join('\n')}`
        ).join('</tr>\n<tr>') + '</tr></table><br>\n' +
      `<ol class="steps"><li><a class="btn btn-outline-info btn-sm" href="data:text/csv;charset=utf-8,${encodeURI(dataToCsv(fromHwAtoConfigModel.metaData, hwAtoEntries, false))}"
      target="_blank" download="${fromHwAtoConfigModel.name}.csv">Download CSV</a> : Import and deploy</li>\n` +
      `<li><a class="btn btn-outline-info btn-sm" href="https://confluence.oraclecorp.com/confluence/x/LdIJRg" target="_blank">Regenerate JSON</a></li></ol>`
    );

  } else {
    render("ato-helper-body", "Some Parts are not available in either Item Master, hwPartAttributes, or both");
    console.log('parts not in Item Master', partNumArr.filter(pn => !fromItemMaster.rawData.some(d => d.partNumber === pn)));
    console.log('parts not in hwPartAttributes', partNumArr.filter(pn => !fromHwPartAttributes.rawData.some(d => d.part_number === pn)));
  }
};

const eolCollectRequiredInfo = () => {
  const content = `
    <form>
      <div class="form-group">
        <label for="partNumber">Part Number</label>
        <input type="text" class="form-control ato-info-input required" id="part-number">
      </div>
      <button id="submit-ato-info" class="btn btn-primary" disabled>Submit</button>
    </form>
  `;
  render("ato-helper-body", "Remove ATO Model", content, true);
  $("#start-over").removeClass('d-none');

  $(".ato-info-input").on("change keyup", (e) => {
    const emptyInputs = $(".ato-info-input.required").filter((i, e) => $(e).val().trim() === "");
    if (emptyInputs.length === 0) {
      $('#submit-ato-info').prop('disabled', false);
    }
  });

  $("#submit-ato-info").on("click", (e) => {
    e.preventDefault();
    $("#submit-ato-info").addClass('d-none'); // Removes submit button
    $(".ato-info-input").prop('disabled', true); // Disable inputs

    const partNumber = $("#part-number").val();
    eolCalculateChanges(partNumber);
  });
};

const eolCalculateChanges = async (partNumber) => {
  render(
    "ato-helper-body",
    "JET UI Steps (hwATOconfigModel):",
    `<ol class="steps"><li>Look for entry where <strong>configuration = ${partNumber}</strong> and change <strong>is_active to N</strong>, then deploy</li>\n` +
    `<li><a class="btn btn-outline-info btn-sm" href="https://confluence.oraclecorp.com/confluence/x/LdIJRg" target="_blank">Regenerate JSON</a></li></ol>`
  );
};

const showAtoHelper = () => {
  "use strict";
  var modalHtml = `
    <!-- Modal -->
    <div class="modal fade" id="atoHelperModal" role="dialog">
      <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content d-none">
          <div class="modal-header">
            <h5 class="modal-title">CPQ ATO Model Helper</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="ato-helper-body">Examining CPQ Instance...</div>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-danger" id="start-over">Start Over</button>
            <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
          </div>
        </div>
      </div>
    </div>
  `;

  var okToolBar = document.querySelector("ul.ok-tool-bar");
  if (!okToolBar) {
    okToolBar = document.createElement("ul");
    okToolBar.className = "ok-tool-bar";
    $("body").append(okToolBar);
  }

  var showPerfStatsBtn = document.createElement("li");
  var text = document.createTextNode("ATO Helper");
  showPerfStatsBtn.appendChild(text);
  showPerfStatsBtn.id = "btn-btn-ato-helper";
  showPerfStatsBtn.className = "btn-ato-helper";
  showPerfStatsBtn.dataset.toggle = "modal";
  showPerfStatsBtn.dataset.target = "#atoHelperModal";

  //--- Add nodes to page
  okToolBar.append(showPerfStatsBtn);
  $("body").append(modalHtml);

  $("#btn-btn-ato-helper, #start-over").on("click", () => {
    $(".modal-content").removeClass("d-none");
    renderAtoSelectOperation();
    $("#start-over").addClass('d-none');
  });
};

// Don't run on login page
let notLoginPage = !document.querySelector("#login-form .login-links");

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


// Styling Stuff
var bm_css_src = `
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-ato-helper {
  color: #fff;
  background-color: #94AFAF;
  border-color: #94AFAF;
}
.section-title {
  margin: 2em 0 1em;
}
ol.steps {
  border: 1px solid #aaa;
  border-radius: 10px;
  box-shadow: 0 0 20px #aaa;
  padding-left: 2em;
}
ol.steps li {
  margin: 0.7em;
}
body {
  margin: auto;
}
table.table-limited-width {
  display: block;
  overflow-x: auto;
  font-size: 10px;
}
.danger {
  color: red;
  font-weight: bold;
}
`;

GM_addStyle(bm_css_src);