Automatic Upkeep Tracker

Automatically keeps track of your upkeep transactions for each user with date selector

// ==UserScript==
// @name         Automatic Upkeep Tracker
// @namespace    upkeep.sharing.mobile
// @version      2.9
// @description  Automatically keeps track of your upkeep transactions for each user with date selector
// @author       ANITABURN
// @match        https://www.torn.com/properties.php*
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
  "use strict";
  const localStorageKey = "upkeep_tracker:settings";

  let attempts = 0;
  const maxAttempts = 20;

  function findInsertionPoint() {
    attempts++;

    const payBillsContainer = document.querySelector(
      "div.pay-bills.cont-gray.bottom-round"
    );
    const upkeepPaymentsWrap = document.querySelector(
      "div.upkeep-payments-wrap"
    );

    if (payBillsContainer && upkeepPaymentsWrap) {
      insertCalculator(upkeepPaymentsWrap);
      return true;
    }

    return false;
  }

  function insertCalculator(targetElement) {
    if (document.getElementById("upkeep-totals-calculator")) return;
    let { user1DisplayName, user2DisplayName } = JSON.parse(
      localStorage.getItem(localStorageKey)
    ) || {
      user1DisplayName: "User 1",
      user2DisplayName: "User 2",
    };

    const container = document.createElement("div");
    container.id = "upkeep-totals-calculator";
    container.style.cssText = `
                margin: 15px 0;
                padding: 20px;
                background: #3a3a3a;
                border: 1px solid #555;
                border-radius: 8px;
                color: white;
            `;

    const headerDiv = document.createElement("div");
    headerDiv.style.cssText = `
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 20px;
            `;

    const title = document.createElement("h3");
    title.textContent = "Upkeep Totals";
    title.style.cssText = `
                margin: 0;
                color: white;
                font-size: 18px;
            `;
    headerDiv.appendChild(title);

    const editBtn = document.createElement("button");
    editBtn.textContent = "Edit";
    editBtn.style.cssText = `
                background: #4ecdc4;
                color: white;
                border: none;
                padding: 6px 12px;
                border-radius: 4px;
                cursor: pointer;
                font-size: 12px;
            `;
    headerDiv.appendChild(editBtn);

    container.appendChild(headerDiv);

    const dateDiv = document.createElement("div");
    dateDiv.style.cssText = `
                margin-bottom: 15px;
            `;

    const dateLabel = document.createElement("label");
    dateLabel.textContent = "From: ";
    dateLabel.style.cssText = `
                color: #ccc;
                margin-right: 10px;
                font-weight: bold;
            `;
    dateDiv.appendChild(dateLabel);

    const dateInput = document.createElement("input");
    dateInput.type = "date";
    dateInput.style.cssText = `
                padding: 5px;
                border: 1px solid #666;
                border-radius: 3px;
                background: #2a2a2a;
                color: white;
            `;
    dateDiv.appendChild(dateInput);

    const editSection = document.createElement("div");
    editSection.id = "upkeep-edit-section";
    editSection.style.cssText = `
            display: none;
            margin-top: 20px;
            padding: 20px;
            background: #404040;
            border-radius: 6px;
            border: 1px solid #555;
        `;

    const nameInputsDiv = document.createElement("div");
    nameInputsDiv.style.cssText = `display: flex; gap: 15px; margin-top: 15px;`;
    nameInputsDiv.innerHTML = `
            <div style="flex: 1;">
                <label style="display: block; margin-bottom: 5px; font-size: 12px; color: #ccc;">User 1 Display Name:</label>
                <input id="upkeep-user1-name-input" type="text" value="${user1DisplayName}" style="width: 100%; padding: 5px; border: 1px solid #666; border-radius: 3px; background: #2a2a2a; color: white; box-sizing: border-box;">
            </div>
            <div style="flex: 1;">
                <label style="display: block; margin-bottom: 5px; font-size: 12px; color: #ccc;">User 2 Display Name:</label>
                <input id="upkeep-user2-name-input" type="text" value="${user2DisplayName}" style="width: 100%; padding: 5px; border: 1px solid #666; border-radius: 3px; background: #2a2a2a; color: white; box-sizing: border-box;">
            </div>
        `;

    dateDiv.id = "";
    dateInput.id = "upkeep-date-input";
    editSection.appendChild(dateDiv);
    editSection.appendChild(nameInputsDiv);
    container.appendChild(editSection);

    const usersDiv = document.createElement("div");
    usersDiv.style.cssText = `
                display: flex;
                gap: 0;
                margin-bottom: 15px;
            `;

    const user1Div = document.createElement("div");
    user1Div.style.cssText = `
                flex: 1;
                padding: 15px;
                background: #4a4a4a;
                border-left: 4px solid #e168ff;
                text-align: center;
                border-top-left-radius: 6px;
                border-bottom-left-radius: 6px;
            `;
    user1Div.innerHTML = `
                <div style="font-size: 12px; color: #ccc; margin-bottom: 5px;" id="user1-name">${user1DisplayName}</div>
                <div style="font-size: 16px; font-weight: bold; color: #e168ff;" id="user1-total">$0</div>
            `;

    const user2Div = document.createElement("div");
    user2Div.style.cssText = `
                flex: 1;
                padding: 15px;
                background: #4a4a4a;
                border-right: 4px solid #68acff;
                text-align: center;
                border-top-right-radius: 6px;
                border-bottom-right-radius: 6px;
            `;
    user2Div.innerHTML = `
                <div style="font-size: 12px; color: #ccc; margin-bottom: 5px;" id="user2-name">${user2DisplayName}</div>
                <div style="font-size: 16px; font-weight: bold; color: #68acff;" id="user2-total">$0</div>
            `;

    usersDiv.appendChild(user1Div);
    usersDiv.appendChild(user2Div);
    container.appendChild(usersDiv);

    const totalDiv = document.createElement("div");
    totalDiv.style.cssText = `
                padding: 12px;
                background: #4a4a4a;
                border-radius: 6px;
                text-align: center;
                border-bottom: 4px solid #68ebff;
            `;
    totalDiv.innerHTML = `
                <div style="font-size: 12px; color: #ccc; margin-bottom: 3px;">Total:</div>
                <div style="font-size: 18px; font-weight: bold; color: #68ebff;" id="grand-total">$0</div>
            `;
    container.appendChild(totalDiv);

    targetElement.parentNode.insertBefore(container, targetElement);

    function saveSettings() {
      const user1Input = document.getElementById("upkeep-user1-name-input");
      const user2Input = document.getElementById("upkeep-user2-name-input");

      user1DisplayName = user1Input.value.trim() || "User 1";
      user2DisplayName = user2Input.value.trim() || "User 2";

      localStorage.setItem(
        localStorageKey,
        JSON.stringify({ user1DisplayName, user2DisplayName })
      );

      document.getElementById("user1-name").textContent = user1DisplayName;
      document.getElementById("user2-name").textContent = user2DisplayName;
    }

    function toggleEditMode() {
      const editSection = document.getElementById("upkeep-edit-section");
      const isEditing = editSection.style.display !== "none";
      if (isEditing) {
        saveSettings();
        updateTotals();
        editSection.style.display = "none";
        editBtn.textContent = "Edit";
        editBtn.style.background = "#4ecdc4";
      } else {
        editSection.style.display = "block";
        editBtn.textContent = "Done";
        editBtn.style.background = "#e74c3c";
      }
    }
    editBtn.addEventListener("click", toggleEditMode);

    function parseDateTime(dtString) {
      const [part1, part2] = dtString.split(" ");
      if (!part1 || !part2) return null;

      let timePart, datePart;
      if (part1.includes(":")) {
        timePart = part1;
        datePart = part2;
      } else if (part2.includes(":")) {
        timePart = part2;
        datePart = part1;
      } else {
        return null;
      }

      const [hours, minutes, seconds] = timePart.split(":").map(Number);
      const [day, month, year] = datePart.split("/").map(Number);
      const fullYear = year < 100 ? 2000 + year : year;
      return new Date(fullYear, month - 1, day, hours, minutes, seconds);
    }

    function parseAmount(amountStr) {
      return Number(amountStr.replace(/[^0-9.-]+/g, ""));
    }

    function getCleanUsername(userAnchor) {
      let raw = "";
      userAnchor.childNodes.forEach((node) => {
        if (node.nodeType === Node.TEXT_NODE) {
          raw += node.textContent.trim();
        }
      });
      return raw.trim();
    }

    const transactionStorageKey = "upkeep_tracker:transactions";

    function syncAndGetAllTransactions() {
      const storedData = JSON.parse(
        localStorage.getItem(transactionStorageKey)
      ) || { transactions: [] };
      let allTransactions = storedData.transactions;
      const existingIds = new Set(allTransactions.map((t) => t.uniqueId));
      let newTransactionsFound = false;

      const paymentRows = document.querySelectorAll(
        "ul.upkeep-payments li:not(.title) ul.payments"
      );

      paymentRows.forEach((row) => {
        const dateSpan = row.querySelector("span.transaction-date");
        const timeSpan = row.querySelector("span.transaction-time");
        const userAnchor = row.querySelector("a.user.name");
        const amountLi = row.querySelector("li.amount");

        if (!dateSpan || !timeSpan || !userAnchor || !amountLi) return;

        const dateTimeString1 =
          dateSpan.textContent.trim() + " " + timeSpan.textContent.trim();
        const dateTimeString2 =
          timeSpan.textContent.trim() + " " + dateSpan.textContent.trim();
        const uniqueId = parseDateTime(dateTimeString1)
          ? dateTimeString1
          : dateTimeString2;
        const date = parseDateTime(uniqueId);

        if (date && !existingIds.has(uniqueId)) {
          const amount = parseAmount(amountLi.textContent);
          const username = getCleanUsername(userAnchor);
          allTransactions.push({
            user: username,
            date: date.toISOString(),
            amount: amount,
            uniqueId: uniqueId,
          });
          newTransactionsFound = true;
        }
      });

      if (newTransactionsFound) {
        allTransactions.sort((a, b) => new Date(b.date) - new Date(a.date));
        localStorage.setItem(
          transactionStorageKey,
          JSON.stringify({ transactions: allTransactions })
        );
      }

      return allTransactions;
    }

    function updateTotals() {
      const startDateValue = dateInput.value;
      let startDate = null;
      if (startDateValue) {
        const [year, month, day] = startDateValue.split("-").map(Number);
        startDate = new Date(year, month - 1, day, 0, 0, 0);
      }

      const allTransactions = syncAndGetAllTransactions();

      const totals = {};
      let totalAmount = 0;
      let users = [];
      const seenUsers = new Set();

      allTransactions.forEach((transaction) => {
        const transactionDate = new Date(transaction.date);

        if (!seenUsers.has(transaction.user)) {
          users.push(transaction.user);
          seenUsers.add(transaction.user);
        }

        if (!startDate || transactionDate >= startDate) {
          if (!totals[transaction.user]) {
            totals[transaction.user] = 0;
          }
          totals[transaction.user] += transaction.amount;
          totalAmount += transaction.amount;
        }
      });

      const user1Name = document.getElementById("user1-name");
      const user1Total = document.getElementById("user1-total");
      const user2Name = document.getElementById("user2-name");
      const user2Total = document.getElementById("user2-total");
      const grandTotal = document.getElementById("grand-total");

      if (users.length >= 1) {
        user1Total.textContent = "$" + (totals[users[0]] || 0).toLocaleString();
      } else {
        user1Total.textContent = "$0";
      }

      if (users.length >= 2) {
        user2Total.textContent = "$" + (totals[users[1]] || 0).toLocaleString();
      } else {
        user2Total.textContent = "$0";
      }

      grandTotal.textContent = "$" + totalAmount.toLocaleString();
    }

    dateInput.addEventListener("change", updateTotals);

    const thirtyDaysAgo = new Date();
    thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
    dateInput.value = thirtyDaysAgo.toISOString().split("T")[0];

    setTimeout(updateTotals, 1000);
  }

  function init() {
    if (findInsertionPoint()) return;
    if (attempts < maxAttempts) setTimeout(init, 500);
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }
})();