added table view
This commit is contained in:
parent
01db4285c2
commit
0380d18cd7
@ -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!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
ALLOWED_HOSTS = ["localhost", "192.168.2.68"]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
@ -27,6 +27,8 @@ urlpatterns = [
|
||||
path("import/", views.import_view, name="import"),
|
||||
path("projects/", views.projects_view, name="projects"),
|
||||
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>/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"),
|
||||
|
@ -10,6 +10,7 @@ def navbar_links(request):
|
||||
return {
|
||||
"nav_links": [
|
||||
{'view': 'dashboard', 'label': 'Dashboard'},
|
||||
{'view': 'table', 'label': 'Table'},
|
||||
{'view': 'projects', 'label': 'Projects'},
|
||||
{'view': 'parents', 'label': 'Parents'},
|
||||
{'view': 'import', 'label': 'Import'},
|
||||
|
140
dispatcher/static/table.css
Normal file
140
dispatcher/static/table.css
Normal 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
272
dispatcher/static/table.js
Normal 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())
|
||||
})
|
@ -1,6 +1,6 @@
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.db.models import Sum, Q, QuerySet
|
||||
from django.http import JsonResponse
|
||||
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.forms import ProjectForm, ImportForm
|
||||
from dispatcher.models import Project, Parent, Task
|
||||
from dispatcher.models import Project, Parent
|
||||
|
||||
|
||||
def dashboard_view(request):
|
||||
@ -35,6 +35,9 @@ def project_view(request, id):
|
||||
context["form"] = ProjectForm(instance=project)
|
||||
return render(request, "project.html", context)
|
||||
|
||||
def table_view(request):
|
||||
return render(request, "table.html")
|
||||
|
||||
@require_POST
|
||||
def set_parent(request, 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)})
|
||||
|
||||
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(
|
||||
total_duration=Sum(
|
||||
"project__task__duration",
|
||||
@ -131,3 +134,36 @@ class ParentsView(generic.ListView):
|
||||
model = Parent
|
||||
template_name = "parents.html"
|
||||
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
65
templates/table.html
Normal 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 %}
|
Loading…
x
Reference in New Issue
Block a user