Mint.com tags display

Show tags in the transactions listing on Mint.com.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name        Mint.com tags display
// @match       https://mint.intuit.com/transactions
// @connect     mint.intuit.com
// @description Show tags in the transactions listing on Mint.com.
// @namespace   com.warkmilson.mint.js
// @author      Mark Wilson (update by Shaun Williams)
// @version     2.0.1
// @homepage    https://github.com/mddub/mint-tags-display
// @grant       none
// @noframes
// ==/UserScript==
//

(function() {

  //------------------------------------------------------------------------------
  // Logging
  //------------------------------------------------------------------------------

  const logging = false
  function log(...args) {
    if (logging) console.info('MINT_TAGS', ...args)
  }

  //------------------------------------------------------------------------------
  // Track state by watching XHR
  //------------------------------------------------------------------------------

  // State

  const state = {
    txnTags: {}, // txn id -> [tag name]
    tagOrder: [], // [tag name]
    tagName: {}, // tag id -> tag name
  }
  window._MINT_TAGS = state

  // Update state

  const apiUrl = path => `https://mint.intuit.com/pfm/v1${path}`

  const apiHooks = {
    // when transactions are fetched, save tags belonging to each transaction
    [apiUrl('/transactions/search')]: data => {
      for (const {id,tagData} of data.Transaction) {
        state.txnTags[id] = tagData?.tags.map(tag => tag.name)
      }
    },
    // when the master tag list is fetched, save it
    [apiUrl('/tags')]: data => {
      state.tagOrder = data.Tag.map(tag => tag.name)
      state.tagName = Object.fromEntries(data.Tag.map(tag => [tag.id, tag.name]))
    },
  }

  // when transactions are edited, update our tag records
  function handleTxnEdits(edits) {
    const idsToUpdate = []
    for (const {id,tagData} of edits) {
      if (tagData) {
        state.txnTags[id] = tagData.tags.map(tag => state.tagName[tag.id])
        idsToUpdate.push(id)
      }
    }
    setTimeout(() => idsToUpdate.forEach(updateRowTags), 500)
  }

  // hook XHR to intercept api calls
  function watchXHR() {
    const origOpen = XMLHttpRequest.prototype.open

    XMLHttpRequest.prototype.open = function(method, url) {
      const self = this

      // save XHR responses when needed
      self.addEventListener("readystatechange", function() {
        const hook = apiHooks[url]
        if (self.readyState === 4 && hook) {
          const data = JSON.parse(self.responseText)
          log('HOOKING', url, data)
          hook(data)
        }
      }, false)

      // intercept edits to transactions
      const txnsUrl = apiUrl('/transactions')
      if (method == 'PUT' && url.startsWith(txnsUrl)) {
        const origSend = self.send
        self.send = function(body) {
          const data = JSON.parse(body)
          const edits = url==txnsUrl ? data.Transaction : [{...data, id:url.slice(txnsUrl.length+1)}]
          log('HOOKING EDITS', edits)
          handleTxnEdits(edits)
          origSend.apply(self, arguments)
        }
      }

      origOpen.apply(self, arguments)
    }
  }

  //------------------------------------------------------------------------------
  // Render DOM
  //------------------------------------------------------------------------------

  var TAG_COLORS = [
    // source: http://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12
    // background, foreground
    ['#a6cee3', '#000'],
    ['#b2df8a', '#000'],
    ['#fb9a99', '#000'],
    ['#fdbf6f', '#000'],
    ['#cab2d6', '#000'],
    ['#ffff99', '#000'],
    ['#1f78b4', '#fff'],
    ['#33a02c', '#fff'],
    ['#e31a1c', '#fff'],
    ['#ff7f00', '#fff'],
    ['#6a3d9a', '#fff'],
    ['#b15928', '#fff']
  ];

  function getTagStyle(tag) {
    const i = state.tagOrder.indexOf(tag)
    const [bg,fg] = TAG_COLORS[i]
    return `background:${bg}; color:${fg}`
  }

  // re-render our custom tag annotations in this row
  function updateRowTags(id) {
    log('UPDATING ROW', id)
    const td = document.querySelector(`tr[data-automation-id$=_${id}] td:nth-child(4)`)
    if (!td) return

    const tags = state.txnTags[id]
    const tagsDiv = () => td.querySelector('.gm-tags')
    if (tags?.length) {
      if (!tagsDiv()) td.innerHTML += '<div class="gm-tags" style="font-size:10px; display:inline-block"></div>'
      tagsDiv().innerHTML = tags.map(tag => `<span class="gm-tag" style="${getTagStyle(tag)}; margin-left:4px; padding:0 2px">${tag}</span>`).join('')
    } else {
      tagsDiv()?.remove()
    }
  }

  const rowId = row => row?.dataset?.automationId?.match(/TRANSACTION_TABLE_ROW_(READ|EDIT)_(.*)/)?.[2]

  function initRender() {
    if (!document.querySelector('tr[data-automation-id]')) {
      return setTimeout(initRender, 500)
    }
    log('FOUND TABLE')
    for (const row of document.querySelectorAll('tr[data-automation-id]')) {
      updateRowTags(rowId(row))
    }
    renderWhenDomChanges()
  }

  function renderWhenDomChanges() {
    log('WATCHING FOR CHANGES')
    const observer = new MutationObserver(() => {
      observer.disconnect()
      document.querySelectorAll('tr[data-automation-id]').forEach(row => updateRowTags(rowId(row)))
      log('RELAUNCHING WATCH')
      renderWhenDomChanges()
    })
    observer.observe(document.body, {subtree: true, childList: true, characterData: true})
  }

  //------------------------------------------------------------------------------
  // Main
  //------------------------------------------------------------------------------

  watchXHR()
  initRender()

})();