Esketit - Add Net Return to Statement

Proving that it's important

// ==UserScript==
// @name         Esketit - Add Net Return to Statement
// @namespace    http://esketit.com/
// @version      2025-05-01
// @description  Proving that it's important
// @author       rs232
// @match        https://*esketit.com/investor/account-statement
// @icon         https://www.google.com/s2/favicons?sz=32&domain_url=https%3A%2F%2Fwww.esketit.com
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Helper: parse a currency string (e.g. "€2 089,10") to a number.
    function parseCurrency(value) {
        return parseFloat(value.replace('€', '').replace(/\s/g, '').replace(',', '.')) || 0;
    }

    // Calculate Net Return from the 5 rows in the Summary table.
    function calculateNetReturn() {
        const rows = [
            "Interest received",
            "Bonus received",
            "Referral bonus received",
            "Secondary market income",
            "Secondary market expense"
        ];
        let total = 0;
        rows.forEach(rowLabel => {
            const row = document.evaluate(
                `//tr[td[text()='${rowLabel}']]`,
                document,
                null,
                XPathResult.FIRST_ORDERED_NODE_TYPE,
                null
            ).singleNodeValue;
            if (row) {
                const valueCell = row.querySelector('td:nth-child(2)');
                if (valueCell) {
                    const valueText = valueCell.textContent.replace('€', '').replace(/\s/g, '').replace(',', '.');
                    total += parseCurrency(valueText);
                }
            }
        });
        return total;
    }

    // Update the Net Return row below the Closing Balance row.
    function addNetReturnRow() {
        const closingBalanceRow = document.evaluate(
            "//tr[td[text()='Closing balance']]",
            document,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null
        ).singleNodeValue;
        if (closingBalanceRow) {
            const netReturn = calculateNetReturn();
            const formattedNetReturn = `€${netReturn.toFixed(2).replace('.', ',').replace(/\B(?=(\d{3})+(?!\d))/g, '.')}`;
            let netReturnRow = document.getElementById('net-return-row');
            if (!netReturnRow) {
                netReturnRow = document.createElement('tr');
                netReturnRow.id = 'net-return-row';
                netReturnRow.setAttribute('data-v-344f568a', '');
                closingBalanceRow.parentNode.insertBefore(netReturnRow, closingBalanceRow.nextSibling);
            }
            netReturnRow.innerHTML = `
                <td data-v-344f568a="" style="font-weight: bold; color: green;">Net return</td>
                <td data-v-344f568a="" style="text-align: right; font-weight: bold; color: green;">${formattedNetReturn}</td>
            `;
            // Always display the Net Return row with a pale green background.
            netReturnRow.style.backgroundColor = "#e6ffe6";
            // Colorize the Summary rows after updating the Net Return value.
            colorizeSummaryRows();
        } else {
            console.warn("Closing Balance row not found.");
        }
    }

    // Colorizes each Summary row:
    // - For "Interest received", "Bonus received", "Referral bonus received", and "Secondary market income":
    //   if their value > 0, set the label and value text to green and the row background to a pale green.
    // - For "Secondary market expense": if its value is negative, set the label and value text to red and the row background to a pale red.
    function colorizeSummaryRows() {
        const greenRows = [
            "Interest received",
            "Bonus received",
            "Referral bonus received",
            "Secondary market income"
        ];
        greenRows.forEach(label => {
            const row = document.evaluate(
                `//tr[td[text()='${label}']]`,
                document,
                null,
                XPathResult.FIRST_ORDERED_NODE_TYPE,
                null
            ).singleNodeValue;
            if (row) {
                const valueCell = row.querySelector('td:nth-child(2)');
                if (valueCell) {
                    const value = parseCurrency(valueCell.textContent);
                    if (value > 0) {
                        row.querySelector('td:nth-child(1)').style.color = "green";
                        row.querySelector('td:nth-child(2)').style.color = "green";
                        row.style.backgroundColor = "#e6ffe6"; // pale green background
                    } else {
                        row.querySelector('td:nth-child(1)').style.color = "";
                        row.querySelector('td:nth-child(2)').style.color = "";
                        row.style.backgroundColor = "";
                    }
                }
            }
        });
        // For "Secondary market expense"
        const redLabel = "Secondary market expense";
        const redRow = document.evaluate(
            `//tr[td[text()='${redLabel}']]`,
            document,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null
        ).singleNodeValue;
        if (redRow) {
            const valueCell = redRow.querySelector('td:nth-child(2)');
            if (valueCell) {
                const value = parseCurrency(valueCell.textContent);
                if (value < 0) {
                    redRow.querySelector('td:nth-child(1)').style.color = "red";
                    redRow.querySelector('td:nth-child(2)').style.color = "red";
                    redRow.style.backgroundColor = "#ffe6e6"; // pale red background
                } else {
                    redRow.querySelector('td:nth-child(1)').style.color = "";
                    redRow.querySelector('td:nth-child(2)').style.color = "";
                    redRow.style.backgroundColor = "";
                }
            }
        }
    }

    // Debounce helper to avoid multiple rapid recalculations.
    let recalcTimeout;
    function scheduleRecalculation() {
        if (recalcTimeout) clearTimeout(recalcTimeout);
        recalcTimeout = setTimeout(addNetReturnRow, 50);
    }

    // Monitor the value cells for changes.
    function monitorValueChanges() {
        const rows = [
            "Interest received",
            "Bonus received",
            "Referral bonus received",
            "Secondary market income",
            "Secondary market expense"
        ];
        rows.forEach(rowLabel => {
            const row = document.evaluate(
                `//tr[td[text()='${rowLabel}']]`,
                document,
                null,
                XPathResult.FIRST_ORDERED_NODE_TYPE,
                null
            ).singleNodeValue;
            if (row) {
                const valueCell = row.querySelector('td:nth-child(2)');
                if (valueCell) {
                    const observer = new MutationObserver(scheduleRecalculation);
                    observer.observe(valueCell, { childList: true, characterData: true, subtree: true });
                }
            }
        });
    }

    // On page load, add the Net Return row and start monitoring.
    window.addEventListener('load', () => {
        addNetReturnRow();
        monitorValueChanges();
    });
})();