// ==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];
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));
}
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]);
}
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;
});
}
if (searchTerm.trim() !== '') {
const term = searchTerm.toLowerCase();
result = result.filter(item => {
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);
}
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;
}
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);
}
}
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);
}
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');
}
const interval = setInterval(() => {
if (document.querySelector('.transaction-table')) {
init();
clearInterval(interval);
}
}, 1000);
})();2 views