Skip to content

Instantly share code, notes, and snippets.

@metaodi
Last active April 9, 2024 05:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save metaodi/4222b9ce80ed50f4d2deb71e7bf63316 to your computer and use it in GitHub Desktop.
Save metaodi/4222b9ce80ed50f4d2deb71e7bf63316 to your computer and use it in GitHub Desktop.
A user script to improve the E3 experience
// ==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);
});
})();
@metaodi
Copy link
Author

metaodi commented Apr 9, 2024

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.

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment