Client Tag Manager for Capitect

Adds color-coded tags (S, C, M, G) next to client performance numbers

Size

18.6 KB

Version

1.1.6

Created

Mar 11, 2026

Updated

10 days ago

1// ==UserScript==
2// @name		Client Tag Manager for Capitect
3// @description		Adds color-coded tags (S, C, M, G) next to client performance numbers
4// @version		1.1.6
5// @match		https://*.milewealth.capitect.com/*
6// @icon		https://milewealth.capitect.com/favicon.ico
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    // Client name to tag mapping
12    const clientTags = {
13        'Beck Maw Family': 'C',
14        'Brosnan Household': 'M',
15        'Chan Household': 'M',
16        'Crook (Rick and Bridget) Family': 'M',
17        'Davis Household': 'M',
18        'Fisher Family': 'M',
19        'Hahn Family': 'G',
20        'Hutchison Household': 'M',
21        'Jordan Family': 'M',
22        'Kelley Family': 'S',
23        'Kelly Family': 'M',
24        'Ker Family': 'M',
25        'Klein Household': 'M',
26        'Kurachi Family': 'C',
27        'Landers Household': 'S',
28        'Lennig (Matthew) Household': 'M',
29        'Lennig (Mike and Rosie) Family': 'G',
30        'Lind Family': 'M',
31        'Masters Family': 'M',
32        'McDowell Household': 'G',
33        'Monroe Family': 'C',
34        'O\'Hara Family': 'M',
35        'Ouimette Family': 'C',
36        'Patterson (Marc Becky) Family': 'M',
37        'Patterson Household': 'M',
38        'Ring (Bob) Household': 'M',
39        'Robinson Family': 'G',
40        'Rodarte Family': 'G',
41        'Sacchetto Family': 'G',
42        'Sammon Family': 'M',
43        'Sawyer Javitt Family': 'C',
44        'Skarstad Family': 'M',
45        'Thornton Family': 'M',
46        'Ullrich Restitutionary Trust': 'G',
47        'Usman Family': 'M',
48        'Whalen Family': 'M',
49        'Whisenand Family': 'M',
50        'Wilson (Mark/Maria) Family': 'G',
51        'Wilson (Rosalie) Household': 'M'
52    };
53
54    // Tag color mapping
55    const tagColors = {
56        'S': '#0047AB',  // Deep Blue
57        'C': '#4ECDC4',  // Teal
58        'M': '#FFD700',  // Gold
59        'G': '#FF8C00'   // Orange
60    };
61
62    // Add styles for the tags
63    TM_addStyle(`
64        .client-tag {
65            display: inline-block;
66            margin-left: 8px;
67            padding: 2px 8px;
68            border-radius: 4px;
69            font-size: 11px;
70            font-weight: bold;
71            color: #fff;
72            vertical-align: baseline;
73        }
74        
75        .performance-button {
76            position: fixed;
77            top: 20px;
78            right: 20px;
79            padding: 10px 20px;
80            background-color: #007bff;
81            color: white;
82            border: none;
83            border-radius: 6px;
84            font-size: 14px;
85            font-weight: bold;
86            cursor: pointer;
87            z-index: 10000;
88            box-shadow: 0 2px 8px rgba(0,0,0,0.2);
89        }
90        
91        .performance-button:hover {
92            background-color: #0056b3;
93        }
94        
95        .performance-popup {
96            position: fixed;
97            top: 50%;
98            left: 50%;
99            transform: translate(-50%, -50%);
100            background: white;
101            padding: 30px;
102            border-radius: 12px;
103            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
104            z-index: 10001;
105            min-width: 500px;
106            max-width: 600px;
107        }
108        
109        .performance-popup-overlay {
110            position: fixed;
111            top: 0;
112            left: 0;
113            right: 0;
114            bottom: 0;
115            background: rgba(0,0,0,0.5);
116            z-index: 10000;
117        }
118        
119        .performance-popup h2 {
120            margin: 0 0 20px 0;
121            color: #333;
122            font-size: 24px;
123        }
124        
125        .performance-popup-close {
126            position: absolute;
127            top: 15px;
128            right: 15px;
129            background: none;
130            border: none;
131            font-size: 24px;
132            cursor: pointer;
133            color: #666;
134        }
135        
136        .performance-popup-close:hover {
137            color: #000;
138        }
139        
140        .performance-category {
141            margin: 15px 0;
142            padding: 15px;
143            border-radius: 8px;
144            background: #f8f9fa;
145        }
146        
147        .performance-category-header {
148            display: flex;
149            align-items: center;
150            margin-bottom: 8px;
151        }
152        
153        .performance-category-tag {
154            display: inline-block;
155            padding: 4px 10px;
156            border-radius: 4px;
157            font-size: 12px;
158            font-weight: bold;
159            color: #fff;
160            margin-right: 10px;
161        }
162        
163        .performance-category-name {
164            font-size: 16px;
165            font-weight: bold;
166            color: #333;
167        }
168        
169        .performance-category-stats {
170            display: flex;
171            justify-content: space-between;
172            margin-top: 8px;
173        }
174        
175        .performance-stat {
176            flex: 1;
177        }
178        
179        .performance-stat-label {
180            font-size: 12px;
181            color: #666;
182            margin-bottom: 4px;
183        }
184        
185        .performance-stat-value {
186            font-size: 18px;
187            font-weight: bold;
188            color: #333;
189        }
190    `);
191
192    function addTagsToClients() {
193        console.log('Adding tags to clients...');
194        
195        // Find all table rows in the performance table
196        const rows = document.querySelectorAll('table.table tbody.ng-scope tr');
197        console.log(`Found ${rows.length} client rows`);
198        
199        rows.forEach(row => {
200            // Get the client name from the anchor tag
201            const clientNameLink = row.querySelector('td a.ng-binding');
202            if (!clientNameLink) return;
203            
204            const clientName = clientNameLink.textContent.trim();
205            
206            // Check if this client has a tag
207            const tag = clientTags[clientName];
208            if (!tag) {
209                console.log(`No tag found for client: ${clientName}`);
210                return;
211            }
212            
213            // Check if tag already exists
214            if (clientNameLink.parentElement.querySelector('.client-tag')) return;
215            
216            // Create and add the tag
217            const tagElement = document.createElement('span');
218            tagElement.className = 'client-tag';
219            tagElement.textContent = tag;
220            tagElement.style.backgroundColor = tagColors[tag];
221            
222            // Append tag after the client name link
223            clientNameLink.parentElement.appendChild(tagElement);
224            console.log(`Added tag ${tag} for ${clientName}`);
225        });
226    }
227
228    function parseMoneyValue(text) {
229        // Remove $, commas, parentheses and convert to number
230        const cleaned = text.replace(/[$,()]/g, '').trim();
231        const value = parseFloat(cleaned);
232        return text.includes('(') ? -value : value;
233    }
234
235    function parsePercentage(text) {
236        // Remove % and convert to number
237        return parseFloat(text.replace('%', '').trim());
238    }
239
240    function calculatePerformanceByTag() {
241        const tagData = {
242            'S': { name: 'Stable Growth', totalBalance: 0, totalPerformance: 0, count: 0, excluded: 0, clients: [] },
243            'C': { name: 'Conservative Growth', totalBalance: 0, totalPerformance: 0, count: 0, excluded: 0, clients: [] },
244            'M': { name: 'Moderate Growth', totalBalance: 0, totalPerformance: 0, count: 0, excluded: 0, clients: [] },
245            'G': { name: 'Growth', totalBalance: 0, totalPerformance: 0, count: 0, excluded: 0, clients: [] }
246        };
247
248        const rows = document.querySelectorAll('table.table tbody.ng-scope tr');
249        
250        rows.forEach(row => {
251            const clientNameLink = row.querySelector('td a.ng-binding');
252            if (!clientNameLink) return;
253            
254            const clientName = clientNameLink.textContent.trim();
255            const tag = clientTags[clientName];
256            if (!tag) return;
257
258            // Get begin balance (1st text-right cell, not hidden)
259            const balanceCells = row.querySelectorAll('td.text-right:not(.edit-column)');
260            const beginBalanceCell = balanceCells[0]; // First text-right cell is begin balance
261            if (!beginBalanceCell) return;
262
263            const beginBalanceText = beginBalanceCell.textContent.trim();
264            const beginBalance = parseMoneyValue(beginBalanceText);
265
266            // Skip clients with $0 begin balance
267            if (beginBalance === 0) {
268                tagData[tag].excluded += 1;
269                console.log(`${clientName} (${tag}): Excluded - Begin Balance is $0`);
270                return;
271            }
272
273            // Get ending balance (3rd text-right cell, not hidden)
274            const endBalanceCell = balanceCells[2]; // Third text-right cell is end balance
275            if (!endBalanceCell) return;
276
277            const balanceText = endBalanceCell.textContent.trim();
278            const endBalance = parseMoneyValue(balanceText);
279
280            // Get performance percentage (last text-right cell, not edit-column)
281            const performanceCell = balanceCells[balanceCells.length - 1]; // Last text-right cell
282            if (!performanceCell) return;
283
284            // Find the percentage span - look for the span that contains %
285            const allSpans = performanceCell.querySelectorAll('span.ng-binding.ng-scope');
286            const percentageSpan = Array.from(allSpans).find(span => span.textContent.includes('%'));
287            if (!percentageSpan) return;
288
289            const performanceText = percentageSpan.textContent.trim();
290            const performance = parsePercentage(performanceText);
291
292            console.log(`${clientName} (${tag}): Balance=${endBalance}, Performance=${performance}%`);
293
294            tagData[tag].totalBalance += endBalance;
295            tagData[tag].totalPerformance += performance;
296            tagData[tag].count += 1;
297            tagData[tag].clients.push({ name: clientName, balance: endBalance, performance: performance });
298        });
299
300        return tagData;
301    }
302
303    function getCurrentPage() {
304        const activePage = document.querySelector('ul.pagination li.pagination-page.active a');
305        return activePage ? parseInt(activePage.textContent.trim()) : 1;
306    }
307
308    function getAllPages() {
309        const pageButtons = document.querySelectorAll('ul.pagination li.pagination-page a');
310        return Array.from(pageButtons).map(btn => parseInt(btn.textContent.trim()));
311    }
312
313    function clickPage(pageNumber) {
314        const pageButtons = document.querySelectorAll('ul.pagination li.pagination-page a');
315        const targetButton = Array.from(pageButtons).find(btn => parseInt(btn.textContent.trim()) === pageNumber);
316        if (targetButton) {
317            targetButton.click();
318            return true;
319        }
320        return false;
321    }
322
323    function waitForPageLoad(timeout = 2000) {
324        return new Promise(resolve => setTimeout(resolve, timeout));
325    }
326
327    async function calculatePerformanceByTagAllPages() {
328        const currentPage = getCurrentPage();
329        const allPages = getAllPages();
330        
331        console.log(`Current page: ${currentPage}, Total pages: ${allPages.length}`);
332        
333        const aggregatedData = {
334            'S': { name: 'Stable Growth', totalBalance: 0, totalPerformance: 0, count: 0, excluded: 0, clients: [] },
335            'C': { name: 'Conservative Growth', totalBalance: 0, totalPerformance: 0, count: 0, excluded: 0, clients: [] },
336            'M': { name: 'Moderate Growth', totalBalance: 0, totalPerformance: 0, count: 0, excluded: 0, clients: [] },
337            'G': { name: 'Growth', totalBalance: 0, totalPerformance: 0, count: 0, excluded: 0, clients: [] }
338        };
339
340        // Loop through all pages
341        for (const pageNum of allPages) {
342            console.log(`Processing page ${pageNum}...`);
343            
344            // Navigate to page if not already there
345            if (getCurrentPage() !== pageNum) {
346                clickPage(pageNum);
347                await waitForPageLoad();
348            }
349
350            // Calculate data for current page
351            const pageData = calculatePerformanceByTag();
352            
353            // Aggregate the data
354            for (const tag in pageData) {
355                aggregatedData[tag].totalBalance += pageData[tag].totalBalance;
356                aggregatedData[tag].totalPerformance += pageData[tag].totalPerformance;
357                aggregatedData[tag].count += pageData[tag].count;
358                aggregatedData[tag].excluded += pageData[tag].excluded;
359                aggregatedData[tag].clients.push(...pageData[tag].clients);
360            }
361        }
362
363        // Return to original page
364        if (getCurrentPage() !== currentPage) {
365            console.log(`Returning to page ${currentPage}...`);
366            clickPage(currentPage);
367            await waitForPageLoad();
368        }
369
370        return aggregatedData;
371    }
372
373    function formatCurrency(value) {
374        return new Intl.NumberFormat('en-US', {
375            style: 'currency',
376            currency: 'USD',
377            minimumFractionDigits: 0,
378            maximumFractionDigits: 0
379        }).format(value);
380    }
381
382    function getPerformanceTimeframe() {
383        const timeframeButton = document.querySelector('cap-performance-period-dropdown button.ng-binding');
384        return timeframeButton ? timeframeButton.textContent.trim().replace(/\s*\n.*$/, '') : 'Unknown Period';
385    }
386
387    async function showPerformancePopup() {
388        const timeframe = getPerformanceTimeframe();
389
390        // Create overlay
391        const overlay = document.createElement('div');
392        overlay.className = 'performance-popup-overlay';
393        overlay.onclick = () => {
394            overlay.remove();
395            popup.remove();
396        };
397
398        // Create popup
399        const popup = document.createElement('div');
400        popup.className = 'performance-popup';
401
402        // Create close button
403        const closeButton = document.createElement('button');
404        closeButton.className = 'performance-popup-close';
405        closeButton.innerHTML = '×';
406        closeButton.onclick = () => {
407            overlay.remove();
408            popup.remove();
409        };
410
411        // Create title
412        const title = document.createElement('h2');
413        title.textContent = `Performance by Category - ${timeframe}`;
414
415        // Add loading message
416        const loadingMsg = document.createElement('p');
417        loadingMsg.textContent = 'Loading data from all pages...';
418        loadingMsg.style.textAlign = 'center';
419        loadingMsg.style.color = '#666';
420
421        popup.appendChild(closeButton);
422        popup.appendChild(title);
423        popup.appendChild(loadingMsg);
424
425        document.body.appendChild(overlay);
426        document.body.appendChild(popup);
427
428        // Get data from all pages
429        const tagData = await calculatePerformanceByTagAllPages();
430
431        // Remove loading message
432        loadingMsg.remove();
433
434        // Add category sections
435        const tagOrder = ['S', 'C', 'M', 'G'];
436        tagOrder.forEach(tag => {
437            const data = tagData[tag];
438            if (data.count === 0) return;
439
440            const avgPerformance = data.totalPerformance / data.count;
441
442            const categoryDiv = document.createElement('div');
443            categoryDiv.className = 'performance-category';
444
445            const headerDiv = document.createElement('div');
446            headerDiv.className = 'performance-category-header';
447
448            const tagSpan = document.createElement('span');
449            tagSpan.className = 'performance-category-tag';
450            tagSpan.textContent = tag;
451            tagSpan.style.backgroundColor = tagColors[tag];
452
453            const nameSpan = document.createElement('span');
454            nameSpan.className = 'performance-category-name';
455            nameSpan.textContent = data.name;
456
457            headerDiv.appendChild(tagSpan);
458            headerDiv.appendChild(nameSpan);
459
460            const statsDiv = document.createElement('div');
461            statsDiv.className = 'performance-category-stats';
462
463            const balanceDiv = document.createElement('div');
464            balanceDiv.className = 'performance-stat';
465            balanceDiv.innerHTML = `
466                <div class="performance-stat-label">Total Balance</div>
467                <div class="performance-stat-value">${formatCurrency(data.totalBalance)}</div>
468            `;
469
470            const performanceDiv = document.createElement('div');
471            performanceDiv.className = 'performance-stat';
472            performanceDiv.innerHTML = `
473                <div class="performance-stat-label">Avg Performance</div>
474                <div class="performance-stat-value">${avgPerformance.toFixed(2)}%</div>
475            `;
476
477            const countDiv = document.createElement('div');
478            countDiv.className = 'performance-stat';
479            countDiv.innerHTML = `
480                <div class="performance-stat-label">Clients</div>
481                <div class="performance-stat-value">${data.count}${data.excluded > 0 ? ` <span style="font-size: 12px; color: #999; margin-left: 8px;">(${data.excluded} excluded)</span>` : ''}</div>
482            `;
483
484            statsDiv.appendChild(balanceDiv);
485            statsDiv.appendChild(performanceDiv);
486            statsDiv.appendChild(countDiv);
487
488            categoryDiv.appendChild(headerDiv);
489            categoryDiv.appendChild(statsDiv);
490
491            popup.appendChild(categoryDiv);
492        });
493    }
494
495    function addPerformanceButton() {
496        // Check if button already exists
497        if (document.querySelector('.performance-button')) return;
498
499        const button = document.createElement('button');
500        button.className = 'performance-button';
501        button.textContent = 'Performance';
502        button.onclick = showPerformancePopup;
503
504        document.body.appendChild(button);
505        console.log('Performance button added');
506    }
507
508    // Debounce function to avoid excessive calls
509    function debounce(func, wait) {
510        let timeout;
511        return function executedFunction(...args) {
512            const later = () => {
513                clearTimeout(timeout);
514                func(...args);
515            };
516            clearTimeout(timeout);
517            timeout = setTimeout(later, wait);
518        };
519    }
520
521    // Initialize when the page is ready
522    function init() {
523        console.log('Client Tag Manager initialized');
524        
525        // Add tags initially
526        setTimeout(addTagsToClients, 2000);
527        
528        // Add performance button
529        setTimeout(addPerformanceButton, 2000);
530        
531        // Watch for DOM changes (for when the table updates)
532        const debouncedAddTags = debounce(addTagsToClients, 500);
533        const observer = new MutationObserver(debouncedAddTags);
534        
535        // Observe the entire document for changes
536        observer.observe(document.body, {
537            childList: true,
538            subtree: true
539        });
540    }
541
542    // Run when body is ready
543    TM_runBody(init);
544})();