Last active
June 28, 2024 21:39
-
-
Save leonardAlbert/2ce80fc3cadb1da5c9efcc9a11c799da to your computer and use it in GitHub Desktop.
Script for updating the consumed/paused hours in Time Control.
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
// ==UserScript== | |
// @name Custom Timesheet Script | |
// @namespace custom-timesheet-script | |
// @version 0.1 | |
// @description Script for updating the consumed/paused hours in Time Control. | |
// @author Leonard A M Pedro | |
// @author Randler Ferraz Leal | |
// @grant none | |
// | |
// @note Instructions: | |
// @note * Paste the script into the browser console to apply the changes. | |
// @note ** For best use, install the chrome extension for the timesheet domain. | |
// @chrome extension https://chrome.google.com/webstore/detail/user-javascript-and-css/nbhcbdghjpllgmfilhnhkllmkecfmpld | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
const appV = { | |
// | |
enabled: true, | |
// | |
debugMode: false, | |
// | |
onLoad: false, | |
// Session Key | |
sessionStorageKey: 'appV-custom-timesheet-script', | |
// Preview Values | |
previewValues: { | |
label: false, | |
time: false, | |
}, | |
// | |
working: { | |
goals: 8, | |
status: false, | |
totalTime: '00:00', | |
totalTimestamp: 0, | |
}, | |
// | |
breaking: { | |
goals: 1, | |
status: false, | |
totalTime: '00:00', | |
totalTimestamp: 0, | |
}, | |
// | |
timeEntries: [{ | |
start: false, | |
end: false, | |
}, ], | |
// | |
api: { | |
domain: 'https://' + window.location.hostname, | |
token: false, | |
urls: { | |
day: '/employee-days/day', | |
daily: '/employee-days/daily', | |
profile: '/employee/current-profile', | |
delay: '/tasks/delay-average', | |
period: '/period', | |
entries: '/day-entries', | |
}, | |
}, | |
news: { | |
api: { | |
domain: 'https://www.tabnews.com.br/api/v1', | |
token: false, | |
urls: { | |
last: '/contents?page=1&per_page=15&strategy=relevant', | |
}, | |
}, | |
}, | |
// | |
notifications: { | |
endWorkDay: { | |
title: 'Controle de Horas - Fim do Expediente', | |
body: { | |
body: 'Hora de marcar o ponto!', | |
}, | |
alert: false, | |
snooze: false, | |
snoozeMinutes: 5, | |
}, | |
endBreakTime: { | |
title: 'Controle de Horas - Pausa', | |
body: { | |
body: 'Hora de marcar o ponto!', | |
}, | |
alert: false, | |
snooze: false, | |
snoozeMinutes: 1, | |
}, | |
}, | |
// Run tasks. | |
run: function() { | |
appV.scriptApiSettings(); | |
appV.scriptViewHome(); | |
appV.scriptAdjustPageStyle(); | |
appV.addCardTabNews(); | |
}, | |
// ============================================================ | |
// === SESSION STORAGE | |
// ============================================================ | |
// | |
getSessionValue: function(pointer) { | |
let key = appV.sessionStorageKey; | |
const storedData = sessionStorage.getItem(key); | |
if (!storedData) return undefined; | |
try { | |
const parsedData = JSON.parse(storedData); | |
const keys = pointer.split('.'); | |
let value = parsedData; | |
for (let key of keys) { | |
value = value[key]; | |
if (value === undefined) return undefined; | |
} | |
return value; | |
} catch (error) { | |
console.error('Error parsing JSON from sessionStorage:', error); | |
return undefined; | |
} | |
}, | |
// | |
setSessionValue: function(pointer, newValue) { | |
let key = appV.sessionStorageKey; | |
let storedData = sessionStorage.getItem(key); | |
let dataToUpdate = {}; | |
if (storedData) { | |
try { | |
dataToUpdate = JSON.parse(storedData); | |
} catch (error) { | |
console.error( | |
'Error parsing JSON from sessionStorage:', | |
error | |
); | |
return; | |
} | |
} | |
let currentLevel = dataToUpdate; | |
const keys = pointer.split('.'); | |
for (let i = 0; i < keys.length - 1; i++) { | |
if (!currentLevel[keys[i]]) { | |
currentLevel[keys[i]] = {}; | |
} | |
currentLevel = currentLevel[keys[i]]; | |
} | |
currentLevel[keys[keys.length - 1]] = newValue; | |
sessionStorage.setItem(key, JSON.stringify(dataToUpdate)); | |
}, | |
// ============================================================ | |
// === SCRIPT API SETTINGS | |
// ============================================================ | |
// | |
scriptApiSettings: function() { | |
// Loop schedule - 1 second. | |
setInterval(function() { | |
appV.getApiToken(); | |
}, 1000); | |
}, | |
getApiToken: function() { | |
// Check if it already exists. | |
if (appV.api.token != false) return; | |
for (let i = 0; i < sessionStorage.length; i++) { | |
let key = sessionStorage.key(i); | |
let value = sessionStorage.getItem(key); | |
try { | |
let jsonValue = JSON.parse(value); | |
if ( | |
jsonValue.tokenType === 'Bearer' && | |
jsonValue.target && | |
jsonValue.target.startsWith('api://') | |
) { | |
appV.api.token = jsonValue.secret; | |
} | |
} catch (e) { | |
continue; | |
} | |
} | |
if (appV.debugMode) console.log(appV.api.token); // @DEBUG | |
}, | |
// ============================================================ | |
// === SCRIPT ADJUST PAGE STYLE | |
// ============================================================ | |
// | |
scriptAdjustPageStyle: function() { | |
// Loop schedule - 1 millisecond. | |
setInterval(function() { | |
appV.adjustStyle(); | |
appV.onLoad = | |
document.querySelector('.entry-container .entry-loader') != | |
null; | |
}, 1); | |
}, | |
// Adjust Page Style | |
adjustStyle: function() { | |
// Background color | |
document | |
.querySelectorAll('.mat-drawer-container') | |
.forEach(function(element, index) { | |
element.style.backgroundColor = '#383838'; | |
}); | |
// Link color | |
document | |
.querySelectorAll( | |
'.main-container .mat-tab-link, .main-container .mat-tab-link.active-link' | |
) | |
.forEach(function(element, index) { | |
element.style.color = '#ffffff'; | |
}); | |
}, | |
// ============================================================ | |
// === SCRIPT VIEW HOME | |
// ============================================================ | |
// Run script on home page. | |
scriptViewHome: function() { | |
// Enable Notifications. | |
Notification.requestPermission(); | |
// Loop schedule - 1 second. | |
setInterval(function() { | |
// Run only home page. | |
if (!/^\/$/.test(window.location.pathname)) return; | |
if (appV.onLoad) { | |
appV.previewValues.label = false; | |
appV.previewValues.time = false; | |
return; | |
} | |
if (appV.debugMode) console.log('DEBUG: ', appV); // @DEBUG | |
appV.savePreviewValues(); | |
appV.addRemaingAppointmentToday(); | |
appV.addOnOffButton(); | |
if (!appV.enabled) return; | |
appV.getTimeEntries(); | |
appV.updateTotalHoursLabel(); | |
appV.updateHoursWorked(); | |
appV.notificationEndWorkDay(); | |
appV.updateHoursBreak(); | |
appV.notificationBreakTime(); | |
}, 1000); | |
// Loop schedule - 1 minute. | |
setInterval(function() { | |
// Run only home page. | |
if (!/^\/$/.test(window.location.pathname)) return; | |
appV.reloadPage(); | |
}, 60000); | |
}, | |
// Reload pate at midnight. | |
reloadPage: function() { | |
let dateNow = new Date(); | |
if (dateNow.getHours() == 0 && dateNow.getMinutes() == 0) | |
location.reload(); | |
}, | |
// Save preview value of Total. | |
savePreviewValues: function() { | |
if (appV.previewValues.time && appV.previewValues.title) return; | |
appV.previewValues.time = document.querySelector( | |
'.entry-container .entry-actions .container-hours-label .hours-label' | |
).innerHTML; | |
appV.previewValues.title = document.querySelector( | |
'.entry-container .entry-actions .total-hours .hours-title' | |
).innerHTML; | |
}, | |
// | |
addRemaingAppointmentToday: function() { | |
if (!appV.enabled) { | |
if (document.getElementById('remaining-hours')) { | |
document.getElementById('remaining-hours').remove(); | |
} | |
return; | |
} | |
let totalAppointmentToday = document.querySelector( | |
'.amount-hours > .value' | |
).innerText; | |
let hours = parseInt(totalAppointmentToday.split(':')[0]); | |
let minutes = parseInt(totalAppointmentToday.split(':')[1]); | |
// Calculating remaining hours and minutes | |
let remainingHoursAppointment = 8 - hours; | |
let remainingMinutesAppointment = 60 - minutes; | |
if (remainingMinutesAppointment === 60) { | |
remainingMinutesAppointment = 0; | |
} else { | |
remainingHoursAppointment -= 1; | |
} | |
// Adjusting if minutes are negative | |
if (remainingMinutesAppointment < 0) { | |
remainingMinutesAppointment += 60; | |
remainingHoursAppointment -= 1; | |
} | |
// Ensuring that the remainingHoursAppointment value is at most 8 | |
if (remainingHoursAppointment > 8) { | |
remainingHoursAppointment = 8; | |
remainingMinutesAppointment = 0; | |
} | |
// Ensuring that the minutes format is always two digits | |
let remainingMinutesString = | |
remainingMinutesAppointment < 10 ? | |
'0' + remainingMinutesAppointment : | |
remainingMinutesAppointment; | |
let remainingHoursString = | |
remainingHoursAppointment < 10 ? | |
'0' + remainingHoursAppointment : | |
remainingHoursAppointment; | |
// Adjusting if remainingHoursAppointment is less than 0 | |
if (remainingHoursAppointment < 0) { | |
remainingHoursAppointment = 0; | |
remainingMinutesAppointment = 0; | |
remainingMinutesString = '00'; | |
remainingHoursString = '00'; | |
} | |
let remainingTimeString = `${remainingHoursString}:${remainingMinutesString}`; | |
let contentRemaining = document.getElementById('remaining-hours'); | |
if (!contentRemaining) { | |
let newHtml = document.createElement('div'); | |
newHtml.className = 'amount-hours'; | |
newHtml.id = 'remaining-hours'; | |
newHtml.style = `align-items: center; | |
background-color: #fceeda; | |
border-radius: 8px; | |
box-sizing: border-box; | |
display: flex; | |
height: 40px; | |
justify-content: space-between; | |
margin: 16px 0 0 10px; | |
padding: 12px 16px 12px 10px; | |
width: 240px;`; | |
newHtml.innerHTML = `<br /> | |
<span class="label" | |
style="color: #ab5710; | |
font-size: 14px; | |
font-weight: 600;"> | |
Restantes para apontar: | |
</span> | |
<span id="total-remaining" | |
style="font-size: 16px;font-weight: 600;color: #a75a0a;" | |
class="value"> | |
${remainingTimeString} | |
</span>`; | |
let container = | |
document.querySelector('.amount-hours').parentNode; | |
container.appendChild(newHtml); | |
} else { | |
document.getElementById('total-remaining').innerText = | |
remainingTimeString; | |
} | |
}, | |
// Add Card with news. | |
addCardTabNews: function() { | |
// Loop schedule - 1 millisecond. | |
setInterval(function() { | |
let html = `<app-generic-card size="large" id="card-tab-news"> | |
<mat-card class="mat-card mat-focus-indicator mat-generic-card large"> | |
<label class="title">📰 Tech Notícias: </label><em id="content-news">carregando ...</em> | |
</mat-card> | |
</app-generic-card>`; | |
if (!document.getElementById('card-tab-news') && document | |
.querySelectorAll('.main-content .card-section')[1]) { | |
document | |
.querySelectorAll('.main-content .card-section')[1] | |
.insertAdjacentHTML('afterbegin', html); | |
appV.populateTabNewsContent(); | |
} | |
}, 1); | |
// appV.getContentNews(); | |
// Loop schedule 30 minutes. | |
// setInterval(function () { | |
// appV.getContentNews(); | |
// }, 1800000); | |
}, | |
// | |
populateTabNewsContent: function() { | |
let currentIndex = 0; | |
const newsList = appV.getSessionValue('appV.news.content'); | |
const newsContainer = document.getElementById("content-news"); | |
const updateNews = function() { | |
if (typeof newsList === 'undefined') | |
return; | |
newsContainer.textContent = '<a target="_blank">' + newsList[currentIndex]["title"] + '</a>'; | |
const linkElement = document.createElement('a'); | |
linkElement.textContent = newsList[currentIndex]["title"]; | |
linkElement.setAttribute('href', 'https://www.tabnews.com.br/' + newsList[currentIndex]["owner_username"] + '/' + newsList[currentIndex]["slug"]); | |
linkElement.setAttribute('target', '_blank'); | |
linkElement.style.color = 'inherit'; | |
// linkElement.style.textDecoration = 'none'; | |
newsContainer.innerHTML = ''; | |
newsContainer.appendChild(linkElement); | |
currentIndex = (currentIndex + 1) % newsList.length; | |
} | |
// Loop schedule 15 minutes. | |
setInterval(appV.getContentApiNews(true), 900000); | |
// Loop schedule 8 secconds. | |
setInterval(updateNews, 8000); | |
updateNews(); | |
}, | |
// | |
getContentApiNews: function(forceUpdate = false) { | |
let hasContent = appV.getSessionValue( | |
'appV.news.content'); | |
if (typeof hasContent !== 'undefined' && forceUpdate == false) | |
return; | |
fetch( | |
appV.news.api.domain + appV.news.api.urls.last, { | |
method: 'GET', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
} | |
) | |
.then((response) => { | |
if (response.ok) { | |
return response.json(); | |
} else { | |
throw new Error('Error: ' + response.status); | |
} | |
}) | |
.then((data) => { | |
try { | |
let content = JSON.parse(JSON.stringify(data)); | |
appV.setSessionValue( | |
'appV.news.content', content); | |
if (appV.debugMode) console.log('getContentApiNews', content); // @DEBUG | |
} catch (e) { | |
console.error('Error invalid JSON:', e); | |
} | |
}) | |
.catch((error) => console.error('Error: ', error)); | |
}, | |
// Add On-Off button on page. | |
addOnOffButton: function() { | |
if ( | |
document.querySelector( | |
'.entry-container .entry-actions .mat-slide-toggle-bar #appV-enabled' | |
) | |
) | |
return; | |
let classChecked = appV.enabled ? 'mat-checked' : ''; | |
let html = `<span id="appV-buttom" class="mat-slide-toggle-label mat-slide-toggle ${classChecked} mat-slide-toggle-bar"> | |
<input type="checkbox" role="switch" class="mat-slide-toggle-input cdk-visually-hidden" id="appV-enabled" tabindex="0" aria-checked="${appV.enabled}"> | |
<span class="mat-slide-toggle-thumb-container"> | |
<span class="mat-slide-toggle-thumb"></span> | |
<span mat-ripple="" class="mat-ripple mat-slide-toggle-ripple mat-focus-indicator"> | |
<span class="mat-ripple-element mat-slide-toggle-persistent-ripple"></span> | |
</span> | |
</span> | |
</span>`; | |
document | |
.querySelector('.entry-container .entry-actions') | |
.insertAdjacentHTML('afterbegin', html.concat(' ')); | |
document | |
.querySelector('.entry-container .entry-actions #appV-buttom') | |
.addEventListener('click', function(element, index) { | |
appV.enabled = !appV.enabled; | |
document.querySelector( | |
'.entry-container .entry-actions #appV-enabled' | |
).ariaChecked = appV.enabled; | |
document | |
.querySelector( | |
'.entry-container .entry-actions #appV-buttom' | |
) | |
.classList.remove('mat-checked'); | |
if (appV.enabled) { | |
document | |
.querySelector( | |
'.entry-container .entry-actions #appV-buttom' | |
) | |
.classList.add('mat-checked'); | |
} else { | |
document.querySelector( | |
'.entry-container .entry-actions .container-hours-label .hours-label' | |
).innerHTML = appV.previewValues.time; | |
document.querySelector( | |
'.entry-container .entry-actions .total-hours .hours-title' | |
).innerHTML = appV.previewValues.title; | |
} | |
}); | |
return; | |
}, | |
// Populate entries time | |
getTimeEntries: function() { | |
appV.timeEntries = [{ | |
start: false, | |
end: false, | |
}, ]; | |
document | |
.querySelectorAll('.entry .entries-container') | |
.forEach(function(element, index) { | |
let startTime = element.querySelector( | |
'.entry-time-container .entry-time-label' | |
).textContent; | |
let endTime = element.querySelector( | |
'.exit-time-container .exit-time-label' | |
).textContent; | |
let entrie = { | |
start: false, | |
end: false, | |
}; | |
if (startTime.match(/([0-9]{2}\:[0-9]{2})/gm) != null) { | |
entrie.start = startTime; | |
} | |
if (endTime.match(/([0-9]{2}\:[0-9]{2})/gm) != null) { | |
entrie.end = endTime; | |
} | |
appV.timeEntries[index] = entrie; | |
}); | |
// Update status. | |
appV.working.status = false; | |
appV.breaking.status = false; | |
let entriesLength = appV.timeEntries.length; | |
if ( | |
(entriesLength == 1 && | |
appV.timeEntries[0].start && | |
!appV.timeEntries[0].end) || | |
(entriesLength > 1 && | |
appV.timeEntries[entriesLength - 1].start && | |
!appV.timeEntries[entriesLength - 1].end) | |
) { | |
appV.working.status = true; | |
appV.breaking.status = false; | |
} else if ( | |
entriesLength == 1 && | |
appV.timeEntries[0].start && | |
appV.timeEntries[0].end | |
) { | |
appV.working.status = false; | |
appV.breaking.status = true; | |
} | |
return; | |
}, | |
// Send notification break time. | |
notificationBreakTime: function() { | |
let hoursBreakTimestamp = Math.floor(appV.breaking.goals * 3600000); | |
if ( | |
appV.breaking.status && | |
appV.breaking.totalTimestamp >= hoursBreakTimestamp | |
) { | |
if (!appV.notifications.endBreakTime.alert) { | |
appV.notifications.endBreakTime.alert = true; | |
new Notification( | |
appV.notifications.endBreakTime.title, | |
appV.notifications.endBreakTime.body | |
); | |
} else if (!appV.notifications.endBreakTime.snooze) { | |
appV.notifications.endBreakTime.snooze = true; | |
setTimeout(function() { | |
appV.notifications.endBreakTime.snooze = false; | |
new Notification( | |
appV.notifications.endBreakTime.title, | |
appV.notifications.endBreakTime.body | |
); | |
}, appV.notifications.endBreakTime.snoozeMinutes * 60000); | |
} | |
} | |
return; | |
}, | |
// Send notification end work day. | |
notificationEndWorkDay: function() { | |
let hoursDailyGoalTimestamp = Math.floor( | |
appV.working.goals * 3600000 | |
); | |
if ( | |
appV.working.status && | |
appV.working.totalTimestamp >= hoursDailyGoalTimestamp | |
) { | |
if (!appV.notifications.endWorkDay.alert) { | |
appV.notifications.endWorkDay.alert = true; | |
new Notification( | |
appV.notifications.endWorkDay.title, | |
appV.notifications.endWorkDay.body | |
); | |
} else if (!appV.notifications.endWorkDay.snooze) { | |
appV.notifications.endWorkDay.snooze = true; | |
setTimeout(function() { | |
appV.notifications.endWorkDay.snooze = false; | |
new Notification( | |
appV.notifications.endWorkDay.title, | |
appV.notifications.endWorkDay.body | |
); | |
}, appV.notifications.endWorkDay.snoozeMinutes * 60000); | |
} | |
} | |
return; | |
}, | |
// Update Label Total Worked. | |
updateTotalHoursLabel: function(label = 'Total', emogi = '😃') { | |
// | |
if (appV.working.status) { | |
label = 'Worked'; | |
emogi = '🚀'; | |
} else if (appV.breaking.status) { | |
label = 'Paused'; | |
emogi = '☕'; | |
} | |
document.querySelector( | |
'.entry-container .entry-actions .total-hours .hours-title' | |
).innerHTML = ''.concat(emogi, ';  ', label); | |
return; | |
}, | |
// Update Hours Worked. | |
updateHoursWorked: function() { | |
appV.working.totalTimestamp = 0; | |
if (!appV.working.status) return; | |
appV.timeEntries.forEach(function(element, index) { | |
let startDate = new Date(); | |
let endDate = new Date(); | |
let startTime = element.start; | |
let endTime = element.end; | |
if (!startTime && !endTime) return; | |
if (!startTime) { | |
startTime = ''.concat( | |
startDate.getHours(), | |
':', | |
startDate.getMinutes() | |
); | |
} | |
if (!endTime) { | |
endTime = ''.concat( | |
endDate.getHours(), | |
':', | |
endDate.getMinutes() | |
); | |
} | |
startDate.setHours(startTime.split(':')[0]); | |
startDate.setMinutes(startTime.split(':')[1]); | |
endDate.setHours(endTime.split(':')[0]); | |
endDate.setMinutes(endTime.split(':')[1]); | |
appV.working.totalTimestamp += endDate - startDate; | |
}); | |
let diffHours = Math.floor( | |
(appV.working.totalTimestamp % 86400000) / 3600000 | |
); | |
let diffMinutes = Math.round( | |
((appV.working.totalTimestamp % 86400000) % 3600000) / 60000 | |
); | |
appV.working.totalTime = ''.concat( | |
'0'.concat(diffHours).slice(-2), | |
':', | |
'0'.concat(diffMinutes).slice(-2) | |
); | |
document.querySelector( | |
'.entry-container .entry-actions .container-hours-label .hours-label' | |
).innerHTML = appV.working.totalTime; | |
return; | |
}, | |
// Update Hours Paused. | |
updateHoursBreak: function() { | |
appV.breaking.totalTimestamp = 0; | |
if (!appV.breaking.status) return; | |
let entriesLength = appV.timeEntries.length; | |
let startDate = new Date(); | |
let endDate = new Date(); | |
let startTime = appV.timeEntries[entriesLength - 1].end; | |
let endTime = ''.concat( | |
endDate.getHours(), | |
':', | |
endDate.getMinutes() | |
); | |
startDate.setHours(startTime.split(':')[0]); | |
startDate.setMinutes(startTime.split(':')[1]); | |
endDate.setHours(endTime.split(':')[0]); | |
endDate.setMinutes(endTime.split(':')[1]); | |
appV.breaking.totalTimestamp += endDate - startDate; | |
let diffHours = Math.floor( | |
(appV.breaking.totalTimestamp % 86400000) / 3600000 | |
); | |
let diffMinutes = Math.round( | |
((appV.breaking.totalTimestamp % 86400000) % 3600000) / 60000 | |
); | |
appV.breaking.totalTime = ''.concat( | |
'0'.concat(diffHours).slice(-2), | |
':', | |
'0'.concat(diffMinutes).slice(-2) | |
); | |
document.querySelector( | |
'.entry-container .entry-actions .container-hours-label .hours-label' | |
).innerHTML = appV.breaking.totalTime; | |
return; | |
}, | |
}; | |
// Run script. | |
appV.run(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment