added weekly view

This commit is contained in:
Louis Heredero 2025-01-27 18:50:24 +01:00
parent 500ff54537
commit 01db4285c2
Signed by: HEL
GPG Key ID: 8D83DE470F8544E7
6 changed files with 213 additions and 60 deletions

View File

@ -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)

View File

@ -15,10 +15,13 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin 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 from dispatcher import views
register_converter(DateConverter, "date")
urlpatterns = [ urlpatterns = [
path("", views.dashboard_view, name="dashboard"), path("", views.dashboard_view, name="dashboard"),
path("import/", views.import_view, name="import"), path("import/", views.import_view, name="import"),
@ -27,4 +30,5 @@ urlpatterns = [
path("projects/<int:id>/", views.project_view, name="project"), path("projects/<int:id>/", views.project_view, name="project"),
path("projects/<int:id>/set_parent", views.set_parent, name="set_parent"), path("projects/<int:id>/set_parent", views.set_parent, name="set_parent"),
path("stats/by-month/<int:year>/<int:month>/", views.get_stats_by_month, name="stats_by_month"), path("stats/by-month/<int:year>/<int:month>/", views.get_stats_by_month, name="stats_by_month"),
path("stats/between/<date:start_date>/<date:end_date>/", views.get_stats_between, name="stats_between"),
] ]

View File

@ -1,4 +1,4 @@
.monthly { .by-range {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 1.2em; padding: 1.2em;
@ -6,13 +6,23 @@
gap: 1.2em; gap: 1.2em;
} }
.monthly .controls { .by-range .controls {
display: flex; display: flex;
gap: 1.2em; gap: 2.4em;
align-items: center; 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); background-color: var(--dark3);
color: var(--light1); color: var(--light1);
padding: 0.4em 0.8em; padding: 0.4em 0.8em;
@ -20,24 +30,24 @@
cursor: pointer; cursor: pointer;
} }
.monthly .controls button:hover { .by-range .controls button:hover, .by-range .controls select:hover {
background-color: var(--dark4); background-color: var(--dark4);
} }
.monthly #list { .by-range #list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.8em; gap: 0.8em;
} }
.monthly #list .row { .by-range #list .row {
display: flex; display: flex;
gap: 1.2em; gap: 1.2em;
padding: 0.4em 0.8em; padding: 0.4em 0.8em;
background-color: var(--dark3); background-color: var(--dark3);
} }
.monthly #list .no-data { .by-range #list .no-data {
font-style: italic; font-style: italic;
padding: 0.4em 0.8em; padding: 0.4em 0.8em;
background-color: var(--dark2); background-color: var(--dark2);

View File

@ -1,22 +1,36 @@
let prevBtn, nextBtn, month let prevMonthBtn, nextMonthBtn, month
let prevWeekBtn, nextWeekBtn, week
let curYear = new Date().getFullYear() let curYear = new Date().getFullYear()
let curMonth = new Date().getMonth() 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 = [ const MONTHS = [
"Janvier", "January",
"Février", "February",
"Mars", "March",
"Avril", "April",
"Mai", "May",
"Juin", "June",
"Juillet", "July",
"At", "August",
"Septembre", "September",
"Octobre", "October",
"Novembre", "November",
"Décembre" "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) { function formatDuration(duration) {
let hours = Math.floor(duration / 60) let hours = Math.floor(duration / 60)
duration -= hours * 60 duration -= hours * 60
@ -36,7 +50,7 @@ function prevMonth() {
curMonth += 12 curMonth += 12
curYear -= 1 curYear -= 1
} }
updateList() updateListMonthly()
} }
function nextMonth() { function nextMonth() {
curMonth = curMonth + 1 curMonth = curMonth + 1
@ -44,10 +58,18 @@ function nextMonth() {
curMonth -= 12 curMonth -= 12
curYear += 1 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 year = curYear.toString().padStart(4, "0")
let month = (curMonth + 1).toString().padStart(2, "0") let month = (curMonth + 1).toString().padStart(2, "0")
let today = new Date() let today = new Date()
@ -63,32 +85,82 @@ function updateList() {
if (res.status !== "success") { if (res.status !== "success") {
return return
} }
let data = res.data.filter(parent => parent.duration !== null) setListElements(res.data.parents)
let list = document.getElementById("list") })
let template = document.querySelector(".monthly .template.row").cloneNode(true) }
template.classList.remove("template")
list.innerHTML = "" function updateListWeekly() {
if (data.length === 0) { let weekDay = (curWeekDate.getDay() + 6) % 7
let noData = document.querySelector(".monthly .template.no-data").cloneNode(true) let startDate = new Date(curWeekDate.valueOf() - weekDay * DAY_MS)
noData.classList.remove("template") let endDate = new Date(startDate.valueOf() + 6 * DAY_MS)
list.appendChild(noData)
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 return
} }
data.forEach(parent => { setListElements(res.data.parents)
let row = template.cloneNode(true) })
row.querySelector(".name").innerText = `${parent.name} (${parent.project_num})` }
row.querySelector(".duration").innerText = formatDuration(parent.duration)
list.appendChild(row) 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", () => { window.addEventListener("load", () => {
prevBtn = document.getElementById("prev") prevMonthBtn = document.getElementById("prev-month")
nextBtn = document.getElementById("next") nextMonthBtn = document.getElementById("next-month")
month = document.getElementById("month") month = document.getElementById("month")
prevBtn.addEventListener("click", () => prevMonth()) prevWeekBtn = document.getElementById("prev-week")
nextBtn.addEventListener("click", () => nextMonth()) nextWeekBtn = document.getElementById("next-week")
updateList() 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()
}) })

View File

@ -1,5 +1,7 @@
from datetime import timedelta
from django.core.files.uploadedfile import UploadedFile 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.http import JsonResponse
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.views import generic from django.views import generic
@ -71,17 +73,59 @@ def get_stats_by_month(request, year: int, month: int):
) )
) )
) )
data = [ projects = Project.objects.annotate(
{ total_duration=Sum(
"id": parent.id, "task__duration",
"name": parent.name, filter=Q(
"project_num": parent.project_num, task__date__year=year,
"duration": parent.total_duration task__date__month=month
} )
for parent in parents )
] )
return JsonResponse({"status": "success", "data": data}) 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): class ParentsView(generic.ListView):
model = Parent model = Parent

View File

@ -7,16 +7,27 @@
{% endblock %} {% endblock %}
{% block footer %}{% endblock %} {% block footer %}{% endblock %}
{% block main %} {% block main %}
<div class="monthly"> <div class="by-range">
<div class="template row"> <div class="template row">
<div class="name"></div> <div class="name"></div>
<div class="duration"></div> <div class="duration"></div>
</div> </div>
<div class="template no-data">No Data</div> <div class="template no-data">No Data</div>
<div class="controls"> <div class="controls">
<button id="prev"><</button> <div id="month-sel" class="group">
<div id="month">Mois</div> <button id="prev-month"><</button>
<button id="next">></button> <div id="month">Month</div>
<button id="next-month">></button>
</div>
<div id="week-sel" class="group hidden">
<button id="prev-week"><</button>
<div id="week">Week</div>
<button id="next-week">></button>
</div>
<select id="range-sel">
<option value="monthly">Monthly</option>
<option value="weekly">Weekly</option>
</select>
</div> </div>
<div id="list"></div> <div id="list"></div>
</div> </div>