Add referee assignment/eligibility system, team bulk-add, member Groups, and referee management dashboard with PDF export

Builds the referee workflow end to end: club-defined RefereeLevel/RefereeProfile
eligibility tied to teams, EventReferee assignment (member or external, with
fee/km payment tracking), an admin dashboard with KPI tiles, date-grouped game
tiles and range filters, and a downloadable payment form PDF modeled on the
club's existing paper document (using Club.legal_name when set). Also lands
team roster bulk-add, member mass-upload with family linking, and the
members.Group model, developed alongside this work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 21:39:19 +02:00
parent 86e28c317f
commit 309bd4d83e
54 changed files with 4574 additions and 88 deletions

View File

@@ -3,7 +3,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from rosterchief.base import UUIDModel
from rosterchief.base import ClubScopedModel, UUIDModel
class Family(UUIDModel):
@@ -99,3 +99,42 @@ class FamilyMembership(models.Model):
def __str__(self):
return f"{self.family} - {self.member} ({self.get_role_display()})"
class Group(ClubScopedModel):
"""An arbitrary named collection of members -- deliberately generic, not
team-shaped and not aware of any specific use: "all coaches", "all team
managers", an ad-hoc committee. A Team's roster is a separate, more
specific concept (teams.TeamMembership); nothing here assumes team
semantics. Referee eligibility (who can ref which team) is a member-level
fact (teams.RefereeProfile), not a Group concern -- Group carries no
referee-specific knowledge at all."""
name = models.CharField(_("name"), max_length=255)
class Meta:
verbose_name = _("group")
verbose_name_plural = _("groups")
ordering = ["name"]
constraints = [
models.UniqueConstraint(fields=["club", "name"], name="unique_group_name_per_club"),
]
def __str__(self):
return self.name
class GroupMembership(UUIDModel):
group = models.ForeignKey(Group, on_delete=models.CASCADE, related_name="memberships", verbose_name=_("group"))
member = models.ForeignKey(Member, on_delete=models.CASCADE, related_name="group_memberships", verbose_name=_("member"))
class Meta:
verbose_name = _("group membership")
verbose_name_plural = _("group memberships")
ordering = ["group", "member__last_name", "member__first_name"]
constraints = [
models.UniqueConstraint(fields=["group", "member"], name="unique_member_per_group"),
]
def __str__(self):
return f"{self.group} - {self.member}"