NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Toggl Timer Page Enhancement // @description Highlights overlapping time entries and adds JIRA links in Timer page // @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/timer // @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.'); }); } // Exit if no configuration if (!jiraUrl || !maxHours) { console.log('Toggl Enhancement: Configuration missing, script will not run'); return; } function findTimeEntryRoot(element) { let current = element; while (current && current.parentElement) { if (current.parentElement.tagName.toLowerCase() === 'ul') { return current; } current = current.parentElement; } return null; } function parseDate(dateText) { const today = new Date(); const currentYear = today.getFullYear(); if (dateText.includes('Today')) { return today.toLocaleDateString('en-CA'); // YYYY-MM-DD format } if (dateText.includes('Yesterday')) { const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); return yesterday.toLocaleDateString('en-CA'); } const dateStr = dateText.split(',')[1].trim(); return `${currentYear} ${dateStr}`; } function getDurationInHours(start, end) { return (end - start) / (1000 * 60 * 60); } function getCurrentEntry() { const durationElement = document.querySelector('.time-format-utils__duration'); if (!durationElement) return null; const now = new Date(); const duration = durationElement.textContent; if (!duration) return null; const parts = duration.replace(/[^0-9:]/g, '').split(':'); if (parts.length !== 3) return null; const hours = parseInt(parts[0]); const minutes = parseInt(parts[1]); const seconds = parseInt(parts[2]); const durationMs = (hours * 60 * 60 + minutes * 60 + seconds) * 1000; const end = now; const start = new Date(end - durationMs); return { element: durationElement.closest('div[class*="TimerFormContent"]'), start: start, end: end, duration: durationMs / (1000 * 60 * 60) }; } function addJiraLinks() { document.querySelectorAll('[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('[class*="TotalTime"]').forEach(element => { const root = findTimeEntryRoot(element); if (root) { root.style.removeProperty('background-color'); } }); // Reset current entry highlight const currentEntryElement = document.querySelector('div[class*="TimerFormContent"]'); if (currentEntryElement) { currentEntryElement.style.removeProperty('background-color'); } const dateBlocks = document.querySelectorAll('[class*="ListTitle"]'); const timeEntries = []; // Get current running entry const currentEntry = getCurrentEntry(); if (currentEntry) { timeEntries.push(currentEntry); } dateBlocks.forEach(dateBlock => { const dateStr = parseDate(dateBlock.textContent); const parentUl = dateBlock.closest('ul'); if (!parentUl) return; const entries = parentUl.querySelectorAll('[class*="TotalTime"] span'); entries.forEach(timeElement => { const timeString = timeElement.textContent; if (!timeString) return; const root = findTimeEntryRoot(timeElement); if (!root) return; const [start, end] = timeString.split(' - '); const startDate = new Date(`${dateStr} ${start}`); const endDate = new Date(`${dateStr} ${end}`); timeEntries.push({ element: root, start: startDate, end: endDate, duration: getDurationInHours(startDate, endDate) }); }); }); timeEntries.forEach((entry, i) => { if (entry.duration > maxHours) { entry.element.style.setProperty('background-color', '#fff3cd', 'important'); } for (let j = i + 1; j < timeEntries.length; j++) { if (entry.start < timeEntries[j].end && timeEntries[j].start < entry.end) { if (entry.element) { entry.element.style.setProperty('background-color', '#ffebee', 'important'); } if (timeEntries[j].element) { timeEntries[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 }); })();