Last active
February 11, 2025 18:11
-
-
Save iComputerfreak/3fd27de2139a485404be56d4a9ed44ec to your computer and use it in GitHub Desktop.
Show break time sum in Clockify
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Disclaimer: Created mainly with the help of ChatGPT | |
// Inject this with some kind of browser addon to run when the clockify tracker loads. | |
// ==UserScript== | |
// @name Clockify Breaks | |
// @description Tracks your break times for the current day, including the current running timer. | |
// @match https://app.clockify.me/tracker | |
// ==/UserScript== | |
// Stores the current state of time-tracker entries for comparison | |
let previousEntriesState = null; | |
// Cache the last calculated break time for use when the view is rebuilt | |
let cachedBreakData = { totalBreakMinutes: 0, individualBreaks: [] }; | |
/** | |
* Polls for changes in time-tracker entries and triggers break time recalculation if necessary. | |
*/ | |
function startPolling() { | |
console.log("Starting polling for changes in time entries..."); | |
setInterval(() => { | |
const currentEntriesState = getTimeTrackerEntriesState(); | |
// Check if the list of entries changed | |
if (!areStatesEqual(previousEntriesState, currentEntriesState)) { | |
console.log("Time entries changed. Re-calculating breaks..."); | |
previousEntriesState = currentEntriesState; | |
cachedBreakData = calculateBreakTimes(); | |
// Force re-rendering of the break times | |
ensureBreakTimeDisplay(true); | |
} else { | |
// Check if the break time display is missing and re-add it if needed | |
ensureBreakTimeDisplay(false); | |
} | |
}, 1000); // Polling interval: 1 second | |
} | |
/** | |
* Retrieves the current state of time-tracker entries and includes the running timer as an additional entry. | |
* @returns {Array} Array of objects containing start and end times for each entry. | |
*/ | |
function getTimeTrackerEntriesState() { | |
const entryGroup = document.querySelector('entry-group'); | |
if (!entryGroup) { | |
console.error('No <entry-group> element found.'); | |
return []; | |
} | |
const clCard = entryGroup.querySelector('.cl-card'); | |
if (!clCard) { | |
console.error('No .cl-card element found inside <entry-group>.'); | |
return []; | |
} | |
const timeTrackerEntries = clCard.querySelectorAll('time-tracker-entry'); | |
const timeBlocks = []; | |
timeTrackerEntries.forEach(entry => { | |
const timeInputs = entry.querySelectorAll('input-time-ampm input'); | |
if (timeInputs.length === 2) { | |
const startTime = timeInputs[0].value; | |
const endTime = timeInputs[1].value; | |
if (!startTime.includes(':')) { | |
if (startTime.length >= 3) { | |
startTime = startTime.slice(0, -2) + ":" + startTime.slice(-2); | |
} | |
} | |
if (!endTime.includes(':')) { | |
if (endTime.length >= 3) { | |
endTime = endTime.slice(0, -2) + ":" + endTime.slice(-2); | |
} | |
} | |
if (startTime && endTime) { | |
timeBlocks.push({ start: startTime, end: endTime }); | |
} | |
} | |
}); | |
// Add the running timer entry if available | |
const runningTimerEntry = getRunningTimerEntry(); | |
if (runningTimerEntry) { | |
timeBlocks.push(runningTimerEntry); | |
} | |
return timeBlocks; | |
} | |
/** | |
* Retrieves the current running timer's start and end time as an entry. | |
* @returns {Object|null} The running timer entry or null if not available. | |
*/ | |
function getRunningTimerEntry() { | |
const timerElement = document.querySelector('span.cl-input-time-picker-sum'); | |
if (!timerElement) { | |
console.warn("No running timer found."); | |
return null; | |
} | |
const timerDuration = timerElement.textContent.trim(); // "hh:mm:ss" | |
// Timer not running | |
if (timerDuration === "00:00:00") { | |
return null; | |
} | |
const durationInMinutes = parseDurationToMinutes(timerDuration); | |
if (durationInMinutes === null) { | |
console.error("Failed to parse running timer duration."); | |
return null; | |
} | |
const now = new Date(); | |
const endMinutes = now.getHours() * 60 + now.getMinutes(); | |
const startMinutes = endMinutes - durationInMinutes; | |
return { start: formatMinutesToTime(startMinutes), end: formatMinutesToTime(endMinutes) }; | |
} | |
/** | |
* Parses a duration string (hh:mm:ss) into total minutes. | |
* @param {string} duration Duration string in hh:mm:ss format. | |
* @returns {number|null} Total duration in minutes, or null if parsing fails. | |
*/ | |
function parseDurationToMinutes(duration) { | |
const parts = duration.split(':').map(Number); | |
if (parts.length !== 3) return null; | |
const [hours, minutes, seconds] = parts; | |
return hours * 60 + minutes + Math.floor(seconds / 60); | |
} | |
/** | |
* Formats a time in minutes into HH:MM string. | |
* @param {number} totalMinutes Total minutes from midnight. | |
* @returns {string} Formatted time as HH:MM. | |
*/ | |
function formatMinutesToTime(totalMinutes) { | |
const hours = Math.floor(totalMinutes / 60) % 24; | |
const minutes = totalMinutes % 60; | |
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; | |
} | |
/** | |
* Checks if two states of time-tracker entries are equal. | |
* @param {Array} state1 First state. | |
* @param {Array} state2 Second state. | |
* @returns {boolean} True if both states are equal, otherwise false. | |
*/ | |
function areStatesEqual(state1, state2) { | |
if (!state1 || !state2 || state1.length !== state2.length) return false; | |
return state1.every((entry, index) => | |
entry.start === state2[index].start && entry.end === state2[index].end | |
); | |
} | |
/** | |
* Calculates the break times based on the current time-tracker entries. | |
* @returns {Object} Object containing totalBreakMinutes and individualBreaks. | |
*/ | |
function calculateBreakTimes() { | |
const timeBlocks = getTimeTrackerEntriesState(); | |
if (timeBlocks.length < 2) { | |
console.log('Not enough time entries to calculate breaks.'); | |
return { totalBreakMinutes: 0, individualBreaks: [] }; | |
} | |
let totalBreakMinutes = 0; | |
const individualBreaks = []; | |
try { | |
// Convert times to minutes and sort blocks by start time | |
const blocksInMinutes = timeBlocks.map(block => ({ | |
start: parseTimeToMinutes(block.start), | |
end: parseTimeToMinutes(block.end), | |
})).sort((a, b) => a.start - b.start); | |
// Calculate total breaks | |
for (let i = 1; i < blocksInMinutes.length; i++) { | |
const breakMinutes = blocksInMinutes[i].start - blocksInMinutes[i - 1].end; | |
// Only count breaks > 1 minute | |
if (breakMinutes > 1) { | |
totalBreakMinutes += breakMinutes; | |
individualBreaks.push(breakMinutes); | |
} | |
} | |
} catch (error) { | |
console.error('Error calculating break times:', error); | |
return { totalBreakMinutes: 0, individualBreaks: [] }; | |
} | |
return { totalBreakMinutes, individualBreaks }; | |
} | |
/** | |
* Displays the calculated break times in the UI. | |
* @param {number} totalBreakMinutes Total break time in minutes. | |
* @param {Array} individualBreaks Array of individual break times. | |
*/ | |
function displayBreakTimes(totalBreakMinutes, individualBreaks) { | |
const approvalHeader = document.querySelector('approval-header'); | |
if (!approvalHeader) { | |
console.error('No <approval-header> element found.'); | |
return; | |
} | |
const formattedBreakTime = formatBreakTime(totalBreakMinutes, individualBreaks); | |
// Ensure a wrapper for break time exists | |
let breakTimeWrapper = approvalHeader.querySelector('.break-time-wrapper'); | |
if (!breakTimeWrapper) { | |
breakTimeWrapper = document.createElement('div'); | |
breakTimeWrapper.className = 'break-time-wrapper cl-d-flex cl-align-items-end cl-mt-2'; | |
approvalHeader.appendChild(breakTimeWrapper); | |
} | |
// Update the break time display | |
breakTimeWrapper.innerHTML = ` | |
<div class="cl-h6 cl-mb-0 cl-lh-1 cl-white-space-no-wrap">Break time:</div> | |
<div class="break-time cl-h2 cl-mb-0 cl-ml-2 cl-lh-1">${formattedBreakTime}</div> | |
`; | |
} | |
/** | |
* Ensures the break time display is present in the UI. If missing, re-add it using cached data. | |
*/ | |
function ensureBreakTimeDisplay(forced) { | |
const approvalHeader = document.querySelector('approval-header'); | |
if (!approvalHeader) { | |
console.error('No <approval-header> element found.'); | |
return; | |
} | |
// Check if the break time wrapper already exists | |
let breakTimeWrapper = approvalHeader.querySelector('.break-time-wrapper'); | |
if (!breakTimeWrapper || forced) { | |
displayBreakTimes(cachedBreakData.totalBreakMinutes, cachedBreakData.individualBreaks); | |
} | |
} | |
/** | |
* Parses a time string (HH:MM) into minutes. | |
* @param {string} time Time string in HH:MM format. | |
* @returns {number} Total minutes. | |
*/ | |
function parseTimeToMinutes(time) { | |
const [hours, minutes] = time.split(':').map(Number); | |
return hours * 60 + minutes; | |
} | |
/** | |
* Formats the break time into a human-readable string. | |
* @param {number} totalBreakMinutes Total break time in minutes. | |
* @param {Array} individualBreaks Array of individual break times. | |
* @returns {string} Formatted break time string. | |
*/ | |
function formatBreakTime(totalBreakMinutes, individualBreaks) { | |
const hours = Math.floor(totalBreakMinutes / 60); | |
const minutes = totalBreakMinutes % 60; | |
let result = `${hours > 0 ? `${hours}h ` : ''}${minutes}m`; | |
if (individualBreaks.length > 1) { | |
result += ' (' + individualBreaks.map(breakMinutes => `${breakMinutes}m`).join(', ') + ')'; | |
} | |
return result; | |
} | |
// Start the polling process | |
startPolling(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment