Last active
April 9, 2024 05:37
-
-
Save metaodi/4222b9ce80ed50f4d2deb71e7bf63316 to your computer and use it in GitHub Desktop.
A user script to improve the E3 experience
This file contains 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 E3 Improver | |
// @namespace https://metaodi.ch | |
// @version 0.28 | |
// @description Make E3 even better :) | |
// @author Stefan Oderbolz | |
// @match https://e3online.prd.szh.loc/ | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=szh.loc | |
// @grant GM_setValue | |
// @grant GM_getValue | |
// @run-at document-idle | |
// @downloadURL https://gist.github.com/metaodi/4222b9ce80ed50f4d2deb71e7bf63316/raw/E3-Improver.user.js | |
// @updateURL https://gist.github.com/metaodi/4222b9ce80ed50f4d2deb71e7bf63316/raw/E3-Improver.user.js | |
// @require https://rawgit.com/metaodi/ics.js/master/ics.deps.min.js | |
// ==/UserScript== | |
/* globals ics */ | |
function findChildElement(start, tagName, idPattern) { | |
var targets = start.getElementsByTagName(tagName); | |
for (const elem of targets) { | |
if (elem.id !== undefined && elem.id.match(idPattern)) { | |
return elem; | |
} | |
} | |
return null; | |
} | |
function findAllChildElements(start, tagName, idPattern) { | |
var targets = start.getElementsByTagName(tagName); | |
var target = []; | |
for (const elem of targets) { | |
if (elem.id !== undefined && elem.id.match(idPattern)) { | |
target.push(elem); | |
} | |
} | |
return target; | |
} | |
function convertToDate(s) { | |
var parts = s.split(':'); | |
var d = new Date(); | |
d.setHours(...parts); | |
return d; | |
} | |
function calcHours(times) { | |
var hours = 0.0; | |
var start; | |
while( (start = times.shift()) !== undefined ) { | |
var end = times.shift(); | |
var startDate = convertToDate(start); | |
var endDate = convertToDate(end); | |
var diff = endDate.getTime() - startDate.getTime(); | |
hours += (diff / (60 * 60 * 1000)); | |
} | |
return (Math.round(hours * 100) / 100); | |
} | |
function calcMin(s) { | |
var parts = s.split(":", 2) | |
var hours = parseInt(parts[0]) | |
var factor = 1; | |
if (hours < 0) { | |
factor = -1; | |
} | |
var minutes = parseInt(parts[1]) | |
return factor * ((Math.abs(hours) * 60) + minutes); | |
} | |
function minToHour(min) { | |
var sign = ''; | |
if (min < 0) { | |
sign = '-'; | |
} | |
var abs_min = Math.abs(min); | |
var new_min = abs_min % 60; | |
var hours = (abs_min - new_min) / 60; | |
var rhours = String((hours)).padStart(2, '0'); | |
var rminutes = String(new_min).padStart(2, '0'); | |
return `${sign}${rhours}:${rminutes}`; | |
} | |
function addToDom(text, parentElement) { | |
var tag = document.getElementById('total-hours'); | |
if (!tag) { | |
tag = document.createElement("p"); | |
tag.id = 'total-hours'; | |
} | |
tag.innerHTML = text; | |
parentElement.parentNode.appendChild(tag); | |
} | |
function findCodeColumn(cells, str) { | |
var found = false | |
var re = new RegExp(str, "g"); | |
for (var i in cells) { | |
if (re.exec(cells[i].textContent)) { | |
return i; | |
} | |
} | |
return null; | |
} | |
function swissDateToJsDate(s) { | |
var parts = s.split('.'); | |
var d = new Date(parts[2], parseInt(parts[1]) - 1, parts[0]); | |
return d; | |
} | |
function addDays(date, days) { | |
var result = new Date(date); | |
result.setDate(result.getDate() + days); | |
return result; | |
} | |
function getAbsences() { | |
var absenceTable = findChildElement(document, "table", /Vertical.*MainLayoutEdit_.*dviEmployeeAbsences_.*_DXMainTable/i); | |
if (!absenceTable) { | |
// absenceTable not available | |
return | |
} | |
// get holidays | |
var absences = []; | |
var rows = findAllChildElements(absenceTable, "tr", /Vertical.*MainLayoutEdit_.*dviEmployeeAbsences_.*_DXDataRow/i); | |
var found = false | |
var codeIndex = findCodeColumn(absenceTable.rows[1].cells, "Zeitcode 1"); | |
var startIndex = findCodeColumn(absenceTable.rows[1].cells, "Von"); | |
var endIndex = findCodeColumn(absenceTable.rows[1].cells, "Bis"); | |
var prozentIndex = findCodeColumn(absenceTable.rows[1].cells, "Prozent 1"); | |
if (!codeIndex || !startIndex || !endIndex) { | |
console.error("Spalte 'Zeitcode 1', 'Von' oder 'Bis' nicht gefunden"); | |
return; | |
} | |
for (const row of absenceTable.rows) { | |
var holiday = false; | |
if (row.cells[codeIndex].textContent.match(/^Ferien/)) { | |
holiday = true | |
} | |
var dateRe = new RegExp(/.*(\d{2}.\d{2}.\d{4})/) | |
if (!row.cells[startIndex].textContent.match(dateRe)) { | |
continue; | |
} | |
absences.push({ | |
'start': dateRe.exec(row.cells[startIndex].textContent)[1], | |
'startDate': swissDateToJsDate(dateRe.exec(row.cells[startIndex].textContent)[1]), | |
'end': dateRe.exec(row.cells[endIndex].textContent)[1], | |
'endDate': swissDateToJsDate(dateRe.exec(row.cells[endIndex].textContent)[1]), | |
'percent': row.cells[prozentIndex].textContent, | |
'text': row.cells[codeIndex].textContent, | |
'holiday': holiday | |
}); | |
} | |
return absences; | |
} | |
function saveHolidays() { | |
var absences = getAbsences(); | |
const daysInFuture = calcDaysInTheFuture(absences); | |
GM_setValue("holidaysInFuture", daysInFuture); | |
console.log("Ferien gespeichert:", daysInFuture); | |
} | |
function saveHourSaldo() { | |
var counterTable = findChildElement(document, "table", /Vertical.*MainLayoutEdit_.*dviCounterValues_.*_DXMainTable/i); | |
var nameIndex = findCodeColumn(counterTable.rows[1].cells, "Name"); | |
var valueIndex = findCodeColumn(counterTable.rows[1].cells, "Wert per Vortag"); | |
var value = undefined; | |
for (const row of counterTable.rows) { | |
if (row.cells[nameIndex].textContent === 'Arbeitszeitsaldo') { | |
value = row.cells[valueIndex].textContent | |
break; | |
} | |
} | |
GM_setValue("hourSaldo", calcMin(value)); | |
console.log("Arbeitszeitsaldo gespeichert:", value, calcMin(value)); | |
} | |
function calcDaysInTheFuture(absences) { | |
var dates = []; | |
for (const absence of absences) { | |
if (!absence.holiday) { | |
continue; | |
} | |
var currentDate = absence.startDate; | |
while (currentDate <= absence.endDate) { | |
dates.push({'date': currentDate, 'percent': absence.percent}); | |
currentDate = addDays(currentDate, 1); | |
} | |
} | |
const datesInFuture = dates.filter(holiday => holiday.date >= new Date()); | |
const daysInFuture = datesInFuture.map(holiday => 1 * holiday.percent / 100); | |
var sum = 0; | |
for (const item of daysInFuture) { | |
sum += item; | |
} | |
return sum; | |
} | |
function displayHolidays() { | |
var numberOfFutureHolidays = GM_getValue("holidaysInFuture"); | |
console.log("Ferien (geplant):", numberOfFutureHolidays); | |
var counterTable = findChildElement(document, "table", /Vertical.*MainLayoutEdit_.*dviCounterValues_.*_DXMainTable/i); | |
var nameIndex = findCodeColumn(counterTable.rows[1].cells, "Name"); | |
var valueIndex = findCodeColumn(counterTable.rows[1].cells, "Wert per Vortag"); | |
for (const row of counterTable.rows) { | |
if (row.cells[nameIndex].textContent === 'Feriensaldo') { | |
var value = row.cells[valueIndex].textContent.match(/(\d+,?\d?)/)[1] | |
row.cells[valueIndex].innerHTML = value + " <em>(" + numberOfFutureHolidays + ")</em>"; | |
} | |
} | |
var tag = document.getElementById('legend-plan'); | |
if (!tag) { | |
tag = document.createElement("p"); | |
tag.id = 'legend-plan'; | |
} | |
tag.innerHTML = "<em>Werte in Klammern sind geplant.</em>"; | |
counterTable.parentNode.appendChild(tag); | |
} | |
function updateHours() { | |
// find table with stamps | |
var tables = document.getElementsByTagName("table"); | |
var stampTable = findChildElement(document, "table", /Vertical.*MainLayoutEdit_.*_dviTodayStamps_.*_DXMainTable/i); | |
if (!stampTable) { | |
// stampTable not available | |
return | |
} | |
// get time logs | |
var times = []; | |
var rows = findAllChildElements(stampTable, "tr", /Vertical.*MainLayoutEdit_.*_dviTodayStamps_.*_DXDataRow/i); | |
for (const row of rows) { | |
// ignore older entries | |
if (row.style['background-color'] === 'rgb(229, 229, 229)') { | |
continue; | |
} | |
var directionTable = findChildElement(row, "table", /Vertical.*MainLayoutEdit_.*_dviTodayStamps_.*_xaf_Direction_View/i); | |
if (directionTable) { | |
var direction = directionTable.querySelector("span").textContent; | |
if (direction == 'Undefiniert') { | |
//ignore undefined directions for calculations | |
continue; | |
} | |
} | |
var time = findChildElement(row, "span", /Vertical.*MainLayoutEdit_.*_dviTodayStamps_.*_xaf_StartOn_View/i); | |
var timeText = time.textContent; | |
if (timeText) { | |
var match = timeText.match(/.*(\d{2}:\d{2})/) | |
times.push(match[1]); | |
} | |
} | |
var d = new Date(); | |
var open = false; | |
var len = times.length; | |
// if the array is odd, then the last entry is missing | |
// add the current time at the end to simulate a stamp right now | |
if ((len % 2) !== 0) { | |
open = true; | |
times.push(`${d.getHours()}:${d.getMinutes()}`); | |
} | |
var hours = calcHours(times); | |
var rhours = String(Math.floor(hours)).padStart(2, '0'); | |
var minutes = (hours - rhours) * 60; | |
var rminutes = String(Math.round(minutes)).padStart(2, '0'); | |
GM_setValue("hoursToday", Math.round(hours*60)); | |
if (open) { | |
addToDom(`Zeit (offen): <strong>${hours} Std. / ${rhours}:${rminutes}h</strong>`, stampTable); | |
console.log(`Zeit (offen): ${hours}h`); | |
} else { | |
addToDom(`Zeit (abgeschlossen): <strong>${hours} Std. / ${rhours}:${rminutes}h</strong>`, stampTable); | |
console.log(`Zeit (abgeschlossen): ${hours}h`); | |
} | |
}; | |
function downloadCalendar() { | |
var calendarMenu = findChildElement(document, "div", /Vertical.*MainLayoutEdit_.*dviEmployeeAbsences_ToolBar_Menu_DXSA1/i) | |
if (calendarMenu) { | |
var cal = ics('default', "'-//metaodi//e3-improver//DE'", "E3 Absenzen"); | |
var absences = getAbsences(); | |
for (const absence of absences) { | |
var text = absence.text; | |
var start = absence.startDate; | |
start.setHours(8) | |
var end = absence.endDate; | |
end.setHours(17); | |
if (absence.percent == "50") { | |
text += " 50%"; | |
start.setHours(8); | |
end.setHours(12); | |
} | |
var status = 'busy'; | |
if (absence.holiday || absence.text.match(/Betriebsferien/)) { | |
status = 'oof'; | |
} | |
cal.addEvent(`${text} (E3)`, text, '', start, end, status); | |
} | |
var dl = document.getElementById('ics-download'); | |
if (dl) { | |
return; | |
} | |
dl = document.createElement("li"); | |
dl.id = 'ics-download'; | |
dl.className = 'dxm-item dropDownExport menuActionImageSVG'; | |
dl.innerHTML = "<a class=\"dxm-content dxm-hasText dx dxalink\" href=\"#\" role=\"menuitem\" style=\"float: none;\"><img class=\"dxm-image dx-vam\" src=\"https://systemuicons.com/images/icons/calendar_month.svg\" style=\"height:24px;width:24px;\"><span class=\"dx-vam dxm-contentText\">iCal Datei</span></a><b class=\"dx-clear\"></b>"; | |
var item = dl.querySelector('a'); | |
dl.addEventListener("click", function(e) { | |
e.preventDefault(); | |
cal.download('E3 Absenzen'); | |
}); | |
dl.addEventListener("mouseover", function(e) { | |
dl.className = 'dxm-item dropDownExport menuActionImageSVG dxm-hovered'; | |
item.style.color = 'rgb(44, 134, 211)'; | |
}); | |
dl.addEventListener("mouseout", function(e) { | |
dl.className = 'dxm-item dropDownExport menuActionImageSVG'; | |
item.style.color = 'rgb(74, 74, 74)'; | |
}); | |
var formatList = calendarMenu.querySelector('ul'); | |
formatList.prepend(dl); | |
} | |
} | |
function addRemainingButton() { | |
var iframes = findAllChildElements(document, "iframe", /Vertical_PopupWindow.*_CIF-1/i); | |
if (iframes && iframes[0]) { | |
console.log("Dialog found, check if Leistung dialog..."); | |
var iframe = iframes[0]; | |
var dialog = iframe.contentWindow.document.getElementById('Dialog_UPVH'); | |
var restButton = iframe.contentWindow.document.getElementById('rest-button'); | |
if (dialog && !restButton) { | |
console.log("Found Leistung Dialog!"); | |
var button = findChildElement(iframe.contentWindow.document, "li", /Dialog_.*MainLayoutEdit_.*Time_BatchReport_RightSide_Menu_DXI2.*/i); | |
var timeSplitMenu = findChildElement(iframe.contentWindow.document, "div", /Dialog_.*_MainLayoutEdit_.*E3Time_BatchReport_RightSide_Menu/i) | |
if (!timeSplitMenu) { | |
return; | |
} | |
var menuList = timeSplitMenu.querySelector('ul'); | |
var spacer = iframe.contentWindow.document.createElement("li"); | |
spacer.id = 'spacer-item'; | |
spacer.className = 'dxm-spacing'; | |
spacer.style = 'height:6px;'; | |
menuList.append(spacer); | |
var dl = iframe.contentWindow.document.createElement("li"); | |
dl.id = 'rest-button'; | |
dl.title = 'Rest einfügen'; | |
dl.className = 'dxm-item hasImage captionAndImage smallImage'; | |
dl.style = 'min-width: 127px;'; | |
var timeInputs = iframe.contentWindow.document.querySelectorAll('td.dxic input'); | |
var lastFocusInput; | |
timeInputs.forEach((ti) => { | |
ti.addEventListener("blur", function(e) { | |
lastFocusInput = ti; | |
}); | |
}); | |
dl.addEventListener("click", function(e) { | |
e.preventDefault(); | |
var restStr = findChildElement(iframe.contentWindow.document, "input", /Dialog_.*_MainLayoutEdit_.*_dviInTransactionMissingMinuteCountString_Edit.*/i).value; | |
if (lastFocusInput) { | |
lastFocusInput.value = restStr; | |
} | |
}); | |
dl.innerHTML = "<a class=\"dxm-content dxm-hasText dx dxalink\" href=\"#\" title=\"Rest\" role=\"menuitem\" style=\"float: none;\"><img class=\"dxm-image dx-vam\" src=\"/DXX.axd?handlerName=ImageResource&name=add&enbl=True&fldr=TemplatesV2Images&v=fab07171e94d5c717d92f769ab9f4902\" style=\"height:16px;width:16px;\"><span class=\"dx-vam dxm-contentText\">Rest einfügen</span></a><b class=\"dx-clear\"></b></li>"; | |
menuList.append(dl); | |
} | |
} | |
setTimeout(addRemainingButton, 5000); | |
} | |
function calcHourTotal() { | |
var dataTable = findChildElement(document, "table", /Vertical.*MainLayoutEdit_.*dviDayDatas_.*_DXMainTable/i); | |
var istIndex = findCodeColumn(dataTable.rows[1].cells, "Ist-Z"); | |
var istValues = []; | |
var sollValues = []; | |
var arbValues = []; | |
var hasIstValueToday = false; | |
for (var i = 0, row; row = dataTable.rows[i]; i++) { | |
//for (const row of dataTable.rows) { | |
var innerTable = row.cells[istIndex].querySelector('table'); | |
if (!innerTable) { | |
continue; | |
} | |
var istValue = innerTable.rows[0].cells[0].textContent; | |
if (istValue && istValue != "Ist-Z") { | |
istValues.push(calcMin(istValue)); | |
if (i == dataTable.rows.length - 1) { | |
hasIstValueToday = true; | |
} | |
} | |
var sollValue = innerTable.rows[0].cells[1].textContent; | |
if (sollValue && sollValue != "Sollz") { | |
sollValues.push(calcMin(sollValue)); | |
} | |
var arbValue = innerTable.rows[0].cells[2].textContent; | |
if (arbValue && arbValue != "AZS in h") { | |
arbValues.push(calcMin(arbValue)); | |
} | |
} | |
var totalIst = istValues.reduce((a, b) => a + b, 0); | |
var totalSoll = sollValues.reduce((a, b) => a + b, 0); | |
var totalArb = arbValues.reduce((a, b) => a + b, 0); | |
var currentSaldo = minToHour(totalIst - totalSoll); | |
console.log("Saldo (Ist - Soll):", currentSaldo); | |
var hoursToday = GM_getValue("hoursToday"); | |
var virtualSaldo; | |
if (hasIstValueToday) { | |
virtualSaldo = minToHour(totalIst + hoursToday - istValues[istValues.length - 1] - totalSoll); | |
} else { | |
virtualSaldo = minToHour(totalIst + hoursToday - totalSoll); | |
} | |
console.log("Saldo (virtuell, Ist + Heute - Soll):", virtualSaldo); | |
var hourSaldo = GM_getValue("hourSaldo"); | |
var totalSaldo = minToHour(hourSaldo); | |
console.log("Arbeitszeitsaldo (Vortag):", totalSaldo); | |
var saldoDisplay = `<strong>Arbeitszeitsaldo (Vortag)</strong>: ${totalSaldo} | <strong>Aktuelle Ansicht:</strong> ${currentSaldo} (<em>${virtualSaldo}</em>)`; | |
var totalHours = document.getElementById('total-hours'); | |
if (!totalHours) { | |
var topMenu = findChildElement(document, "div", /Vertical.*MainLayoutEdit_.*dviDayDatas_UPToolBar/i); | |
totalHours = document.createElement("p"); | |
totalHours.id = 'total-hours'; | |
totalHours.className = 'nf_rightMenu_AC'; | |
totalHours.style = 'padding-right: 35px;'; | |
totalHours.innerHTML = saldoDisplay; | |
topMenu.append(totalHours); | |
} else { | |
totalHours.innerHTML = saldoDisplay; | |
} | |
} | |
function replaceFerienAntragImage() { | |
var iframes = findAllChildElements(document, "iframe", /Vertical_PopupWindow.*_CIF-1/i); | |
var docu = window.document; | |
if (iframes && iframes[0]) { | |
docu = iframes[0].contentWindow.document; | |
} | |
var titles = { | |
'Mit/ohne beantragte Absenzen': { | |
'size': 16, | |
'viewBox': "0 0 21 21", | |
'docu': window.document | |
}, | |
'Mit/ohne Anträge': { | |
'size': 16, | |
'viewBox': "0 0 21 21", | |
'docu': window.document | |
}, | |
'Beantragen': { | |
'size': 25, | |
'viewBox': "-2 0 21 21", | |
'docu': docu | |
} | |
}; | |
for (let title in titles) { | |
var img_link = titles[title].docu.querySelector(`a[title='${title}']`); | |
var img = titles[title].docu.querySelector(`a[title='${title}'] img`); | |
var inbox_svg = `<svg viewBox="${titles[title].viewBox}" width="${titles[title].size}" xmlns="http://www.w3.org/2000/svg" height="${titles[title].size}"><g fill="none" stroke-width="2" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" transform="translate(2.5 1.5)"><path d="m10 3h2.3406038c.4000282 0 .7615663.23839685.9191451.6060807l2.7402511 6.3939193v4c0 1.1045695-.8954305 2-2 2h-12c-1.1045695 0-2-.8954305-2-2v-4l2.74025113-6.3939193c.15757879-.36768385.51911692-.6060807.91914503-.6060807h2.34060384"></path><path d="m11 6.086-3 2.914-3-2.914"></path><path d="m8 0v9"></path><path d="m0 10h4c.55228475 0 1 .4477153 1 1v1c0 .5522847.44771525 1 1 1h4c.5522847 0 1-.4477153 1-1v-1c0-.5522847.4477153-1 1-1h4"></path></g></svg>`; | |
if (img && img_link) { | |
console.log("Icon für Ferienantrag ersetzen..."); | |
img_link.innerHTML = inbox_svg | |
} | |
} | |
} | |
function updateHolidays() { | |
if (document.querySelectorAll('span.XafVCap-Second.MainMenuTruncateCaption')[0].textContent === 'Meine Absenzen') { | |
saveHolidays(); | |
downloadCalendar(); | |
} | |
if (document.querySelectorAll('span.XafVCap-Second.MainMenuTruncateCaption')[0].textContent === 'Mein Kalender') { | |
displayHolidays(); | |
updateHours(); | |
saveHourSaldo(); | |
} | |
} | |
function updateCalcHours() { | |
if (document.querySelectorAll('span.XafVCap-Second.MainMenuTruncateCaption')[0].textContent === 'Meine Zeitkontrolle') { | |
calcHourTotal(); | |
} | |
} | |
function busyExecute() { | |
// run all functions busy for some seconds, then cancel it | |
var intervalId = window.setInterval(function() { | |
updateHours(); | |
downloadCalendar(); | |
addRemainingButton(); | |
updateHolidays(); | |
updateCalcHours(); | |
replaceFerienAntragImage(); | |
}, 200); | |
window.setTimeout(function() { | |
if (intervalId) { | |
clearInterval(intervalId); | |
} | |
}, 5000); | |
var iframes = findAllChildElements(document, "iframe", /Vertical_PopupWindow.*/i); | |
if (iframes && iframes[0]) { | |
var icontent = iframes[0].contentWindow.document.getElementById('Content'); | |
icontent.addEventListener("click", function() { | |
busyExecute(); | |
}); | |
} | |
} | |
(function() { | |
'use strict'; | |
busyExecute(); | |
var content = document.getElementById('Content'); | |
content.addEventListener("click", function() { | |
window.setTimeout(function() { | |
busyExecute(); | |
var calendarButton = document.getElementById('Vertical_mainMenu_Menu_ITCNT0_xaf_a0_B'); | |
if (calendarButton) { | |
displayHolidays(); | |
saveHourSaldo(); | |
calendarButton.addEventListener("click", function() { | |
window.setTimeout(updateHours, 3000); | |
window.setTimeout(displayHolidays, 3000); | |
window.setTimeout(saveHourSaldo, 3000); | |
}); | |
} | |
var absenceButton = document.getElementById('Vertical_mainMenu_Menu_ITCNT0_xaf_a3_B'); | |
if (absenceButton) { | |
saveHolidays(); | |
downloadCalendar(); | |
absenceButton.addEventListener("click", function() { | |
window.setTimeout(saveHolidays, 1000); | |
}); | |
} | |
}, 2000); | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Es ist künftig nötig den "Entwicklermodus" einzuschalten um Userscripts in Chrome-basierten Browsern nutzen zu können.
Dokumentation: https://developer.chrome.com/docs/extensions/reference/api/userScripts?hl=de#developer_mode_for_extension_users
Dazu die Spezial-Seite
chrome://extensions
im Browser aufrufen und den Entwicklermodus einschalten.