feat: add person stats to dashboard
This commit is contained in:
@@ -269,6 +269,13 @@ table.clickable tbody>tr:hover {
|
||||
.text-green {
|
||||
color: #23be58;
|
||||
}
|
||||
.text-gray {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
#by_month_history td:first-of-type {
|
||||
text-decoration: dotted underline;
|
||||
}
|
||||
|
||||
#profileMenu {
|
||||
position: absolute;
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>Personen</h3>
|
||||
<p>
|
||||
Personen gesamt: <b>{{person.by_count.total}}</b>
|
||||
(davon haben <b>{{person.by_count.single_visit}}</b> die Werkstatt nur 1x besucht
|
||||
und <b>{{person.by_count.long_not_seen}}</b> waren seit über einem Jahr nicht mehr da)
|
||||
</p>
|
||||
<p>
|
||||
<b>{{person.by_count.no_booking}}</b> haben noch nie Werkstattzeit gebucht.
|
||||
<b>{{person.by_count.no_course}}</b> haben nie eine Einweisung gemacht.
|
||||
</p>
|
||||
|
||||
<h3>Nach Art</h3>
|
||||
<div class="div-by-side">
|
||||
|
||||
<div>
|
||||
@@ -42,23 +54,41 @@
|
||||
</div>
|
||||
|
||||
<h3>{{ head.bookings }}</h3>
|
||||
<table class="table table-sm">
|
||||
{% for year, stat in booking.by_month.items reversed %}
|
||||
<table id="by_month_history" class="table table-sm">
|
||||
{% for year, stat in by_month %}
|
||||
<tr class="thead">
|
||||
<th><h4 class="mb-0">{{ year }}</h4></th>
|
||||
<th>∑</th><th>Jan</th><th>Feb</th><th>Mär</th><th>Apr</th><th>Mai</th><th>Jun</th><th>Jul</th><th>Aug</th><th>Sep</th><th>Okt</th><th>Nov</th><th>Dez</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Personen</th>
|
||||
{% for sum, count, people in stat %}
|
||||
<td{% if not people %} class="text-gray"{% endif %}>
|
||||
{% if people and not forloop.first %}
|
||||
<a href="{% url 'person:list' %}?created.y={{year}}&created.m={{forloop.counter0}}">{{ people }}</a>
|
||||
{% else %}
|
||||
{{ people }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Buchungen</th>
|
||||
{% for sum, count in stat %}<td>{{ count }}</td>{% endfor %}
|
||||
{% for sum, count, people in stat %}
|
||||
<td{% if not count %} class="text-gray"{% endif %}>{{ count }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Minuten (ø)</th>
|
||||
{% for sum, count in stat %}<td>{{ sum|divide:count|floatformat:1 }}</td>{% endfor %}
|
||||
{% for sum, count, people in stat %}
|
||||
<td{% if not sum %} class="text-gray"{% endif %}>{{ sum|divide:count|floatformat:1 }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Minuten</th>
|
||||
{% for sum, count in stat %}<td>{{ sum }}</td>{% endfor %}
|
||||
{% for sum, count, people in stat %}
|
||||
<td{% if not sum %} class="text-gray"{% endif %}>{{ sum }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr class="blank"></tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from django.db.models import Q, F, Sum, Count, ExpressionWrapper, IntegerField
|
||||
from django.db.models.functions import Substr
|
||||
from django.db.models.lookups import LessThan
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from app.base.models import Booking, BookingType, Trait, TraitMapping
|
||||
from app.base.models import (
|
||||
Booking, BookingType, CourseVisit, Person, Trait, TraitMapping
|
||||
)
|
||||
from app.base.views.login import LoginRequired
|
||||
from app.base.views.model_views.base import ViewOptions
|
||||
|
||||
@@ -23,30 +26,20 @@ class DashboardView(DashboardOptions, TemplateView):
|
||||
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,
|
||||
'by_type': stats_for_booking('type'),
|
||||
}
|
||||
context['trait'] = {
|
||||
'labels': trait_types,
|
||||
'by_type': stats_for_traits(),
|
||||
}
|
||||
context['person'] = {
|
||||
'by_count': stats_for_person_count(),
|
||||
}
|
||||
context['by_month'] = stats_by_month()
|
||||
context['head'] = {
|
||||
'person': Person._meta.verbose_name_plural,
|
||||
'bookings': Booking._meta.verbose_name_plural,
|
||||
'booking_types': BookingType._meta.verbose_name_plural,
|
||||
'traits': Trait._meta.verbose_name_plural,
|
||||
@@ -55,9 +48,59 @@ class DashboardView(DashboardOptions, TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
def stats_for_person_count():
|
||||
today = date.today()
|
||||
last_year = today.replace(year=today.year - 1)
|
||||
|
||||
stats = Person.objects.aggregate(
|
||||
total=Count(1),
|
||||
single_visit=Sum(LessThan(
|
||||
# TODO: other DBs dont use microseconds
|
||||
ExpressionWrapper(F('last_visit') - F('created'),
|
||||
output_field=IntegerField()),
|
||||
3 * 86_400_000_000)), # 3 days
|
||||
long_not_seen=Sum(Q(last_visit__lt=last_year)),
|
||||
)
|
||||
|
||||
stats['no_booking'] = stats['total'] - Booking.objects.aggregate(
|
||||
c=Count('user', distinct=True))['c']
|
||||
stats['no_course'] = stats['total'] - CourseVisit.objects.aggregate(
|
||||
c=Count('participant', distinct=True))['c']
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def stats_by_month():
|
||||
stats = {}
|
||||
|
||||
for stat in stats_for_person_by_month():
|
||||
year, month = stat['month'].split('-')
|
||||
if year not in stats:
|
||||
stats[year] = [[0, 0, 0] for x in range(13)]
|
||||
stats[year][0][2] += stat['count']
|
||||
stats[year][int(month)][2] += stat['count']
|
||||
|
||||
for stat in stats_for_booking('month'):
|
||||
year, month = stat['month'].split('-')
|
||||
if year not in stats:
|
||||
stats[year] = [[0, 0, 0] for x in range(13)]
|
||||
stats[year][0][0] += stat['sum']
|
||||
stats[year][0][1] += stat['count']
|
||||
stats[year][int(month)][0] += stat['sum']
|
||||
stats[year][int(month)][1] += stat['count']
|
||||
|
||||
return sorted(stats.items(), reverse=True)
|
||||
|
||||
|
||||
def stats_for_person_by_month():
|
||||
return Person.objects.values(
|
||||
month=Substr('created', 1, 7), # YYYY-MM
|
||||
).annotate(count=Count(1)).order_by('month')
|
||||
|
||||
|
||||
def stats_for_booking(groupby: str):
|
||||
_Q = Booking.objects.filter(Q(end_time__isnull=False)).annotate(
|
||||
# for whatever reason django uses nano seconds
|
||||
# TODO: other DBs dont use microseconds
|
||||
diff=ExpressionWrapper(
|
||||
(F('end_time') - F('begin_time')) / 60_000_000,
|
||||
output_field=IntegerField()),
|
||||
|
||||
@@ -33,6 +33,9 @@ class PersonOptions(ViewOptions[Person], LoginRequired):
|
||||
list_filter = {
|
||||
'trait': 'traits__pk',
|
||||
'course': 'courses__pk',
|
||||
'created': 'created',
|
||||
'created.y': 'created__year',
|
||||
'created.m': 'created__month',
|
||||
}
|
||||
list_columns = ['display_name', 'birth_date', 'last_visit']
|
||||
list_render = {
|
||||
|
||||
Reference in New Issue
Block a user