Redmine shows friendly time in tickets
// ==UserScript==
// @name Redmine Friendly Time
// @namespace http://tampermonkey.net/
// @version 0.99.91
// @description Redmine shows friendly time in tickets
// @author Massive Friendly Fire
// @include http://redmine.m-games-ltd.com/*
// @compatible firefox
// @compatible chrome
// @grant none
// @homepageURL https://github.com/MassiveFriendlyFire/redmine-friendly-time#readme
// @supportURL https://github.com/MassiveFriendlyFire/redmine-friendly-time/issues
// ==/UserScript==
//Description:
//This script replaces time of update in redmine tickets
//from inaccurate values e.g. "last updated about 2 hours"
//to accurate values e.g "last updated 2 hours 13 minutes"
(function () {
//CORE
var LOGGING_ENABLED = false;
var MY_LOG = function (value) {
if (LOGGING_ENABLED) {
console.log(value);
}
};
var toastrCss = document.createElement('link');
toastrCss.href = 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css';
toastrCss.rel = 'stylesheet';
var toastrJs = document.createElement('script');
toastrJs.src = 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js';
var FACss = document.createElement('link');
FACss.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css';
FACss.rel = 'stylesheet';
document.head.appendChild(toastrCss);
document.head.appendChild(toastrJs);
document.head.appendChild(FACss);
//replace this regex if script is not working, it must match A title tags
var mainRegex = /^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2})$/;
//define locale strings
var ruStrings = ["дн.", "ч.", "мин.", "меньше 1 минуты", 'сек.'];
var engStrings = ["days", "hour", "min", "right now"];
//default locale is russian
var scriptStrings = ruStrings;
//define CONSTS
var ISSUE_PAUSED_STR = 'Приостановлена';
var ISSUE_IN_WORK_STR = 'В работе';
//DOCUMENT CONSTS
var REDMINE_FORM_SUMBIT_ELEMENTS = document.getElementsByName('commit');
var ISSUE_STATUS_SELECT_OPTIONS_GROUP_ELEMENT = document.getElementById('issue_status_id');
var ISSUE_STATUS_STR_ELEMENT = document.getElementsByClassName('status attribute')[0].childNodes[1];
var EDIT_ISSUE_LINKS_PANEL_ELEMENT = document.getElementsByClassName('contextual')[1];
var EDIT_ISSUE_FORM_ELEMENT = document.getElementById('issue-form');
var EDIT_ISSUE_LABOUR_COSTS_ELEMENT = document.getElementById('time_entry_hours');
var EDIT_ISSUE_LABOUR_TYPE_SELECT_OPTIONS_GROUP_ELEMENT = document.getElementById('time_entry_activity_id');
var ISSUE_HISTORY_LIST_ELEMENT = document.getElementById('history');
var ISSUE_HISTORY_LAST_ELEMENT = getHistoryLastElement(ISSUE_HISTORY_LIST_ELEMENT);
var MY_USER_ID = getMyUserId(document.getElementById('loggedas'));
var ISSUE_HISTORY_LAST_ELEMENT_UPDATE_TIME = ISSUE_HISTORY_LAST_ELEMENT.children[0].children[0].children[3].title;
//VARS
var VO_links = document.getElementsByTagName("a");
var VO_milliseconds;
var VO_days;
var VO_hours;
var VO_minutes;
var VO_seconds;
var VO_currentTime;
var VO_labourCostsPrevValue;
var VO_issuePaused = true;
var VO_manualEdit = false;
//define methods
/**
* format time to human readable
* @param milliseconds
* @returns {string}
*/
var formatMilliseconds = function (milliseconds) {
var seconds = Math.round(milliseconds / 1000 % 60) - 1;
var minutes = parseInt((milliseconds / (1000 * 60)) % 60);
var hours = parseInt((milliseconds / (1000 * 60 * 60)) % 24);
var days = parseInt(milliseconds / (1000 * 60 * 60 * 24));
minutes = (minutes < 10) ? "0" + minutes : minutes;
var result = "";
if (days < 1 && hours < 1 && minutes < 1) {
result = scriptStrings[3];
} else {
if (days > 0) {
result = days + " " + scriptStrings[0] + " ";
}
if (days === 0 && hours === 0) {
result = minutes + " " + scriptStrings[2] + ' ' + seconds + ' ' + scriptStrings[4];
} else {
result = result + hours + " " + scriptStrings[1] + " " + minutes + " " + scriptStrings[2]+ ' ' + seconds + ' ' + scriptStrings[4];
}
}
MY_LOG('result = ' + result);
return result;
};
/**
* check if string is date and count time betweet date and current time
* @param string
* @returns {*}
*/
var getMillisecondsIfStringIsDate = function (string) {
var matches = string.match(mainRegex);
if (matches !== null) {
var year = parseInt(matches[3], 10);
var month = parseInt(matches[2], 10) - 1; // months are 0-11
var day = parseInt(matches[1], 10);
var hour = parseInt(matches[4], 10);
var minute = parseInt(matches[5], 10);
var second = 0;
//check first format: dd.mm.yyyy hh:mm
var parsedDate = new Date(year, month, day, hour, minute, second);
if (parsedDate.getFullYear() === year || parsedDate.getMonth() == month || parsedDate.getDate() === day || parsedDate.getHours() === hour || parsedDate.getMinutes() === minute) {
return Math.abs(VO_currentTime - parsedDate);
}
//check second format: mm.dd.yyyy hh:mm
parsedDate = new Date(year, day - 1, month + 1, hour, minute, second);
if (parsedDate.getFullYear() === year || parsedDate.getMonth() == day - 1 || parsedDate.getDate() === month + 1 || parsedDate.getHours() === hour || parsedDate.getMinutes() === minute) {
return Math.abs(VO_currentTime - parsedDate);
}
//not found
}
return null;
};
/**
* load last element from history of redmine issue changes
* @returns {*}
*/
function getHistoryLastElement() {
var last;
if (ISSUE_HISTORY_LIST_ELEMENT.children.length > 0) {
last = ISSUE_HISTORY_LIST_ELEMENT.children[ISSUE_HISTORY_LIST_ELEMENT.children.length - 1];
} else {
MY_LOG('ERROR: Unable to get last history element');
return;
}
if (last.id.indexOf('change') === -1) {
MY_LOG('ERROR: Last history element is not a change');
return;
}
return last;
}
/**
* in redmine
* @param loggedAsDiv
* @returns {*}
*/
function getMyUserId(loggedAsDiv) {
var splitted = loggedAsDiv.children[0].href.split('/');
if (splitted.length < 1) {
MY_LOG('ERROR: Something wrong with "LOGGED AS" div. Unable to get current user id');
return;
}
return splitted[splitted.length - 1];
}
/**
* reload vars for time after last update
*/
function reloadIssueTimeVars() {
VO_currentTime = new Date();
VO_milliseconds = getMillisecondsIfStringIsDate(ISSUE_HISTORY_LAST_ELEMENT_UPDATE_TIME);
VO_minutes = parseInt((VO_milliseconds / (1000 * 60)) % 60);
VO_hours = parseInt((VO_milliseconds / (1000 * 60 * 60)) % 24);
VO_days = parseInt(VO_milliseconds / (1000 * 60 * 60 * 24));
VO_seconds = parseInt((VO_milliseconds / 1000) % 60);
}
/**
* set status
*/
function prepareEditIssueStatus() {
var findValue;
var toggleFrom = ISSUE_PAUSED_STR;
var toggleTo = ISSUE_IN_WORK_STR;
reloadIssueStatus();
if (!VO_issuePaused) {
var temp = toggleFrom;
toggleFrom = toggleTo;
toggleTo = temp;
}
for (var i = 0; i < ISSUE_STATUS_SELECT_OPTIONS_GROUP_ELEMENT.length; i++) {
if (ISSUE_STATUS_SELECT_OPTIONS_GROUP_ELEMENT[i].innerHTML === toggleTo) {
findValue = ISSUE_STATUS_SELECT_OPTIONS_GROUP_ELEMENT[i].value;
break;
}
}
ISSUE_STATUS_SELECT_OPTIONS_GROUP_ELEMENT.value = findValue;
ISSUE_STATUS_SELECT_OPTIONS_GROUP_ELEMENT.style.background = 'lightgreen';
MY_LOG('toggleStatus');
}
/**
* set labour costs
*/
function prepareEditIssueLabourCosts() {
if (VO_minutes === undefined) {
reloadIssueTimeVars();
}
if (VO_issuePaused) {
return;
}
MY_LOG('prevlabcosts ' + VO_labourCostsPrevValue);
if (!VO_manualEdit && VO_labourCostsPrevValue !== undefined && EDIT_ISSUE_LABOUR_COSTS_ELEMENT.value !== VO_labourCostsPrevValue) {
EDIT_ISSUE_LABOUR_COSTS_ELEMENT.style.background = '';
EDIT_ISSUE_LABOUR_TYPE_SELECT_OPTIONS_GROUP_ELEMENT.value = '';
EDIT_ISSUE_LABOUR_TYPE_SELECT_OPTIONS_GROUP_ELEMENT.style.background = '';
VO_manualEdit = true;
return;
}
if (VO_manualEdit) {
return;
}
var hoursValue = VO_days * 24 + VO_hours + VO_minutes / 60 + VO_seconds / 3600;
VO_labourCostsPrevValue = roundUpto(hoursValue, 5);
EDIT_ISSUE_LABOUR_COSTS_ELEMENT.value = VO_labourCostsPrevValue;
EDIT_ISSUE_LABOUR_COSTS_ELEMENT.style.background = 'lightgreen';
}
/**
* set labour type
*/
function prepareEditIssueLabourType() {
if (VO_issuePaused) {
return;
}
EDIT_ISSUE_LABOUR_TYPE_SELECT_OPTIONS_GROUP_ELEMENT.value = 9;
EDIT_ISSUE_LABOUR_TYPE_SELECT_OPTIONS_GROUP_ELEMENT.style.background = 'lightgreen';
}
/**
* create link for toggle issue between work-paused
*/
function createTaskEasyToggleHref() {
var ICON = document.createElement('i');
ICON.classList.add('fa');
var LINK = document.createElement('a');
LINK.id = 'easy-toggle-link';
LINK.href = '#';
var title1 = ' Взять в работу';
var title2 = ' Приостановить';
reloadIssueStatus();
if (VO_issuePaused) {
ICON.classList.add('fa-play');
LINK.innerHTML = title1;
} else {
ICON.classList.add('fa-pause');
LINK.innerHTML = title2;
}
EDIT_ISSUE_LINKS_PANEL_ELEMENT.prepend(LINK);
EDIT_ISSUE_LINKS_PANEL_ELEMENT.prepend(ICON);
LINK.onclick = function () {
easyTaskEasyToggleOnclickAction()
};
}
/**
* action for toggle link
*/
function easyTaskEasyToggleOnclickAction() {
MY_LOG('submitting form...');
if (isSimpleSubmitAllowed()) {
if (!VO_issuePaused) {
showNotificationPopup('Приостанавливаю задачу с трудозатратами: ' + formatMilliseconds(VO_milliseconds));
setTimeout(function() {
EDIT_ISSUE_FORM_ELEMENT.submit();
}, 1500);
} else {
EDIT_ISSUE_FORM_ELEMENT.submit();
}
} else {
thereIsNotAllAreSimpleWithYourIssue();
}
}
/**
* check all fields are good to easy change status
* @returns {boolean}
*/
function isSimpleSubmitAllowed() {
var userHrefValue = ISSUE_HISTORY_LAST_ELEMENT.children[0].children[0].children[2].href;
var splitted = userHrefValue.split('/');
if (splitted.length < 1) {
MY_LOG('ERROR: Something wrong with user href value');
return false;
}
var userId = splitted[splitted.length - 1];
MY_LOG('user id = ' + userId);
if (userId !== MY_USER_ID) {
MY_LOG('DEBUG: User Id mismatch');
return false;
}
if (VO_issuePaused) {
return true;
}
var details = ISSUE_HISTORY_LAST_ELEMENT.children[0].children[1];
MY_LOG('details.children.length ' + details.children.length);
if (details.children.length < 1) {
MY_LOG('Details looks not like status changed message.. Check failed.');
return false;
}
for (var i = 0; i < details.children.length; i++) {
var liElement = details.children[i];
if (liElement.tagName != 'LI') {
MY_LOG('Unable to find status change in last history element... Check failed.');
return false;
}
if (liElement.children[0].innerHTML === 'Статус') {
if (liElement.children[2].innerHTML === 'В работе') {
MY_LOG('DEBUG: Issue In Work By You and You can Simply Submit');
return true;
}
if (VO_issuePaused) {
MY_LOG('DEBUG: Issue Paused and You can Simply Submit');
return true;
}
}
}
MY_LOG('Unable to find status change in last history element... Check failed.');
return false;
}
/**
* reload status
*/
function reloadIssueStatus() {
MY_LOG('ISSUE_STATUS_STR_ELEMENT.innerHTML ' + ISSUE_STATUS_STR_ELEMENT.innerHTML);
MY_LOG('ISSUE_PAUSED_STR ' + ISSUE_PAUSED_STR);
MY_LOG('ISSUE_IN_WORK_STR ' + ISSUE_IN_WORK_STR);
if (ISSUE_STATUS_STR_ELEMENT.innerHTML === ISSUE_PAUSED_STR) {
MY_LOG('reloadIssueStatus ' + true);
VO_issuePaused = true;
} else if (ISSUE_STATUS_STR_ELEMENT.innerHTML === ISSUE_IN_WORK_STR) {
MY_LOG('reloadIssueStatus ' + false);
VO_issuePaused = false;
}
}
/**
* if not all good for easy toggle issue running this method
* @returns {boolean}
*/
function thereIsNotAllAreSimpleWithYourIssue() {
MY_LOG('notAllAreSimpleWithYourIssue');
showAndScrollTo("update", "issue_notes"); //REDMINE FUNCTION.
showNotificationPopup('Не всё так просто с вашей задачей... Проверьте всё еще разок. Может быть длительность трудозатрат нужно изменить?');
return false;
}
/**
* external notifications added in CORE section, enjoy
* @param caption
*/
function showNotificationPopup(caption, noTimeout) {
var timeout = 750;
if (noTimeout) {
timeout = 0;
}
setTimeout(function () {
toastr.info(caption)
}, timeout);
}
/**
* math operation round to decimal places
* @param number
* @param upto
* @returns {string}
*/
function roundUpto(number, upto) {
return new Number(number).toFixed(upto);
}
function updateTimes() {
for (var i = 0; i < VO_links.length; i++) {
var milliseconds = getMillisecondsIfStringIsDate(VO_links[i].title);
if (milliseconds !== null) {
VO_links[i].innerHTML = formatMilliseconds(milliseconds);
}
}
}
//Run stage
//iterate links and replace inner html if link matches date time
reloadIssueTimeVars();
updateTimes();
createTaskEasyToggleHref();
//autochange options values for edit mode
setTimeout(function () {
setInterval(function() {
reloadIssueTimeVars();
MY_LOG(VO_currentTime);
updateTimes();
prepareEditIssueLabourCosts();
}, 25000);
setInterval(function() {
reloadIssueTimeVars();
prepareEditIssueLabourCosts();
}, 1500);
reloadIssueTimeVars();
prepareEditIssueStatus();
prepareEditIssueLabourCosts();
prepareEditIssueLabourType();
}, 600);
})();