Skip to content

Instantly share code, notes, and snippets.

@iComputerfreak
Last active February 11, 2025 18:11
Show Gist options
  • Save iComputerfreak/3fd27de2139a485404be56d4a9ed44ec to your computer and use it in GitHub Desktop.
Save iComputerfreak/3fd27de2139a485404be56d4a9ed44ec to your computer and use it in GitHub Desktop.
Show break time sum in Clockify
// 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