Oblosys / Diqq tempo

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