added table view

This commit is contained in:
Louis Heredero 2025-01-28 21:14:15 +01:00
parent 01db4285c2
commit 0380d18cd7
Signed by: HEL
GPG Key ID: 8D83DE470F8544E7
7 changed files with 520 additions and 4 deletions

View File

@ -27,7 +27,7 @@ SECRET_KEY = 'django-insecure-^*x5i_#=$9kuj6k^v0cy5dqefmzo%j*i&0w93i%!zmgsa_z)2z
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = ["localhost", "192.168.2.68"]
# Application definition # Application definition

View File

@ -27,6 +27,8 @@ urlpatterns = [
path("import/", views.import_view, name="import"), path("import/", views.import_view, name="import"),
path("projects/", views.projects_view, name="projects"), path("projects/", views.projects_view, name="projects"),
path("parents/", views.ParentsView.as_view(), name="parents"), path("parents/", views.ParentsView.as_view(), name="parents"),
path("table/<date:start_date>/<date:end_date>/", views.get_table_data, name="table_data"),
path("table/", views.table_view, name="table"),
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"),

View File

@ -10,6 +10,7 @@ def navbar_links(request):
return { return {
"nav_links": [ "nav_links": [
{'view': 'dashboard', 'label': 'Dashboard'}, {'view': 'dashboard', 'label': 'Dashboard'},
{'view': 'table', 'label': 'Table'},
{'view': 'projects', 'label': 'Projects'}, {'view': 'projects', 'label': 'Projects'},
{'view': 'parents', 'label': 'Parents'}, {'view': 'parents', 'label': 'Parents'},
{'view': 'import', 'label': 'Import'}, {'view': 'import', 'label': 'Import'},

140
dispatcher/static/table.css Normal file
View File

@ -0,0 +1,140 @@
#table {
border-collapse: collapse;
font-size: 80%;
table-layout: auto;
width: 100%;
--border: solid var(--light1) 1px;
th {
font-weight: bold;
padding: 0.4em 0.8em;
}
.clockings {
.dates {
th {
text-align: center;
}
}
.clocking {
th:first-child {
text-align: right;
}
.total-header {
writing-mode: vertical-lr;
transform: rotate(180deg);
padding: 0.8em 0.4em;
}
.time-input {
max-width: 4em; /* not perfect */
input {
width: 100%;
text-align: right;
}
input[type="time"]::-webkit-calendar-picker-indicator {
display: none;
}
}
}
}
.week-names {
text-align: center;
font-weight: bold;
td {
padding: 0.4em;
&:not(:first-child) {
border: var(--border);
}
}
}
.day-names, .day-dates {
text-align: center;
td {
padding: 0.2em;
}
}
.day-dates {
border-bottom: var(--border);
}
col.day-5, col.day-6 {
background-color: var(--dark3);
}
colgroup.tailers, col.day-0 {
border-left: var(--border);
}
colgroup.headers, col.day-6 {
border-right: solid var(--light1) 1px;
}
.times .project td:nth-child(2) {
text-align: center;
}
.times {
td {
padding: 0.4em;
}
tr {
&.project {
border-top: solid #71717185 1px;
td {
text-align: right;
&:first-child {
text-align: left;
}
&:nth-child(2) {
text-align: center;
}
}
}
&.parent {
border-top: var(--border);
td {
font-weight: bold;
}
}
}
}
.separator {
height: 2em;
}
}
.controls {
display: flex;
gap: 0.8em;
align-items: center;
margin-bottom: 1em;
button {
background-color: var(--dark3);
color: var(--light1);
padding: 0.4em 0.8em;
border: none;
cursor: pointer;
&:hover {
background-color: var(--dark4);
}
}
}

272
dispatcher/static/table.js Normal file
View File

@ -0,0 +1,272 @@
let table
let prevMonthBtn, nextMonthBtn, month
let curMonth = new Date().getMonth()
const DAYS_SHORT = [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun"
]
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
]
const SEC_MS = 1000
const MIN_MS = SEC_MS * 60
const HOUR_MS = MIN_MS * 60
const DAY_MS = HOUR_MS * 24
//////////////////////////////////////////////////////////
// //
// Taken from: https://weeknumber.com/how-to/javascript //
// //
//////////////////////////////////////////////////////////
Date.prototype.getWeek = function() {
let date = new Date(this.getTime())
date.setHours(0, 0, 0, 0)
// Thursday in current week decides the year.
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7)
// January 4 is always in week 1.
let week1 = new Date(date.getFullYear(), 0, 4)
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7)
}
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 getTemplate(cls) {
let elmt = document.querySelector(".template." + cls).cloneNode(true)
elmt.classList.remove("template")
return elmt
}
class Table {
constructor(elmt) {
this.table = elmt
this.clockings = elmt.querySelector(".clockings")
this.times = elmt.querySelector(".times")
this.weekNames = elmt.querySelector(".week-names")
this.dayNames = elmt.querySelector(".day-names")
this.dayDates = elmt.querySelector(".day-dates")
this.columns = elmt.querySelector(".columns")
this.timeInputTemplate = getTemplate("time-input")
this.nDays = 0
let today = new Date()
this.curMonth = today.getMonth()
this.curYear = today.getFullYear()
this.startDate = today
this.endDate = today
this.update()
}
update() {
let year = this.curYear.toString().padStart(4, "0")
let month = (this.curMonth + 1).toString().padStart(2, "0")
let date = new Date(`${year}-${month}-01`)
let txt = MONTHS[this.curMonth]
if (new Date().getFullYear() !== this.curYear) {
txt += " " + year
}
document.getElementById("month").innerText = txt
this.clear()
this.addMonth(date)
this.fetchData()
}
prevMonth() {
this.curMonth--
if (this.curMonth < 0) {
this.curMonth += 12
this.curYear -= 1
}
this.update()
}
nextMonth() {
this.curMonth++
if (this.curMonth >= 12) {
this.curMonth -= 12
this.curYear += 1
}
this.update()
}
clear() {
this.columns.innerHTML = ""
this.clockings.querySelectorAll(".dates th").forEach(c => c.remove())
this.clockings.querySelectorAll(".clocking td").forEach(c => c.remove())
this.weekNames.querySelectorAll("td").forEach(c => c.remove())
this.dayNames.querySelectorAll("td").forEach(c => c.remove())
this.dayDates.innerHTML = ""
this.times.querySelectorAll("tr.project").forEach(r => r.remove())
this.nDays = 0
}
addClockings(date) {
let dates = this.clockings.querySelector(".dates")
let dateCell = document.createElement("th")
let weekDay = DAYS_SHORT[(date.getDay() + 1) % 7]
let monthDay = date.getDate().toString().padStart(2, "0")
dateCell.innerText = `${weekDay} ${monthDay}`
dates.appendChild(dateCell)
let clockings = this.clockings.querySelectorAll(".clocking")
clockings.forEach((clocking, i) => {
let cell = clocking.insertCell(this.nDays + 1)
if (i === 5) {
return
}
cell.replaceWith(this.timeInputTemplate.cloneNode(true))
})
}
addWeek(startDate, length=7) {
let weekNum = startDate.getWeek()
let weekName = this.weekNames.insertCell(-1)
weekName.innerText = `Week n° ${weekNum}`
weekName.colSpan = length
for (let i = 0; i < length; i++) {
let date = new Date(startDate.valueOf() + i * DAY_MS)
let dayPadded = date.getDate().toString().padStart(2, "0")
let monthPadded = (date.getMonth() + 1).toString().padStart(2, "0")
this.addClockings(date)
let dayName = this.dayNames.insertCell(this.nDays + 2)
let dayDate = this.dayDates.insertCell(this.nDays)
let weekDay = (date.getDay() + 6) % 7
dayName.innerText = DAYS_SHORT[weekDay]
dayDate.innerText = `${dayPadded}/${monthPadded}`
let col = document.createElement("col")
col.classList.add(`day-${weekDay}`)
this.columns.appendChild(col)
this.nDays++
}
}
addProject(id, name, sagexNum, times, isParent=false) {
let row = this.times.insertRow(-1)
row.classList.add("project")
if (isParent) {
row.classList.add("parent")
}
row.dataset.id = id
row.insertCell(-1).innerText = name
row.insertCell(-1).innerText = sagexNum
times.forEach(time => {
let t = time.toString().split(".")
if (t.length > 1 ){
t[1] = t[1].slice(0, 2)
}
row.insertCell(-1).innerText = t.join(".")
})
}
addMonth(anchorDate) {
this.startDate = new Date(anchorDate)
this.startDate.setDate(1)
let startDay = (this.startDate.getDay() + 6) % 7
let length = 7 - startDay
this.addWeek(this.startDate, length)
let monday = new Date(this.startDate.valueOf() + length * DAY_MS)
let nextMonday
let startMonth = this.startDate.getMonth()
while (monday.getMonth() === startMonth) {
nextMonday = new Date(monday.valueOf() + 7 * DAY_MS)
let len = 7
if (nextMonday.getMonth() !== startMonth) {
len -= nextMonday.getDate() - 1
}
this.addWeek(monday, len)
monday = nextMonday
}
this.endDate = new Date(monday.valueOf() - monday.getDate() * DAY_MS)
}
fetchData() {
let startDate = formatDate(this.startDate)
let endDate = formatDate(this.endDate)
fetch(`/table/${startDate}/${endDate}/`).then(res => {
if (res.ok) {
return res.json()
}
}).then(res => {
if (res && res.status === "success") {
this.displayData(res.data)
}
})
}
displayData(data) {
data.parents.forEach(parent => {
this.addProject(
parent.id,
parent.name,
parent.project_num,
Array(this.nDays).fill(""),
true
)
parent.projects.forEach(project => {
let durations = Array(this.nDays).fill("")
project.tasks.forEach(task => {
let date = new Date(task.date)
let i = Math.floor((date.valueOf() - this.startDate.valueOf()) / DAY_MS)
durations[i] = task.duration / 60
})
this.addProject(
project.id,
project.name,
"",
durations
)
})
})
}
}
window.addEventListener("load", () => {
prevMonthBtn = document.getElementById("prev-month")
nextMonthBtn = document.getElementById("next-month")
month = document.getElementById("month")
table = new Table(document.getElementById("table"))
prevMonthBtn.addEventListener("click", () => table.prevMonth())
nextMonthBtn.addEventListener("click", () => table.nextMonth())
})

View File

@ -1,6 +1,6 @@
import datetime
from datetime import timedelta from datetime import timedelta
from django.core.files.uploadedfile import UploadedFile
from django.db.models import Sum, Q, QuerySet 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
@ -9,7 +9,7 @@ from django.views.decorators.http import require_POST
from dispatcher.core import import_tasks from dispatcher.core import import_tasks
from dispatcher.forms import ProjectForm, ImportForm from dispatcher.forms import ProjectForm, ImportForm
from dispatcher.models import Project, Parent, Task from dispatcher.models import Project, Parent
def dashboard_view(request): def dashboard_view(request):
@ -35,6 +35,9 @@ def project_view(request, id):
context["form"] = ProjectForm(instance=project) context["form"] = ProjectForm(instance=project)
return render(request, "project.html", context) return render(request, "project.html", context)
def table_view(request):
return render(request, "table.html")
@require_POST @require_POST
def set_parent(request, id): def set_parent(request, id):
project = get_object_or_404(Project, id=id) project = get_object_or_404(Project, id=id)
@ -84,7 +87,7 @@ def get_stats_by_month(request, year: int, month: int):
) )
return JsonResponse({"status": "success", "data": format_stats(parents, projects)}) return JsonResponse({"status": "success", "data": format_stats(parents, projects)})
def get_stats_between(request, start_date, end_date): def get_stats_between(request, start_date: datetime.date, end_date: datetime.date):
parents = Parent.objects.annotate( parents = Parent.objects.annotate(
total_duration=Sum( total_duration=Sum(
"project__task__duration", "project__task__duration",
@ -131,3 +134,36 @@ class ParentsView(generic.ListView):
model = Parent model = Parent
template_name = "parents.html" template_name = "parents.html"
context_object_name = "elements" context_object_name = "elements"
def get_table_data(request, start_date: datetime.date, end_date: datetime.date):
end_date = end_date + timedelta(days=1)
parents = Parent.objects.all().order_by("id")
data = {
"parents": [
{
"id": parent.id,
"name": parent.name,
"project_num": parent.project_num,
"projects": [
{
"id": project.id,
"name": project.name,
"tasks": [
{
"date": task["date"],
"duration": task["duration"]
}
for task in project.task_set.filter(
date__gte=start_date,
date__lt=end_date
).values("date").order_by("date").annotate(duration=Sum("duration"))
]
}
for project in parent.project_set.order_by("id")
]
}
for parent in parents
]
}
return JsonResponse({"status": "success", "data": data})

65
templates/table.html Normal file
View File

@ -0,0 +1,65 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Table{% endblock %}
{% block head %}
<link rel="stylesheet" href="{% static "table.css" %}">
<script src="{% static "table.js" %}"></script>
{% endblock %}
{% block main %}
<div class="controls">
<button id="prev-month"><</button>
<div id="month">Month</div>
<button id="next-month">></button>
</div>
<table class="template">
<td class="template time-input"><input type="time"></td>
</table>
<table id="table">
<colgroup class="headers">
<col class="name">
<col class="num">
</colgroup>
<colgroup class="columns"></colgroup>
<colgroup class="tailers">
<col span="4">
</colgroup>
<tbody class="clockings">
<tr class="dates">
<td colspan="2"></td>
</tr>
<tr class="clocking">
<th colspan="2">Check-IN AM</th>
<th rowspan="6" class="total-header">TOTAL</th>
<th rowspan="6" class="total-header">% working time</th>
<th rowspan="6" class="total-header">% imputed time</th>
<th rowspan="6" class="total-header">time on sagex</th>
</tr>
<tr class="clocking">
<th colspan="2">Check-OUT AM</th>
</tr>
<tr class="clocking">
<th colspan="2">Check-IN PM</th>
</tr>
<tr class="clocking">
<th colspan="2">Check-OUT PM</th>
</tr>
<tr class="clocking">
<th colspan="2">Remote</th>
</tr>
<tr class="clocking">
<th colspan="2">TOTAL</th>
</tr>
</tbody>
<tr class="separator"></tr>
<tbody class="times">
<tr class="week-names">
<th colspan="2"></th>
</tr>
<tr class="day-names">
<th rowspan="2">Name</th>
<th rowspan="2">N° sagex</th>
</tr>
<tr class="day-dates"></tr>
</tbody>
</table>
{% endblock %}