Amex Bonus Tracker

This script is used to track amex bunos progress

// ==UserScript==
// @name         Amex Bonus Tracker
// @namespace    http://tampermonkey.net/
// @version      v1.0.6
// @description  This script is used to track amex bunos progress
// @author       You
// @match        https://global.americanexpress.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=americanexpress.com
// @grant        none
// @license MIT 

// ==/UserScript==

(function () {
  'use strict';

  const excludedDescriptions = [
    'MEMBERSHIP FEE',
    'PAYPAL ACCT PAYMENT RECEIVED - THANK YOU',
    'AUTOPAY PAYMENT - THANK YOU',
    'CR ADJ FOR BALANCE TRA',
    'ONLINE PAYMENT - THANK YOU',
    'MOBILE PAYMENT - THANK YOU',
    'AMEX AIRLINE FEE REIMBURSEMENT',
    'SHOP SAKS WITH PLATINUM CREDIT',
    'DELL CREDIT',
    'AMEX DINING CREDIT',
    'HILTON RESORT CREDIT',
    'AMEX FLIGHT CREDIT',
    'HILTON STATEMENT CREDIT',
    'AMEX DUNKIN\' CREDIT',
    'RENEWAL MEMBERSHIP FEE',
    'ADJUST MEMBERSHIP FEE',
    'MEM RWDS AIRLINE TAX OFFSET FEE',
    'RETURN PAYMENT FEE',
    'AMEX SEND: TRANSFER TO CARD',
    'AMEX SEND: ADD MONEY'
  ];

  function clickUntilGone() {
    // Get all buttons with the specified class
    var buttons = document.querySelectorAll('.btn.btn-sm.css-19hct2l');

    // If buttons exist, click the first one
    if (buttons.length > 0) {
      buttons[0].click();
      // Wait for a short delay to allow any potential changes in the DOM
      setTimeout(clickUntilGone, 200); // Adjust delay as needed
    } else {
      console.log('No more buttons to click.');
    }
  }

  function computeAndRender() {
    let parentElement = document.querySelector('[data-module-name="axp-activity-feed-transactions-table-transactions"]');
    let childElements = parentElement.querySelectorAll('.position-relative');
    let totalEligibleAmount = 0;
    let totalAmount = 0;
    let includedCount = 0;
    let excludedCount = 0;

    // Object to store the amount for each kind of excluded transaction
    let excludedAmounts = {};
    excludedDescriptions.forEach(desc => {
      excludedAmounts[desc] = 0;
    });

    let transactionRows = [];

    childElements.forEach((childElement) => {
      let descriptionElement = childElement.querySelector('.description');
      let priceElement = childElement.querySelector('.hidden-md-up.col-sm-4.col-sm-4.pad-responsive-r');

      if (descriptionElement && priceElement) {
        let description = descriptionElement.innerText;
        let price = parseFloat(priceElement.innerText.replace('$', '').replace(',', ''));
        totalAmount += price;

        if (!excludedDescriptions.includes(description)) {
          includedCount++;
          totalEligibleAmount += price;
          transactionRows.push(createTransactionRow(description, price, '🟢'));
        } else {
          excludedCount++;
          excludedAmounts[description] += price;
          transactionRows.push(createTransactionRow(description, price, '🔴'));
        }
      }
    });

    let summaryRows = [
      createSummaryRow('Total Eligible Spending for Bonus:', totalEligibleAmount, '🟢'),
      createSummaryRow('Total Amount:', totalAmount, '🟡'),
      createSummaryRow('Included Transactions:', includedCount, '🟡', false),
      createSummaryRow('Excluded Transactions:', excludedCount, '🟡', false),
    ];

    let excludedTransactionRows = Object.entries(excludedAmounts)
      .filter(([desc, amount]) => amount !== 0)
      .map(([desc, amount]) => createTransactionRow(desc, amount, '🔴'));

    // Create a new element to display the results in the desired format
    let resultContainer = document.createElement('div');
    resultContainer.className = 'axp-activity-balance card margin-b section-container transactions-data-table-v';
    resultContainer.id = 'amex-welcome-bonus-tracker';
    resultContainer.innerHTML = `
<div data-module-name="axp-activity-balance/Header"  class="card-block pad-1">
  <div class="pad-l col-md-9 col-xs-12">
    <h2 class="heading-4 heading-5-v">Amex Welcome Bonus Tracker</h2>
  </div>
</div>
<div data-module-name="axp-activity-balance/BalancesDataTable">
  <table class="table data-table axp-activity-balance__data-table__dataTable___6Mijm">
    <thead class="axp-activity-balance__data-table__tableHeader___ZW3ub">
    </thead>
    <tbody>
    <tr><td colspan="2"><strong>Summary</strong></td></tr>
    ${summaryRows.join('')}
    <tr><td colspan="2"><strong>Amount for each kind of excluded transaction:</strong></td></tr>
    ${excludedTransactionRows.join('')}
    <tr><td colspan="2"><strong>Transactions</strong></td></tr>
    ${transactionRows.join('')}
    </tbody>
  </table>
</div>
`;

    // Find the target element to insert the result container
    let targetElement = document.querySelector('div[data-module-name="axp-activity/Content/Range/ActivityMultiBalance"]')
      ?? document.querySelector('div[data-module-name="axp-activity/Content/Range/ActivityBalance"]')
      ?? document.querySelector('div[data-module-name="axp-activity-search-activity-balance"]');

    if (targetElement) {
      targetElement.parentNode.insertBefore(resultContainer, targetElement.nextSibling);
    } else {
      console.log('Target element not found');
    }
  }

  function createTransactionRow(description, amount, emoji) {
    return `
<tr class="row-sm-size">
  <td headers="header-blank0-0" class="" style="width: 100%;">
    <div class="pad-l body-2-v" data-module-name="axp-activity-balance/BalanceName" style="width: 100%; text-wrap: nowrap;">
      <span style="padding-right: 5px; text-transform: capitalize;">${emoji} ${description}</span>
    </div>
  </td>
  <td headers="header-blank1-0" class="text-align-right" style="width: 100%;">
    <p data-module-name="axp-activity-balance/BalanceAmount" class="axp-activity-balance__data-table__balanceDisplayTag___MdUas body-2-v ${amount < 0 ? 'dls-green' : ''}">
      ${amount >= 0 ? '$' : '-$'}${Math.abs(amount).toFixed(2)}
    </p>
  </td>
</tr>
`;
  }

  function createSummaryRow(label, value, emoji, isCurrency = true) {
    return `
<tr class="row-sm-size">
  <td headers="header-blank0-0" class="" style="width: 100%;">
    <div class="pad-l body-2-v" data-module-name="axp-activity-balance/BalanceName" style="width: 100%; text-wrap: nowrap;">
      <span style="padding-right: 5px; text-transform: capitalize;">${emoji} ${label}</span>
    </div>
  </td>
  <td headers="header-blank1-0" class="text-align-right" style="width: 100%;">
    <p data-module-name="axp-activity-balance/BalanceAmount" class="axp-activity-balance__data-table__balanceDisplayTag___MdUas body-2-v ${value < 0 && isCurrency ? 'dls-green' : ''}">
      ${isCurrency ? (value >= 0 ? '$' : '-$') + Math.abs(value).toFixed(2) : value}
    </p>
  </td>
</tr>
`;
  }

  function removeResultContainer() {
    let elementToRemove = document.getElementById('amex-welcome-bonus-tracker');
    if (elementToRemove) {
      elementToRemove.remove();
    }
  }


  function handleMutations(mutationsList, observer) {
    // Check if the target element is now available
    let targetElement = document.querySelector('div[data-module-name="axp-activity/Content/Range/ActivityMultiBalance"]')
      ?? document.querySelector('div[data-module-name="axp-activity/Content/Range/ActivityBalance"]')
      ?? document.querySelector('div[data-module-name="axp-activity-search-activity-balance"]');
    let buttonElement = document.getElementById('amex-welcome-bonus-tracker-button');

    if (targetElement && !buttonElement) {
      // Insert the button if the target element is found
      let button = document.createElement('button');
      button.textContent = 'Show Amex Spending Tracker';
      button.id = 'amex-welcome-bonus-tracker-button'

      // Style the button
      button.style.padding = '12px 24px';
      button.style.fontSize = '16px';
      button.style.fontWeight = 'bold';
      button.style.color = '#fff';
      button.style.backgroundColor = '#006fcf';
      button.style.border = 'none';
      button.style.borderRadius = '6px';
      button.style.cursor = 'pointer';
      button.style.transition = 'background-color 0.3s ease';

      // Add hover effect
      button.addEventListener('mouseover', function () {
        button.style.backgroundColor = '#0057a8';
      });

      button.addEventListener('mouseout', function () {
        button.style.backgroundColor = '#006fcf';
      });

      button.onclick = () => {
        removeResultContainer();
        clickUntilGone()
        computeAndRender();
      };
      button.style.margin = '10px 0'; // Add some margin for better spacing

      targetElement.parentNode.insertBefore(button, targetElement.nextSibling);
    }
  }

  const observerOptions = {
    childList: true, // Observe changes to the child nodes of the observed element
    subtree: true, // Include all descendant nodes of the observed element
  };

  // Create a MutationObserver instance
  const observer = new MutationObserver(handleMutations);

  // Start observing mutations on the body element
  observer.observe(document.body, observerOptions);
})();