Scaffold the mobile member app: PWA shell, push, and app-shell tokens
New `mobile` Django app mounted at /app/ -- the installed PWA for Member mode (M1-M7, see design_handoff_rosterchief_platform/README.md). Coach mode (C1-C6) is a later phase and has no routes yet. Foundation pieces: - assets/mobile.css: Tailwind v4 theme reusing management.css's design tokens (same per-club --tenant-* theming pattern), plus the ice/coach accent and mobile's 14px card radius. - PushSubscription model + pywebpush-based sender (mobile/services/push.py), wired to notifications.Notification via a post_save signal so the existing notification system gains a push channel without knowing about PWAs itself. - Per-club manifest.webmanifest + service worker (served at /app/sw.js) + a server-rendered fallback home-screen icon (club initials on secondary_color) for clubs without an uploaded logo -- confirmed with the user as the fallback, never a generic RosterChief mark. - App shell (base.html): navy header, person switcher (every child a signed-in parent manages, plus "Me"), bottom tab bar, safe-area insets. Vendored htmx + Alpine for the screens built on top of it. - Placeholder views/routes for all seven M1-M7 screens so the shell is fully wired end-to-end before each screen is built out individually. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
69
mobile/mixins.py
Normal file
69
mobile/mixins.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Shared scaffolding for every Member-mode screen (design_handoff_rosterchief_platform/
|
||||
README.md: "there is no parent app... every per-member screen carries a person
|
||||
switcher at the top" + "the [Coach/Member] switcher only renders for an account
|
||||
holding >=1 staff role"). One mixin so M1-M7's views (and the subagents building
|
||||
them) don't each re-derive this.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from club.services.access import current_season, has_management_access
|
||||
from members.models import FamilyMembership, Member
|
||||
from members.views import ClubScopedPublicMixin
|
||||
from notifications.models import Notification
|
||||
|
||||
|
||||
class PersonScopeMixin(ClubScopedPublicMixin):
|
||||
"""Resolves the signed-in account's own Member record plus every child
|
||||
they're a parent/guardian of *in this club* (mirrors members.views.MyFamilyView's
|
||||
own query -- kept separate rather than imported from there, since that view
|
||||
is public/unauthenticated-reachable and this one is always behind login).
|
||||
|
||||
``?as=<member-id>`` re-scopes the current screen to one managed person,
|
||||
same as the design doc's horizontally-scrolling chip row -- it re-scopes in
|
||||
place rather than navigating. Falls back to the account's own Member, then
|
||||
to the first managed child (e.g. a parent with no Member record of their own).
|
||||
"""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.me = Member.objects.filter(user=request.user).first() if request.user.is_authenticated else None
|
||||
self.managed_people = self._managed_people(request)
|
||||
self.scope_person = self._resolve_scope_person(request)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def _managed_people(self, request):
|
||||
if self.me is None:
|
||||
return []
|
||||
children = list(
|
||||
Member.objects.filter(
|
||||
family_memberships__role=FamilyMembership.FamilyRole.CHILD,
|
||||
family_memberships__family__memberships__member=self.me,
|
||||
family_memberships__family__memberships__role__in=[FamilyMembership.FamilyRole.PARENT, FamilyMembership.FamilyRole.GUARDIAN],
|
||||
member_of__club=request.club,
|
||||
).distinct()
|
||||
)
|
||||
return [self.me, *children]
|
||||
|
||||
def _resolve_scope_person(self, request):
|
||||
requested_id = request.GET.get("as")
|
||||
if requested_id:
|
||||
for person in self.managed_people:
|
||||
if str(person.pk) == requested_id:
|
||||
return person
|
||||
return self.managed_people[0] if self.managed_people else None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
unread_notification_count = 0
|
||||
if self.managed_people:
|
||||
unread_notification_count = Notification.objects.filter(club=self.request.club, member__in=self.managed_people, read_at__isnull=True).count()
|
||||
|
||||
return super().get_context_data(
|
||||
me=self.me,
|
||||
managed_people=self.managed_people,
|
||||
scope_person=self.scope_person,
|
||||
has_staff_access=self.me is not None and has_management_access(self.request.user, self.request.club),
|
||||
unread_notification_count=unread_notification_count,
|
||||
season=current_season(self.request.club),
|
||||
vapid_public_key=settings.VAPID_PUBLIC_KEY,
|
||||
**kwargs,
|
||||
)
|
||||
Reference in New Issue
Block a user