From 46479ce6d8aabb8e5cb87e058b8e1a4685002784 Mon Sep 17 00:00:00 2001 From: relikd Date: Fri, 2 Jun 2023 18:21:40 +0200 Subject: [PATCH] feat: initial dashboard --- app/base/static/css/style.css | 15 +++++- app/base/templates/dashboard.html | 63 +++++++++++++++++++++++ app/base/templatetags/utils.py | 5 ++ app/base/urls.py | 2 + app/base/views/dashboard.py | 83 +++++++++++++++++++++++++++++++ 5 files changed, 167 insertions(+), 1 deletion(-) create mode 100755 app/base/templates/dashboard.html create mode 100755 app/base/views/dashboard.py diff --git a/app/base/static/css/style.css b/app/base/static/css/style.css index 8c3d437..5ea02b1 100755 --- a/app/base/static/css/style.css +++ b/app/base/static/css/style.css @@ -133,12 +133,15 @@ table th { background-color: var(--bg-dim); } -thead>tr { +thead>tr, tr.thead { border: 1px solid #DDD; position: sticky; top: 4rem; z-index: 98; } +tr.thead>th { + vertical-align: bottom; +} /* .detail-table { width: max-content; @@ -161,6 +164,10 @@ tbody>tr { border: 1px solid #DDD; } +tr.blank { + height: 3ex; +} + td.table-actions { padding: 0; text-align: center; @@ -295,6 +302,12 @@ table.clickable tbody>tr:hover { text-align: center; } +.div-by-side>div { + display: inline-block; + vertical-align: top; + margin-right: 1em; +} + #page-content-wrapper { margin-bottom: 15px; } diff --git a/app/base/templates/dashboard.html b/app/base/templates/dashboard.html new file mode 100755 index 0000000..d4e82c5 --- /dev/null +++ b/app/base/templates/dashboard.html @@ -0,0 +1,63 @@ +{% extends 'base.html' %} +{% load utils %} + +{% block content %} + +
+ +
+ + + + + {% for stat in trait.by_type %} + + + + + {% endfor %} +

{{ head.traits }}

Personen
{{ trait.labels|get_item:stat.trait }}{{ stat.count }}
+
+ +
+ + + + + {% for stat in booking.by_type %} + + + + + + + {% endfor %} +

{{ head.booking_types }}

BuchungenDauer (ø)Gesamt
{{ booking.labels|get_item:stat.type }}{{ stat.count }}{{ stat.sum|divide:stat.count|floatformat:1 }} Min.{{ stat.sum }} Min.
+
+ +
+ +

{{ head.bookings }}

+ + {% for year, stat in booking.by_month.items reversed %} + + + + + + + {% for sum, count in stat %}{% endfor %} + + + + {% for sum, count in stat %}{% endfor %} + + + + {% for sum, count in stat %}{% endfor %} + + + {% endfor %} +

{{ year }}

JanFebMärAprMaiJunJulAugSepOktNovDez
Buchungen{{ count }}
Minuten (ø){{ sum|divide:count|floatformat:1 }}
Minuten{{ sum }}
+ +{% endblock content %} diff --git a/app/base/templatetags/utils.py b/app/base/templatetags/utils.py index d2fc7b8..68c91dc 100755 --- a/app/base/templatetags/utils.py +++ b/app/base/templatetags/utils.py @@ -25,6 +25,11 @@ def invert(number): return - number +@register.filter +def divide(number: 'int|float', other: 'int|float'): + return number / (other or 1) + + @register.filter def get_item(dictionary, key): return dictionary and dictionary.get(key) diff --git a/app/base/urls.py b/app/base/urls.py index 858aa14..e09a971 100755 --- a/app/base/urls.py +++ b/app/base/urls.py @@ -5,6 +5,7 @@ from django.urls import include, path from django.views.generic import RedirectView from app.base.views.settings import SettingsView +from app.base.views.dashboard import DashboardView from app.base.views.trigger.toggle_checkin import ToggleCheckinView from app.base.views.trigger.update_note import UpdateNoteView @@ -14,6 +15,7 @@ urlpatterns = [ path('login/', LoginView.as_view(), name='login'), path('logout/', LogoutView.as_view(next_page='login'), name='logout'), path('settings/', SettingsView.as_view(), name='settings'), + path('dashboard/', DashboardView.as_view(), name='dashboard'), path('users//toggle-checkin/', ToggleCheckinView.as_view(), name='toggle-checkin'), path('users//update-note/', diff --git a/app/base/views/dashboard.py b/app/base/views/dashboard.py new file mode 100755 index 0000000..bbb6625 --- /dev/null +++ b/app/base/views/dashboard.py @@ -0,0 +1,83 @@ +from django.db.models import Q, F, Sum, Count, ExpressionWrapper, IntegerField +from django.db.models.functions import Substr +from django.views.generic import TemplateView + +from app.base.models import Booking, BookingType +from app.base.models.trait import Trait +from app.base.models.trait_mapping import TraitMapping +from app.base.views.login import LoginRequired +from app.base.views.model_views.base import ViewOptions + +from datetime import date +from typing import Any + + +class DashboardOptions(ViewOptions, LoginRequired): + icon = 'chart-line' + title = 'Dashboard' + + +class DashboardView(DashboardOptions, TemplateView): + template_name = 'dashboard.html' + + def get_context_data(self, **kwargs) -> 'dict[str, Any]': + context = super().get_context_data(**kwargs) + book_types = dict(BookingType.objects.values_list('key', 'label')) + trait_types = dict(Trait.objects.values_list('key', 'label')) + + # Booking stats + + book_by_type = stats_for_booking('type') + book_by_month = {} + + for stat in stats_for_booking('month'): + year, month = stat['month'].split('-') + if year not in book_by_month: + book_by_month[year] = [[0, 0] for x in range(13)] + book_by_month[year][0][0] += stat['sum'] + book_by_month[year][0][1] += stat['count'] + book_by_month[year][int(month)][0] += stat['sum'] + book_by_month[year][int(month)][1] += stat['count'] + + context['booking'] = { + 'labels': book_types, + 'by_type': book_by_type, + 'by_month': book_by_month, + } + context['trait'] = { + 'labels': trait_types, + 'by_type': stats_for_traits(), + } + context['head'] = { + 'bookings': Booking._meta.verbose_name_plural, + 'booking_types': BookingType._meta.verbose_name_plural, + 'traits': Trait._meta.verbose_name_plural, + } + + return context + + +def stats_for_booking(groupby: str): + _Q = Booking.objects.filter(Q(end_time__isnull=False)).annotate( + # for whatever reason django uses nano seconds + diff=ExpressionWrapper( + (F('end_time') - F('begin_time')) / 60_000_000, + output_field=IntegerField()), + ).filter(diff__lt=1440) # filter 24+ hours. Probably wrong checkout + + if groupby == 'month': + _Q = _Q.annotate(month=Substr('begin_time', 1, 7)) # YYYY-MM + + return _Q.values(groupby).annotate( + sum=Sum('diff', output_field=IntegerField()), + count=Count(1), + ).values(groupby, 'sum', 'count').order_by(groupby) + + +def stats_for_traits(): + return TraitMapping.objects.filter( + Q(valid_from__lte=date.today()), + Q(valid_until=None) | Q(valid_until__gte=date.today()), + ).values('trait').annotate( + count=Count(1), + ).values('trait', 'count').order_by('trait')