From 0380d18cd7981a5693405a9f37a5ce3652d692f6 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 28 Jan 2025 21:14:15 +0100 Subject: [PATCH] added table view --- TimeDispatcher/settings.py | 2 +- TimeDispatcher/urls.py | 2 + context_processors.py | 1 + dispatcher/static/table.css | 140 +++++++++++++++++++ dispatcher/static/table.js | 272 ++++++++++++++++++++++++++++++++++++ dispatcher/views.py | 42 +++++- templates/table.html | 65 +++++++++ 7 files changed, 520 insertions(+), 4 deletions(-) create mode 100644 dispatcher/static/table.css create mode 100644 dispatcher/static/table.js create mode 100644 templates/table.html diff --git a/TimeDispatcher/settings.py b/TimeDispatcher/settings.py index c5e0b7d..70c0850 100644 --- a/TimeDispatcher/settings.py +++ b/TimeDispatcher/settings.py @@ -27,7 +27,7 @@ SECRET_KEY = 'django-insecure-^*x5i_#=$9kuj6k^v0cy5dqefmzo%j*i&0w93i%!zmgsa_z)2z # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ["localhost", "192.168.2.68"] # Application definition diff --git a/TimeDispatcher/urls.py b/TimeDispatcher/urls.py index 9e7a6b4..c049238 100644 --- a/TimeDispatcher/urls.py +++ b/TimeDispatcher/urls.py @@ -27,6 +27,8 @@ urlpatterns = [ path("import/", views.import_view, name="import"), path("projects/", views.projects_view, name="projects"), path("parents/", views.ParentsView.as_view(), name="parents"), + path("table///", views.get_table_data, name="table_data"), + path("table/", views.table_view, name="table"), path("projects//", views.project_view, name="project"), path("projects//set_parent", views.set_parent, name="set_parent"), path("stats/by-month///", views.get_stats_by_month, name="stats_by_month"), diff --git a/context_processors.py b/context_processors.py index 0fea96a..1a0606d 100644 --- a/context_processors.py +++ b/context_processors.py @@ -10,6 +10,7 @@ def navbar_links(request): return { "nav_links": [ {'view': 'dashboard', 'label': 'Dashboard'}, + {'view': 'table', 'label': 'Table'}, {'view': 'projects', 'label': 'Projects'}, {'view': 'parents', 'label': 'Parents'}, {'view': 'import', 'label': 'Import'}, diff --git a/dispatcher/static/table.css b/dispatcher/static/table.css new file mode 100644 index 0000000..533b880 --- /dev/null +++ b/dispatcher/static/table.css @@ -0,0 +1,140 @@ +#table { + border-collapse: collapse; + font-size: 80%; + table-layout: auto; + width: 100%; + + --border: solid var(--light1) 1px; + + th { + font-weight: bold; + padding: 0.4em 0.8em; + } + + .clockings { + .dates { + th { + text-align: center; + } + } + + .clocking { + th:first-child { + text-align: right; + } + + .total-header { + writing-mode: vertical-lr; + transform: rotate(180deg); + padding: 0.8em 0.4em; + } + + .time-input { + max-width: 4em; /* not perfect */ + + input { + width: 100%; + text-align: right; + } + + input[type="time"]::-webkit-calendar-picker-indicator { + display: none; + } + } + } + } + + .week-names { + text-align: center; + font-weight: bold; + td { + padding: 0.4em; + + &:not(:first-child) { + border: var(--border); + } + + } + } + .day-names, .day-dates { + text-align: center; + + td { + padding: 0.2em; + } + } + + .day-dates { + border-bottom: var(--border); + } + + col.day-5, col.day-6 { + background-color: var(--dark3); + } + + colgroup.tailers, col.day-0 { + border-left: var(--border); + } + + colgroup.headers, col.day-6 { + border-right: solid var(--light1) 1px; + } + + .times .project td:nth-child(2) { + text-align: center; + } + + .times { + td { + padding: 0.4em; + } + + tr { + &.project { + border-top: solid #71717185 1px; + td { + text-align: right; + + &:first-child { + text-align: left; + } + &:nth-child(2) { + text-align: center; + } + } + } + + &.parent { + border-top: var(--border); + + td { + font-weight: bold; + } + } + } + + } + + .separator { + height: 2em; + } +} + +.controls { + display: flex; + gap: 0.8em; + align-items: center; + margin-bottom: 1em; + + button { + background-color: var(--dark3); + color: var(--light1); + padding: 0.4em 0.8em; + border: none; + cursor: pointer; + + &:hover { + background-color: var(--dark4); + } + } +} \ No newline at end of file diff --git a/dispatcher/static/table.js b/dispatcher/static/table.js new file mode 100644 index 0000000..2963b66 --- /dev/null +++ b/dispatcher/static/table.js @@ -0,0 +1,272 @@ +let table +let prevMonthBtn, nextMonthBtn, month +let curMonth = new Date().getMonth() + +const DAYS_SHORT = [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" +] + +const MONTHS = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" +] + +const SEC_MS = 1000 +const MIN_MS = SEC_MS * 60 +const HOUR_MS = MIN_MS * 60 +const DAY_MS = HOUR_MS * 24 + +////////////////////////////////////////////////////////// +// // +// Taken from: https://weeknumber.com/how-to/javascript // +// // +////////////////////////////////////////////////////////// +Date.prototype.getWeek = function() { + let date = new Date(this.getTime()) + date.setHours(0, 0, 0, 0) + + // Thursday in current week decides the year. + date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7) + + // January 4 is always in week 1. + let week1 = new Date(date.getFullYear(), 0, 4) + + // Adjust to Thursday in week 1 and count number of weeks from date to week1. + return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7) +} + +function formatDate(date) { + let year = date.getFullYear().toString().padStart(4, "0") + let month = (date.getMonth() + 1).toString().padStart(2, "0") + let day = date.getDate().toString().padStart(2, "0") + return `${year}-${month}-${day}` +} + +function getTemplate(cls) { + let elmt = document.querySelector(".template." + cls).cloneNode(true) + elmt.classList.remove("template") + return elmt +} + +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.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() + 1) % 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) => { + let cell = clocking.insertCell(this.nDays + 1) + if (i === 5) { + return + } + + cell.replaceWith(this.timeInputTemplate.cloneNode(true)) + }) + } + + 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++ + } + } + + 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 + times.forEach(time => { + let t = time.toString().split(".") + if (t.length > 1 ){ + t[1] = t[1].slice(0, 2) + } + row.insertCell(-1).innerText = t.join(".") + }) + } + + 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.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) { + data.parents.forEach(parent => { + this.addProject( + parent.id, + parent.name, + parent.project_num, + Array(this.nDays).fill(""), + true + ) + + parent.projects.forEach(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) + durations[i] = task.duration / 60 + }) + this.addProject( + project.id, + project.name, + "", + durations + ) + }) + }) + } +} + +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()) +}) \ No newline at end of file diff --git a/dispatcher/views.py b/dispatcher/views.py index daaea98..a9094a2 100644 --- a/dispatcher/views.py +++ b/dispatcher/views.py @@ -1,6 +1,6 @@ +import datetime from datetime import timedelta -from django.core.files.uploadedfile import UploadedFile from django.db.models import Sum, Q, QuerySet from django.http import JsonResponse from django.shortcuts import render, get_object_or_404, redirect @@ -9,7 +9,7 @@ from django.views.decorators.http import require_POST from dispatcher.core import import_tasks from dispatcher.forms import ProjectForm, ImportForm -from dispatcher.models import Project, Parent, Task +from dispatcher.models import Project, Parent def dashboard_view(request): @@ -35,6 +35,9 @@ def project_view(request, id): context["form"] = ProjectForm(instance=project) return render(request, "project.html", context) +def table_view(request): + return render(request, "table.html") + @require_POST def set_parent(request, id): project = get_object_or_404(Project, id=id) @@ -84,7 +87,7 @@ def get_stats_by_month(request, year: int, month: int): ) return JsonResponse({"status": "success", "data": format_stats(parents, projects)}) -def get_stats_between(request, start_date, end_date): +def get_stats_between(request, start_date: datetime.date, end_date: datetime.date): parents = Parent.objects.annotate( total_duration=Sum( "project__task__duration", @@ -131,3 +134,36 @@ class ParentsView(generic.ListView): model = Parent template_name = "parents.html" context_object_name = "elements" + + +def get_table_data(request, start_date: datetime.date, end_date: datetime.date): + end_date = end_date + timedelta(days=1) + parents = Parent.objects.all().order_by("id") + data = { + "parents": [ + { + "id": parent.id, + "name": parent.name, + "project_num": parent.project_num, + "projects": [ + { + "id": project.id, + "name": project.name, + "tasks": [ + { + "date": task["date"], + "duration": task["duration"] + } + for task in project.task_set.filter( + date__gte=start_date, + date__lt=end_date + ).values("date").order_by("date").annotate(duration=Sum("duration")) + ] + } + for project in parent.project_set.order_by("id") + ] + } + for parent in parents + ] + } + return JsonResponse({"status": "success", "data": data}) \ No newline at end of file diff --git a/templates/table.html b/templates/table.html new file mode 100644 index 0000000..5a4a3eb --- /dev/null +++ b/templates/table.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} +{% load static %} +{% block title %}Table{% endblock %} +{% block head %} + + +{% endblock %} +{% block main %} +
+ +
Month
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Check-IN AMTOTAL% working time% imputed timetime on sagex
Check-OUT AM
Check-IN PM
Check-OUT PM
Remote
TOTAL
NameN° sagex
+{% endblock %} \ No newline at end of file