Skip to content

Instantly share code, notes, and snippets.

@Chaphasilor
Last active June 27, 2023 11:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Chaphasilor/54bb6dd2b12825e8ad5b7c364c1f4479 to your computer and use it in GitHub Desktop.
Save Chaphasilor/54bb6dd2b12825e8ad5b7c364c1f4479 to your computer and use it in GitHub Desktop.
A Tampermonkey script for importing csv data into Proz
// ==UserScript==
// @name Proz Helper
// @author Chaphasilor
// @namespace https://github.com/Chaphasilor
// @source https://gist.githubusercontent.com/Chaphasilor/54bb6dd2b12825e8ad5b7c364c1f4479/
// @updateURL https://gist.githubusercontent.com/Chaphasilor/54bb6dd2b12825e8ad5b7c364c1f4479/raw/
// @downloadURL https://gist.githubusercontent.com/Chaphasilor/54bb6dd2b12825e8ad5b7c364c1f4479/raw/
// @version 1.1.5
// @description Lets you import .csv files into proz!
// @match https://proz.gsi.de/index.php*
// @match https://proz.gsi.de/zerf.php*
// @run-at document-end
// @grant none
// ==/UserScript==
/**
* Your .csv-file has to be in the following format:
*
* ;;P1;P2;...;P9
* [this second line is ignored, you could put the corresponding project names here]
* D1;T1;P1H1;P2H1;...;P9H1
* D2;T2;P1H2;P2H2;...;P9H2
* D3;T3;P1H3;P2H3;...;P9H3
*
* Here P[1-9] refers to the cost center id ((Stamm-)Kostenstelle) shown in the project selection dropdown, D[n] refers to the nth date, formatted as YYYY-MM-DD, T[n] refers to the total hours for that day and P[m]H[n] refers to the amount of hours for project m on the nth date.
*
* In order to submit hours you need at least the first three lines, meaning your project's const center ids, then one line with any content, then at least one date along with the hours for that date.
*
* You can submit hours for up to 9 different projects, or just a single one.
*
* Blank lines after the first two lines are ignored, so you could add any amount of blank lines in between your date lines
*
* Make sure to always include the same amount of hours as the amount of projects, so if you provide cost center ids up to P4, you'll have to provide your total hours and then 4 sets of 'sub'-hours for your 4 projects.
*
* Trailing semicolons at the end of a line will cause an internal error, try to avoid this! (it should still work, but I haven't fully tested it)
*
* Caution: Already submitted hours WON'T be overwritten! You have to change them manually.
*/
(function () {
'use strict';
window.proz = {};
window.proz.headers = {
'Host': 'proz.gsi.de',
'Connection': 'keep-alive',
'Content-Length': '169',
'Cache-Control': 'max-age=0',
'Origin': 'https://proz.gsi.de',
'Upgrade-Insecure-Requests': '1',
'Content-Type': 'application/x-www-form-urlencoded',
'Sec-Fetch-User': '?1',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'navigate',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US,en;q=0.9',
};
/**
* Sends the provided hours to protz
*
* @param {number} staffId Personalnummer
* @param {Date} date Das Datum
* @param {[[string, string]]} hours Ein Array von Arrays [[projectID, hours],...], maximal 10 Elemente
*/
window.proz.submitHours = function submitHours(staffId, date, hours) {
return new Promise((resolve, reject) => {
if (hours.length > 0) {
let hoursString = '';
let dateString = '';
dateString += date.getFullYear();
dateString += date.getMonth() < 9 ? '0' + parseInt(date.getMonth() + 1) : parseInt(date.getMonth() + 1);
dateString += date.getDate() < 10 ? '0' + parseInt(date.getDate()) : parseInt(date.getDate());
for (let o in hours) {
hoursString += '&';
hoursString += 'proj' + parseInt(parseInt(o) + 1) + '=' + hours[o][0];
hoursString += '&';
hoursString += 'std' + parseInt(parseInt(o) + 1) + '=' + hours[o][1];
}
fetch('https://proz.gsi.de/check_formular.php', {
method: 'POST',
mode: 'no-cors',
cache: 'no-cache',
headers: window.proz.headers,
body: 'lang=0&persnr=' + staffId + '&adat_jmt=' + dateString + '&action=n' + hoursString
}).then(response => {
response.text().then(text => {
if (text == "<H3>Angaben schon gespeichert, k&ouml;nnen nicht nochmal gespeichert werden!</H3><BR>") { // date already submitted
// TODO add an option to overwrite the date by using 'option=c' in the request body
return reject("This date has already been submitted previously! Previously submitted dates won't be overwritten!");
}
let parser = new DOMParser();
let doc = parser.parseFromString(text, "text/html");
if (doc.title == "PROZ@GSI: Pr&uuml;fung Eingabeformular" || doc.title == "PROZ@GSI: Prüfung Eingabeformular") { // returned confirmation page
return reject("Input error, maybe this was a holiday?");
}
return resolve(text);
}).catch(err => reject(err));
}).catch(err => reject(err));
} else {
resolve();
}
});
};
// var hours = [{date: Date1, projectHours: [[Project1ID, Hours1], [Project2ID, Hours2]]}, {date: Date2, projectHours: [[Project1ID, Hours1], [Project2ID, Hours2]]}]
// TODO hour amounts like 0.75 (less then 1) are not parse correctly, but with 0 hours instead
window.proz.formatHours = async function formatHours(inputString) {
let output = [];
let lines = inputString.split('\n');
let firstLine = lines.shift();
let projects = firstLine.split(';');
projects.shift(); // dump first two lines
projects.shift(); // they are empty
// console.log(projects);
for (let o in projects) {
try {
projects[o] = await window.proz.parseProjectId(projects[o]);
} catch (err) {
window.proz.showOutput(err, 'error');
}
}
lines.shift(); // dump next line
while (lines.length > 0) {
let line = lines.shift();
if (line.trim() != "" && line.replace(/;/gm, '').trim() != "") {
let values = line.split(';');
let dateString = values.shift();
let dateArray = dateString.split('-');
let date = new Date(dateArray[0], parseInt(dateArray[1] - 1), dateArray[2]);
let totalHours = values.shift();
let hoursForDate = {};
hoursForDate.date = date;
let projectHours = [];
if (parseInt(totalHours) != 0) {
for (let i in values) {
if (parseFloat(values[i]) > 0) {
projectHours.push([projects[i], values[i]]);
}
}
} else {
for (let i in values) {
projectHours.push([projects[i], 0]);
}
}
hoursForDate.projectHours = projectHours;
output.push(hoursForDate);
}
}
return output;
};
window.proz.parseProjectId = function parseProjectId(costCenter) {
return new Promise((resolve, reject) => {
if (window.proz.allProjects != undefined) {
let relevantProject = window.proz.allProjects.find(option => {
let id = option.innerHTML.split(')')[option.innerHTML.split(')').length - 2];
if (id != undefined) {
id = id.split('(')[id.split('(').length - 1];
} else {
return false; // wrong option
}
return (parseInt(id) == parseInt(costCenter));
});
if (relevantProject != undefined) {
resolve(relevantProject.value);
} else {
reject("No match for given cost center");
}
} else {
let now = new Date(Date.now());
let dateString = "";
dateString += now.getFullYear();
dateString += now.getMonth() < 9 ? '0' + parseInt(now.getMonth() + 1) : parseInt(now.getMonth() + 1);
dateString += "01";
fetch('https://proz.gsi.de/zerf_ein.php?' + dateString + '&n', {
method: 'POST',
mode: 'no-cors',
cache: 'no-cache',
headers: window.proz.headers,
}).then(response => {
response.text().then(html => {
let parser = new DOMParser();
let doc = parser.parseFromString(html, "text/html");
window.proz.allProjects = [...doc.querySelector("body > form > table > tbody > tr:nth-child(2) > td:nth-child(2) > select").options];
let relevantProject = window.proz.allProjects.find(option => {
let id = option.innerHTML.split(')')[option.innerHTML.split(')').length - 2];
if (id != undefined) {
id = id.split('(')[id.split('(').length - 1];
} else {
return false; // wrong option
}
return id == costCenter;
});
if (relevantProject != undefined) {
resolve(relevantProject.value);
} else {
reject("No match for given cost center");
}
}).catch(err => reject(err));
}).catch(err => reject(err));
}
});
};
window.proz.importHours = async function importHours() {
window.proz.ta.disabled = true;
window.proz.fileInput.disabled = true;
window.proz.submitButton.disabled = true;
window.proz.output.innerHTML = "";
let successfullSubmissions = 0;
let staffId = document.querySelector("#page > div.c3 > p:nth-child(1)").innerHTML.split('(')[1].split(',')[0];
window.proz.submitButton.innerHTML = "Parsing data...";
let inputData = "";
if (window.proz.fileInput.files.length != 0) {
inputData = await window.proz.readFile(window.proz.fileInput.files[0]);
} else {
inputData = document.querySelector('#prozInput').value;
}
let hours = await window.proz.formatHours(inputData);
// console.log(hours);
window.proz.submitButton.innerHTML = "";
for (let o in hours) {
window.proz.submitButton.innerHTML = window.proz.calcProgressBar(successfullSubmissions, hours.length);
try {
window.proz.showOutput("Uploading hours for " + hours[o].date.toLocaleDateString())
let result = await window.proz.submitHours(staffId, hours[o].date, hours[o].projectHours);
// console.log([result]);
window.proz.showOutput("Done");
successfullSubmissions++;
}
catch (err) {
window.proz.showOutput(err, 'error');
}
}
window.proz.showOutput("Done! Uploaded " + successfullSubmissions + "/" + hours.length + " successfully!", 'done');
// alert("Done! Uploaded " + successfullSubmissions + "/" + hours.length + " successfully!");
window.proz.ta.disabled = false;
window.proz.submitButton.innerHTML = "Submit hours!";
window.proz.fileInput.disabled = false;
window.proz.submitButton.disabled = false;
};
window.proz.readFile = function readFile(inputFile) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = function () {
resolve(this.result);
};
reader.onerror = function () {
reject(this.error);
};
reader.readAsText(inputFile);
});
}
window.proz.calcProgressBar = function calcProgressBar(current, total) {
const maxBars = 14;
let bars = Math.round((current / total) * maxBars);
let barsString = "";
for (let i = 0; i < bars; i++) {
barsString += "█";
}
return barsString;
};
window.proz.showOutput = function showOutput(message, type) {
let line = document.createElement('span');
line.innerText = message;
if (type == "error") {
line.style.color = 'red';
} else if (type == "done") {
line.style.color = 'green';
}
window.proz.output.appendChild(line);
window.proz.output.appendChild(document.createElement('br'));
}
let wrapper = document.createElement('div');
wrapper.innerHTML = "<hr/><h3>Proz Helper</h3> <h4>...denn Proz ist Rotz!</h4> <p>Just select a .csv or .txt file containing your data, or paste the text into the text area below, then click the button!</p>";
window.proz.ta = document.createElement('textarea');
window.proz.ta.id = "prozInput";
window.proz.ta.placeholder = "paste your data here";
window.proz.ta.style.width = "150px";
window.proz.submitButton = document.createElement('button');
window.proz.submitButton.style.width = "150px";
window.proz.submitButton.style.height = "2em";
window.proz.submitButton.style.textAlign = "left";
window.proz.submitButton.setAttribute('onclick', "window.proz.importHours();");
window.proz.submitButton.innerHTML = "Submit hours!";
window.proz.fileInput = document.createElement('input');
window.proz.fileInput.type = "file";
window.proz.output = document.createElement('div');
window.proz.output.style.height = "500px";
wrapper.appendChild(window.proz.ta);
wrapper.appendChild(document.createElement('br'));
wrapper.appendChild(window.proz.fileInput);
wrapper.appendChild(document.createElement('br'));
wrapper.appendChild(window.proz.submitButton);
wrapper.appendChild(window.proz.output);
document.body.appendChild(wrapper);
})();
@Chaphasilor
Copy link
Author

Proz Helper - Help

Installation instructions

  1. Download the Tampermonkey® browser addon for your browser.

  2. Click on the extention's icon in the navigation bar, then on Dashboard. The extention's dashboard page will open in a new tab.
    The extention dialog

  3. In the upper right corner, click on Utilities.
    The extention's menu bar

  4. At the bottom, paste the following link into the Install from URL-field, then hit Install:
    https://gist.githubusercontent.com/Chaphasilor/54bb6dd2b12825e8ad5b7c364c1f4479/raw/

    Paste the link

  5. On the following screen, hit Install once more.
    Confirm again

  6. You can now close the Tampermonkey® dashboard page.

  7. Now go to the proz page and log in. If you were logged in already, log out first.

  8. You should now see a small, red 1 on top of the Tampermonkey® icon.
    The red indicator means that one script is currently running

  9. You can now scroll down to the bottom of the page and will see the script working!
    Proz Helper

Congratulations, you just installed Proz Helper :D

How to use

  1. If you want to upload your working hours to proz using Proz Helper, you'll have to generate some csv data containing your hours. The format you need to use is explained in the script itself (see above).

  2. Once you've generated your csv data, you can either just copy and paste the data (in text form) in to the text area that says paste your data here, or you can save it as a .csv or .txt file and load it using the file upload button right below the text area.

  3. After pasting/loading the data, you can click on Submit hours! and watch the script do it's magic! For now, you might have to scroll down a bit to see the whole output. Once the green text appears, the upload process is done.

  4. Reload the page now to see your changes reflected in your calendar. If you're uploading last month's hours, you might have to go back one month to see your changes :)

Happy Prozing!


Keep in mind that this script/program is in no way official. I take no responsibility for any wrong hours logged into proz. Use it at your own risk.

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