Show teams the account holder coaches on the Me page

New beyond the design mock: a "Teams I coach" card listing every
current-season StaffAssignment self.me holds (team + role), each row
linking straight into Coach mode for that team via ?team=<pk>, which
CoachScopeMixin already resolves and persists to the session. Shown for
any staffed team, not just ones self.me manages -- Coach mode's own
screens already render read-only for a non-management position, so
there's nothing to hide on this summary row.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-21 22:51:57 +02:00
parent 53b3c56594
commit c0c7ec9f1e
3 changed files with 61 additions and 10 deletions

View File

@@ -7,10 +7,10 @@
calls: no license/eligibility field backing "licence OK", so each row's
meta line is real roster data instead; "Household & contacts" has no
screen to lead to and is omitted, same for the mockup's "Coach mode"
promo (base.html's own precedent -- no Coach mode screens exist yet, so
it's never rendered, not even as a dead/inert link). "Payments & dues"
does lead somewhere (mobile:payments) and carries its "N OPEN" pill only
once open_dues_count is actually > 0.
promo. "Payments & dues" does lead somewhere (mobile:payments) and
carries its "N OPEN" pill only once open_dues_count is actually > 0.
"Teams I coach" is new, beyond the mockup -- one row per current-season
staff assignment, linking straight into Coach mode for that team.
The avatar/name/subtitle row lives in header_extra -- merged into the
shared navy app-header (base.html) rather than a separately-coloured
block of its own, matching the design canvas's own M5 markup.
@@ -66,6 +66,25 @@
</div>
</div>
{% if staff_assignments %}
<div>
<div class="mb-2 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "Teams I coach" %}</div>
<div class="m-card overflow-hidden">
{% for assignment in staff_assignments %}
{% if not forloop.first %}<div class="h-px bg-rule"></div>{% endif %}
<a class="flex items-center gap-3 p-3.5" href="{% url "mobile:coach_today" %}?team={{ assignment.team.pk }}">
<span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-ink font-display text-xs font-extrabold text-ice">{{ assignment.team.short_name|slice:":2" }}</span>
<div class="min-w-0 flex-1">
<div class="text-[15px] font-semibold text-ink">{{ assignment.team.name }}</div>
<div class="text-xs text-muted">{{ assignment.position }}</div>
</div>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" class="shrink-0 text-dim" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5l7 7-7 7"/></svg>
</a>
{% endfor %}
</div>
</div>
{% endif %}
<div class="m-card overflow-hidden">
<a class="flex items-center gap-3 p-3.5" href="{% url "mobile:edit_profile" me.pk %}">
<span class="flex-1 text-[15px] font-semibold text-ink">{% trans "Personal details" %}</span>

View File

@@ -1343,6 +1343,26 @@ class MeViewTests(TestCase):
self.assertContains(response, "Team manager U16")
def test_no_teams_card_without_a_staff_assignment(self):
self.client.force_login(self.user)
response = self._get()
self.assertNotContains(response, "Teams I coach")
def test_teams_card_lists_each_current_season_staff_assignment(self):
team = Team.objects.create(club=self.club, name="U16", short_name="U16")
position = Position.objects.create(club=self.club, name="Physio", short_name="PHY", staff_position=True, management_position=False)
StaffAssignment.objects.create(team=team, member=self.member, season=self.season, position=position)
self.client.force_login(self.user)
response = self._get()
self.assertContains(response, "Teams I coach")
self.assertContains(response, "U16")
self.assertContains(response, "Physio")
self.assertContains(response, reverse("mobile:coach_today") + "?team=" + str(team.pk))
def test_empty_account_gets_a_graceful_empty_state(self):
bare_user = User.objects.create_user(email="new@example.com", password="pw-secret-123")
self.client.force_login(bare_user)

View File

@@ -33,7 +33,7 @@ from members.models import FamilyMembership, Member
from members.views import ClubScopedPublicMixin
from news.models import News
from notifications.models import Notification
from teams.models import TeamMembership
from teams.models import StaffAssignment, TeamMembership
from .forms import MemberProfileForm
from .mixins import PersonScopeMixin
@@ -530,11 +530,18 @@ class MeView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
``self.me`` and into M7 (notifications).
The mockup's "Household & contacts" row and its "Coach mode" promo card
have nowhere to lead in this build (no dedicated screen, no Coach mode
screens at all yet -- see base.html's own comment) and are deliberately
omitted rather than built as dead or inert links. "Payments & dues" does
lead somewhere -- PaymentsView below -- with its "N OPEN" pill only
rendered once there's actually a balance owed.
have nowhere to lead in this build (no dedicated screen) and are
deliberately omitted rather than built as dead or inert links.
"Payments & dues" does lead somewhere -- PaymentsView below -- with its
"N OPEN" pill only rendered once there's actually a balance owed.
"Teams I coach" is new, beyond the mockup: one row per current-season
StaffAssignment self.me holds (team + position/role), each linking
straight into Coach mode for that team (mobile:coach_today?team=<pk>,
which mobile.coach_mixins.CoachScopeMixin's own ?team= handling already
resolves and persists). Shown for any staffed team, not just ones
self.me *manages* -- Coach mode's own screens already render read-only
for a non-management position, so there's nothing to hide here.
There's no license/eligibility field on Member or ClubMembership to power
the mockup's "licence OK" text, so each managed person's meta line is
@@ -586,11 +593,16 @@ class MeView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
open_dues_count = len(open_dues_rows(club, self.managed_people, season))
staff_assignments = []
if self.me is not None and season is not None:
staff_assignments = list(StaffAssignment.objects.filter(member=self.me, season=season).select_related("team", "position").order_by("team__name"))
return super().get_context_data(
member_since=member_since,
team_manager_label=team_manager_label,
people_rows=people_rows,
open_dues_count=open_dues_count,
staff_assignments=staff_assignments,
**kwargs,
)