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})();