Skip to content

Instantly share code, notes, and snippets.

@inertia186
Last active April 18, 2026 01:17
Show Gist options
  • Select an option

  • Save inertia186/bcd0279715a13adbc8d701caeacb465f to your computer and use it in GitHub Desktop.

Select an option

Save inertia186/bcd0279715a13adbc8d701caeacb465f to your computer and use it in GitHub Desktop.
fix-lop-org
// ==UserScript==
// @name fix-lop-org
// @namespace https://gist.github.com/inertia186/bcd0279715a13adbc8d701caeacb465f
// @version 1.0.5
// @description Public-facing monkey menu for lop.org: open a replacement navigation panel built from #textured-cssmenu.
// @match https://www.lop.org/*
// @match https://lop.org/*
// @updateURL https://gist.githubusercontent.com/inertia186/bcd0279715a13adbc8d701caeacb465f/raw/fix-lop-org.user.js
// @downloadURL https://gist.githubusercontent.com/inertia186/bcd0279715a13adbc8d701caeacb465f/raw/fix-lop-org.user.js
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const SCRIPT_VERSION = '1.0.5';
const ROOT_SELECTOR = '#textured-cssmenu';
const POA_FORM_SELECTOR = 'form[id="_poa_WAR_poaportlet_:poaForm"]';
const POA_HEADER_SELECTOR = '#_poa_WAR_poaportlet_\\:poaForm > h1';
const STATEMENT_HEADING_TEXT = 'Statement Summary';
const RECENT_TRANSACTIONS_HEADING_TEXT = 'Recent Transactions';
const BADGE_ID = 'fix-lop-org-badge';
const PANEL_ID = 'fix-lop-org-panel';
const TREE_ID = 'fix-lop-org-tree';
const BACKDROP_ID = 'fix-lop-org-backdrop';
const STATEMENT_MONKEY_ID = 'fix-lop-org-statement-monkey';
const STATEMENT_PANEL_ID = 'fix-lop-org-statement-panel';
const STATEMENT_BODY_ID = 'fix-lop-org-statement-body';
const RECENT_MONKEY_ID = 'fix-lop-org-recent-monkey';
const RECENT_PANEL_ID = 'fix-lop-org-recent-panel';
const RECENT_BODY_ID = 'fix-lop-org-recent-body';
const state = {
initialized: false,
open: false,
statementOpen: false,
recentOpen: false,
};
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
function log(...args) {
console.log('[fix-lop-org]', `v${SCRIPT_VERSION}`, ...args);
}
function addStyles() {
GM_addStyle(`
#${BADGE_ID} {
position: fixed;
top: 10px;
right: 10px;
z-index: 2147483647;
min-width: 44px;
height: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 0 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.97);
border: 1px solid rgba(0, 0, 0, 0.18);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.18);
font-size: 14px;
line-height: 1;
user-select: none;
cursor: pointer;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: #222;
}
#${BADGE_ID}:hover {
background: rgba(255, 255, 255, 1);
}
#${BADGE_ID} .fix-lop-org-badge-emoji {
font-size: 18px;
}
#${BADGE_ID} .fix-lop-org-badge-version {
font-weight: 600;
color: #333;
}
#${STATEMENT_MONKEY_ID},
#${RECENT_MONKEY_ID} {
display: inline-flex;
align-items: center;
gap: 8px;
margin-left: 12px;
padding: 8px 12px;
border-radius: 999px;
border: 1px solid rgba(0, 0, 0, 0.16);
background: rgba(255, 255, 255, 0.98);
color: #222;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
cursor: pointer;
font-size: 14px;
line-height: 1;
vertical-align: middle;
}
#${STATEMENT_MONKEY_ID}:hover,
#${RECENT_MONKEY_ID}:hover {
background: #fff;
}
#${STATEMENT_PANEL_ID},
#${RECENT_PANEL_ID} {
position: fixed;
top: 56px;
left: 50%;
transform: translateX(-50%);
width: min(760px, calc(100vw - 24px));
max-height: calc(100vh - 72px);
overflow: auto;
z-index: 2147483646;
background: #fff;
color: #222;
border: 1px solid rgba(0, 0, 0, 0.18);
border-radius: 16px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.28);
padding: 16px;
padding-top: 44px;
display: none;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
line-height: 1.4;
}
#${STATEMENT_PANEL_ID}.fix-lop-open,
#${RECENT_PANEL_ID}.fix-lop-open {
display: block;
}
#${STATEMENT_PANEL_ID} .fix-lop-help,
#${RECENT_PANEL_ID} .fix-lop-help {
margin-bottom: 14px;
}
#${STATEMENT_BODY_ID} .fix-lop-summary-grid,
#${RECENT_BODY_ID} .fix-lop-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
#${STATEMENT_BODY_ID} .fix-lop-summary-card,
#${RECENT_BODY_ID} .fix-lop-summary-card {
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: #fafafa;
padding: 12px;
}
#${STATEMENT_BODY_ID} .fix-lop-summary-label,
#${RECENT_BODY_ID} .fix-lop-summary-label {
font-size: 12px;
color: #666;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
#${STATEMENT_BODY_ID} .fix-lop-summary-value,
#${RECENT_BODY_ID} .fix-lop-summary-value {
font-size: 18px;
font-weight: 700;
color: #222;
}
#${STATEMENT_BODY_ID} .fix-lop-section,
#${RECENT_BODY_ID} .fix-lop-section {
margin-top: 18px;
}
#${STATEMENT_BODY_ID} .fix-lop-section h3,
#${RECENT_BODY_ID} .fix-lop-section h3 {
margin: 0 0 10px;
font-size: 15px;
}
#${STATEMENT_BODY_ID} table.fix-lop-summary-table,
#${RECENT_BODY_ID} table.fix-lop-summary-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
#${STATEMENT_BODY_ID} table.fix-lop-summary-table th,
#${STATEMENT_BODY_ID} table.fix-lop-summary-table td,
#${RECENT_BODY_ID} table.fix-lop-summary-table th,
#${RECENT_BODY_ID} table.fix-lop-summary-table td {
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
padding: 8px 10px;
text-align: left;
vertical-align: top;
}
#${STATEMENT_BODY_ID} table.fix-lop-summary-table th:last-child,
#${STATEMENT_BODY_ID} table.fix-lop-summary-table td:last-child,
#${RECENT_BODY_ID} table.fix-lop-summary-table th:last-child,
#${RECENT_BODY_ID} table.fix-lop-summary-table td:last-child {
text-align: right;
white-space: nowrap;
}
#${STATEMENT_BODY_ID} .fix-lop-muted,
#${RECENT_BODY_ID} .fix-lop-muted {
color: #666;
}
#${BACKDROP_ID} {
position: fixed;
inset: 0;
z-index: 2147483645;
background: rgba(0, 0, 0, 0.25);
display: none;
}
#${BACKDROP_ID}.fix-lop-open {
display: block;
}
#${PANEL_ID} {
position: fixed;
top: 56px;
right: 10px;
width: min(520px, calc(100vw - 20px));
max-height: calc(100vh - 72px);
overflow: auto;
z-index: 2147483646;
background: #fff;
color: #222;
border: 1px solid rgba(0, 0, 0, 0.18);
border-radius: 14px;
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
padding: 14px;
display: none;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
#${PANEL_ID}.fix-lop-open {
display: block;
}
#${PANEL_ID} .fix-lop-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
#${PANEL_ID} .fix-lop-title {
font-size: 16px;
font-weight: 700;
}
#${PANEL_ID} .fix-lop-close,
#${STATEMENT_PANEL_ID} .fix-lop-close,
#${RECENT_PANEL_ID} .fix-lop-close {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(0, 0, 0, 0.16);
background: #f7f7f7;
color: #222;
border-radius: 999px;
padding: 0;
cursor: pointer;
font-size: 20px;
line-height: 1;
font-weight: 400;
}
#${STATEMENT_PANEL_ID} .fix-lop-close,
#${RECENT_PANEL_ID} .fix-lop-close {
position: absolute;
top: 12px;
right: 12px;
}
#${PANEL_ID} .fix-lop-help {
margin: 0 0 12px;
font-size: 13px;
color: #555;
}
#${TREE_ID},
#${TREE_ID} ul {
list-style: none;
margin: 0;
padding: 0;
}
#${TREE_ID} ul {
margin-left: 18px;
margin-top: 6px;
}
#${TREE_ID} li {
margin: 4px 0;
}
#${TREE_ID} .fix-lop-row {
display: flex;
align-items: center;
gap: 8px;
min-height: 32px;
}
#${TREE_ID} .fix-lop-toggle {
width: 28px;
height: 28px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.14);
background: #f6f6f6;
color: #222;
cursor: pointer;
flex: 0 0 auto;
}
#${TREE_ID} .fix-lop-toggle[hidden] {
visibility: hidden;
}
#${TREE_ID} .fix-lop-link {
color: #0b57d0;
text-decoration: none;
line-height: 1.35;
}
#${TREE_ID} .fix-lop-link:hover {
text-decoration: underline;
}
#${TREE_ID} li.fix-lop-collapsed > ul {
display: none;
}
#${TREE_ID} .fix-lop-empty {
color: #666;
font-style: italic;
}
`);
}
function createBadge() {
let badge = document.getElementById(BADGE_ID);
if (!badge) {
badge = document.createElement('button');
badge.type = 'button';
badge.id = BADGE_ID;
document.body.appendChild(badge);
}
badge.innerHTML = `<span class="fix-lop-org-badge-emoji">🐒</span><span class="fix-lop-org-badge-version">v${SCRIPT_VERSION}</span>`;
badge.title = `fix-lop-org monkey menu v${SCRIPT_VERSION}`;
badge.setAttribute('aria-label', `Open monkey menu, version ${SCRIPT_VERSION}`);
badge.addEventListener('click', togglePanel);
}
function createPanelShell() {
let backdrop = document.getElementById(BACKDROP_ID);
if (!backdrop) {
backdrop = document.createElement('div');
backdrop.id = BACKDROP_ID;
backdrop.addEventListener('click', closePanel);
document.body.appendChild(backdrop);
}
let panel = document.getElementById(PANEL_ID);
if (!panel) {
panel = document.createElement('aside');
panel.id = PANEL_ID;
panel.setAttribute('aria-label', 'Monkey menu');
panel.innerHTML = `
<div class="fix-lop-header">
<div class="fix-lop-title">🐒 Monkey Menu</div>
<button type="button" class="fix-lop-close" aria-label="Close monkey menu">×</button>
</div>
<p class="fix-lop-help">Replacement navigation built from the page’s own menu links.</p>
<div id="${TREE_ID}"></div>
`;
panel.querySelector('.fix-lop-close')?.addEventListener('click', closePanel);
document.body.appendChild(panel);
}
}
function getMenuRoot() {
return $(ROOT_SELECTOR);
}
function textOfAnchor(anchor) {
return (anchor?.textContent || '').replace(/\s+/g, ' ').trim();
}
function extractNodeFromLi(li) {
const anchor = $(':scope > a', li);
if (!anchor) return null;
const label = textOfAnchor(anchor);
const href = anchor.getAttribute('href') || '';
const children = [];
const directList = $(':scope > ul', li);
if (directList) {
for (const childLi of $$(':scope > li', directList)) {
const childNode = extractNodeFromLi(childLi);
if (childNode) children.push(childNode);
}
}
return {
label: label || href || '(untitled)',
href,
children,
};
}
function extractMenuTree() {
const root = getMenuRoot();
if (!root) return [];
const topUl = $(':scope > ul', root);
if (!topUl) return [];
const tree = [];
for (const li of $$(':scope > li', topUl)) {
const node = extractNodeFromLi(li);
if (node) tree.push(node);
}
return tree;
}
function buildTreeItem(node, depth = 0) {
const li = document.createElement('li');
const row = document.createElement('div');
row.className = 'fix-lop-row';
const toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'fix-lop-toggle';
const hasChildren = node.children.length > 0;
if (hasChildren) {
li.classList.add('fix-lop-collapsed');
toggle.textContent = '▸';
toggle.setAttribute('aria-label', `Expand ${node.label}`);
toggle.addEventListener('click', () => {
const collapsed = li.classList.toggle('fix-lop-collapsed');
toggle.textContent = collapsed ? '▸' : '▾';
toggle.setAttribute('aria-label', `${collapsed ? 'Expand' : 'Collapse'} ${node.label}`);
});
} else {
toggle.hidden = true;
toggle.textContent = '•';
}
const link = document.createElement('a');
link.className = 'fix-lop-link';
link.textContent = node.label;
link.href = node.href || '#';
if (!node.href) {
link.addEventListener('click', (event) => event.preventDefault());
}
row.appendChild(toggle);
row.appendChild(link);
li.appendChild(row);
if (hasChildren) {
const ul = document.createElement('ul');
for (const child of node.children) {
ul.appendChild(buildTreeItem(child, depth + 1));
}
li.appendChild(ul);
}
return li;
}
function renderTree() {
const treeHost = document.getElementById(TREE_ID);
if (!treeHost) return;
treeHost.innerHTML = '';
const nodes = extractMenuTree();
if (!nodes.length) {
const empty = document.createElement('div');
empty.className = 'fix-lop-empty';
empty.textContent = 'No #textured-cssmenu links found on this page.';
treeHost.appendChild(empty);
return;
}
const ul = document.createElement('ul');
for (const node of nodes) {
ul.appendChild(buildTreeItem(node));
}
treeHost.appendChild(ul);
}
function openPanel() {
createPanelShell();
renderTree();
document.getElementById(PANEL_ID)?.classList.add('fix-lop-open');
document.getElementById(BACKDROP_ID)?.classList.add('fix-lop-open');
state.open = true;
}
function closePanel() {
document.getElementById(PANEL_ID)?.classList.remove('fix-lop-open');
document.getElementById(BACKDROP_ID)?.classList.remove('fix-lop-open');
state.open = false;
}
function togglePanel() {
if (state.open) {
closePanel();
} else {
openPanel();
}
}
function createStatementPanelShell() {
let panel = document.getElementById(STATEMENT_PANEL_ID);
if (!panel) {
panel = document.createElement('aside');
panel.id = STATEMENT_PANEL_ID;
panel.setAttribute('aria-label', 'Monkey statement summary');
panel.innerHTML = `
<div class="fix-lop-header">
<div class="fix-lop-title">🐒 Monkey Statement Summary</div>
<button type="button" class="fix-lop-close" aria-label="Close monkey statement summary">×</button>
</div>
<p class="fix-lop-help">A human-readable summary extracted from the page’s own statement data.</p>
<div id="${STATEMENT_BODY_ID}"></div>
`;
panel.querySelector('.fix-lop-close')?.addEventListener('click', closeStatementPanel);
document.body.appendChild(panel);
}
}
function createRecentPanelShell() {
let panel = document.getElementById(RECENT_PANEL_ID);
if (!panel) {
panel = document.createElement('aside');
panel.id = RECENT_PANEL_ID;
panel.setAttribute('aria-label', 'Monkey recent transactions');
panel.innerHTML = `
<div class="fix-lop-header">
<div class="fix-lop-title">🐒 Monkey Recent Transactions</div>
<button type="button" class="fix-lop-close" aria-label="Close monkey recent transactions">×</button>
</div>
<p class="fix-lop-help">A human-readable summary extracted from the page’s visible recent transactions data.</p>
<div id="${RECENT_BODY_ID}"></div>
`;
panel.querySelector('.fix-lop-close')?.addEventListener('click', closeRecentPanel);
document.body.appendChild(panel);
}
}
function textOf(node) {
return (node?.textContent || '').replace(/\s+/g, ' ').trim();
}
function getVisibleTabPanel() {
const form = $(POA_FORM_SELECTOR);
if (!form) return null;
const tabs = $$('.ui-tabs-panel', form);
return tabs.find((tab) => !tab.classList.contains('ui-helper-hidden') && tab.getAttribute('aria-hidden') !== 'true') || tabs[0] || null;
}
function extractStatementSummary() {
const form = $(POA_FORM_SELECTOR);
const panel = getVisibleTabPanel();
if (!form || !panel) return null;
const activeTabLink = $('.ui-tabs-header.ui-state-active a, .ui-tabs-header.ui-tabs-selected a', form);
const propertyAddressLabel = Array.from($$('label', panel)).find((label) => textOf(label) === 'Property Address');
const propertyAddress = propertyAddressLabel?.nextElementSibling ? textOf(propertyAddressLabel.nextElementSibling) : '';
const monthButtons = $$('.poa-stmt-info-button', panel).map((el) => textOf(el)).filter(Boolean);
const activeMonth = textOf($('.ui-area-btn-statement-active', panel)) || monthButtons[0] || '';
const paymentLink = $('span[id$="makePaymentButton"] a', panel)?.getAttribute('href') || '';
const printLink = $('span[id$="printButton"] a', panel)?.getAttribute('href') || '';
const tables = $$('.ui-datatable table', panel);
const summaryTable = tables.find((table) => /Statement Date/i.test(textOf(table.querySelector('thead'))));
const detailTable = tables.find((table) => /Description/i.test(textOf(table.querySelector('thead'))));
const agingTable = tables.find((table) => /Current/i.test(textOf(table.querySelector('thead'))) && /Over 30/i.test(textOf(table.querySelector('thead'))));
const summaryRow = summaryTable ? $$('tbody tr', summaryTable)[0] : null;
const summaryCells = summaryRow ? $$('td', summaryRow).map(textOf) : [];
const detailRows = detailTable ? $$('tbody tr', detailTable).map((tr) => {
const cells = $$('td', tr).map(textOf);
if (!cells.some(Boolean)) return null;
return {
date: cells[0] || '',
ref: cells[1] || '',
description: cells[2] || '',
amount: cells[3] || '',
surcharge: cells[4] || '',
svc: cells[5] || '',
tax: cells[6] || '',
total: cells[7] || cells[8] || '',
};
}).filter(Boolean) : [];
const agingHeaders = agingTable ? $$('thead th', agingTable).map(textOf) : [];
const agingValues = agingTable ? $$('tbody tr:first-child td', agingTable).map(textOf) : [];
const aging = agingHeaders.map((label, index) => ({
label,
value: agingValues[index] || '',
})).filter((item) => item.label || item.value);
return {
title: textOf(activeTabLink) || 'Statement',
propertyAddress,
activeMonth,
availableMonths: monthButtons,
statementDate: summaryCells[0] || '',
dueDate: summaryCells[1] || '',
balanceDue: summaryCells[2] || '',
paymentLink,
printLink,
detailRows,
aging,
};
}
function renderStatementSummary() {
const host = document.getElementById(STATEMENT_BODY_ID);
if (!host) return;
const summary = extractStatementSummary();
if (!summary) {
host.innerHTML = '<div class="fix-lop-empty">No statement summary data found on this page.</div>';
return;
}
const actionLinks = [
summary.paymentLink ? `<a class="fix-lop-link" href="${summary.paymentLink}">Make Payment</a>` : '',
summary.printLink ? `<a class="fix-lop-link" href="${summary.printLink}" target="_blank" rel="noopener noreferrer">Print Statement</a>` : '',
].filter(Boolean).join(' <span class="fix-lop-muted">•</span> ');
const detailRowsHtml = summary.detailRows.map((row) => `
<tr>
<td>${row.date || ''}</td>
<td>${row.description || row.ref || ''}</td>
<td>${row.total || row.amount || ''}</td>
</tr>
`).join('');
const agingHtml = summary.aging.map((item) => `
<div class="fix-lop-summary-card">
<div class="fix-lop-summary-label">${item.label}</div>
<div class="fix-lop-summary-value">${item.value || '—'}</div>
</div>
`).join('');
host.innerHTML = `
<div class="fix-lop-summary-grid">
<div class="fix-lop-summary-card">
<div class="fix-lop-summary-label">Property</div>
<div class="fix-lop-summary-value" style="font-size:16px">${summary.title || '—'}</div>
</div>
<div class="fix-lop-summary-card">
<div class="fix-lop-summary-label">Statement Month</div>
<div class="fix-lop-summary-value">${summary.activeMonth || '—'}</div>
</div>
<div class="fix-lop-summary-card">
<div class="fix-lop-summary-label">Statement Date</div>
<div class="fix-lop-summary-value">${summary.statementDate || '—'}</div>
</div>
<div class="fix-lop-summary-card">
<div class="fix-lop-summary-label">Due Date</div>
<div class="fix-lop-summary-value">${summary.dueDate || '—'}</div>
</div>
<div class="fix-lop-summary-card">
<div class="fix-lop-summary-label">Balance Due</div>
<div class="fix-lop-summary-value">${summary.balanceDue || '—'}</div>
</div>
</div>
${summary.propertyAddress ? `<div class="fix-lop-section"><h3>Address</h3><div>${summary.propertyAddress}</div></div>` : ''}
${actionLinks ? `<div class="fix-lop-section"><h3>Actions</h3><div>${actionLinks}</div></div>` : ''}
${summary.availableMonths.length ? `<div class="fix-lop-section"><h3>Available Statement Months</h3><div>${summary.availableMonths.join(', ')}</div></div>` : ''}
${summary.detailRows.length ? `<div class="fix-lop-section"><h3>Charges and Totals</h3><table class="fix-lop-summary-table"><thead><tr><th>Date</th><th>Description</th><th>Total</th></tr></thead><tbody>${detailRowsHtml}</tbody></table></div>` : ''}
${summary.aging.length ? `<div class="fix-lop-section"><h3>Aging</h3><div class="fix-lop-summary-grid">${agingHtml}</div></div>` : ''}
`;
}
function openStatementPanel() {
createPanelShell();
createStatementPanelShell();
renderStatementSummary();
document.getElementById(BACKDROP_ID)?.classList.add('fix-lop-open');
document.getElementById(STATEMENT_PANEL_ID)?.classList.add('fix-lop-open');
state.statementOpen = true;
}
function closeStatementPanel() {
document.getElementById(STATEMENT_PANEL_ID)?.classList.remove('fix-lop-open');
document.getElementById(BACKDROP_ID)?.classList.remove('fix-lop-open');
state.statementOpen = false;
}
function toggleStatementPanel() {
if (state.statementOpen) {
closeStatementPanel();
} else {
openStatementPanel();
}
}
function getPoaHeadingByText(expectedText) {
const header = $(POA_HEADER_SELECTOR);
if (!header) return null;
const headingText = textOf(header).replace(/^\W+/, '').trim();
if (headingText !== expectedText) return null;
return header;
}
function createStatementMonkey() {
const header = getPoaHeadingByText(STATEMENT_HEADING_TEXT);
if (!header) return;
let monkey = document.getElementById(STATEMENT_MONKEY_ID);
if (!monkey) {
monkey = document.createElement('button');
monkey.type = 'button';
monkey.id = STATEMENT_MONKEY_ID;
monkey.innerHTML = '<span>🐒</span>';
monkey.title = 'Open Monkey Statement Summary';
monkey.setAttribute('aria-label', 'Open Monkey Statement Summary');
monkey.addEventListener('click', toggleStatementPanel);
header.appendChild(monkey);
}
}
function extractRecentTransactionsSummary() {
const form = $(POA_FORM_SELECTOR);
const panel = getVisibleTabPanel();
if (!form || !panel) return null;
const activeTabLink = $('.ui-tabs-header.ui-state-active a, .ui-tabs-header.ui-tabs-selected a', form);
const activeTabTitle = textOf(activeTabLink) || 'Recent Transactions';
const isAmenitiesTab = activeTabTitle === 'Amenities';
const propertyAddressLabel = Array.from($$('label', panel)).find((label) => textOf(label) === 'Property Address');
const propertyAddress = propertyAddressLabel?.parentElement?.nextElementSibling ? textOf($('label', propertyAddressLabel.parentElement.nextElementSibling)) : (propertyAddressLabel?.nextElementSibling ? textOf(propertyAddressLabel.nextElementSibling) : '');
const folioLabel = $('label[id$="folioSelection_label"]', panel);
const folioName = textOf(folioLabel).replace(/^\u00a0+|\u00a0+$/g, '').trim();
const statementSummaryLink = $('.information-bar a[href*="statementsummary"]', panel)?.getAttribute('href') || '';
const statementSummaryAmount = textOf($('.information-bar a[href*="statementsummary"] .info-bar-right, .information-bar a[href*="statementsummary"] span', panel));
const statementSummaryLabel = textOf($('.information-bar a[href*="statementsummary"]', panel)?.closest('.information-bar'));
const amenitiesPaymentLink = $$('a', document).find((a) => /Make a Payment - Amenities/i.test(textOf(a)))?.getAttribute('href') || '';
const directNavigationLink = isAmenitiesTab && amenitiesPaymentLink ? amenitiesPaymentLink : statementSummaryLink;
const directNavigationLabel = isAmenitiesTab && amenitiesPaymentLink ? 'Go to Amenities Payment' : 'Go to Statement Summary';
const tables = $$('.ui-datatable table', panel);
const recentPaymentsTable = tables.find((table) => {
const firstRow = $('tbody tr', table);
const cells = firstRow ? $$('td', firstRow).map(textOf) : [];
return cells.length >= 4 && /^\d{2}\/\d{2}\/\d{4}$/.test(cells[0] || '') && /\$|\(/.test(cells[cells.length - 1] || '');
});
const rows = recentPaymentsTable ? $$('tbody tr', recentPaymentsTable).map((tr) => {
const cells = $$('td', tr).map(textOf);
if (!cells.some(Boolean)) return null;
return {
date: cells[0] || '',
ref: cells[1] || '',
type: cells[2] || '',
amount: cells[3] || '',
};
}).filter(Boolean) : [];
return {
title: activeTabTitle,
isAmenitiesTab,
propertyAddress,
folioName,
paymentCount: rows.length,
latestDate: rows[0]?.date || '',
latestAmount: rows[0]?.amount || '',
statementSummaryLink,
statementSummaryAmount,
statementSummaryLabel,
amenitiesPaymentLink,
directNavigationLink,
directNavigationLabel,
rows,
};
}
function renderRecentTransactionsSummary() {
const host = document.getElementById(RECENT_BODY_ID);
if (!host) return;
const summary = extractRecentTransactionsSummary();
if (!summary) {
host.innerHTML = '<div class="fix-lop-empty">No recent transactions data found on this page.</div>';
return;
}
const rowsHtml = summary.rows.map((row) => `
<tr>
<td>${row.date || ''}</td>
<td>${row.ref || ''}</td>
<td>${row.type || ''}</td>
<td>${row.amount || ''}</td>
</tr>
`).join('');
const actionLinks = [
summary.directNavigationLink ? `<a class="fix-lop-link" href="${summary.directNavigationLink}">${summary.directNavigationLabel}</a>` : '',
summary.isAmenitiesTab && summary.statementSummaryLink && summary.directNavigationLink !== summary.statementSummaryLink ? `<a class="fix-lop-link" href="${summary.statementSummaryLink}">Statement Summary (intermediate)</a>` : '',
].filter(Boolean).join(' <span class="fix-lop-muted">•</span> ');
host.innerHTML = `
<div class="fix-lop-summary-grid">
<div class="fix-lop-summary-card">
<div class="fix-lop-summary-label">Property</div>
<div class="fix-lop-summary-value" style="font-size:16px">${summary.title || '—'}</div>
</div>
<div class="fix-lop-summary-card">
<div class="fix-lop-summary-label">Folio</div>
<div class="fix-lop-summary-value">${summary.folioName || '—'}</div>
</div>
<div class="fix-lop-summary-card">
<div class="fix-lop-summary-label">Transactions Shown</div>
<div class="fix-lop-summary-value">${summary.paymentCount || 0}</div>
</div>
<div class="fix-lop-summary-card">
<div class="fix-lop-summary-label">Latest Date</div>
<div class="fix-lop-summary-value">${summary.latestDate || '—'}</div>
</div>
<div class="fix-lop-summary-card">
<div class="fix-lop-summary-label">Latest Amount</div>
<div class="fix-lop-summary-value">${summary.latestAmount || '—'}</div>
</div>
</div>
${summary.propertyAddress ? `<div class="fix-lop-section"><h3>Address</h3><div>${summary.propertyAddress}</div></div>` : ''}
${actionLinks ? `<div class="fix-lop-section"><h3>Navigation</h3><div>${actionLinks}</div></div>` : ''}
${summary.statementSummaryLink ? `<div class="fix-lop-section"><h3>Statement Seam</h3><div class="fix-lop-muted">${summary.statementSummaryLabel || 'Statement Summary'}${summary.statementSummaryAmount ? ` — ${summary.statementSummaryAmount}` : ''}</div></div>` : ''}
${summary.rows.length ? `<div class="fix-lop-section"><h3>Recent Payments</h3><table class="fix-lop-summary-table"><thead><tr><th>Date</th><th>Reference</th><th>Type</th><th>Amount</th></tr></thead><tbody>${rowsHtml}</tbody></table></div>` : ''}
`;
}
function openRecentPanel() {
createPanelShell();
createRecentPanelShell();
renderRecentTransactionsSummary();
document.getElementById(BACKDROP_ID)?.classList.add('fix-lop-open');
document.getElementById(RECENT_PANEL_ID)?.classList.add('fix-lop-open');
state.recentOpen = true;
}
function closeRecentPanel() {
document.getElementById(RECENT_PANEL_ID)?.classList.remove('fix-lop-open');
document.getElementById(BACKDROP_ID)?.classList.remove('fix-lop-open');
state.recentOpen = false;
}
function toggleRecentPanel() {
if (state.recentOpen) {
closeRecentPanel();
} else {
openRecentPanel();
}
}
function createRecentMonkey() {
const header = getPoaHeadingByText(RECENT_TRANSACTIONS_HEADING_TEXT);
if (!header) return;
let monkey = document.getElementById(RECENT_MONKEY_ID);
if (!monkey) {
monkey = document.createElement('button');
monkey.type = 'button';
monkey.id = RECENT_MONKEY_ID;
monkey.innerHTML = '<span>🐒</span>';
monkey.title = 'Open Monkey Recent Transactions';
monkey.setAttribute('aria-label', 'Open Monkey Recent Transactions');
monkey.addEventListener('click', toggleRecentPanel);
header.appendChild(monkey);
}
}
function bindGlobalKeys() {
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
if (state.recentOpen) closeRecentPanel();
if (state.statementOpen) closeStatementPanel();
if (state.open) closePanel();
}
}, true);
}
function init() {
if (state.initialized) return;
addStyles();
createBadge();
createPanelShell();
createStatementPanelShell();
createRecentPanelShell();
createStatementMonkey();
createRecentMonkey();
bindGlobalKeys();
state.initialized = true;
log('Monkey menu ready.');
}
init();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment