Toggl - Weekly report

Calculate and display the work day percentages

2019-09-13 या दिनांकाला. सर्वात नवीन आवृत्ती पाहा.

// ==UserScript==
// @name         Toggl - Weekly report
// @namespace    https://github.com/fabiencrassat
// @version      0.8.3
// @description  Calculate and display the work day percentages
// @author       Fabien Crassat <[email protected]>
// @include      https://toggl.com/app/*
// @grant        none
// ==/UserScript==

/*global $, console */

/*eslint no-console: ["error", { allow: ["info", "warn", "error"] }] */

"use strict";

const weekDays = [0, 1, 2, 3, 4, 5, 6];
const urlToFollow = /^https:\/\/toggl\.com\/app\/reports\/weekly\/\d+\/period\/([a-z])\w+/;
const apiTimeEntriesUrlToFollow = "https://toggl.com/reports/api/v3/workspace/2294752/weekly/time_entries";
const apiProjectsUrlToFollow = "https://toggl.com/api/v9/me/projects";
const decimalLenght = 2;
const debugMode = false;

let projects = [];

var oldFetch = fetch; // must be on the global scope

const buildFetch = function(doSomethingWithResponse) {
    fetch = function(url, options) {
        var promise = oldFetch(url, options);
        // Do something with the promise
        promise.then(doSomethingWithResponse).catch(function(error) {
            console.error("Error in fetch processing", error);
        });
        return promise;
    };
};
const backFetch = function() {
    fetch = oldFetch;
};

const sleep = function() {
    return new Promise((resolve) => setTimeout(resolve, 1000));
};

const cleanDisplay = function() {
    $(".fcr-toggl").remove();
};

const percentage = function(numerator, denumerator) {
    if (!numerator || !denumerator || denumerator === 0) {
        return 0;
    }
    return (numerator / denumerator);
};

const calculate = function(weeklyData) {
    /**
     * The weeklyData argument in the V3 API has this values
     * [
     *   {
     *     "user_id": 2644339,
     *     "project_id": 150741509,
     *     "seconds": // Mon, Tue, Wed, Thu, Fri, Sat, Sun
     *       [0, 7200, 0, 0, 0, 0, 0],
     *   },
     *   {...}
     * ]
     *
     * The result:
     * [
     *   {
     *     client: '',
     *     project: 'name',
     *     data: // Mon, Tue, Wed, Thu, Fri, Sat, Sun
     *       [0, 0.23, 0, 0, 0, 0, 0],
     *     conso: sum(weeklyData[].seconds) / sum(allProjectsDays),
     *   },
     *   {...}
     * ]
     */

    let projectSum = {};
    let daysSum = [];
    weeklyData.forEach(function(line) {
      // Sum the line
      projectSum[line.project_id] = line.seconds.reduce(function(acc, cur) {
        return acc + cur;
      });
      // Sum the days
      weekDays.forEach(function(day) {
        daysSum[day] = (daysSum[day] || 0) + line.seconds[day];
      });
    });

    // Sum the week
    let weekSum = daysSum.reduce(function(acc, cur) {
      return acc + cur;
    });

    let result = [];
    weeklyData.forEach(function(line) {
      let data = [];
      line.seconds.forEach(function(day, index) {
        data.push(percentage(day, daysSum[index]));
      });
      const project = projects.find((project) => project.id === line.project_id);
      const projectName = project ? project.name : null;
      result.push({
        client: "",
        project: projectName,
        data: data,
        conso: percentage(projectSum[line.project_id], weekSum)
      });
    });

    return result;
};

const filterDataFromProject = function(data, lineElement) {
    const text = $(lineElement).find(".css-70qvj9.efdmxuc2 > span:first-child").text();
    if (text.trim() === "Without project") {
        return data.find((value) => value.project === null);
    }
    return data.find((value) => value.project === text);
};

const displayInTheLine = function(lineElement, data) {
    // For each line, select only days and total columns
    const columns = $(lineElement).find(".css-7ajft7.euf6jrl1");
    if (columns.length <= 0) {
        console.warn("There is no display column", columns);
        return;
    }
    columns.each(function(indexColumn) {
        let dataInCeil = 0;
        if (columns.length === indexColumn + 1) {
          dataInCeil = data.conso;
        } else {
          dataInCeil = data.data[indexColumn];
        }
        if (dataInCeil !== 0) {
            $(this).append("<p class='fcr-toggl'><br />" + dataInCeil.toFixed(decimalLenght) + "</p>");
        }
    });
};

const display = function(data = []) {
    // Select the data line in the tab
    const displayLines = $(".css-1v0lzu.euf6jrl0:not(:first, :last)");
    if (!displayLines || displayLines.length === 0) {
        console.warn("There is no display line", displayLines);
        return;
    }
    displayLines.each(function() {
        displayInTheLine(this, filterDataFromProject(data, this));
    });
};

const calculateAndDisplay = async function(data) {
    await sleep(); // Need to wait to the table built
    cleanDisplay();
    display(calculate(data));
};

const fillProjects = async function(data = []) {
    projects = data.map((project) => {
        return { id: project.id, name: project.name, clientId : project.client_id };
    });
};

const checkResponseAndUrl = function(response, url, apiRUl) {
    return response.ok && response.status === 200 && url && url.startsWith(apiRUl);
};

const response = function(response) {
    const responseClone = response.clone(); // clone to consume json body stream response
    const url = response.url;
    if (checkResponseAndUrl(responseClone, url, apiProjectsUrlToFollow)) {
        console.info("Url to follow found!", url);
        responseClone.json().then(fillProjects);
    }
    if (checkResponseAndUrl(responseClone, url, apiTimeEntriesUrlToFollow)) {
        console.info("Url to follow found!", url);
        responseClone.json().then(calculateAndDisplay);
    }
};

const fireOnChange = function(url = "") {
    // Check if we are in the good page
    if (urlToFollow.test(url)) {
        buildFetch(response);
        return true;
    }
    backFetch();
    return false;
};

console.info("== Toggl - Weekly report ==");
// Follow the HTML5 url change in the API browser
(function (old) {
    window.history.pushState = function () {
        old.apply(window.history, arguments);
        fireOnChange(window.location.href);
    };
}(window.history.pushState));
fireOnChange(location.href);