From 01db4285c2bec1cb0171cb460699ea5a739d3009 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Mon, 27 Jan 2025 18:50:24 +0100 Subject: [PATCH] added weekly view --- TimeDispatcher/converters.py | 12 +++ TimeDispatcher/urls.py | 6 +- dispatcher/static/dashboard.css | 26 ++++-- dispatcher/static/dashboard.js | 144 ++++++++++++++++++++++++-------- dispatcher/views.py | 66 ++++++++++++--- templates/dashboard.html | 19 ++++- 6 files changed, 213 insertions(+), 60 deletions(-) create mode 100644 TimeDispatcher/converters.py diff --git a/TimeDispatcher/converters.py b/TimeDispatcher/converters.py new file mode 100644 index 0000000..361f617 --- /dev/null +++ b/TimeDispatcher/converters.py @@ -0,0 +1,12 @@ +from datetime import datetime + + +class DateConverter: + regex = '\d{4}-\d{1,2}-\d{1,2}' + format = '%Y-%m-%d' + + def to_python(self, value): + return datetime.strptime(value, self.format).date() + + def to_url(self, value): + return value.strftime(self.format) \ No newline at end of file diff --git a/TimeDispatcher/urls.py b/TimeDispatcher/urls.py index 9831430..9e7a6b4 100644 --- a/TimeDispatcher/urls.py +++ b/TimeDispatcher/urls.py @@ -15,10 +15,13 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, register_converter +from TimeDispatcher.converters import DateConverter from dispatcher import views +register_converter(DateConverter, "date") + urlpatterns = [ path("", views.dashboard_view, name="dashboard"), path("import/", views.import_view, name="import"), @@ -27,4 +30,5 @@ urlpatterns = [ 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"), + path("stats/between///", views.get_stats_between, name="stats_between"), ] diff --git a/dispatcher/static/dashboard.css b/dispatcher/static/dashboard.css index daebb8c..0394c06 100644 --- a/dispatcher/static/dashboard.css +++ b/dispatcher/static/dashboard.css @@ -1,4 +1,4 @@ -.monthly { +.by-range { display: flex; flex-direction: column; padding: 1.2em; @@ -6,13 +6,23 @@ gap: 1.2em; } -.monthly .controls { +.by-range .controls { display: flex; - gap: 1.2em; + gap: 2.4em; align-items: center; } -.monthly .controls button { +.by-range .controls .group { + display: flex; + gap: 0.8em; + align-items: center; +} + +.by-range .controls .group.hidden { + display: none; +} + +.by-range .controls button, .by-range .controls select { background-color: var(--dark3); color: var(--light1); padding: 0.4em 0.8em; @@ -20,24 +30,24 @@ cursor: pointer; } -.monthly .controls button:hover { +.by-range .controls button:hover, .by-range .controls select:hover { background-color: var(--dark4); } -.monthly #list { +.by-range #list { display: flex; flex-direction: column; gap: 0.8em; } -.monthly #list .row { +.by-range #list .row { display: flex; gap: 1.2em; padding: 0.4em 0.8em; background-color: var(--dark3); } -.monthly #list .no-data { +.by-range #list .no-data { font-style: italic; padding: 0.4em 0.8em; background-color: var(--dark2); diff --git a/dispatcher/static/dashboard.js b/dispatcher/static/dashboard.js index e8d7b33..4fa8a37 100644 --- a/dispatcher/static/dashboard.js +++ b/dispatcher/static/dashboard.js @@ -1,22 +1,36 @@ -let prevBtn, nextBtn, month +let prevMonthBtn, nextMonthBtn, month +let prevWeekBtn, nextWeekBtn, week let curYear = new Date().getFullYear() let curMonth = new Date().getMonth() +let curWeekDate = new Date() + +const SEC_MS = 1000 +const MIN_MS = SEC_MS * 60 +const HOUR_MS = MIN_MS * 60 +const DAY_MS = HOUR_MS * 24 const MONTHS = [ - "Janvier", - "Février", - "Mars", - "Avril", - "Mai", - "Juin", - "Juillet", - "Août", - "Septembre", - "Octobre", - "Novembre", - "Décembre" + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" ] +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 formatDuration(duration) { let hours = Math.floor(duration / 60) duration -= hours * 60 @@ -36,7 +50,7 @@ function prevMonth() { curMonth += 12 curYear -= 1 } - updateList() + updateListMonthly() } function nextMonth() { curMonth = curMonth + 1 @@ -44,10 +58,18 @@ function nextMonth() { curMonth -= 12 curYear += 1 } - updateList() + updateListMonthly() +} +function prevWeek() { + curWeekDate = new Date(curWeekDate.valueOf() - 7 * DAY_MS) + updateListWeekly() +} +function nextWeek() { + curWeekDate = new Date(curWeekDate.valueOf() + 7 * DAY_MS) + updateListWeekly() } -function updateList() { +function updateListMonthly() { let year = curYear.toString().padStart(4, "0") let month = (curMonth + 1).toString().padStart(2, "0") let today = new Date() @@ -63,32 +85,82 @@ function updateList() { 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) + setListElements(res.data.parents) + }) +} + +function updateListWeekly() { + let weekDay = (curWeekDate.getDay() + 6) % 7 + let startDate = new Date(curWeekDate.valueOf() - weekDay * DAY_MS) + let endDate = new Date(startDate.valueOf() + 6 * DAY_MS) + + let today = new Date() + let txt = `Week of ${MONTHS[startDate.getMonth()]} ${startDate.getDate()}` + if (startDate.getFullYear() !== today.getFullYear()) { + txt += " " + startDate.getFullYear().toString().padStart(4, "0") + } + document.getElementById("week").innerText = txt + + fetch(`stats/between/${formatDate(startDate)}/${formatDate(endDate)}/`).then(res => { + return res.json() + }).then(res => { + if (res.status !== "success") { 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) - }) + setListElements(res.data.parents) + }) +} + +function setListElements(data) { + data = data.filter(parent => parent.duration !== null) + let list = document.getElementById("list") + let template = document.querySelector(".by-range .template.row").cloneNode(true) + template.classList.remove("template") + list.innerHTML = "" + if (data.length === 0) { + let noData = document.querySelector(".by-range .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") + prevMonthBtn = document.getElementById("prev-month") + nextMonthBtn = document.getElementById("next-month") month = document.getElementById("month") - prevBtn.addEventListener("click", () => prevMonth()) - nextBtn.addEventListener("click", () => nextMonth()) - updateList() + prevWeekBtn = document.getElementById("prev-week") + nextWeekBtn = document.getElementById("next-week") + week = document.getElementById("week") + + prevMonthBtn.addEventListener("click", () => prevMonth()) + nextMonthBtn.addEventListener("click", () => nextMonth()) + prevWeekBtn.addEventListener("click", () => prevWeek()) + nextWeekBtn.addEventListener("click", () => nextWeek()) + + let monthGrp = document.getElementById("month-sel") + let weekGrp = document.getElementById("week-sel") + + rangeSel = document.getElementById("range-sel") + rangeSel.addEventListener("change", () => { + let mode = rangeSel.value + if (mode === "weekly") { + monthGrp.classList.add("hidden") + weekGrp.classList.remove("hidden") + updateListWeekly() + } else { + monthGrp.classList.remove("hidden") + weekGrp.classList.add("hidden") + updateListMonthly() + } + }) + + updateListMonthly() }) \ No newline at end of file diff --git a/dispatcher/views.py b/dispatcher/views.py index af912e5..daaea98 100644 --- a/dispatcher/views.py +++ b/dispatcher/views.py @@ -1,5 +1,7 @@ +from datetime import timedelta + from django.core.files.uploadedfile import UploadedFile -from django.db.models import Sum, Q +from django.db.models import Sum, Q, QuerySet from django.http import JsonResponse from django.shortcuts import render, get_object_or_404, redirect from django.views import generic @@ -71,17 +73,59 @@ def get_stats_by_month(request, year: int, month: int): ) ) ) - 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}) + projects = Project.objects.annotate( + total_duration=Sum( + "task__duration", + filter=Q( + task__date__year=year, + task__date__month=month + ) + ) + ) + return JsonResponse({"status": "success", "data": format_stats(parents, projects)}) +def get_stats_between(request, start_date, end_date): + parents = Parent.objects.annotate( + total_duration=Sum( + "project__task__duration", + filter=Q( + project__task__date__gte=start_date, + project__task__date__lt=end_date + timedelta(days=1) + ) + ) + ) + projects = Project.objects.annotate( + total_duration=Sum( + "task__duration", + filter=Q( + task__date__gte=start_date, + task__date__lt=end_date + timedelta(days=1) + ) + ) + ) + return JsonResponse({"status": "success", "data": format_stats(parents, projects)}) + +def format_stats(parents: QuerySet[Parent], projects: QuerySet[Project]): + data = { + "parents": [ + { + "id": parent.id, + "name": parent.name, + "project_num": parent.project_num, + "duration": parent.total_duration + } + for parent in parents + ], + "projects": [ + { + "id": project.id, + "name": project.name, + "duration": project.total_duration + } + for project in projects + ] + } + return data class ParentsView(generic.ListView): model = Parent diff --git a/templates/dashboard.html b/templates/dashboard.html index 7cc1597..a29ea05 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -7,16 +7,27 @@ {% endblock %} {% block footer %}{% endblock %} {% block main %} -
+
No Data
- -
Mois
- +
+ +
Month
+ +
+ +