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