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()
    }
  }
}