362 lines
13 KiB
JavaScript
362 lines
13 KiB
JavaScript
let table
|
|
let prevMonthBtn, nextMonthBtn, month
|
|
|
|
class Table {
|
|
constructor(elmt) {
|
|
this.table = elmt
|
|
this.clockings = elmt.querySelector(".clockings")
|
|
this.times = elmt.querySelector(".times")
|
|
this.weekNames = elmt.querySelector(".week-names")
|
|
this.dayNames = elmt.querySelector(".day-names")
|
|
this.dayDates = elmt.querySelector(".day-dates")
|
|
this.columns = elmt.querySelector(".columns")
|
|
|
|
this.timeInputTemplate = getTemplate("time-input")
|
|
|
|
this.nDays = 0
|
|
|
|
let today = new Date()
|
|
this.curMonth = today.getMonth()
|
|
this.curYear = today.getFullYear()
|
|
this.startDate = today
|
|
this.endDate = today
|
|
|
|
this.totalProjects = 0
|
|
this.clockingTotals = []
|
|
this.clockingRemotes = []
|
|
|
|
this.update()
|
|
}
|
|
|
|
update() {
|
|
let year = this.curYear.toString().padStart(4, "0")
|
|
let month = (this.curMonth + 1).toString().padStart(2, "0")
|
|
let date = new Date(`${year}-${month}-01`)
|
|
let txt = MONTHS[this.curMonth]
|
|
if (new Date().getFullYear() !== this.curYear) {
|
|
txt += " " + year
|
|
}
|
|
document.getElementById("month").innerText = txt
|
|
this.clear()
|
|
this.addMonth(date)
|
|
this.fetchData()
|
|
}
|
|
|
|
prevMonth() {
|
|
this.curMonth--
|
|
if (this.curMonth < 0) {
|
|
this.curMonth += 12
|
|
this.curYear -= 1
|
|
}
|
|
this.update()
|
|
}
|
|
|
|
nextMonth() {
|
|
this.curMonth++
|
|
if (this.curMonth >= 12) {
|
|
this.curMonth -= 12
|
|
this.curYear += 1
|
|
}
|
|
this.update()
|
|
}
|
|
|
|
clear() {
|
|
this.columns.innerHTML = ""
|
|
this.clockings.querySelectorAll(".dates th").forEach(c => c.remove())
|
|
this.clockings.querySelectorAll(".clocking td").forEach(c => c.remove())
|
|
this.weekNames.querySelectorAll("td").forEach(c => c.remove())
|
|
this.dayNames.querySelectorAll("td").forEach(c => c.remove())
|
|
this.dayDates.innerHTML = ""
|
|
this.times.querySelectorAll("tr.project").forEach(r => r.remove())
|
|
this.nDays = 0
|
|
}
|
|
|
|
addClockings(date) {
|
|
let dates = this.clockings.querySelector(".dates")
|
|
let dateCell = document.createElement("th")
|
|
let weekDay = DAYS_SHORT[(date.getDay() + 6) % 7]
|
|
let monthDay = date.getDate().toString().padStart(2, "0")
|
|
dateCell.innerText = `${weekDay} ${monthDay}`
|
|
dates.appendChild(dateCell)
|
|
|
|
let clockings = this.clockings.querySelectorAll(".clocking")
|
|
|
|
clockings.forEach((clocking, i) => {
|
|
if (!(clocking.dataset.type || clocking.classList.contains("total"))) {
|
|
return;
|
|
}
|
|
let cell = clocking.insertCell(this.nDays + 1)
|
|
if (clocking.classList.contains("total")) {
|
|
return
|
|
}
|
|
|
|
let inputCell = this.timeInputTemplate.cloneNode(true)
|
|
let input = inputCell.querySelector("input")
|
|
input.addEventListener("input", () => {
|
|
this.setClocking(date, clocking.dataset.type, input.value)
|
|
})
|
|
cell.replaceWith(inputCell)
|
|
})
|
|
}
|
|
|
|
addWeek(startDate, length=7) {
|
|
let weekNum = startDate.getWeek()
|
|
let weekName = this.weekNames.insertCell(-1)
|
|
weekName.innerText = `Week n° ${weekNum}`
|
|
weekName.colSpan = length
|
|
for (let i = 0; i < length; i++) {
|
|
let date = new Date(startDate.valueOf() + i * DAY_MS)
|
|
let dayPadded = date.getDate().toString().padStart(2, "0")
|
|
let monthPadded = (date.getMonth() + 1).toString().padStart(2, "0")
|
|
|
|
this.addClockings(date)
|
|
let dayName = this.dayNames.insertCell(this.nDays + 2)
|
|
let dayDate = this.dayDates.insertCell(this.nDays)
|
|
let weekDay = (date.getDay() + 6) % 7
|
|
dayName.innerText = DAYS_SHORT[weekDay]
|
|
dayDate.innerText = `${dayPadded}/${monthPadded}`
|
|
|
|
let col = document.createElement("col")
|
|
col.classList.add(`day-${weekDay}`)
|
|
this.columns.appendChild(col)
|
|
this.nDays++
|
|
}
|
|
let weekRemoteTotalCell = this.clockings.querySelector(".clocking.remote-total").insertCell(-1)
|
|
weekRemoteTotalCell.colSpan = length
|
|
let weekTotalCell = this.clockings.querySelector(".clocking.total2").insertCell(-1)
|
|
weekTotalCell.colSpan = length
|
|
}
|
|
|
|
addProject(id, name, sagexNum, times, isParent=false, isProductive=false) {
|
|
let row = this.times.insertRow(-1)
|
|
row.classList.add("project")
|
|
if (isParent) {
|
|
row.classList.add("parent")
|
|
if (isProductive) {
|
|
row.classList.add("productive")
|
|
}
|
|
}
|
|
row.dataset.id = id
|
|
row.insertCell(-1).innerText = name
|
|
row.insertCell(-1).innerText = sagexNum
|
|
let total = 0
|
|
times.forEach(time => {
|
|
if (time) {
|
|
total += time
|
|
}
|
|
let t = time.toString().split(".")
|
|
if (t.length > 1) {
|
|
t[1] = t[1].slice(0, 2)
|
|
}
|
|
row.insertCell(-1).innerText = t.join(".")
|
|
})
|
|
let totalStr = total.toString().split(".")
|
|
if (totalStr.length > 1) {
|
|
totalStr[1] = totalStr[1].slice(0, 2)
|
|
}
|
|
row.insertCell(-1).innerText = totalStr.join(".")
|
|
|
|
if (isParent) {
|
|
row.dataset.total = total
|
|
if (isProductive) {
|
|
this.totalProjects += total
|
|
}
|
|
}
|
|
row.insertCell(-1)
|
|
row.insertCell(-1)
|
|
row.insertCell(-1)
|
|
}
|
|
|
|
addMonth(anchorDate) {
|
|
this.startDate = new Date(anchorDate)
|
|
this.startDate.setDate(1)
|
|
|
|
let startDay = (this.startDate.getDay() + 6) % 7
|
|
let length = 7 - startDay
|
|
this.addWeek(this.startDate, length)
|
|
let monday = new Date(this.startDate.valueOf() + length * DAY_MS)
|
|
let nextMonday
|
|
|
|
let startMonth = this.startDate.getMonth()
|
|
|
|
while (monday.getMonth() === startMonth) {
|
|
nextMonday = new Date(monday.valueOf() + 7 * DAY_MS)
|
|
|
|
let len = 7
|
|
if (nextMonday.getMonth() !== startMonth) {
|
|
len -= nextMonday.getDate() - 1
|
|
}
|
|
this.addWeek(monday, len)
|
|
monday = nextMonday
|
|
}
|
|
|
|
this.clockings.querySelector(".clocking.total").insertCell(-1).rowSpan = 2
|
|
this.endDate = new Date(monday.valueOf() - monday.getDate() * DAY_MS)
|
|
}
|
|
|
|
fetchData() {
|
|
let startDate = formatDate(this.startDate)
|
|
let endDate = formatDate(this.endDate)
|
|
fetch(`/table/${startDate}/${endDate}/`).then(res => {
|
|
if (res.ok) {
|
|
return res.json()
|
|
}
|
|
}).then(res => {
|
|
if (res && res.status === "success") {
|
|
this.displayData(res.data)
|
|
}
|
|
})
|
|
}
|
|
|
|
displayData(data) {
|
|
this.displayClockings(data.clockings)
|
|
this.totalProjects = 0
|
|
data.parents.forEach(parent => {
|
|
let parentDurations = Array(this.nDays).fill(0)
|
|
|
|
let projects = parent.projects.map(project => {
|
|
let durations = Array(this.nDays).fill("")
|
|
project.tasks.forEach(task => {
|
|
let date = new Date(task.date)
|
|
let i = Math.floor((date.valueOf() - this.startDate.valueOf()) / DAY_MS)
|
|
let hours = task.duration / 60
|
|
durations[i] = hours
|
|
parentDurations[i] += hours
|
|
})
|
|
return {
|
|
id: project.id,
|
|
name: project.name,
|
|
durations: durations
|
|
}
|
|
})
|
|
|
|
this.addProject(
|
|
parent.id,
|
|
parent.name,
|
|
parent.project_num,
|
|
parentDurations.map(v => v === 0 ? "" : v),
|
|
true,
|
|
parent.is_productive
|
|
)
|
|
|
|
projects.forEach(project => {
|
|
this.addProject(project.id, project.name, "", project.durations)
|
|
})
|
|
})
|
|
this.updateTotals()
|
|
}
|
|
|
|
displayClockings(clockings) {
|
|
this.clockingTotals = Array(this.nDays).fill(0)
|
|
this.clockingRemotes = Array(this.nDays).fill(0)
|
|
clockings.forEach(clocking => {
|
|
let date = new Date(clocking.date)
|
|
if (date < this.startDate || date > this.endDate) {
|
|
return
|
|
}
|
|
let i = Math.floor((date - this.startDate) / DAY_MS)
|
|
this.clockings.querySelectorAll("tr.clocking[data-type]").forEach(row => {
|
|
let type = row.dataset.type
|
|
let cell = row.cells[i + 1]
|
|
if (clocking[type]) {
|
|
cell.querySelector("input").value = clocking[type]
|
|
}
|
|
})
|
|
let totalRow = this.clockings.querySelector(".clocking.total")
|
|
let totalCell = totalRow.cells[i + 1]
|
|
let hours = +clocking.total / 60
|
|
this.clockingTotals[i] = hours
|
|
totalCell.innerText = hours === 0 ? "" : Math.round(hours * 100) / 100
|
|
|
|
if (clocking.remote !== null) {
|
|
let remoteParts = clocking.remote.split(":").map(v => +v)
|
|
let remoteHours = remoteParts[0] + remoteParts[1] / 60
|
|
if (remoteParts.length >= 3) {
|
|
remoteHours += remoteParts[2] / 3600
|
|
}
|
|
this.clockingRemotes[i] = remoteHours
|
|
}
|
|
})
|
|
}
|
|
|
|
setClocking(date, type, time) {
|
|
this.post(`/clockings/${formatDate(date)}/`, {
|
|
[type]: time
|
|
}).then(res => {
|
|
let row = this.clockings.querySelector(".clocking.total")
|
|
let i = Math.floor((date - this.startDate) / DAY_MS)
|
|
let cell = row.cells[i + 1]
|
|
let minutes = +res.clocking.total
|
|
let hours = minutes / 60
|
|
this.clockingTotals[i] = hours
|
|
cell.innerText = hours === 0 ? "" : Math.round(hours * 100) / 100
|
|
this.updateTotals()
|
|
})
|
|
}
|
|
|
|
updateTotals() {
|
|
let totalClockings = this.clockingTotals.reduce((a, b) => a + b, 0)
|
|
let totalProjects = this.totalProjects
|
|
this.clockings.querySelector(".clocking.total").cells[this.nDays + 1].innerText = Math.round(totalClockings * 100) / 100
|
|
|
|
let startI = 0
|
|
Array.from(this.clockings.querySelector(".clocking.remote-total").cells).forEach((cell, i) => {
|
|
let endI = startI + cell.colSpan
|
|
let remote = this.clockingRemotes.slice(startI, endI).reduce((a, b) => a + b, 0)
|
|
let hour = Math.floor(remote)
|
|
let min = Math.floor((remote - hour) * 60)
|
|
cell.innerText = hour.toString().padStart(2, "0") + ":" + min.toString().padStart(2, "0")
|
|
startI = endI
|
|
})
|
|
|
|
startI = 0
|
|
Array.from(this.clockings.querySelector(".clocking.total2").cells).forEach((cell, i) => {
|
|
let endI = startI + cell.colSpan
|
|
let total = this.clockingTotals.slice(startI, endI).reduce((a, b) => a + b, 0)
|
|
cell.innerText = Math.round(total * 100) / 100
|
|
startI = endI
|
|
})
|
|
|
|
if (totalClockings === 0) {
|
|
console.log("Total clockings = 0")
|
|
return
|
|
}
|
|
if (totalProjects === 0) {
|
|
console.log("Total projects = 0")
|
|
return
|
|
}
|
|
this.times.querySelectorAll(".project.parent.productive").forEach(parent => {
|
|
let total = +parent.dataset.total
|
|
let workingTimeRatio = total / totalClockings
|
|
let imputedTimeRatio = total / totalProjects
|
|
parent.cells[this.nDays + 3].innerText = formatPercentage(workingTimeRatio)
|
|
parent.cells[this.nDays + 4].innerText = formatPercentage(imputedTimeRatio)
|
|
let sagexTime = imputedTimeRatio * totalClockings
|
|
parent.cells[this.nDays + 5].innerText = Math.round(sagexTime * 100) / 100
|
|
})
|
|
}
|
|
|
|
post(endpoint, data) {
|
|
let fd = new FormData()
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
fd.set(key, value)
|
|
})
|
|
return req(endpoint, {
|
|
method: "POST",
|
|
body: fd
|
|
}).then(res => {
|
|
return res.json()
|
|
})
|
|
}
|
|
}
|
|
|
|
window.addEventListener("load", () => {
|
|
prevMonthBtn = document.getElementById("prev-month")
|
|
nextMonthBtn = document.getElementById("next-month")
|
|
month = document.getElementById("month")
|
|
|
|
table = new Table(document.getElementById("table"))
|
|
|
|
prevMonthBtn.addEventListener("click", () => table.prevMonth())
|
|
nextMonthBtn.addEventListener("click", () => table.nextMonth())
|
|
}) |