Holistiplan Scenario Highlighter

Highlights specific financial fields (Taxable Social Security, Loss Carryforward, Adjusted Gross Income) in selected scenarios

Size

10.2 KB

Version

1.1.4

Created

Nov 21, 2025

Updated

about 2 months ago

1// ==UserScript==
2// @name		Holistiplan Scenario Highlighter
3// @description		Highlights specific financial fields (Taxable Social Security, Loss Carryforward, Adjusted Gross Income) in selected scenarios
4// @version		1.1.4
5// @match		https://*.app.holistiplan.com/*
6// @icon		https://holistiplan-assets-static.s3.amazonaws.com/static/images/favicon.054031c9cb9a.ico
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    console.log('Holistiplan Highlight Extension loaded');
12
13    // Debounce function for MutationObserver
14    function debounce(func, wait) {
15        let timeout;
16        return function executedFunction(...args) {
17            const later = () => {
18                clearTimeout(timeout);
19                func(...args);
20            };
21            clearTimeout(timeout);
22            timeout = setTimeout(later, wait);
23        };
24    }
25
26    // Initialize the extension
27    async function init() {
28        console.log('Initializing Highlight button...');
29        
30        // Wait for the page to be fully loaded
31        if (document.readyState === 'loading') {
32            document.addEventListener('DOMContentLoaded', addHighlightButton);
33        } else {
34            addHighlightButton();
35        }
36
37        // Watch for dynamic content changes
38        const observer = new MutationObserver(debounce(() => {
39            if (!document.querySelector('#highlight-button')) {
40                addHighlightButton();
41            }
42        }, 500));
43
44        observer.observe(document.body, {
45            childList: true,
46            subtree: true
47        });
48    }
49
50    // Add the Highlight button next to Access Log
51    function addHighlightButton() {
52        // Find the Access Log button
53        const accessLogButton = document.querySelector('.btn.btn-primary.btn-sm.ml-2.hide_print');
54        
55        if (!accessLogButton) {
56            console.log('Access Log button not found yet');
57            return;
58        }
59
60        // Check if button already exists
61        if (document.querySelector('#highlight-button')) {
62            console.log('Highlight button already exists');
63            return;
64        }
65
66        // Create the Highlight button
67        const highlightButton = document.createElement('div');
68        highlightButton.id = 'highlight-button';
69        highlightButton.className = 'btn btn-warning btn-sm ml-2 hide_print';
70        highlightButton.innerHTML = 'Highlight <i class="fa fa-highlighter"></i>';
71        highlightButton.style.cursor = 'pointer';
72
73        // Add click event
74        highlightButton.addEventListener('click', handleHighlightClick);
75
76        // Insert the button after Access Log button
77        accessLogButton.parentNode.insertBefore(highlightButton, accessLogButton.nextSibling);
78        
79        console.log('Highlight button added successfully');
80    }
81
82    // Handle the highlight button click
83    async function handleHighlightClick() {
84        console.log('Highlight button clicked');
85
86        // Get all scenario columns
87        const scenarios = getScenarioColumns();
88        
89        if (scenarios.length === 0) {
90            alert('No scenarios found on this page.');
91            return;
92        }
93
94        // Create a prompt with scenario options
95        let promptMessage = 'Which scenario would you like to highlight?\n\n';
96        scenarios.forEach((scenario, index) => {
97            promptMessage += `${index + 1}. ${scenario.name}\n`;
98        });
99        promptMessage += '\nEnter the number of the scenario:';
100
101        const userInput = prompt(promptMessage);
102        
103        if (!userInput) {
104            console.log('User cancelled');
105            return;
106        }
107
108        const scenarioIndex = parseInt(userInput) - 1;
109        
110        if (isNaN(scenarioIndex) || scenarioIndex < 0 || scenarioIndex >= scenarios.length) {
111            alert('Invalid scenario number. Please try again.');
112            return;
113        }
114
115        const selectedScenario = scenarios[scenarioIndex];
116        console.log('Selected scenario:', selectedScenario);
117
118        // Highlight the fields in the selected scenario
119        highlightFields(selectedScenario);
120    }
121
122    // Get all scenario columns from the page
123    function getScenarioColumns() {
124        const scenarios = [];
125        
126        // Find the header row
127        const headerRow = document.querySelector('.row-container.header-row');
128        if (!headerRow) {
129            console.log('Header row not found');
130            return scenarios;
131        }
132
133        // Get all scenario header data slots
134        const headerDataSlots = headerRow.querySelectorAll('.data-slot.header-data');
135        
136        headerDataSlots.forEach((dataSlot, index) => {
137            // Get the scenario index text (e.g., "SCENARIO 1")
138            const scenarioIndexElement = dataSlot.querySelector('.header-text.scenario-index');
139            
140            if (scenarioIndexElement) {
141                const scenarioName = scenarioIndexElement.textContent.trim();
142                scenarios.push({
143                    name: scenarioName,
144                    columnIndex: index, // Use 0-based index for array access
145                    headerElement: dataSlot
146                });
147            }
148        });
149
150        console.log('Found scenarios:', scenarios);
151        return scenarios;
152    }
153
154    // Highlight specific fields in the selected scenario
155    function highlightFields(scenario) {
156        console.log('Highlighting fields for scenario:', scenario.name);
157
158        // Clear any previous highlights
159        clearHighlights();
160
161        let highlightedCount = 0;
162
163        // Find all row containers
164        const allRowContainers = document.querySelectorAll('.row-container');
165        
166        allRowContainers.forEach(rowContainer => {
167            // Get the label from this row
168            const labelElement = rowContainer.querySelector('.label-slot .main-label');
169            if (!labelElement) return;
170
171            const labelText = labelElement.textContent.trim().toLowerCase();
172            
173            // Get the parent div's ID
174            const parentDiv = rowContainer.parentElement;
175            const rowId = parentDiv ? parentDiv.id.toLowerCase() : '';
176
177            // Check if this row contains one of our target fields
178            let shouldHighlight = false;
179            
180            // Federal Taxable Social Security only (exclude state returns)
181            if ((labelText.includes('taxable social security') || rowId.includes('taxable_social_security')) &&
182                rowId === 'social_security_taxable') {
183                shouldHighlight = true;
184            } else if (labelText.includes('adjusted gross income') ||
185                       labelText === 'agi' ||
186                       rowId.includes('adjusted_gross_income') ||
187                       rowId === 'agi') {
188                shouldHighlight = true;
189            }
190
191            if (shouldHighlight) {
192                // Get all data slots in this row
193                const dataSlots = rowContainer.querySelectorAll('.data-slot');
194                
195                // Highlight the data slot at the scenario's column index
196                if (dataSlots[scenario.columnIndex]) {
197                    const targetSlot = dataSlots[scenario.columnIndex];
198                    const targetElement = targetSlot.querySelector('.scenario-analysis-element');
199                    
200                    if (targetElement) {
201                        targetElement.classList.add('highlighted-field');
202                        targetElement.style.backgroundColor = '#ffeb3b';
203                        targetElement.style.border = '2px solid #ff9800';
204                        targetElement.style.transition = 'all 0.3s ease';
205                        highlightedCount++;
206                        console.log('Highlighted:', labelText, 'in column', scenario.columnIndex);
207                    }
208                }
209            }
210        });
211
212        // Also look for "loss to carry forward" which is nested inside other elements
213        const allElements = document.querySelectorAll('.scenario-analysis-element');
214        allElements.forEach(element => {
215            const elementLabel = element.querySelector('.element-label');
216            if (elementLabel) {
217                const labelText = elementLabel.textContent.trim().toLowerCase();
218                
219                if (labelText.includes('loss to carry forward') || labelText.includes('loss carryforward')) {
220                    // Check if this element belongs to the selected scenario
221                    const elementId = element.id;
222                    if (elementId && elementId.endsWith('_' + (scenario.columnIndex + 1))) {
223                        // Highlight only the inner element with sub-element class, not the parent
224                        const subElement = element.querySelector('.element.body-element.sub-element');
225                        if (subElement) {
226                            subElement.classList.add('highlighted-field');
227                            subElement.style.backgroundColor = '#ffeb3b';
228                            subElement.style.border = '2px solid #ff9800';
229                            subElement.style.transition = 'all 0.3s ease';
230                            highlightedCount++;
231                            console.log('Highlighted loss to carry forward in scenario', scenario.columnIndex + 1);
232                        }
233                    }
234                }
235            }
236        });
237
238        if (highlightedCount > 0) {
239            console.log(`Successfully highlighted ${highlightedCount} fields`);
240            
241            // Store the highlight state
242            GM.setValue('highlightedScenario', {
243                scenarioName: scenario.name,
244                columnIndex: scenario.columnIndex,
245                timestamp: Date.now()
246            });
247        } else {
248            alert('No matching fields found to highlight. The fields we look for are:\n- Taxable Social Security (Federal)\n- Loss to Carry Forward\n- Adjusted Gross Income');
249        }
250    }
251
252    // Clear all highlights
253    function clearHighlights() {
254        const highlightedElements = document.querySelectorAll('.highlighted-field');
255        highlightedElements.forEach(element => {
256            element.classList.remove('highlighted-field');
257            element.style.backgroundColor = '';
258            element.style.border = '';
259        });
260        console.log('Cleared previous highlights');
261    }
262
263    // Start the extension
264    init();
265})();
Holistiplan Scenario Highlighter | Robomonkey