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 calls: no license/eligibility field backing "licence OK", so each row's
meta line is real roster data instead; "Household & contacts" has no 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" 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 promo. "Payments & dues" does lead somewhere (mobile:payments) and
it's never rendered, not even as a dead/inert link). "Payments & dues" carries its "N OPEN" pill only once open_dues_count is actually > 0.
does lead somewhere (mobile:payments) and carries its "N OPEN" pill only "Teams I coach" is new, beyond the mockup -- one row per current-season
once open_dues_count is actually > 0. staff assignment, linking straight into Coach mode for that team.
The avatar/name/subtitle row lives in header_extra -- merged into the The avatar/name/subtitle row lives in header_extra -- merged into the
shared navy app-header (base.html) rather than a separately-coloured shared navy app-header (base.html) rather than a separately-coloured
block of its own, matching the design canvas's own M5 markup. block of its own, matching the design canvas's own M5 markup.
@@ -66,6 +66,25 @@
</div> </div>
</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"> <div class="m-card overflow-hidden">
<a class="flex items-center gap-3 p-3.5" href="{% url "mobile:edit_profile" me.pk %}"> <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> <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") 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): def test_empty_account_gets_a_graceful_empty_state(self):
bare_user = User.objects.create_user(email="new@example.com", password="pw-secret-123") bare_user = User.objects.create_user(email="new@example.com", password="pw-secret-123")
self.client.force_login(bare_user) self.client.force_login(bare_user)

View File

@@ -33,7 +33,7 @@ from members.models import FamilyMembership, Member
from members.views import ClubScopedPublicMixin from members.views import ClubScopedPublicMixin
from news.models import News from news.models import News
from notifications.models import Notification from notifications.models import Notification
from teams.models import TeamMembership from teams.models import StaffAssignment, TeamMembership
from .forms import MemberProfileForm from .forms import MemberProfileForm
from .mixins import PersonScopeMixin from .mixins import PersonScopeMixin
@@ -530,11 +530,18 @@ class MeView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
``self.me`` and into M7 (notifications). ``self.me`` and into M7 (notifications).
The mockup's "Household & contacts" row and its "Coach mode" promo card 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 have nowhere to lead in this build (no dedicated screen) and are
screens at all yet -- see base.html's own comment) and are deliberately deliberately omitted rather than built as dead or inert links.
omitted rather than built as dead or inert links. "Payments & dues" does "Payments & dues" does lead somewhere -- PaymentsView below -- with its
lead somewhere -- PaymentsView below -- with its "N OPEN" pill only "N OPEN" pill only rendered once there's actually a balance owed.
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 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 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)) 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( return super().get_context_data(
member_since=member_since, member_since=member_since,
team_manager_label=team_manager_label, team_manager_label=team_manager_label,
people_rows=people_rows, people_rows=people_rows,
open_dues_count=open_dues_count, open_dues_count=open_dues_count,
staff_assignments=staff_assignments,
**kwargs, **kwargs,
) )