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(); })();