Last active
April 18, 2026 01:17
-
-
Save inertia186/bcd0279715a13adbc8d701caeacb465f to your computer and use it in GitHub Desktop.
fix-lop-org
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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