"""Platform and per-club statistics. ``club_statistics`` returns a list of stat *groups*, so growing the model later means adding an entry here and nothing else. ``clubs_with_totals`` annotates in a single query — the club list must not fan out into N+1. """ from decimal import Decimal from django.db.models import Count, Q, Sum from django.utils import timezone from club.models import Club, ClubMembership, ClubRole, Season from events.models import Event from members.models import Member from shop.models import Cart, Order from teams.models import StaffAssignment, Team, TeamMembership ZERO = Decimal("0.00") PAID_STATUSES = (Order.OrderStatus.PAID, Order.OrderStatus.DELIVERED) OWED_STATUSES = (Order.OrderStatus.PENDING, Order.OrderStatus.PARTIALLY_PAID) def clubs_with_totals(queryset=None): """Clubs annotated with headline counts (one query, no N+1).""" clubs = Club.objects.all() if queryset is None else queryset return clubs.annotate( member_count=Count("clubmemberships__member", distinct=True), team_count=Count("teams", distinct=True), event_count=Count("events", distinct=True), admin_count=Count("clubroles", filter=Q(clubroles__role=ClubRole.Roles.ADMIN), distinct=True), ) def platform_totals(): return { "clubs": Club.objects.active().count(), "archived_clubs": Club.objects.archived().count(), "members": Member.objects.count(), "admins": ClubRole.objects.filter(role=ClubRole.Roles.ADMIN).count(), } def _money(queryset): return queryset.aggregate(total=Sum("total"))["total"] or ZERO def club_statistics(club): """Stat groups for one club. Add new groups here as the domain grows.""" season = Season.covering(club, timezone.localdate()) now = timezone.now() memberships = ClubMembership.objects.filter(club=club) events = Event.objects.filter(club=club) orders = Order.objects.filter(club=club) return [ { "title": "Members", "icon": "users", "stats": [ ("Members", memberships.values("member").distinct().count()), ("Active this season", memberships.filter(season=season, status=ClubMembership.StatusChoices.ACTIVE).count() if season else 0), ("Pending", memberships.filter(status=ClubMembership.StatusChoices.PENDING).count()), ("Lapsed", memberships.filter(status=ClubMembership.StatusChoices.LAPSED).count()), ], }, { "title": "Teams & staff", "icon": "shield", "stats": [ ("Teams", Team.objects.filter(club=club).count()), ("Players this season", TeamMembership.objects.filter(team__club=club, season=season).count() if season else 0), ("Staff this season", StaffAssignment.objects.filter(team__club=club, season=season).count() if season else 0), ], }, { "title": "Events", "icon": "calendar-days", "stats": [ ("Upcoming", events.filter(start__gte=now).count()), ("This season", events.filter(season=season).count() if season else 0), ], }, { "title": "Shop", "icon": "shopping-cart", "stats": [ ("Orders", orders.count()), ("Revenue", _money(orders.filter(status__in=PAID_STATUSES))), ("Outstanding", _money(orders.filter(status__in=OWED_STATUSES))), ("Open carts", Cart.objects.filter(club=club, status=Cart.CartStatus.OPEN).count()), ], }, ]