shindai / Jira Tempo Worklog Extensions

// ==UserScript==
// @name        Jira Tempo Worklog Extensions
// @description Extends issue detail to set tempo worklog billable times to zero (for one or all worklogs of an issue). Also shows the total worktime/billable time. Tested with Jira V7.12.3
// @author      shindai
// @copyright 2019, shindai (https://openuserjs.org/users/shindai)
// @license MIT
// @include     https://jira*/browse*
// @match       https://jira*/browse*
// @updateURL https://openuserjs.org/meta/shindai/Jira_Tempo_Worklog_Extensions.meta.js
// @require     http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
// @version     0.1.5
// @grant       none
// ==/UserScript==

// PARAMETERS
var showTempoTimeStatistics = true;
//

// initiatilize my specific jQuery and avoid conflicts with Jira one
var $j = jQuery.noConflict(true);
var doing_modifications = false;
$j(document).ready(function()
{
    AddStyling();
    RegisterEvents();

    var timer = 0;
    $j("body").bind("DOMSubtreeModified", function()
    {
        if (timer)
        {
            window.clearTimeout(timer);
        }

        if (!doing_modifications) {
            timer = window.setTimeout(function()
            {
                InitLayout();
            }, 100);
        }
    });
});

// INIT METHODS

function AddStyling()
{
    $j('head').append('<style type="text/css">' +
                     '.btn-billable-zero[data-billable-time="0"]{ opacity: 0.2 }' +
                     '</style>');
}

function RegisterEvents()
{
    $j(document).on("click", ".btn-billable-zero-all", function()
    {
        if (confirm("Are you sure?"))
        {
            var issueId = window.jira.app.issue.getIssueId();
            SetAllWorklogsForIssueToBillableZero(issueId, function(worklog)
            {
                $j(document).trigger("update-worklog-row", worklog);
            }, function(parameter)
             {
                alert("Worklogs edit complete: " + parameter.successCounter + "/" + parameter.total + " were successfully edited!");
                $j(document).trigger("update-worklog-stats");
            });
        }
    });

    $j(document).on("click", ".btn-billable-zero", function(element)
    {
        if (confirm("Are you sure?"))
        {
            var $element;
            if($j(element.target).hasClass('.btn-billable-zero'))
            {
                $element = $j(element.target);
            }
            else
            {
                $element = $j(element.target).closest('.btn-billable-zero');
            }
            var issueId = $element.attr("data-issueid");
            var worklogId = $element.attr("data-worklogid");
            SetOneWorklogForIssueToBillableZero(
                issueId,
                worklogId,
                function(worklog) {
                    alert("Worklog edit complete.");
                    $j(document).trigger("update-worklog-stats");
                    $j(document).trigger("update-worklog-row", worklog);
                },
                function() {
                    alert("Worklog edit failed.");
                },
                function() {}
            );
        }
    });

    $j(document).on('update-worklog-row', function(element, worklog)
    {
        var button = $j(".btn-billable-zero[data-worklogid='" + worklog.originId + "']");
        if(button.length !== 0)
        {
            button.attr("data-billable-time", worklog.billableSeconds)
        }

        var worklogWorkLabel = $j("div[name='value_work_" + worklog.originId + "']");
        if(worklogWorkLabel.length !== 0)
        {
            var billabelTimeLabel = worklogWorkLabel.parent().find('.billable-time-label');
            if(billabelTimeLabel.length === 0)
            {
                billabelTimeLabel = $j("<div class='billable-time-label' title='billable time'></div>").insertAfter(worklogWorkLabel);
            }

            billabelTimeLabel.html("<span class=\"aui-icon aui-icon-small aui-iconfont-tag\" style=\"margin-top: 2px;\"></span> " + (worklog.billableSeconds/60/60) + "h");
        }
    });

    $j(document).on('update-worklog-stats', function()
    {
        var $billableStatsContainer = $j(".billable-stats-container");
        if($billableStatsContainer.length !== 0)
        {
            var issueId = window.jira.app.issue.getIssueId();
            GetWorklogSumForIssue(issueId, function(result)
            {
                $billableStatsContainer.html("<span class=\"aui-icon aui-icon-small aui-iconfont-recent\" style=\"margin-top: 3px;\"></span> worked total: " + (result.totalTimeInSeconds/60/60) + "h | <span class=\"aui-icon aui-icon-small aui-iconfont-tag\" style=\"margin-top: 3px;\"></span> billable total: " + (result.totalTimeBillableInSeconds/60/60)  + "h | <span title=\"billable percentage in comparison to time worked.\"><span class=\"aui-icon aui-icon-small aui-iconfont-graph-bar\" style=\"margin-top: 3px;\"></span> " + result.percentage + "% </span>");
            }, function(worklog){

            });
        }
    });
}

function InitLayout()
{
    try
    {
        doing_modifications = true;
        InitBillableInfoIfRequired();
        InitAllBillableToZeroButtonIfRequired();
        InitSingleBillableToZeroButtonsIfRequired();
    } catch (exception) {
        doing_modifications = false;
        console.log("Error found on the page :" + exception);
    }
    doing_modifications = false;
}

function InitBillableInfoIfRequired()
{
    if(showTempoTimeStatistics === false)
    {
        return;
    }

    var $tempoButtonRow = $j('#tempoIssueViewPanel .sc-esOvli');
    if ($tempoButtonRow.length === 0)
    {
        return;
    }

    var $billableStatsContainer = $j('#tempoIssueViewPanel .sc-esOvli .billable-stats-container');
    if ($billableStatsContainer.length !== 0)
    {
        return;
    }

    var issueId = window.jira.app.issue.getIssueId();

    GetWorklogSumForIssue(issueId, function(result)
    {
        if($j('#tempoIssueViewPanel .sc-esOvli .billable-stats-container').length !== 0)
        {
            return;
        }

        $j("<div class=\"billable-stats-container ghx-label-11\" style=\"display:inline-block; padding: 6px;\"><span class=\"aui-icon aui-icon-small aui-iconfont-recent\" style=\"margin-top: 3px;\"></span> worked total: " + (result.totalTimeInSeconds/60/60) + "h | <span class=\"aui-icon aui-icon-small aui-iconfont-tag\" style=\"margin-top: 3px;\"></span> billable total: " + (result.totalTimeBillableInSeconds/60/60)  + "h | <span title=\"billable percentage in comparison to time worked.\"><span class=\"aui-icon aui-icon-small aui-iconfont-graph-bar\" style=\"margin-top: 3px;\"></span> " + result.percentage + "% </span></div>").prependTo($tempoButtonRow);
    }, function(workflow){
        $j(document).trigger('update-worklog-row', workflow);
    });
}

function InitAllBillableToZeroButtonIfRequired()
{
    var $tempoButtonRow = $j('#tempoIssueViewPanel .sc-esOvli');
    if ($tempoButtonRow.length === 0)
    {
        return;
    }

    var $billableZeroButton = $j('#tempoIssueViewPanel .sc-esOvli .btn-billable-zero-all');
    if ($billableZeroButton.length !== 0)
    {
        return;
    }

    $j("<button class=\"tuiButton btn-billable-zero-all\" title=\"Set the billable time of all worklogs of this issue to 0.\"><span class=\"aui-icon aui-icon-small aui-iconfont-tag\" style=\"margin-top: 2px;\"></span>Set all worklogs to billable = 0</button>").appendTo($tempoButtonRow);
}

function InitSingleBillableToZeroButtonsIfRequired()
{
    var $tempoWorklogsContainer = $j('#tempoIssueViewPanel #issuePanelTableWrapper');
    if ($tempoWorklogsContainer.length === 0)
    {
        return;
    }

    var $rowActionContainers = $j('#tempoIssueViewPanel #issuePanelTableWrapper table tr .tempo-activity-actions-container');
    if ($rowActionContainers.length === 0)
    {
        return;
    }

    var issueId = window.jira.app.issue.getIssueId();
    for (var i = 0; i < $rowActionContainers.length; i++)
    {
        var $rowActionContainer = $j($rowActionContainers[i]);
        if ($rowActionContainer.find(".btn-billable-zero").length > 0)
        {
            continue;
        }
        var selector = ".tuiDropdown__list button[name^=\"edit_work\"]";
        var $editButton = $rowActionContainer.find(selector);
        if ($editButton.length !== 1)
        {
            continue;
        }

        var btnName = $editButton.attr('name');
        var worklogId = btnName.replace("edit_work_", "");
        $j("<button class=\"tuiButton btn-billable-zero\" data-issueid=\"" + issueId + "\" data-worklogid=\"" + worklogId + "\" style=\"width: 54px;\" title=\"Set the billable time of this worklog to 0.\"><span class=\"aui-icon aui-icon-small aui-iconfont-tag\"></span><span class=\"aui-icon aui-icon-small aui-iconfont-new-arrow-down\"></span></button>").appendTo($rowActionContainer);
    }
}

// API HELPER METHODS

function GetWorklogSumForIssue(issueId, sumResultCallback, singleWorklogResultCallback)
{
    GetWorklogsForIssue(issueId, function(data)
    {
        function ResolveAll(worklogIdList)
        {
            var promises = [];

            for (var i = 0; i < worklogIdList.length; i++) {
                var worklogId = worklogIdList[i];
                promises.push(new Promise(function(resolve, reject) {
                    GetWorklogById(worklogId, resolve, reject);
                }));
            }
            return Promise.all(promises);
        }

        var worklogIds = data.activities.map(x => x.id);

        var result = ResolveAll(worklogIds).then(function(results)
        {
            var totalTimeInSeconds = 0;
            var totalTimeBillableInSeconds = 0;
            for (var i = 0; i < results.length; i++)
            {
                var worklog = results[i];

                totalTimeInSeconds += worklog.timeSpentSeconds;
                totalTimeBillableInSeconds += worklog.billableSeconds;

                singleWorklogResultCallback(worklog);
            }

            var percentage = Math.floor((totalTimeBillableInSeconds / totalTimeInSeconds) * 100, 2);

            var result = {
              percentage: percentage,
              totalTimeInSeconds: totalTimeInSeconds,
              totalTimeBillableInSeconds: totalTimeBillableInSeconds
            };
            sumResultCallback(result);
        }, function(reason) {
            console.log('fetching worklogs for statistics failed -> ' + reason);
        });

    });
}

function SetAllWorklogsForIssueToBillableZero(issueId, singleWorklogUpdateCallback, finishedCallback)
{
    $j.get("/rest/tempo-time-activities/1/issue/" + issueId + "/?page=1&size=9999&activityType=work&currentUser=false", function(data)
    {
        var worklogIds = data.activities.map(x => x.id);
        var counter = 0;
        var successCounter = 0;
        var failCounter = 0;
        function showResult() {
            if (counter === worklogIds.length)
            {
                var arg = { successCounter: successCounter, total: worklogIds.length }
                finishedCallback(arg);
            }
        }
        for (var i = 0; i < worklogIds.length; i++)
        {
            var worklogId = worklogIds[i];
            SetOneWorklogForIssueToBillableZero(
                issueId,
                worklogId,
                function() {
                    var worklog = arguments[1];
                    singleWorklogUpdateCallback(worklog);
                    successCounter++;
                },
                function() {
                    failCounter++;
                },
                function() {
                    counter++;
                    showResult();
                }
            );
        }
    });
}

function SetOneWorklogForIssueToBillableZero(issueId, worklogId, successCallback, failCallback, alwaysCallback)
{
    var pObj = {
        billableSeconds: 0,
        originId: worklogId,
        originTaskId: "" + issueId,
        remainingEstimate: null,
        endDate: null,
        includeNonWorkingDays: false
    };
    $j.ajax({
            type: "PUT",
            url: "/rest/tempo-timesheets/4/worklogs/" + worklogId + "/",
            contentType: "application/json",
            data: JSON.stringify(pObj),
        }).done(successCallback)
        .fail(failCallback)
        .always(alwaysCallback);
}

function GetWorklogsForIssue(issueId, successCallback)
{
    $j.get("/rest/tempo-time-activities/1/issue/" + issueId + "/?page=1&size=9999&activityType=work&currentUser=false", successCallback);
}

function GetWorklogById(worklogId, resolve, reject)
{
    $j.get("/rest/tempo-timesheets/4/worklogs/" + worklogId, resolve)
      .fail(reject);
}