added weekly view
This commit is contained in:
parent
500ff54537
commit
01db4285c2
12
TimeDispatcher/converters.py
Normal file
12
TimeDispatcher/converters.py
Normal 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)
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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);
|
||||||
|
@ -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",
|
||||||
"Août",
|
"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()
|
||||||
})
|
})
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user