From 029502d8876bf705e94593802d47fe667a8c5e9c Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sun, 26 Jan 2025 02:01:38 +0100 Subject: [PATCH] added monthly summary on dashboard --- TimeDispatcher/urls.py | 2 + dispatcher/static/base.css | 4 ++ dispatcher/static/dashboard.css | 44 +++++++++++++++ dispatcher/static/dashboard.js | 94 +++++++++++++++++++++++++++++++++ dispatcher/static/projects.js | 14 +++++ dispatcher/views.py | 45 +++++++++++++++- templates/dashboard.html | 14 +++++ templates/projects.html | 1 + 8 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 dispatcher/static/dashboard.js diff --git a/TimeDispatcher/urls.py b/TimeDispatcher/urls.py index 98280b1..9831430 100644 --- a/TimeDispatcher/urls.py +++ b/TimeDispatcher/urls.py @@ -25,4 +25,6 @@ urlpatterns = [ path("projects/", views.projects_view, name="projects"), path("parents/", views.ParentsView.as_view(), name="parents"), 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/dispatcher/static/base.css b/dispatcher/static/base.css index 5ec9123..dab2c97 100644 --- a/dispatcher/static/base.css +++ b/dispatcher/static/base.css @@ -79,4 +79,8 @@ button:hover, select:hover { button, select { cursor: pointer; +} + +.template { + display: none !important; } \ No newline at end of file diff --git a/dispatcher/static/dashboard.css b/dispatcher/static/dashboard.css index e69de29..daebb8c 100644 --- a/dispatcher/static/dashboard.css +++ b/dispatcher/static/dashboard.css @@ -0,0 +1,44 @@ +.monthly { + display: flex; + flex-direction: column; + padding: 1.2em; + border: solid var(--light4) 1px; + gap: 1.2em; +} + +.monthly .controls { + display: flex; + gap: 1.2em; + align-items: center; +} + +.monthly .controls button { + background-color: var(--dark3); + color: var(--light1); + padding: 0.4em 0.8em; + border: none; + cursor: pointer; +} + +.monthly .controls button:hover { + background-color: var(--dark4); +} + +.monthly #list { + display: flex; + flex-direction: column; + gap: 0.8em; +} + +.monthly #list .row { + display: flex; + gap: 1.2em; + padding: 0.4em 0.8em; + background-color: var(--dark3); +} + +.monthly #list .no-data { + font-style: italic; + padding: 0.4em 0.8em; + background-color: var(--dark2); +} \ No newline at end of file diff --git a/dispatcher/static/dashboard.js b/dispatcher/static/dashboard.js new file mode 100644 index 0000000..e8d7b33 --- /dev/null +++ b/dispatcher/static/dashboard.js @@ -0,0 +1,94 @@ +let prevBtn, nextBtn, month +let curYear = new Date().getFullYear() +let curMonth = new Date().getMonth() + +const MONTHS = [ + "Janvier", + "Février", + "Mars", + "Avril", + "Mai", + "Juin", + "Juillet", + "Août", + "Septembre", + "Octobre", + "Novembre", + "Décembre" +] + +function formatDuration(duration) { + let hours = Math.floor(duration / 60) + duration -= hours * 60 + let res = "" + if (hours > 0) { + res += hours.toString() + "h" + res += duration.toString().padStart(2, "0") + } else { + res += duration.toString() + "min" + } + return res +} + +function prevMonth() { + curMonth = curMonth - 1 + if (curMonth < 0) { + curMonth += 12 + curYear -= 1 + } + updateList() +} +function nextMonth() { + curMonth = curMonth + 1 + if (curMonth >= 12) { + curMonth -= 12 + curYear += 1 + } + updateList() +} + +function updateList() { + let year = curYear.toString().padStart(4, "0") + let month = (curMonth + 1).toString().padStart(2, "0") + let today = new Date() + let txt = MONTHS[curMonth] + if (today.getFullYear() !== curYear) { + txt += " " + year.toString().padStart(4, "0") + } + document.getElementById("month").innerText = txt + + fetch(`stats/by-month/${year}/${month}/`).then(res => { + return res.json() + }).then(res => { + if (res.status !== "success") { + return + } + let data = res.data.filter(parent => parent.duration !== null) + let list = document.getElementById("list") + let template = document.querySelector(".monthly .template.row").cloneNode(true) + template.classList.remove("template") + list.innerHTML = "" + if (data.length === 0) { + let noData = document.querySelector(".monthly .template.no-data").cloneNode(true) + noData.classList.remove("template") + list.appendChild(noData) + return + } + data.forEach(parent => { + let row = template.cloneNode(true) + row.querySelector(".name").innerText = `${parent.name} (${parent.project_num})` + row.querySelector(".duration").innerText = formatDuration(parent.duration) + list.appendChild(row) + }) + }) +} + +window.addEventListener("load", () => { + prevBtn = document.getElementById("prev") + nextBtn = document.getElementById("next") + month = document.getElementById("month") + + prevBtn.addEventListener("click", () => prevMonth()) + nextBtn.addEventListener("click", () => nextMonth()) + updateList() +}) \ No newline at end of file diff --git a/dispatcher/static/projects.js b/dispatcher/static/projects.js index 7763704..9c39b6a 100644 --- a/dispatcher/static/projects.js +++ b/dispatcher/static/projects.js @@ -1,6 +1,20 @@ window.addEventListener("load", () => { document.querySelectorAll(".projects tbody tr").forEach(row => { let id = row.dataset.id + let selector = row.querySelector(".parent-sel") + selector.addEventListener("change", () => { + let fd = new FormData() + fd.set("parent_id", selector.value) + let csrftoken = document.querySelector("input[name='csrfmiddlewaretoken']").value + fetch(`${id}/set_parent`, { + method: "POST", + body: fd, + headers: { + "X-CSRFToken": csrftoken + } + }) + }) + row.querySelector("button.see").addEventListener("click", () => { window.location.href = `${id}/` }) diff --git a/dispatcher/views.py b/dispatcher/views.py index f2cfcfa..af912e5 100644 --- a/dispatcher/views.py +++ b/dispatcher/views.py @@ -1,10 +1,13 @@ from django.core.files.uploadedfile import UploadedFile +from django.db.models import Sum, Q +from django.http import JsonResponse from django.shortcuts import render, get_object_or_404, redirect from django.views import generic +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 +from dispatcher.models import Project, Parent, Task def dashboard_view(request): @@ -30,6 +33,21 @@ def project_view(request, id): context["form"] = ProjectForm(instance=project) return render(request, "project.html", context) +@require_POST +def set_parent(request, id): + project = get_object_or_404(Project, id=id) + parent_id = request.POST.get("parent_id") + try: + parent = Parent.objects.get(id=parent_id) + except Parent.DoesNotExist: + parent = None + + project.parent = parent + project.save() + return JsonResponse({"status": "success"}) + + + def import_view(request): if request.method == "POST": form = ImportForm(request.POST, request.FILES) @@ -40,6 +58,31 @@ def import_view(request): return render(request, "import.html") +def get_stats_by_month(request, year: int, month: int): + if month < 1 or month > 12: + return JsonResponse({"status": "error", "error": f"Invalid month {month}"}) + + parents = Parent.objects.annotate( + total_duration=Sum( + "project__task__duration", + filter=Q( + project__task__date__year=year, + project__task__date__month=month + ) + ) + ) + data = [ + { + "id": parent.id, + "name": parent.name, + "project_num": parent.project_num, + "duration": parent.total_duration + } + for parent in parents + ] + return JsonResponse({"status": "success", "data": data}) + + class ParentsView(generic.ListView): model = Parent template_name = "parents.html" diff --git a/templates/dashboard.html b/templates/dashboard.html index bf9594d..7cc1597 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -3,7 +3,21 @@ {% block title %}Dashboard{% endblock %} {% block head %} + {% endblock %} {% block footer %}{% endblock %} {% block main %} +
+
+
+
+
+
No Data
+
+ +
Mois
+ +
+
+
{% endblock %} \ No newline at end of file diff --git a/templates/projects.html b/templates/projects.html index 38ba050..fac6f96 100644 --- a/templates/projects.html +++ b/templates/projects.html @@ -7,6 +7,7 @@ {% endblock %} {% block footer %}{% endblock %} {% block main %} + {% csrf_token %}