Checkpoint: management app redesign, onboarding/signup workflow, and events calendar backend

Large uncommitted body of work accumulated across sessions on this branch --
committing as a checkpoint so it's tracked and future worktree-isolated agents
see the real codebase instead of a stale ancestor commit. Covers the
management app's dedicated Tailwind theme and templates, the club onboarding
requirement/signup workflow (club/services/onboarding.py, requirement/status
models, sign-up dashboard), fee/status auto-activation decoupling, referee
management, and the new events calendar grid service layer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-19 23:34:43 +02:00
parent bff685966d
commit adf1120358
157 changed files with 20342 additions and 4008 deletions

View File

@@ -104,6 +104,42 @@ def clubs_with_health(queryset=None, today=None, now=None):
)
#: Risk tiers for the dashboard's "Club health" table, high risk first. Derived from signals
#: `clubs_with_health` already annotates -- no separate query, and nothing here is invented:
#: a club with no season covering today cannot take a signup, and dues past their grace date
#: are exactly what the archive job is about to act on.
RISK_HIGH, RISK_WATCH, RISK_OK = "high", "watch", "ok"
def club_risk(club, today):
"""The risk tier, plus a human reason naming exactly which signal tripped it -- so the
dashboard can show *why*, not just a colour. Checked in the same order as the tier
logic below: the first matching condition is the one reported."""
if not club.has_season:
return RISK_HIGH, _("No season covers today")
if club.dues_grace_until is not None and club.dues_grace_until < today:
return RISK_HIGH, _("Dues overdue past grace")
if not club.upcoming_events:
return RISK_WATCH, _("No events in the next 30 days")
if club.dues_owed:
return RISK_WATCH, _("Dues outstanding")
return RISK_OK, _("Nothing needs attention")
def clubs_by_risk(queryset=None, today=None):
"""`clubs_with_health`, ordered highest risk first -- the dashboard's Club health table
is "sorted by risk" per the design, and risk is exactly the thing that table is for."""
today = today or timezone.localdate()
order = {RISK_HIGH: 0, RISK_WATCH: 1, RISK_OK: 2}
clubs = list(clubs_with_health(queryset, today=today))
for club in clubs:
club.risk, club.risk_reason = club_risk(club, today)
clubs.sort(key=lambda club: order[club.risk])
return clubs
def platform_totals():
return {
"clubs": Club.objects.active().count(),