Files
RosterChief/mobile/coach_mixins.py
Bernard Siebens 1d159ca5de Redesign the Coach mode shell: ice-blue header, flat sheet, Squad/Schedule tabs
Reworks the coach shell per feedback that the previous dark-ink header
didn't read as visually distinct from Member mode's own navy header, and
that the rounded "sheet overlaps header" treatment wasn't landing:

- .coach-header is now solid ice-blue (--color-ice, never club-themed --
  Coach mode's own signature colour) with --color-ice-ink foreground text,
  instead of dark ink with white text. Every header_extra block across the
  coach templates (attendance, lineup) is recoloured to match.
- .coach-sheet drops the 20px rounded-top/negative-margin overlap -- flush
  edge-to-edge below the header now, same join the member shell already uses.
- The header's "Head coach" label was hardcoded regardless of the account's
  actual role -- CoachScopeMixin now resolves active_team_role from the
  person's own current-season StaffAssignment.position on the active team,
  so a team manager or physio sees their real title, not someone else's.

Tab bar is now Today / Squad / [+] / Schedule -- no Me tab (the account's
own settings already live in Member mode via the role switcher, no need
for a second one). Squad (CoachSquadView) is a new roster+staff screen for
the active team; Schedule (CoachScheduleView) is a new full upcoming-events
list for the team, each row routed straight to the coach-relevant action
(Attendance for a practice, Line-up for a game) rather than the Member-
shell RSVP page. The "+" is a raised ice-blue circle opening a small popup
with New event/New post/Add player -- three genuinely different actions,
so the button opens a menu rather than committing to one destination.
Today's own inline New event/New post/Add player buttons are gone now that
the tab bar covers the same ground.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
2026-08-21 23:21:27 +02:00

86 lines
4.3 KiB
Python

"""Shared scaffolding for every Coach-mode screen (C1-C6) -- the dark-chrome
mirror of mobile/mixins.py's PersonScopeMixin. Scoped by *team*, not by
managed people: a coach acts on one team at a time (the header's team-picker
pill), switching which team is "active" rather than aggregating across
several people the way Member mode's person switcher does.
"""
from club.services.access import current_season, teams_managed_by, teams_staffed_by
from members.models import Member
from members.views import ClubScopedPublicMixin
from teams.models import StaffAssignment
#: Session key remembering which team was last active, so navigating between
#: coach screens (or leaving and coming back) doesn't reset the picker to
#: whichever team happens to sort first.
ACTIVE_TEAM_SESSION_KEY = "coach_active_team_id"
class CoachScopeMixin(ClubScopedPublicMixin):
"""Resolves the signed-in account's Coach-mode standing: which teams
they're on the staff of at all (``staffed_teams`` -- visibility, any
position, see club.services.access.teams_staffed_by), which of those
they actually manage (``managed_teams`` -- management position only,
current season, teams_managed_by), and which one is currently "active"
(the team-picker pill on C1's header).
Every Coach view still needs its own ``LoginRequiredMixin`` (kept
separate, same as PersonScopeMixin, so a view composes whichever other
mixins it needs on top). Nothing here 404s when ``staffed_teams`` is
empty -- each screen renders its own "not staffing a team yet" empty
state instead, same judgment call as HomeView's "No one to show yet".
Actions gated on ``can_manage_active_team`` are hidden in templates, not
disabled -- a coach on staff but without a management position (e.g. a
physio) can see Coach mode but shouldn't see edit affordances they don't
have the authority to use.
``active_team_role`` is ``self.me``'s own StaffAssignment.position on
``active_team`` this season (e.g. "Team manager", "Physio") -- the
header shows this instead of a hardcoded "Head coach", since not every
staffed team is one the account holder actually coaches.
"""
def dispatch(self, request, *args, **kwargs):
self.me = Member.objects.filter(user=request.user).first() if request.user.is_authenticated else None
self.staffed_teams = list(teams_staffed_by(request.user, request.club)) if request.user.is_authenticated else []
self.managed_teams = list(teams_managed_by(request.user, request.club)) if request.user.is_authenticated else []
self.active_team = self._resolve_active_team(request)
self.can_manage_active_team = self.active_team is not None and self.active_team in self.managed_teams
self.active_team_role = self._resolve_active_team_role(request)
return super().dispatch(request, *args, **kwargs)
def _resolve_active_team_role(self, request):
if self.me is None or self.active_team is None:
return None
assignment = StaffAssignment.objects.filter(member=self.me, team=self.active_team, season=current_season(request.club)).select_related("position").first()
return assignment.position if assignment is not None else None
def _resolve_active_team(self, request):
requested_id = request.GET.get("team")
for team in self.staffed_teams:
if requested_id and str(team.pk) == requested_id:
request.session[ACTIVE_TEAM_SESSION_KEY] = str(team.pk)
return team
stored_id = request.session.get(ACTIVE_TEAM_SESSION_KEY)
for team in self.staffed_teams:
if stored_id and str(team.pk) == stored_id:
return team
return self.staffed_teams[0] if self.staffed_teams else None
def get_context_data(self, **kwargs):
kwargs.setdefault("active_tab", getattr(self, "active_tab", ""))
kwargs.setdefault("screen_title", getattr(self, "screen_title", ""))
return super().get_context_data(
me=self.me,
staffed_teams=self.staffed_teams,
active_team=self.active_team,
can_manage_active_team=self.can_manage_active_team,
active_team_role=self.active_team_role,
season=current_season(self.request.club),
**kwargs,
)