JustPaste
HomeCategoriesAboutDonateContactTerms of UsePrivacy Policy
JustPaste

Free online notepad — write and share instantly

Navigate

  • Home
  • Timeline
  • Categories

Info

  • About
  • Donate
  • Contact

Legal

  • Terms of Use
  • Privacy Policy

© 2026 JustPaste.app. All rights reserved.

Made with ♥ by JustPaste

Untitled Page | JustPaste.app
about 2 hours ago2 views
👨‍💻Programming

Untitled Page

// ==UserScript==
// @name         SBI YONO VIEW PAGE 24062026

// @namespace    http://tampermonkey.net/
// @version      5.1
// @description  Safely add Export as PDF beside SBI Download button
// @match        https://yonoretail.sbi.bank.in/*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.4.0/jspdf.umd.min.js
// @run-at       document-idle
// ==/UserScript==
(function () {
    'use strict';

    // ─────────────────────────────────────────────────────────────────────────
    // CONFIGURATION
    // ─────────────────────────────────────────────────────────────────────────
    const rowsPerPage   = 10;
    const pagesPerGroup = 5;

    // ─────────────────────────────────────────────────────────────────────────
    // STATE VARIABLES
    // These live in the IIFE closure — they persist across re-renders
    // because they are JS variables, NOT DOM attributes.
    // ─────────────────────────────────────────────────────────────────────────
    let currentPage     = 1;
    let currentGroup    = 1;
    let searchTerm      = '';       // ← always holds the latest typed value
    let showDebit       = true;
    let showCredit      = true;
    let activeDateRange = null;
    let minAmount       = null;
    let maxAmount       = null;

    // ─────────────────────────────────────────────────────────────────────────
    // DATA  (replace this array with your real data or fetch call)
    // ─────────────────────────────────────────────────────────────────────────
    const myData = [];

    let filteredData = [...myData];

    // ─────────────────────────────────────────────────────────────────────────
    // DATE UTILITIES
    // ─────────────────────────────────────────────────────────────────────────

    /*
     * THEORY: parseDate
     * SBI stores dates as "DD/MM/YYYY" but JS Date() expects "YYYY-MM-DD".
     * We split the string and manually pass (year, month-1, day) to new Date().
     * Month is 0-indexed in JS: Jan=0, Dec=11.
     *
     * Example: "01/05/2020" → new Date(2020, 4, 1) → May 1 2020
     */
    function parseDate(str) {
        if (!str) return null;
        const parts = str.split('/');
        if (parts.length !== 3) return null;
        const [d, m, y] = parts;
        return new Date(Number(y), Number(m) - 1, Number(d));
    }

    // ─────────────────────────────────────────────────────────────────────────
    // DATE RANGE CAPTURE FROM SBI PAGE
    // ─────────────────────────────────────────────────────────────────────────

    /*
     * THEORY: captureDateRange
     * SBI's Angular app renders a text like:
     *   "Showing transactions from 01/05/2020 To 31/05/2020"
     * We use a regex to extract both dates from that rendered text.
     * We cannot read SBI's internal Angular component state directly —
     * only the DOM text is accessible to us.
     */
    function captureDateRange() {
        const el = document.querySelector('.result-div .result');
        if (!el) { activeDateRange = null; return; }

        const text  = el.innerText || el.textContent || '';
        const match = text.match(/from\s+(\d{2}\/\d{2}\/\d{4})\s+To\s+(\d{2}\/\d{2}\/\d{4})/i);

        if (!match) { activeDateRange = null; return; }

        const fromDate = parseDate(match[1]);
        const toDate   = parseDate(match[2]);
        if (!fromDate || !toDate) { activeDateRange = null; return; }

        toDate.setHours(23, 59, 59, 999); // include the entire last day
        activeDateRange = { from: fromDate, to: toDate };
        console.log('[SBI Script] Date range captured:', match[1], '→', match[2]);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // MASTER FILTER — single source of truth for all filtering
    // ─────────────────────────────────────────────────────────────────────────

    /*
     * THEORY: applyAllFilters
     * We always start from the FULL dataset (myData) and chain filters one by one.
     * Each filter returns a smaller subset. The final subset becomes filteredData.
     *
     * Why start from myData every time?
     * If we filtered filteredData, we could never "undo" a filter —
     * data removed in step 1 would be gone forever for step 2.
     *
     * Filter order (cheapest → most expensive):
     *   1. Date range   — simple date comparison
     *   2. Debit/Credit — simple boolean
     *   3. Amount range — simple number comparison
     *   4. Search term  — string.includes() on multiple fields (most expensive)
     *
     * REAL-TIME USE CASE:
     * User types "DARA" → term = "dara"
     * item["Details"] = "TO TRANSFER-UPI/DR/.../DARA SAM/..."
     * "to transfer-upi/dr/.../dara sam/...".includes("dara") → TRUE → row shown
     */
    function applyAllFilters() {

        let result = myData; // ← always reset to full data before filtering

        // ── FILTER 1: Date Range ────────────────────────────────────────────
        if (activeDateRange) {
            result = result.filter(item => {
                const itemDate = parseDate(item["Value Date"]);
                if (!itemDate) return false;
                return itemDate >= activeDateRange.from && itemDate <= activeDateRange.to;
            });
        }

        // ── FILTER 2: Debit / Credit Toggle ────────────────────────────────
        result = result.filter(item => {
            const hasDebit  = Number(item.Debit  || 0) > 0;
            const hasCredit = Number(item.Credit || 0) > 0;
            if (showDebit  && showCredit)  return true;
            if (showDebit  && !showCredit) return hasDebit;
            if (showCredit && !showDebit)  return hasCredit;
            return false;
        });

        // ── FILTER 3: Amount Range ──────────────────────────────────────────
        if (minAmount !== null || maxAmount !== null) {
            result = result.filter(item => {
                const amount = Number(item.Debit || 0) > 0
                    ? Number(item.Debit)
                    : Number(item.Credit || 0);
                if (minAmount !== null && amount < minAmount) return false;
                if (maxAmount !== null && amount > maxAmount) return false;
                return true;
            });
        }

        // ── FILTER 4: Search Term ───────────────────────────────────────────
        /*
         * ROOT CAUSE FIX — why search wasn't working:
         *
         * The filter logic here was always CORRECT.
         * The problem was that addSearchBar() depended on finding SBI's input:
         *   document.querySelector('input.inp[placeholder*="Search by name"]')
         * If that element didn't exist (wrong selector, or Angular hadn't rendered it),
         * the function returned early → our <input> was NEVER injected into the page →
         * user was typing into nothing → searchTerm stayed '' forever.
         *
         * FIX: addSearchBar() now injects our search bar ABOVE the transaction table
         * using a guaranteed anchor (.transaction-table), completely independent
         * of whether SBI's own search input exists or not.
         *
         * Now when user types → input event fires → searchTerm updates → this
         * filter runs correctly.
         *
         * Search covers: Details, Debit, Credit, Cheq No, Balance
         * All lowercased for case-insensitive matching.
         */
        if (searchTerm.trim() !== '') {
            const term = searchTerm.toLowerCase();
            result = result.filter(item => {
                /*
                 * THEORY — why String() is required here:
                 *
                 * item["Cheq No"]  can be a NUMBER like 4897691162095
                 * item["Debit"]    is always a number like 450
                 * item["Credit"]   is "" (empty string) OR a number
                 * item["Balance"]  is always a number like 12500
                 *
                 * The bug:  (item["Cheq No"] || "").toLowerCase()
                 *   → item["Cheq No"] = 4897691162095  (truthy number)
                 *   → || "" never triggers  (because number is truthy)
                 *   → 4897691162095.toLowerCase()  ← TypeError! numbers have no toLowerCase
                 *
                 * The fix: String(item["Cheq No"] ?? '')
                 *   → String(4897691162095) = "4897691162095"  (always a string)
                 *   → "4897691162095".toLowerCase()  ← works fine
                 *
                 * We use ?? (nullish coalescing) instead of || so that
                 * 0 (a valid amount) is not treated as falsy and replaced with ''.
                 * || would convert 0 → '' and miss searching for "0".
                 * ?? only replaces null / undefined, not 0 or false.
                 */
                const details = String(item["Details"]  ?? '').toLowerCase();
                const cheqno  = String(item["Cheq No"]  ?? '').toLowerCase();
                const debit   = String(item["Debit"]    ?? '');
                const credit  = String(item["Credit"]   ?? '');
                const balance = String(item["Balance"]  ?? '');

                return (
                    details.includes(term)  ||
                    cheqno.includes(term)   ||
                    debit.includes(term)    ||
                    credit.includes(term)   ||
                    balance.includes(term)
                );
            });
        }

        filteredData = result;
        currentPage  = 1;
        currentGroup = 1;
        renderTable();
    }

    function applyFilter() {
        captureDateRange();
        applyAllFilters();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // STYLES
    // ─────────────────────────────────────────────────────────────────────────
    function addStyles() {
        if (document.getElementById('tm-style')) return;
        const style = document.createElement('style');
        style.id = 'tm-style';
        style.innerHTML = `
            .transaction-table td:nth-child(3),
            .transaction-table td:nth-child(4) { text-align: right; }
            .transaction-table td { padding: 8px 15px; }

            .tm-amount-cell {
                display: inline-flex;
                align-items: center;
                gap: 5px;
                justify-content: flex-end;
                width: 100%;
            }
            .tm-arrow-icon {
                width: 16px;
                height: 16px;
                flex-shrink: 0;
            }

            /* ── Search bar container ── */
            #tm-search-container {
                display: flex;
                align-items: center;
                gap: 8px;
                margin: 10px 0;
                padding: 6px 0;
            }
            #tm-search-label {
                font-size: 13px;
                font-weight: 600;
                color: rgb(88,83,171);
                white-space: nowrap;
            }
            #tm-search-input {
                padding: 7px 12px;
                border: 1.5px solid #ccc;
                border-radius: 4px;
                font-size: 14px;
                width: 320px;
                outline: none;
                transition: border-color 0.2s, box-shadow 0.2s;
            }
            #tm-search-input:focus {
                border-color: rgb(88,83,171);
                box-shadow: 0 0 0 2px rgba(88,83,171,0.18);
            }
            #tm-search-clear {
                cursor: pointer;
                color: #fff;
                font-size: 14px;
                background: #aaa;
                border: none;
                border-radius: 50%;
                width: 20px;
                height: 20px;
                line-height: 18px;
                text-align: center;
                padding: 0;
                display: none;
                flex-shrink: 0;
            }
            #tm-search-clear:hover { background: #888; }
            #tm-search-count {
                font-size: 12px;
                color: #777;
                white-space: nowrap;
            }
        `;
        document.head.appendChild(style);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // ARROW ICON HELPER
    // ─────────────────────────────────────────────────────────────────────────

    /*
     * THEORY: createArrowIcon
     * We reuse SBI's own SVG assets (already loaded on the page).
     * CSS filter is used to tint the grayscale SVG:
     *   - Credit (money in)  → green  (hue-rotate ~120deg = green)
     *   - Debit  (money out) → red    (hue-rotate ~330deg = red)
     * This avoids loading any external icon library.
     */
    function createArrowIcon(type) {
        const img = document.createElement('img');
        img.classList.add('tm-arrow-icon');
        img.setAttribute('loading', 'lazy');

        if (type === 'credit') {
            img.src   = '../../../../../../assets/images/landing-page/down_arrow_new.svg';
            img.alt   = 'Credit';
            img.title = 'Credit (Money In)';
            img.style.filter = 'invert(40%) sepia(100%) saturate(400%) hue-rotate(90deg)';
        } else {
            img.src   = '../../../../../../assets/images/landing-page/up_arrow_new.svg';
            img.alt   = 'Debit';
            img.title = 'Debit (Money Out)';
            img.style.filter = 'invert(20%) sepia(100%) saturate(500%) hue-rotate(330deg)';
        }

        return img;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // TABLE RENDERER
    // ─────────────────────────────────────────────────────────────────────────

    /*
     * THEORY: renderTable
     * We wipe tbody.innerHTML and redraw only the rows for the current page.
     * This is called after EVERY filter change or page change.
     *
     * FIX: forEach + return → for..of + continue
     * return inside forEach only exits the arrow function callback for that
     * iteration — the outer forEach loop keeps going AND the row still gets
     * appended because we hadn't returned BEFORE appendChild().
     * for..of + continue correctly jumps to the next iteration.
     */
    function renderTable() {
        const tbody = document.querySelector('.transaction-table tbody');
        if (!tbody) return;

        tbody.innerHTML = '';

        const start    = (currentPage - 1) * rowsPerPage;
        const pageData = filteredData.slice(start, start + rowsPerPage);

        // Update result count label in search bar
        const countEl = document.getElementById('tm-search-count');
        if (countEl) {
            countEl.textContent = searchTerm.trim()
                ? `${filteredData.length} result${filteredData.length !== 1 ? 's' : ''} found`
                : '';
        }

        if (pageData.length === 0) {
            tbody.innerHTML = `
                <tr>
                    <td colspan="4" style="text-align:center;color:#999;padding:20px;">
                        No Transactions Found
                    </td>
                </tr>`;
            renderPagination();
            return;
        }

        // ✅ for..of so 'continue' works correctly (return inside forEach does NOT skip rows)
        for (const item of pageData) {
            if (item["Details"] && item["Details"].includes("Balance As On")) continue;

            const tr     = document.createElement('tr');
            const debit  = Number(item.Debit  || 0);
            const credit = Number(item.Credit || 0);

            // Date
            const tdDate = document.createElement('td');
            tdDate.textContent = item["Value Date"];

            // Details
            const tdDetails = document.createElement('td');
            tdDetails.textContent = item["Details"];

            // Amount with arrow icon
            const tdAmount = document.createElement('td');
            if (debit > 0 || credit > 0) {
                const wrapper    = document.createElement('div');
                wrapper.classList.add('tm-amount-cell');

                const isDebit    = debit > 0;
                const amountVal  = isDebit ? debit.toFixed(2) : credit.toFixed(2);
                const arrowImg   = createArrowIcon(isDebit ? 'debit' : 'credit');

                const amountText = document.createElement('span');
                amountText.textContent  = amountVal;
                amountText.style.color  = isDebit ? '#d32f2f' : '#2e7d32';
                amountText.style.fontWeight = '500';

                wrapper.appendChild(arrowImg);
                wrapper.appendChild(amountText);
                tdAmount.appendChild(wrapper);
            }

            // Balance
            const tdBalance = document.createElement('td');
            tdBalance.textContent = Number(item["Balance"]).toFixed(2);

            tr.appendChild(tdDate);
            tr.appendChild(tdDetails);
            tr.appendChild(tdAmount);
            tr.appendChild(tdBalance);
            tbody.appendChild(tr);
        }

        renderPagination();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PAGINATION RENDERER
    // ─────────────────────────────────────────────────────────────────────────

    /*
     * THEORY: Grouped pagination
     * We show pages in groups of pagesPerGroup (5).
     * Page group 1 → pages 1-5, group 2 → pages 6-10, etc.
     * Prev/Next buttons jump between groups.
     * Active page is highlighted in purple.
     */
    function renderPagination() {
        let container = document.getElementById('custom-pagination');
        if (!container) {
            container = document.createElement('div');
            container.id = 'custom-pagination';
            container.style.cssText = 'margin:15px; text-align:center;';
            document.querySelector('.transaction-table').after(container);
        }

        container.innerHTML = '';
        const totalPages = Math.ceil(filteredData.length / rowsPerPage);
        if (totalPages <= 1) return;

        const startPage = (currentGroup - 1) * pagesPerGroup + 1;
        const endPage   = Math.min(startPage + pagesPerGroup - 1, totalPages);

        if (currentGroup > 1) {
            const prevBtn = document.createElement('button');
            prevBtn.innerText = 'Prev';
            prevBtn.style.cssText = 'margin:5px; padding:4px 10px; cursor:pointer;';
            prevBtn.onclick = () => {
                currentGroup--;
                currentPage = startPage - pagesPerGroup;
                renderTable();
            };
            container.appendChild(prevBtn);
        }

        for (let i = startPage; i <= endPage; i++) {
            const btn = document.createElement('button');
            btn.innerText = i;
            btn.style.cssText = 'margin:5px; padding:4px 10px; cursor:pointer;';
            if (i === currentPage) {
                btn.style.background = 'rgb(88,83,171)';
                btn.style.color = '#fff';
            }
            btn.onclick = () => { currentPage = i; renderTable(); };
            container.appendChild(btn);
        }

        if (endPage < totalPages) {
            const nextBtn = document.createElement('button');
            nextBtn.innerText = 'Next';
            nextBtn.style.cssText = 'margin:5px; padding:4px 10px; cursor:pointer;';
            nextBtn.onclick = () => {
                currentGroup++;
                currentPage = endPage + 1;
                renderTable();
            };
            container.appendChild(nextBtn);
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // SEARCH BAR — ROOT CAUSE FIX
    // ─────────────────────────────────────────────────────────────────────────

    /*
     * THEORY: addSearchBar — why the old approach failed
     *
     * OLD approach:
     *   const sbiInput = document.querySelector('input.inp[placeholder*="Search by name"]');
     *   if (!sbiInput) return;   ← PROBLEM: silently exits if SBI's input not found
     *   sbiInput.parentNode.replaceChild(wrapper, sbiInput);
     *
     * Problems with old approach:
     *   1. If SBI's input selector doesn't match (different placeholder, class change,
     *      Angular hasn't rendered it yet) → sbiInput = null → function returns immediately
     *      → our search bar is NEVER injected → user types into nothing.
     *
     *   2. Even if it was injected once, when Angular re-renders the component,
     *      it recreates its own input, but our wrapper was placed INSIDE SBI's
     *      component tree — Angular's re-render wipes it.
     *
     * NEW approach (FIXED):
     *   - We inject our search bar BEFORE the transaction table using
     *     table.parentNode.insertBefore(..., table)
     *   - This anchor (.transaction-table) is guaranteed to exist at init time
     *     because we only call init() AFTER the table is detected.
     *   - Our search bar is OUTSIDE Angular's component tree so re-renders
     *     don't affect it.
     *   - We still try to hide SBI's original search input to avoid duplicates,
     *     but we don't DEPEND on finding it.
     *
     * REAL-TIME USE CASE:
     * User types "DARA" → input event fires → searchTerm = "dara"
     * → applyAllFilters() runs → Details field "...DARA SAM..."
     * → "...dara sam...".includes("dara") = true → row appears in results.
     */
    function addSearchBar() {
        // Don't duplicate if already injected
        if (document.getElementById('tm-search-container')) return;

        const table = document.querySelector('.transaction-table');
        if (!table) return;

        // ── Build search container ─────────────────────────────────────────
        const container = document.createElement('div');
        container.id = 'tm-search-container';

        // Label
        const label = document.createElement('span');
        label.id = 'tm-search-label';
        label.textContent = '🔍 Search:';

        // Input
        const searchInput = document.createElement('input');
        searchInput.id    = 'tm-search-input';
        searchInput.type  = 'text';
        searchInput.placeholder = 'Type name, amount, UPI ID, cheque no...';
        searchInput.value = searchTerm; // restore previous value if re-attaching

        // Clear (×) button
        const clearBtn = document.createElement('button');
        clearBtn.id          = 'tm-search-clear';
        clearBtn.textContent = '×';
        clearBtn.title       = 'Clear search';
        clearBtn.style.display = searchTerm ? 'inline' : 'none';

        // Result count label  e.g. "2 results found"
        const countEl = document.createElement('span');
        countEl.id = 'tm-search-count';

        // ── Event Listeners ────────────────────────────────────────────────
        /*
         * 'input' fires on every single keystroke (not just on blur or Enter).
         * This gives live/instant filtering as the user types.
         * searchTerm is updated first, then applyAllFilters() rerenders the table.
         */
        searchInput.addEventListener('input', function () {
            searchTerm = this.value;                                   // update state
            clearBtn.style.display = searchTerm ? 'inline' : 'none';  // show/hide ×
            applyAllFilters();                                         // refilter + rerender
        });

        clearBtn.addEventListener('click', function () {
            searchInput.value      = '';
            searchTerm             = '';
            clearBtn.style.display = 'none';
            countEl.textContent    = '';
            applyAllFilters();
            searchInput.focus();
        });

        // Also allow Enter key to trigger filter explicitly
        searchInput.addEventListener('keydown', function (e) {
            if (e.key === 'Enter') applyAllFilters();
        });

        container.appendChild(label);
        container.appendChild(searchInput);
        container.appendChild(clearBtn);
        container.appendChild(countEl);

        // ── Inject BEFORE the table (outside Angular's component tree) ─────
        table.parentNode.insertBefore(container, table);

        // ── Optionally hide SBI's own search input to avoid confusion ──────
        // We don't DEPEND on finding it — just hide it if it exists
        const sbiInput = document.querySelector('input.inp[placeholder*="Search by name"]');
        if (sbiInput) {
            const sbiWrapper = sbiInput.closest('.search-wrapper, .input-wrapper, mat-form-field') || sbiInput.parentNode;
            if (sbiWrapper) sbiWrapper.style.display = 'none';
        }

        console.log('[SBI Script] Search bar injected successfully');
    }

    // ─────────────────────────────────────────────────────────────────────────
    // AMOUNT RANGE INPUTS HOOK
    // ─────────────────────────────────────────────────────────────────────────

    /*
     * THEORY: hookAmountInputs — polling for Angular Material inputs
     * Angular Material inputs (mat-input) are created dynamically by Angular.
     * They don't exist at page load — they appear after Angular bootstraps.
     * We use setInterval to poll every 500ms. Once both inputs appear,
     * we attach our listeners and clear the interval.
     */
    function hookAmountInputs() {
        const amountInterval = setInterval(() => {
            const minInput = document.getElementById('mat-input-0');
            const maxInput = document.getElementById('mat-input-1');
            if (!minInput || !maxInput) return;

            clearInterval(amountInterval);

            minInput.addEventListener('input', function () {
                const val = this.value.trim();
                minAmount = val === '' ? null : Number(val);
                applyAllFilters();
            });

            maxInput.addEventListener('input', function () {
                const val = this.value.trim();
                maxAmount = val === '' ? null : Number(val);
                applyAllFilters();
            });

            [minInput, maxInput].forEach(input => {
                input.addEventListener('keydown', e => {
                    if (e.key === 'Enter') applyAllFilters();
                });
            });

            console.log('[SBI Script] Amount inputs hooked');
        }, 500);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // CHECKBOX HOOKS
    // ─────────────────────────────────────────────────────────────────────────

    /*
     * THEORY: hookCheckboxes — same polling pattern as hookAmountInputs
     * Angular renders the debit/credit checkboxes after bootstrap.
     * We poll until they exist, read their initial state, then listen for changes.
     */
    function hookCheckboxes() {
        const checkInterval = setInterval(() => {

            // BUG FIX 1: Dynamic ID — Angular generates IDs like "1302926099Debit0"
            // The prefix number changes every session. The SUFFIX is always stable.
            // Use attribute ends-with selector ($=) instead of getElementById.
            const debitCheckbox  = document.querySelector('[id$="Debit0"]');
            const creditCheckbox = document.querySelector('[id$="Credit1"]');

            // BUG FIX 2: Also find SBI's Apply button by its class
            // Filter should trigger when user clicks Apply, not on checkbox change
            const applyBtn = document.querySelector('button.Apply');

            if (!debitCheckbox || !creditCheckbox || !applyBtn) return;

            clearInterval(checkInterval);

            // BUG FIX 3: Angular checkboxes use aria-checked, not just .checked
            // Read both and combine — whichever is true wins
            function isChecked(el) {
                return el.checked || el.getAttribute('aria-checked') === 'true';
            }

            // Read initial state on hook
            showDebit  = isChecked(debitCheckbox);
            showCredit = isChecked(creditCheckbox);

            // Hook the Apply button click — this is when SBI applies the filter
            // We read checkbox states AT THAT MOMENT so we always get latest values
            applyBtn.addEventListener('click', function () {
                // Wait 100ms — Angular updates aria-checked AFTER the click event
                setTimeout(() => {
                    showDebit  = isChecked(debitCheckbox);
                    showCredit = isChecked(creditCheckbox);

                    // If neither checked (both unchecked) = show everything
                    if (!showDebit && !showCredit) {
                        showDebit  = true;
                        showCredit = true;
                    }

                    console.log('[SBI Script] Apply clicked | debit:', showDebit, '| credit:', showCredit);
                    captureDateRange();
                    applyAllFilters();
                }, 100);
            });

            console.log('[SBI Script] Checkboxes hooked | IDs:', debitCheckbox.id, creditCheckbox.id);
        }, 500);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // FILTER BUTTON
    // ─────────────────────────────────────────────────────────────────────────
    function addFilterButton() {
        if (document.getElementById('tm-filter-btn')) return;

        const btn = document.createElement('button');
        btn.id        = 'tm-filter-btn';
        btn.innerText = 'Apply Duration Filter';
        btn.style.cssText = `
            margin: 10px 0;
            padding: 6px 14px;
            background: rgb(88,83,171);
            color: white;
            border: none;
            cursor: pointer;
            border-radius: 4px;
            font-size: 14px;
        `;
        btn.onclick = applyFilter;

        const table = document.querySelector('.transaction-table');
        table.parentNode.insertBefore(btn, table);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // MUTATION OBSERVER — keeps our injected UI alive after Angular re-renders
    // ─────────────────────────────────────────────────────────────────────────

    /*
     * THEORY: MutationObserver
     * SBI YONO is an Angular SPA. Navigation and filter actions cause Angular
     * to destroy and recreate its components. This removes elements we injected
     * INSIDE Angular's component tree.
     *
     * MutationObserver watches the entire document body for any DOM changes
     * (childList: true, subtree: true). When we detect our search container
     * is missing (removed by Angular), we re-inject it.
     *
     * Our NEW approach places the search bar BEFORE the table (outside Angular's
     * component), so it survives most re-renders. The observer is a safety net
     * for cases where the entire table area gets replaced.
     *
     * childList: true → fires when direct children are added/removed
     * subtree:   true → also fires for all descendant changes
     */
    function watchForDOMChanges() {
        const observer = new MutationObserver(() => {
            const table       = document.querySelector('.transaction-table');
            const myContainer = document.getElementById('tm-search-container');
            const myBtn       = document.getElementById('tm-filter-btn');

            if (table && !myContainer) {
                console.log('[SBI Script] Re-injecting search bar after DOM change');
                addSearchBar();
            }
            if (table && !myBtn) {
                addFilterButton();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree:   true
        });
    }

    // ─────────────────────────────────────────────────────────────────────────
    // INIT
    // ─────────────────────────────────────────────────────────────────────────
    function init() {
        const table = document.querySelector('.transaction-table');
        if (!table) return;

        addStyles();
        addSearchBar();       // ← inject search bar FIRST (before renderTable so count works)
        addFilterButton();
        renderTable();
        hookCheckboxes();
        hookAmountInputs();
        watchForDOMChanges();

        console.log('[SBI Script] Initialized successfully');
    }

    // ─────────────────────────────────────────────────────────────────────────
    // ENTRY POINT — poll until transaction table exists
    // ─────────────────────────────────────────────────────────────────────────

    /*
     * THEORY: Why polling instead of DOMContentLoaded?
     * DOMContentLoaded fires when the HTML is parsed, but Angular hasn't
     * rendered its components yet. The .transaction-table element only exists
     * after Angular's change detection cycle completes.
     * Polling every 1 second is simple and reliable for this use case.
     */
    const interval = setInterval(() => {
        if (document.querySelector('.transaction-table')) {
            init();
            clearInterval(interval);
        }
    }, 1000);

})();
← Back to timeline