| @ -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 | ||||
|  | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
							
								
								
									
										64
									
								
								games/templates/stats.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								games/templates/stats.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block title %}{{ title }}{% endblock title %} | ||||
|  | ||||
| {% load static %} | ||||
|  | ||||
| {% block content %} | ||||
|     <div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto"> | ||||
|         <h1 class="text-5xl text-center my-6">Stats for {{ year }}</h1> | ||||
|         <table class="responsive-table"> | ||||
|             <thead> | ||||
|                 <tr> | ||||
|                     <th class="px-2 sm:px-4 md:px-6 md:py-2">Total hours</th> | ||||
|                     <th class="px-2 sm:px-4 md:px-6 md:py-2">Total games</th> | ||||
|                     <th class="px-2 sm:px-4 md:px-6 md:py-2">Total 2023 games</th> | ||||
|                 </tr> | ||||
|             <tbody> | ||||
|                 <tr> | ||||
|                     <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_hours }}</td> | ||||
|                     <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_games }}</td> | ||||
|                     <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_2023_games }}</td> | ||||
|                 </tr> | ||||
|             </tbody> | ||||
|         </table> | ||||
|         <h1 class="text-5xl text-center my-6">Top games by playtime</h1> | ||||
|         <table class="responsive-table"> | ||||
|             <thead> | ||||
|                 <tr> | ||||
|                     <th class="px-2 sm:px-4 md:px-6 md:py-2">Name</th> | ||||
|                     <th class="px-2 sm:px-4 md:px-6 md:py-2">Playtime (hours)</th> | ||||
|                 </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|                 {% for purchase in top_10_by_playtime %} | ||||
|                 <tr> | ||||
|                     <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"> | ||||
|                         <a href="{% url 'view_game' purchase.edition.game.id  %}">{{ purchase.edition.name }} | ||||
|  | ||||
|                         </a> | ||||
|                     </td> | ||||
|                     <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.formatted_playtime }}</td> | ||||
|                 </tr> | ||||
|                 {% endfor %} | ||||
|             </tbody> | ||||
|         </table> | ||||
|         <h1 class="text-5xl text-center my-6">Platforms by playtime</h1> | ||||
|         <table class="responsive-table"> | ||||
|             <thead> | ||||
|                 <tr> | ||||
|                     <th class="px-2 sm:px-4 md:px-6 md:py-2">Platform</th> | ||||
|                     <th class="px-2 sm:px-4 md:px-6 md:py-2">Playtime (hours)</th> | ||||
|                 </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|                 {% for item in total_playtime_per_platform %} | ||||
|                 <tr> | ||||
|                     <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ item.platform_name }}                    </td> | ||||
|                     <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ item.formatted_playtime }}</td> | ||||
|                 </tr> | ||||
|                 {% endfor %} | ||||
|             </tbody> | ||||
|         </table> | ||||
|     </div> | ||||
| {% endblock content %} | ||||
| @ -73,4 +73,9 @@ urlpatterns = [ | ||||
|         {"filter": "ownership_type"}, | ||||
|         name="list_sessions_by_ownership_type", | ||||
|     ), | ||||
|     path( | ||||
|         "stats/<int:year>", | ||||
|         views.stats, | ||||
|         name="stats_by_year", | ||||
|     ), | ||||
| ] | ||||
|  | ||||
| @ -229,6 +229,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() | ||||
|  | ||||
		Reference in New Issue
	
	Block a user