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 %}
+
+
+
+
+
+
+ {{ head.traits }} | Personen |
+
+ {% for stat in trait.by_type %}
+
+ | {{ trait.labels|get_item:stat.trait }} |
+ {{ stat.count }} |
+
+ {% endfor %}
+
+
+
+
+
+
+ {{ head.booking_types }} | Buchungen | Dauer (ø) | Gesamt |
+
+ {% for stat in booking.by_type %}
+
+ | {{ booking.labels|get_item:stat.type }} |
+ {{ stat.count }} |
+ {{ stat.sum|divide:stat.count|floatformat:1 }} Min. |
+ {{ stat.sum }} Min. |
+
+ {% endfor %}
+
+
+
+
+
+{{ head.bookings }}
+
+ {% for year, stat in booking.by_month.items reversed %}
+
+ {{ year }} |
+ ∑ | Jan | Feb | Mär | Apr | Mai | Jun | Jul | Aug | Sep | Okt | Nov | Dez |
+
+
+ | Buchungen |
+ {% for sum, count in stat %}{{ count }} | {% endfor %}
+
+
+ | Minuten (ø) |
+ {% for sum, count in stat %}{{ sum|divide:count|floatformat:1 }} | {% endfor %}
+
+
+ | Minuten |
+ {% for sum, count in stat %}{{ sum }} | {% endfor %}
+
+
+ {% endfor %}
+
+
+{% 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')