Add Coach mode: dark-chrome shell, mode switcher, C1 Today, C2 Bench attendance
First slice of the coach-mode build (design_handoff_rosterchief_platform/ README.md's "Coach mode" section, C1-C6). Ships the foundation together with the two screens the design doc calls out as the reason coaches install anything at all, rather than landing a Today screen with a dead "Check attendance" button: - mobile/coach_mixins.py's CoachScopeMixin -- the dark-mode mirror of PersonScopeMixin, scoped by active team (via club.services.access' teams_staffed_by/teams_managed_by) instead of managed people. - A standalone dark ink/ice shell (mobile/templates/mobile/coach/base.html) with the mode's signature 20px-radius overlapping sheet, reusing the --color-ice/--color-ink tokens that already existed in assets/mobile.css but were unconsumed until now. - The Coach/Member role switcher is re-added to the member shell, gated on a real staff assignment (has_coach_access), replacing the "deliberately not rendered yet" placeholder. - C1 Today: stat tiles, a tonight's-session card, a silent-players "needs you" row, and an "Also yours" card reusing HomeView's own hero-RSVP pattern scoped to the coach's own member record. - C2 Bench attendance: writes through events.services.attendance. record_check_in, which existed for exactly this and had no caller yet. Read-only for staff on a team without a management position. Line-up (C3), create event (C4), post news (C5), and add-to-roster (C6) follow in later stages -- see the coach-mode plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
71
mobile/coach_mixins.py
Normal file
71
mobile/coach_mixins.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""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
|
||||
|
||||
#: 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.
|
||||
"""
|
||||
|
||||
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
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
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,
|
||||
season=current_season(self.request.club),
|
||||
**kwargs,
|
||||
)
|
||||
Reference in New Issue
Block a user