Lattice Goals - Add Ideal

Determines ideal goal value based on Created on and Due dates

// ==UserScript==
// @name        Lattice Goals - Add Ideal
// @namespace   Violentmonkey Scripts
// @match       http://*.latticehq.com/goals/*
// @match       https://*.latticehq.com/goals/*
// @grant       none
// @version     1.3
// @author      pedro-mass
// @description Determines ideal goal value based on Created on and Due dates
// @run-at      document-idle
// @require     http://code.jquery.com/jquery-3.5.0.min.js
// ==/UserScript==

waitUntilTrue(shouldRun, run);

function isPercentageBasedGoal() {
  // todo: make this actually check if there is a percentage based check?
  return true;
}

function run() {
  console.log("Starting Lattice Goal Percentage calculations...");
  if (!isPercentageBasedGoal()) return;

  const Dates = getDates();
  const GoalStats = getGoalStats();
  const percentageByDate = getRelativePercentage(
    Dates.start,
    Dates.end,
    Dates.current
  );

  const idealValue = Math.round(
    getRelativeValue(GoalStats.start, GoalStats.goal, percentageByDate)
  );

  const goalDirection = GoalStats.start <= GoalStats.end ? 1 : -1;

  return insertIdeal(
    idealValue,
    GoalStats.unit,
    GoalStats.current,
    goalDirection
  );
}

function getRelativeValue(start, end, percentage) {
  const offset = start;
  return (end - offset) * percentage + offset;
}

function getDates() {
  const start = getDate(/^created\n\n/i);
  const end = getDate(/^due\n\n/i);
  const current = Date.now();

  return {
    start,
    end,
    current,
  };
}

function getGoalStats() {
  const unit = getValue(/^start: /i, "span").replace(/\d+/, "");
  const getNumber = (regex) =>
    Number(getValue(regex, "span").replace(unit, ""));
  const start = getNumber(/^start: /i);
  const current = getNumber(/^current: /i);
  const goal = getNumber(/^goal: /i);

  return {
    start,
    goal,
    current,
    unit,
  };
}

function getProgressIndicator(ideal, current) {
  if (current == null) return "";

  if (ideal <= current) {
    return "🎉";
  }

  return "😢";
}

function insertIdeal(ideal, unit, current, isAscendingGoal) {
  if (contains("span", /^ideally: /i).length > 0) {
    return;
  }

  const progressIndicator = isAscendingGoal
    ? getProgressIndicator(ideal, current)
    : getProgressIndicator(current, ideal);

  const idealElem = `<span class="css-1mddpa2">Ideally: <span>${ideal}${unit}</span> <span>${progressIndicator}</span></span>`;
  const goalsContainer = contains("div", /^start:/i);

  return $(goalsContainer).find("span").first().after(idealElem);
}

function shouldRun() {
  const pageCheck = contains("span", /^start: /i);
  return pageCheck != null && pageCheck.length > 0;
}

function getRelativePercentage(startDate, endDate, currentDate) {
  const offset = startDate;
  endDate = endDate - offset;
  currentDate = currentDate - offset;
  return currentDate / endDate;
}

function getDate(regex, selector = "div") {
  return new Date(getValue(regex, selector)).getTime();
}

function getValue(regex, selector) {
  return replaceString(getText(first(contains(selector, regex))), regex, "");
}

function replaceString(string, searchString, newString) {
  if (!string) {
    console.warn("Received bad params", { string, searchString, newString });
    return string;
  }

  return string.replace(searchString, newString);
}

function contains(selector, text) {
  var elements = document.querySelectorAll(selector);
  return Array.prototype.filter.call(elements, function (element) {
    return RegExp(text).test(getText(element));
  });
}

function getText(element) {
  return element.innerText;
}

function first(arr) {
  return arr[0];
}

function waitUntilTrue(checkFn, cb = (x) => x, timeout = 250) {
  const intervalId = setInterval(checkInterval, timeout);
  function checkInterval() {
    if (checkFn()) {
      clearInterval(intervalId);
      cb();
    }
  }
}