NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Diqq tempo
// @namespace https://openuserjs.org/users/Oblosys
// @description Submit Tempo csv to Diqq time portal
// @author Martijn Schrage
// @copyright 2020, Oblosys (https://openuserjs.org/users/Oblosys)
// @license MIT
// @version 0.1.3
// @match https://timeportal.diqq.com/projects/*/timesheet
// @grant none
// @require https://code.jquery.com/jquery-3.4.1.slim.min.js
// ==/UserScript==
// ==OpenUserJS==
// @author Oblosys
// ==/OpenUserJS==
(function () {
'use strict';
const initialize = () => {
if (document.readyState == 'loading') {
document.addEventListener('DOMContentLoaded', addSubmitButton);
}
else {
addSubmitButton();
}
}
const addSubmitButton = async () => {
const loadFileAsText = () => {
const fileToLoad = document.getElementById("input-tempo-csv").files[0];
const fileReader = new FileReader();
fileReader.onload = (fileLoadedEvent) => setDickHours(fileLoadedEvent.target.result);
fileReader.readAsText(fileToLoad, "UTF-8");
}
$('body').prepend(
'<div style="position: absolute;z-index: 1000;padding-left: 20px;padding-top: 18px; color:black;">' +
'Select exported Tempo CSV file: ' +
'<input type="file" id="input-tempo-csv" style="position: relative; top: -1.5px;">' +
'</div>');
$('#input-tempo-csv').change(loadFileAsText);
}
const parseTempoCsv = (tempoCsvStr) => tempoCsvStr.split('\n').slice(1, -1)
.map(line => {
const [, , hoursStr, dateStr] = JSON.parse('[' + line + ']');
return {
date: `${dateStr.split(' ')[0]}T00:00:00.000Z`,
hours: +hoursStr
}
});
const apiRequest = async (endpoint, method, body) => {
const url = `https://timeportal-backend.diqq.com/projectFreelancers/${endpoint}`;
const fetchInit = {
method: method,
...(body ? {
body: JSON.stringify(body)
} : {}),
headers: {
accept: 'application/json',
'accept-language': 'en-US,en;q=0.9,en-GB;q=0.8,nl;q=0.7',
authorization: `Bearer ${localStorage.token}`,
'content-type': 'application/json',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
'ui-culture': 'nl',
},
mode: 'cors',
credentials: 'include',
referrer: window.location,
referrerPolicy: 'no-referrer-when-downgrade',
};
try {
const response = await fetch(url, fetchInit);
if (response.ok) {
const json = await response.json();
console.log('Api response:', json);
return json;
}
else {
console.error('Http error:', response.error);
}
}
catch (error) {
console.error('Network error:', error);
}
};
const setDickHours = async (tempoCsvStr) => {
const tempoEntries = parseTempoCsv(tempoCsvStr)
const projectId = window.location.pathname.match(/^\/projects\/(.+)\/timesheet/)[1];
const timeEntries = await apiRequest(`${projectId}/timeentries`, 'GET');
const entryIdsByDate = timeEntries.times.reduce(
(allEntries, {
date,
id
}) => ({
[new Date(date).toISOString()]: id, // Api response has non-ISO date format ('2019-01-01T00:00:00+00:00' vs '2019-01-01T00:00:00.000Z').
...allEntries,
}), {},
);
console.log('entryIdsByDate', entryIdsByDate);
const setDateHours = async (dateStr, hours) => {
console.log(`Date ${dateStr}: ${hours}h`);
const entryId = entryIdsByDate[dateStr];
if (entryId) {
console.log(`Updating existing entry: ${entryId}`);
await apiRequest(`/timeentries/${entryId}/hours`, 'PUT', {
hours
});
}
else {
console.log('Creating new entry');
await apiRequest(`${projectId}/timeentries`, 'POST', {
date: dateStr,
hours
});
}
};
console.log(`Submitting ${tempoEntries.length} entries for a total of ${tempoEntries.map(({hours}) => hours).reduce((h,tot)=> h+tot,0)} hours.`)
for (const {
date,
hours
} of tempoEntries) {
await setDateHours(date, hours);
}
window.location.reload();
}
initialize();
})();