Skip to content

Instantly share code, notes, and snippets.

@sidthesloth92
Last active June 26, 2020 05:18
Show Gist options
  • Save sidthesloth92/d855fded318b8bf796eae314ca5d40b6 to your computer and use it in GitHub Desktop.
Save sidthesloth92/d855fded318b8bf796eae314ca5d40b6 to your computer and use it in GitHub Desktop.
Gives a table view of the hours logged in JIRA for a user for a given time period.

Description

Gives a table view of the hours logged in JIRA for a user for a given time period.

Instructions

  • Go to this URL: https://bookmarkify.it/36312
  • Drag and drop the bookmarklet to the bookmarks bar.
  • Navigate to https://.atlassian.net and click on the bookmarklet and see the ✨
javascript: (async () => {
var startingDate;
var endingDate;
var currentAuthor;
var issues = [];
var projects = await getProjects();
var authors = await getAuthors();
var projectsSelectHTML = constructProjectsSelectHtml(projects);
var authorsSelectHTML = constructAuthorsSelectHtml(authors);
var outputHTML = `
<div class="worklog-container">
<h1 class="wc__title">Work Log</h1>
<div class="wc_filter_container">
${projectsSelectHTML}
${authorsSelectHTML}
<input type="date" name="fromDateInput" id="fromDateInput" />
<input type="date" name="toDateInput" id="toDateInput" />
</div>
<div class="wc__table-container">
</div>
</div>
`;
document.body.innerHTML = outputHTML;
addEventListeners();
await initUser();
search(false);
async function initUser() {
const token = await fetch("/rest/internal/2/media/user/credentials")
.then((response) => response.json())
.then((response) => {
return response.token;
});
const decodedToken = parseJwt(token);
currentAuthor = decodedToken.clientDetails.userId;
}
function getProjects() {
return fetch("/rest/api/3/project")
.then((response) => response.json())
.then((projects) => {
projects = projects.sort((a, b) => {
return a.name.localeCompare(b.name);
});
return projects;
});
}
function constructProjectsSelectHtml(projects) {
var optionsHtml = "";
projects.forEach((project) => {
optionsHtml += `<option value="${project.key}">${project.name}</option>`;
});
return `
<select name="projectInput" id="projectInput">
<option value="">All Projects</option>
${optionsHtml}
</select>
`;
}
function getAuthors() {
return fetch("/rest/api/3/users/search?maxResults=500")
.then((response) => response.json())
.then((authors) => {
authors = authors
.filter((author) => {
return author.active;
})
.sort((a, b) => {
return a.displayName.localeCompare(b.displayName);
});
return authors;
});
}
function constructAuthorsSelectHtml(authors) {
var authorsHtml = "";
authors.forEach((author) => {
authorsHtml += `<option value="${author.accountId}">${author.displayName}</option>`;
});
return `
<select name="authorInput" id="authorInput">
<option value="">Current User</option>
${authorsHtml}
</select>
`;
}
function addEventListeners() {
var inputElementContainer = document.querySelector(".wc_filter_container");
var searchButton = document.createElement("input");
searchButton.setAttribute("type", "button");
searchButton.setAttribute("value", "Search");
searchButton.addEventListener("click", search);
inputElementContainer.appendChild(searchButton);
var downloadButton = document.createElement("input");
downloadButton.setAttribute("type", "button");
downloadButton.setAttribute("value", "Download");
downloadButton.addEventListener("click", downloadCSV);
inputElementContainer.appendChild(downloadButton);
}
async function search(isReset) {
var project = document.getElementById("projectInput").value;
var author =
document.getElementById("authorInput").value ||
currentAuthor ||
"currentUser()";
var fromDate = document.getElementById("fromDateInput").value;
var toDate = document.getElementById("toDateInput").value;
var projectQuery = project ? `project = ${project} AND` : "";
var fromDateQuery = fromDate ? `'${fromDate}'` : "startOfMonth()";
var toDateQuery = toDate ? `'${toDate}'` : "endOfMonth()";
currentAuthor = author;
startingDate = fromDate && new Date(fromDate);
endingDate = toDate && new Date(toDate);
var url = `/rest/api/3/search?jql=${projectQuery} worklogAuthor = ${author} AND worklogDate >= ${fromDateQuery} AND worklogDate <= ${toDateQuery}`;
console.log(url);
await constructWorkLogTable(url);
}
async function constructWorkLogTable(url) {
issues = await getIssuesThatUserHasEnteredWorkLog(url);
var issueWorkLogRequests = [];
issues.forEach((issue, index) => {
issueWorkLogRequests.push(getIssueWorkLog(issue, index));
});
await Promise.all(issueWorkLogRequests);
issues.forEach((issue) => {
var workLogs = issue.workLogJSONResponse.worklogs;
if (workLogs.length > 0) {
var issueWorkLogStartingDate = new Date(workLogs[0].started);
var issueWorkLogEndingDate = new Date(
workLogs[workLogs.length - 1].started
);
if (!startingDate) {
startingDate = issueWorkLogStartingDate;
} else if (
issueWorkLogStartingDate.getFullYear() <=
startingDate.getFullYear() &&
issueWorkLogStartingDate.getMonth() <= startingDate.getMonth() &&
issueWorkLogStartingDate.getDate() <= startingDate.getDate()
) {
startingDate = issueWorkLogStartingDate;
}
if (!endingDate) {
endingDate = issueWorkLogEndingDate;
} else if (
issueWorkLogEndingDate.getFullYear() >= endingDate.getFullYear() &&
issueWorkLogEndingDate.getMonth() >= endingDate.getMonth() &&
issueWorkLogEndingDate.getDate() >= endingDate.getDate()
) {
endingDate = issueWorkLogEndingDate;
}
}
});
issues = issues.filter((issue) => {
return issue.workLogJSONResponse.worklogs.length > 0;
});
var tableHeaderHTML = constructTableHeaderHTML();
var tableBodyHTML = constructTableBodyHTML();
var tableFooterHTML = constructTableFooterHTML(tableBodyHTML);
var tableHTML = `
<table class="wc__table">
${tableHeaderHTML}
${tableBodyHTML}
${tableFooterHTML}
</table>`;
document.querySelector(".wc__table-container").innerHTML = tableHTML;
}
function getIssuesThatUserHasEnteredWorkLog(url) {
return fetch(url)
.then((response) => response.json())
.then((response) => response.issues);
}
function getIssueWorkLog(issue, index) {
return new Promise(async (resolve) => {
var workLogResponse = await fetch(
`/rest/api/3/issue/${issue.key}/worklog`
);
var workLogJSONResponse = await workLogResponse.json();
workLogJSONResponse.worklogs = workLogJSONResponse.worklogs.filter(
(workLog) => {
var workLogDate = new Date(workLog.started);
if (workLog.author.accountId == currentAuthor) {
var isOnOrAfterStartingDate =
startingDate &&
workLogDate.getFullYear() >= startingDate.getFullYear() &&
workLogDate.getMonth() >= startingDate.getMonth() &&
workLogDate.getDate() >= startingDate.getDate();
var isOnOrBeforeEndingDate =
endingDate &&
workLogDate.getFullYear() <= endingDate.getFullYear() &&
workLogDate.getMonth() <= endingDate.getMonth() &&
workLogDate.getDate() <= endingDate.getDate();
if (!startingDate && !endingDate) {
return true;
} else if (!startingDate && endingDate) {
return isOnOrBeforeEndingDate;
} else if (startingDate && !endingDate) {
return isOnOrAfterStartingDate;
} else {
return isOnOrAfterStartingDate && isOnOrBeforeEndingDate;
}
}
return false;
}
);
workLogJSONResponse.worklogs.sort((worklogOne, worklogTwo) => {
return new Date(worklogOne.started) - new Date(worklogTwo.started);
});
issues[index].workLogJSONResponse = workLogJSONResponse;
resolve();
});
}
function constructTableHeaderHTML() {
var headerColumnsHTML = "";
for (
var c = new Date(startingDate.getTime()), e = endingDate;
c.getFullYear() <= e.getFullYear() &&
c.getMonth() <= e.getMonth() &&
c.getDate() <= e.getDate();
c.setDate(c.getDate() + 1)
) {
var dateString = c.toDateString().split(" ");
headerColumnsHTML += `
<th class="table-cell top-header-cell sticky sticky--top">
${dateString[0]} <br/>
${dateString[1]} <br/>
${dateString[2]} <br/>
${dateString[3]}
</th>`;
}
return `
<thead>
<tr>
<th class="table-cell top-header-cell left-header-cell sticky sticky--left sticky--top sticky--in-front">Issue</th>
${headerColumnsHTML}
</tr>
</thead>
`;
}
function constructTableBodyHTML() {
var tableBodyHTML = "";
issues.forEach((issue) => {
var issueRowHTML = constructIssueHTML(issue);
tableBodyHTML += issueRowHTML;
});
return `<tbody>${tableBodyHTML}</tbody>`;
}
function constructTableFooterHTML(tableBodyHTML) {
var footerColumnsHTML = "";
var table = document.createElement("table");
table.innerHTML = tableBodyHTML;
for (
var c = new Date(startingDate.getTime()), e = endingDate;
c.getFullYear() <= e.getFullYear() &&
c.getMonth() <= e.getMonth() &&
c.getDate() <= e.getDate();
c.setDate(c.getDate() + 1)
) {
var currentDayLogs = Array.from(
table.querySelectorAll(
`.table-cell--logged[data-${c.getFullYear()}-${c.getMonth()}-${c.getDate()}]`
)
);
var totalWorklogSecondsForTheDay = getWorkLogSecondsForTheDay(
currentDayLogs,
c
);
footerColumnsHTML += `<td class="table-cell bottom-header-cell sticky sticky--bottom">${convertSecondsToWorkLogLabel(
totalWorklogSecondsForTheDay
)}</td>`;
}
return `
<tfoot>
<tr>
<td class="table-cell left-header-cell bottom-header-cell sticky sticky--bottom sticky--left sticky--in-front">Total Hours</td>
${footerColumnsHTML}
</tr>
</tfoot>
`;
}
function constructIssueHTML(issue) {
var issueColumnHTML = "";
var workLogs = issue.workLogJSONResponse.worklogs;
var c, e;
for (
c = new Date(startingDate.getTime()), e = endingDate, wi = 0;
c.getFullYear() <= e.getFullYear() &&
c.getMonth() <= e.getMonth() &&
c.getDate() <= e.getDate() &&
wi < workLogs.length;
c.setDate(c.getDate() + 1)
) {
var workLog = workLogs[wi];
var workLogDate = new Date(workLog.started);
if (
workLogDate.getDate() == c.getDate() &&
workLogDate.getMonth() == c.getMonth() &&
workLogDate.getFullYear() == c.getFullYear()
) {
var currentDayWorkLogs = "";
var currentDayWorkLogsTotalSeconds = 0;
while (
workLogDate.getDate() == c.getDate() &&
workLogDate.getMonth() == c.getMonth() &&
workLogDate.getFullYear() == c.getFullYear()
) {
currentDayWorkLogs += workLog.timeSpent;
currentDayWorkLogsTotalSeconds += workLog.timeSpentSeconds;
wi++;
if (wi >= workLogs.length) {
break;
}
workLog = workLogs[wi];
workLogDate = new Date(workLog.started);
}
issueColumnHTML += `<td class="table-cell table-cell--logged" data-issue-key="${
issue.key
}" data-${c.getFullYear()}-${c.getMonth()}-${c.getDate()}="${currentDayWorkLogsTotalSeconds}">${convertSecondsToWorkLogLabel(
currentDayWorkLogsTotalSeconds
)}</td>`;
} else {
issueColumnHTML += `<td class="table-cell"></td>`;
}
}
while (
c.getFullYear() <= e.getFullYear() &&
c.getMonth() <= e.getMonth() &&
c.getDate() <= e.getDate()
) {
issueColumnHTML += `<td class="table-cell"></td>`;
c.setDate(c.getDate() + 1);
}
return `
<tr>
<td class="table-cell left-header-cell sticky sticky--left">
<a href="/browse/${issue.key}">${issue.key}</a>
</td>
${issueColumnHTML}
</tr> `;
}
function downloadCSV() {
var downloadCSVData = [
["Work Date", "Work Hours", "Description", "Project", "Task", "Role"],
];
for (
var c = new Date(startingDate.getTime()), e = endingDate;
c.getFullYear() <= e.getFullYear() &&
c.getMonth() <= e.getMonth() &&
c.getDate() <= e.getDate();
c.setDate(c.getDate() + 1)
) {
var currentDayLogs = Array.from(
document.querySelectorAll(
`.table-cell--logged[data-${c.getFullYear()}-${c.getMonth()}-${c.getDate()}]`
)
);
var totalWorklogSecondsForTheDay = 0;
var descriptionsForTheDay = "";
if (currentDayLogs.length > 0) {
totalWorklogSecondsForTheDay = getWorkLogSecondsForTheDay(
currentDayLogs,
c
);
descriptionsForTheDay = getDescriptionsForTheDay(currentDayLogs);
}
downloadCSVData.push([
c.toLocaleDateString(),
convertToHoursAndMinutes(totalWorklogSecondsForTheDay),
descriptionsForTheDay,
]);
}
startCSVDownload(downloadCSVData);
}
function convertToHoursAndMinutes(totalSeconds) {
var hours;
if (totalSeconds >= 60 * 60) {
hours = totalSeconds / (60 * 60);
totalSeconds = totalSeconds - hours * 60 * 60;
}
var minutes = totalSeconds / 60;
return `${hours ? hours : '00' }:${minutes}`;
}
function getWorkLogSecondsForTheDay(currentDayLogs, c) {
var totalWorklogSecondsForTheDay = 0;
currentDayLogs.forEach((currentLog) => {
totalWorklogSecondsForTheDay += Number(
currentLog.dataset[`${c.getFullYear()}-${c.getMonth()}-${c.getDate()}`]
);
});
return totalWorklogSecondsForTheDay;
}
function getDescriptionsForTheDay(currentDayLogs) {
var descriptionsForTheDay = "";
var issuesKeysForTheDay = [];
currentDayLogs.forEach((currentLog) => {
issuesKeysForTheDay.push(currentLog.dataset["issueKey"]);
});
issues.forEach((issue) => {
if (issuesKeysForTheDay.indexOf(issue.key) != -1) {
var comment = issue.workLogJSONResponse.worklogs[0].comment;
if (comment && comment.content) {
descriptionsForTheDay +=
" " +
issue.key +
" " +
comment.content
.map((content) =>
content.content.map((textNode) => textNode.text).join(" ")
)
.join(" ");
}
}
});
return descriptionsForTheDay;
}
function startCSVDownload(data) {
var csvContent =
"data:text/csv;charset=utf-8," + data.map((r) => r.join(",")).join("\n");
var encodedUri = encodeURI(csvContent);
window.open(encodedUri);
}
function convertSecondsToWorkLogLabel(seconds) {
var workLogLabel = "";
if (seconds >= 60 * 60 * 24 * 7) {
var weeks = Math.trunc(seconds / (60 * 60 * 24 * 7));
seconds -= weeks * (60 * 60 * 24 * 7);
workLogLabel += `${weeks}w `;
}
if (seconds >= 60 * 60 * 24) {
var days = Math.trunc(seconds / (60 * 60 * 24));
seconds -= days * (60 * 60 * 24);
workLogLabel += `${days}h `;
}
if (seconds >= 60 * 60) {
var hours = Math.trunc(seconds / (60 * 60));
seconds -= hours * (60 * 60);
workLogLabel += `${hours}h `;
}
if (seconds >= 60) {
var minutes = Math.trunc(seconds / 60);
seconds -= minutes * 60;
workLogLabel += `${minutes}m `;
}
if (seconds > 0) {
workLogLabel += `${seconds}s`;
}
return workLogLabel;
}
function parseJwt(token) {
var base64Url = token.split(".")[1];
var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
var jsonPayload = decodeURIComponent(
atob(base64)
.split("")
.map(function (c) {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
})
.join("")
);
return JSON.parse(jsonPayload);
}
var tableStyles = document.createElement("style");
tableStyles.innerHTML = `
.worklog-container, .worklog-container * {
box-sizing: border-box;
}
.worklog-container {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100vw;
height: 100vh;
flex-direction: column;
padding: 30px;
}
.wc__table-container {
width: 100%;
max-height: 80vh;
overflow: scroll;
margin: 0 auto;
-webkit-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15);
border-radius: 10px;
}
.wc__table {
border-collapse: collapse;
border-radius: 10px;
}
.sticky {
position: -webkit-sticky;
position: sticky;
}
.sticky--left {
left: 0;
}
.sticky--top {
top: 0;
}
.sticky--bottom {
bottom: 0;
}
.sticky--in-front {
z-index: 10;
}
.wc__table .top-header-cell, .wc__table .bottom-header-cell {
color: white;
background-color: #2C867B;
font-weight: normal
}
.wc__table .table-cell {
padding: 10px 5px;
font-size: 12px;
vertical-align: middle;
min-width: 65px;
text-align: center;
border-right: 1px solid #dbdbdb;
border-bottom: 1px solid #dbdbdb;
}
.wc__table .table-cell:last-child {
border-right: none;
}
.wc__table .left-header-cell {
min-width: 120px;
}
.wc__table tbody .left-header-cell {
background-color: white;
}
.wc__table tfoot .left-header-cell {
font-weight: bold;
}
.wc__table tbody table-cell--logged {
background-color: #D5F0ED;
}
.wc__table tbody tr:nth-child(even) {
background-color: #f2f2f2;
}
select, input[type="text"] {
padding: 10px 5px;
border: 2px solid black;
cursor: pointer;
margin-right: 10px;
}
input[type="date"] {
padding: 9px 5px 9px;
border: 2px solid black;
cursor: pointer;
margin-right: 10px;
}
input[type="button"] {
background: transparent;
padding: 11px 30px;
outline: none;
border: 2px solid black;
cursor: pointer;
margin-left: 50px;
}
input[type="button"]:active {
background: #2c867b;
color: white;
}
.wc__title {
margin-bottom: 10px;
}
.wc_filter_container {
margin-bottom: 20px;
}
`;
document.head.appendChild(tableStyles);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment