NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Toggl Detailed Report Enhancement // @description Highlights overlapping time entries and adds JIRA links in Detailed Report // @version 1.0.1 // @copyright 2024 - Milan Farkas, Claude / WRD Labs (https://wrd.hu) // @namespace wrd.hu // @author milanfarkas // @license MIT // @match https://track.toggl.com/reports/detailed/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // ==/UserScript== (function() { 'use strict'; // Check if we're running in Tampermonkey const isTampermonkey = typeof GM_getValue === 'function' && typeof GM_setValue === 'function' && typeof GM_registerMenuCommand === 'function'; // Get or initialize configuration let jiraUrl = isTampermonkey ? GM_getValue('jiraUrl') : null; let maxHours = isTampermonkey ? GM_getValue('maxHours') : null; // Ensure configuration exists if (isTampermonkey && (!jiraUrl || !maxHours)) { const newJiraUrl = prompt('Enter your JIRA instance URL (e.g., company.atlassian.net):', ''); if (!newJiraUrl) { alert('JIRA URL is required. Script will not run.'); return; } const newMaxHours = prompt('Enter maximum hours before warning:', ''); if (!newMaxHours) { alert('Maximum hours value is required. Script will not run.'); return; } const maxHoursNum = parseFloat(newMaxHours); if (isNaN(maxHoursNum) || maxHoursNum <= 0) { alert('Invalid hours value. Script will not run.'); return; } jiraUrl = newJiraUrl; maxHours = maxHoursNum; GM_setValue('jiraUrl', jiraUrl); GM_setValue('maxHours', maxHours); alert('Configuration saved! Script will now run.'); } // Configuration menu command if (isTampermonkey) { GM_registerMenuCommand('Configure Toggl Enhancement', () => { const newJiraUrl = prompt('Enter your JIRA instance URL (e.g., company.atlassian.net):', jiraUrl); if (newJiraUrl === null) return; const newMaxHours = prompt('Enter maximum hours before warning:', maxHours); if (newMaxHours === null) return; const maxHoursNum = parseFloat(newMaxHours); if (isNaN(maxHoursNum) || maxHoursNum <= 0) { alert('Invalid hours value. Configuration not saved.'); return; } jiraUrl = newJiraUrl; maxHours = maxHoursNum; GM_setValue('jiraUrl', jiraUrl); GM_setValue('maxHours', maxHours); alert('Configuration saved! Refresh the page to apply changes.'); }); } function findTimeEntryRoot(element) { return element.closest('tr'); } function parseDate(dateStr) { return dateStr; } function getDurationInHours(start, end) { return (end - start) / (1000 * 60 * 60); } function addJiraLinks() { document.querySelectorAll('div[class*="TimeEntryDescription"]').forEach(desc => { if (desc.getAttribute('data-jira-processed')) return; const text = desc.textContent; const match = text.match(/^([A-Z0-9]+-\d+)/); if (match) { const ticketId = match[1]; const remainingText = text.substring(ticketId.length); const span = document.createElement('span'); span.textContent = ticketId; span.style.cssText = ` color: #0052CC; text-decoration: none; pointer-events: all; cursor: pointer; display: inline-block; margin-right: 4px; `; span.onmouseover = () => span.style.textDecoration = 'underline'; span.onmouseout = () => span.style.textDecoration = 'none'; span.onclick = (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); window.open(`https://${jiraUrl}/browse/${ticketId}`, '_blank'); return false; }; span.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); return false; }, true); desc.textContent = ''; desc.appendChild(span); desc.appendChild(document.createTextNode(remainingText)); desc.setAttribute('data-jira-processed', 'true'); } }); } function checkOverlaps() { // First reset all highlights document.querySelectorAll('tbody tr').forEach(row => { row.style.removeProperty('background-color'); }); // Group entries by user and date const entriesByUserAndDate = new Map(); document.querySelectorAll('tbody tr').forEach(row => { const timeCell = row.querySelector('div[class*="Time-bodyText"]'); const dateCell = row.querySelector('div[class*="Date-bodyText"]'); const userCell = row.querySelector('td[class*="UserTableCell"]'); if (!timeCell || !dateCell || !userCell) return; const user = userCell.textContent.trim(); const date = dateCell.textContent.trim(); const timeMatch = timeCell.textContent.match(/(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/); if (!timeMatch) return; const key = `${user}-${date}`; if (!entriesByUserAndDate.has(key)) { entriesByUserAndDate.set(key, []); } const [start, end] = timeMatch.slice(1); const startDate = new Date(`${date} ${start}`); const endDate = new Date(`${date} ${end}`); entriesByUserAndDate.get(key).push({ element: row, start: startDate, end: endDate, duration: getDurationInHours(startDate, endDate) }); }); // Check overlaps within each user's daily entries for (const entries of entriesByUserAndDate.values()) { entries.forEach((entry, i) => { if (entry.duration > maxHours) { entry.element.style.setProperty('background-color', '#fff3cd', 'important'); } for (let j = i + 1; j < entries.length; j++) { if (entry.start < entries[j].end && entries[j].start < entry.end) { entry.element.style.setProperty('background-color', '#ffebee', 'important'); entries[j].element.style.setProperty('background-color', '#ffebee', 'important'); } } }); } // Add Jira links after processing entries addJiraLinks(); } // Run initial check after a short delay to ensure page is loaded setTimeout(checkOverlaps, 2000); const observer = new MutationObserver(() => { setTimeout(checkOverlaps, 100); }); observer.observe(document.body, { childList: true, subtree: true }); })();