Build M2 Event detail ("answer for several") for the mobile app

Full event-detail screen: hero header, Face-off/Meet/Where facts (the
design mock's Kit/dressing-room details have no backing Event field, so
they're simply omitted), a per-managed-person RSVP card so a parent with
several kids on the same event can answer for each independently, and a
club-visible squad-response aggregate (counts only, never who answered
what).

Extends the existing quick-RSVP POST (built for M1's Home hero) to also
accept "maybe" for M2's three-way buttons, and adds an optional
next=event_detail redirect target so answering here doesn't bounce back
to Home -- M1's and M3's existing forms are unaffected.

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 13:44:53 +02:00
parent 29acd22b7f
commit 13f369e10c
5 changed files with 333 additions and 8 deletions

View File

@@ -0,0 +1,126 @@
{% extends "mobile/base.html" %}
{% load i18n %}
{% comment %}
M2 -- design_handoff_rosterchief_platform/README.md's M2 section, "answer
for several". See EventDetailView's own docstring (mobile/views.py) for
the judgment calls: no event photos, no Kit/dressing-room fields (the
Event model has none), squad response is counts-only (never who answered
what).
{% endcomment %}
{% block content %}
<div class="-mx-4 -mt-4 flex h-[220px] flex-col justify-end bg-ink p-4 text-white" style="background: linear-gradient(180deg, rgba(11,18,32,.35) 0%, rgba(11,18,32,.55) 60%, rgba(11,18,32,.92) 100%), var(--color-navy)">
<a class="mb-auto flex h-11 w-11 items-center justify-center rounded-full bg-white/15" href="{% url "mobile:home" %}" aria-label="{% trans "Back" %}">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M15 5l-7 7 7 7"/></svg>
</a>
<span class="mb-2 inline-block w-fit rounded bg-club px-2 py-1 font-display text-xs font-extrabold tracking-wide text-white uppercase">
{% if event.kind == "game" %}{% if event.is_home_game %}{% trans "Home game" %}{% else %}{% trans "Away game" %}{% endif %}{% else %}{{ event.get_kind_display }}{% endif %}
</span>
<h1 class="font-display text-4xl leading-[.98] font-extrabold uppercase">{{ event.title }}</h1>
</div>
<div class="m-card flex flex-col gap-2.5 p-4">
<div class="flex gap-3.5">
<span class="w-20 shrink-0 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "Face-off" %}</span>
<span class="text-[15px] font-semibold text-ink">{{ event.start|date:"D d M \a\t H:i" }}</span>
</div>
{% if event.gathering %}
<div class="flex gap-3.5">
<span class="w-20 shrink-0 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "Meet" %}</span>
<span class="text-[15px] font-semibold text-ink">{{ event.gathering|date:"H:i" }}</span>
</div>
{% endif %}
{% if event.location %}
<div class="flex gap-3.5">
<span class="w-20 shrink-0 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "Where" %}</span>
<span class="text-[15px] font-semibold text-ink">
{{ event.location.name }}<br>
<span class="text-[13px] font-normal text-muted">{{ event.location.address }}, {{ event.location.zip_code }} {{ event.location.city }}</span>
</span>
</div>
{% endif %}
</div>
{% if your_answers %}
<div class="m-card p-4">
<span class="font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "Your answers" %}</span>
<div class="mt-3 flex flex-col gap-3">
{% for answer in your_answers %}
{% if not forloop.first %}<div class="h-px bg-rule"></div>{% endif %}
<div>
<div class="mb-2 flex items-center gap-2.5">
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-steel font-display text-sm font-extrabold text-white">{{ answer.member.first_name|slice:":1" }}{{ answer.member.last_name|slice:":1" }}</span>
<div class="min-w-0 flex-1">
<div class="text-[15px] font-semibold text-ink">{{ answer.member.get_full_name }}</div>
{% if answer.membership %}
<div class="text-xs text-muted">
{{ answer.membership.team.short_name }}
{% if answer.membership.jersey_number %}&middot; #{{ answer.membership.jersey_number }}{% endif %}
{% if answer.membership.position %}&middot; {{ answer.membership.position.name }}{% endif %}
</div>
{% endif %}
</div>
{% if answer.attendance.status == "no_response" %}
<span class="pill pill-warn shrink-0">{% trans "No reply" %}</span>
{% endif %}
</div>
<div class="flex gap-1.5">
{% trans "In" as label_in %}
{% trans "Maybe" as label_maybe %}
{% trans "Out" as label_out %}
{% with status=answer.attendance.status %}
<form class="flex-1" method="post" action="{% url "mobile:event_detail" event.pk %}">
{% csrf_token %}
<input type="hidden" name="member_id" value="{{ answer.member.pk }}">
<input type="hidden" name="status" value="present">
<input type="hidden" name="next" value="event_detail">
<button type="submit" class="btn h-11 w-full text-[15px] {% if status == "present" %}btn-positive{% else %}bg-paper border border-stroke text-muted{% endif %}">{{ label_in }}</button>
</form>
<form class="flex-1" method="post" action="{% url "mobile:event_detail" event.pk %}">
{% csrf_token %}
<input type="hidden" name="member_id" value="{{ answer.member.pk }}">
<input type="hidden" name="status" value="maybe">
<input type="hidden" name="next" value="event_detail">
<button type="submit" class="btn h-11 w-full text-[15px] {% if status == "maybe" %}bg-steel text-white{% else %}bg-paper border border-stroke text-muted{% endif %}">{{ label_maybe }}</button>
</form>
<form class="flex-1" method="post" action="{% url "mobile:event_detail" event.pk %}">
{% csrf_token %}
<input type="hidden" name="member_id" value="{{ answer.member.pk }}">
<input type="hidden" name="status" value="absent">
<input type="hidden" name="next" value="event_detail">
<button type="submit" class="btn h-11 w-full text-[15px] {% if status == "absent" %}bg-club text-white{% else %}bg-paper border border-stroke text-muted{% endif %}">{{ label_out }}</button>
</form>
{% endwith %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if squad_summary %}
<div class="m-card p-4">
<div class="flex items-center justify-between">
<span class="font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "Squad response" %}</span>
<span class="text-[13px] text-muted">{% blocktrans with responded=squad_summary.responded total=squad_summary.total %}{{ responded }} of {{ total }}{% endblocktrans %}</span>
</div>
<div class="mt-2.5 flex h-2 overflow-hidden rounded-full bg-rule">
<div class="bg-ok" style="width: {{ squad_summary.in_pct }}%"></div>
<div class="bg-club" style="width: {{ squad_summary.out_pct }}%"></div>
<div class="bg-line" style="width: {{ squad_summary.no_reply_pct }}%"></div>
</div>
<div class="mt-2 flex gap-3.5 text-[13px] text-muted">
<span><strong class="text-ink">{{ squad_summary.in_count }}</strong> {% trans "in" %}</span>
<span><strong class="text-ink">{{ squad_summary.out_count }}</strong> {% trans "out" %}</span>
<span><strong class="text-ink">{{ squad_summary.no_reply_count }}</strong> {% trans "no reply" %}</span>
</div>
</div>
{% endif %}
{% if not your_answers and not squad_summary %}
<div class="m-card p-6 text-center">
<p class="text-sm text-muted">{% trans "No one you manage is invited to this event." %}</p>
</div>
{% endif %}
{% endblock content %}