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'))
"""
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/<int:id>/", views.project_view, name="project"),
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/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;
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);

View File

@ -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",
"At",
"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,13 +85,40 @@ function updateList() {
if (res.status !== "success") {
return
}
let data = res.data.filter(parent => parent.duration !== null)
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
}
setListElements(res.data.parents)
})
}
function setListElements(data) {
data = data.filter(parent => parent.duration !== null)
let list = document.getElementById("list")
let template = document.querySelector(".monthly .template.row").cloneNode(true)
let template = document.querySelector(".by-range .template.row").cloneNode(true)
template.classList.remove("template")
list.innerHTML = ""
if (data.length === 0) {
let noData = document.querySelector(".monthly .template.no-data").cloneNode(true)
let noData = document.querySelector(".by-range .template.no-data").cloneNode(true)
noData.classList.remove("template")
list.appendChild(noData)
return
@ -80,15 +129,38 @@ function updateList() {
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()
})

View File

@ -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,7 +73,41 @@ def get_stats_by_month(request, year: int, month: int):
)
)
)
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,
@ -79,9 +115,17 @@ def get_stats_by_month(request, year: int, month: int):
"duration": parent.total_duration
}
for parent in parents
],
"projects": [
{
"id": project.id,
"name": project.name,
"duration": project.total_duration
}
for project in projects
]
return JsonResponse({"status": "success", "data": data})
}
return data
class ParentsView(generic.ListView):
model = Parent

View File

@ -7,16 +7,27 @@
{% endblock %}
{% block footer %}{% endblock %}
{% block main %}
<div class="monthly">
<div class="by-range">
<div class="template row">
<div class="name"></div>
<div class="duration"></div>
</div>
<div class="template no-data">No Data</div>
<div class="controls">
<button id="prev"><</button>
<div id="month">Mois</div>
<button id="next">></button>
<div id="month-sel" class="group">
<button id="prev-month"><</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 id="list"></div>
</div>