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.8
// @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);

    container.appendChild(dateDiv);


    const editSection = document.createElement("div");
    editSection.id = "upkeep-edit-section";
    editSection.style.cssText = `
        display: none; /* This will be controlled by the Edit button */
        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>
    `;

    // Move the date selector inside the new edit section and give its input a proper ID
    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;
      
      // Try to determine which part is time (has colons) and which is date (has slashes)
      let timePart, datePart;
      if (part1.includes(":")) {
        timePart = part1;
        datePart = part2;
      } else if (part2.includes(":")) {
        timePart = part2;
        datePart = part1;
      } else {
        return null; // Neither part looks like time
      }
      
      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();
    }

    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 totals = {};
      let totalAmount = 0;
      let users = [];

      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;

        // Try both possible orders to handle different HTML structures
        const dateTimeString1 = dateSpan.textContent.trim() + " " + timeSpan.textContent.trim();
        const dateTimeString2 = timeSpan.textContent.trim() + " " + dateSpan.textContent.trim();
        
        // Use whichever one parses successfully
        const dateTimeString = parseDateTime(dateTimeString1) ? dateTimeString1 : dateTimeString2;
        const date = parseDateTime(dateTimeString);

        if (!startDate || (date && date >= startDate)) {
          const amount = parseAmount(amountLi.textContent);
          const username = getCleanUsername(userAnchor);

          if (!totals[username]) {
            totals[username] = 0;
            users.push(username);
          }
          totals[username] += amount;
          totalAmount += 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();
  }
})();