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) { let row = this.times.insertRow(-1) row.classList.add("project") if (isParent) { row.classList.add("parent") } 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 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 ) 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").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()) })