diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2d25d71..7eabf55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
## Unreleased
+### New
+* Add yearly stats page (https://git.kucharczyk.xyz/lukas/timetracker/issues/15)
+
### Enhancements
* Add a button to start session from game overview
diff --git a/games/static/base.css b/games/static/base.css
index 0c5f72d..c755028 100644
--- a/games/static/base.css
+++ b/games/static/base.css
@@ -791,6 +791,16 @@ select {
margin-bottom: 1rem;
}
+.my-5 {
+ margin-top: 1.25rem;
+ margin-bottom: 1.25rem;
+}
+
+.my-6 {
+ margin-top: 1.5rem;
+ margin-bottom: 1.5rem;
+}
+
.mb-1 {
margin-bottom: 0.25rem;
}
@@ -889,10 +899,6 @@ select {
animation: spin 1s linear infinite;
}
-.flex-row {
- flex-direction: row;
-}
-
.flex-col {
flex-direction: column;
}
@@ -905,10 +911,6 @@ select {
align-items: center;
}
-.items-baseline {
- align-items: baseline;
-}
-
.justify-center {
justify-content: center;
}
@@ -1017,6 +1019,11 @@ select {
line-height: 2.5rem;
}
+.text-5xl {
+ font-size: 3rem;
+ line-height: 1;
+}
+
.text-base {
font-size: 1rem;
line-height: 1.5rem;
@@ -1037,11 +1044,6 @@ select {
line-height: 1rem;
}
-.text-sm {
- font-size: 0.875rem;
- line-height: 1.25rem;
-}
-
.font-semibold {
font-weight: 600;
}
diff --git a/games/templates/stats.html b/games/templates/stats.html
new file mode 100644
index 0000000..9ff2c12
--- /dev/null
+++ b/games/templates/stats.html
@@ -0,0 +1,64 @@
+{% extends "base.html" %}
+
+{% block title %}{{ title }}{% endblock title %}
+
+{% load static %}
+
+{% block content %}
+
+
Stats for {{ year }}
+
+
+
+ Total hours |
+ Total games |
+ Total 2023 games |
+
+
+
+ {{ total_hours }} |
+ {{ total_games }} |
+ {{ total_2023_games }} |
+
+
+
+
Top games by playtime
+
+
+
+ Name |
+ Playtime (hours) |
+
+
+
+ {% for purchase in top_10_by_playtime %}
+
+
+ {{ purchase.edition.name }}
+
+
+ |
+ {{ purchase.formatted_playtime }} |
+
+ {% endfor %}
+
+
+
Platforms by playtime
+
+
+
+ Platform |
+ Playtime (hours) |
+
+
+
+ {% for item in total_playtime_per_platform %}
+
+ {{ item.platform_name }} |
+ {{ item.formatted_playtime }} |
+
+ {% endfor %}
+
+
+
+{% endblock content %}
diff --git a/games/urls.py b/games/urls.py
index ec081c2..0bec6ce 100644
--- a/games/urls.py
+++ b/games/urls.py
@@ -73,4 +73,9 @@ urlpatterns = [
{"filter": "ownership_type"},
name="list_sessions_by_ownership_type",
),
+ path(
+ "stats/",
+ views.stats,
+ name="stats_by_year",
+ ),
]
diff --git a/games/views.py b/games/views.py
index bf8f781..b6d123b 100644
--- a/games/views.py
+++ b/games/views.py
@@ -5,6 +5,7 @@ from common.time import now as now_with_tz
from common.time import format_duration
from django.conf import settings
from django.shortcuts import redirect, render
+from django.db.models import Sum, F
from .forms import (
GameForm,
@@ -229,6 +230,48 @@ def list_sessions(
return render(request, "list_sessions.html", context)
+def stats(request, year: int):
+ first_day_of_year = datetime(year, 1, 1)
+ year_sessions = Session.objects.filter(timestamp_start__gte=first_day_of_year)
+ year_purchases = Purchase.objects.filter(session__in=year_sessions).distinct()
+ year_purchases_with_playtime = year_purchases.annotate(
+ total_playtime=Sum(
+ F("session__duration_calculated") + F("session__duration_manual")
+ )
+ )
+ top_10_by_playtime = year_purchases_with_playtime.order_by("-total_playtime")[:10]
+ for purchase in top_10_by_playtime:
+ purchase.formatted_playtime = format_duration(purchase.total_playtime, "%2.0H")
+
+ total_playtime_per_platform = (
+ year_sessions.values("purchase__platform__name") # Group by platform name
+ .annotate(
+ total_playtime=Sum(F("duration_calculated") + F("duration_manual"))
+ ) # Sum the duration_calculated for each group
+ .annotate(platform_name=F("purchase__platform__name")) # Rename the field
+ .values(
+ "platform_name", "total_playtime"
+ ) # Select the renamed field and total_playtime
+ .order_by("-total_playtime") # Optional: Order by the renamed platform name
+ )
+ for item in total_playtime_per_platform:
+ item["formatted_playtime"] = format_duration(item["total_playtime"], "%2.0H")
+
+ context = {
+ "total_hours": format_duration(
+ year_sessions.total_duration_unformatted(), "%2.0H"
+ ),
+ "total_games": year_purchases.count(),
+ "total_2023_games": year_purchases.filter(edition__year_released=year).count(),
+ "top_10_by_playtime_formatted": top_10_by_playtime,
+ "top_10_by_playtime": top_10_by_playtime,
+ "year": year,
+ "total_playtime_per_platform": total_playtime_per_platform,
+ }
+
+ return render(request, "stats.html", context)
+
+
def add_purchase(request):
context = {}
now = datetime.now()