Files
RosterChief/mobile/templates/mobile/coach/attendance.html
Bernard Siebens 366239e60b Fix RSVP buttons broken by hx-boost; require a real Out reason; add optional Maybe reason
The In/Out hero buttons (and Out's reason confirm) stopped working: Alpine
owns the toggle between a row's two sibling forms (buttons vs. the reason
prompt), and hx-boost="true" on <body> had htmx *also* intercepting the
same submit -- both ended up fighting over it. Fixed by marking every
write-action <form> across the mobile app hx-boost="false" (link
navigation, where the smooth-navigation feature actually matters, is
untouched). The one exception worth calling out: coach/lineup.html's form
uses three submit buttons sharing one <form> via formaction overrides --
htmx's boost reads the form's own action rather than the submitter's
formaction override, so a boosted click there would always have posted to
the wrong endpoint regardless of the Alpine conflict.

Also:
- A reason for Out is now mandatory, not just captured -- empty and
  punctuation-only "answers" (a bare ".", "-", "??") are rejected
  server-side (the authoritative check) with textarea required/minlength
  as a client-side nudge on top.
- Maybe can now carry an optional reason too, visible to the same audience
  as Out's (this member/family, and Coach mode's bench attendance) -- one
  shared reason form in event_detail.html's per-person row, its hidden
  status input following whichever of Maybe/Out was tapped.
- The 3-way In/Maybe/Out row (and the 2-way hero In/Out) now use min-w-0 on
  every button so flex-1 actually splits the row evenly -- a longer
  label's own intrinsic width was winning it a bigger share otherwise.
- HomeView's "Needs your answer" list now excludes events whose
  registration deadline has already passed -- replying is no longer
  possible there (same rule EventDetailView.post already enforces), so it
  doesn't belong in a "still needs a reply" list. hero_attendance is
  unaffected -- it always shows the true next event, falling back to a
  read-only pill once its own deadline closes.

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

77 lines
5.0 KiB
HTML

{% extends "mobile/coach/base.html" %}
{% load i18n lucide %}
{% comment %}
C2 -- design_handoff_rosterchief_platform/README.md's C2 section: check
off who actually showed up, a separate axis from their RSVP (see
CoachAttendanceView's own docstring). The mock's "fixed white footer"
Save button is a plain in-flow button instead -- every other coach/member
sub-page in this codebase keeps its action inline rather than adding a
second fixed bar above the shell's own tab bar.
{% endcomment %}
{% block header_extra %}
<div class="mt-3">
<div class="flex items-center justify-between">
<span class="font-display text-xl leading-none font-extrabold text-white uppercase">{% trans "Attendance" %}</span>
<span class="font-mono text-sm text-on-dark">{{ checked_in_count }}/{{ total_count }}</span>
</div>
<div class="mt-1 text-xs text-on-dark-dim">{{ event.title }} &middot; {{ event.start|date:"D d M H:i" }}</div>
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-steel">
<div class="h-full bg-ice" style="width: {% widthratio checked_in_count total_count|default:1 100 %}%"></div>
</div>
</div>
{% endblock header_extra %}
{% block content %}
<div class="flex gap-2">
<a class="flex h-9 flex-1 items-center justify-center rounded-full font-display text-xs font-extrabold tracking-wide uppercase {% if not filter_param %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?">
{% trans "All" %} {{ total_count }}
</a>
<a class="flex h-9 flex-1 items-center justify-center rounded-full font-display text-xs font-extrabold tracking-wide uppercase {% if filter_param == "silent" %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?filter=silent">
{% trans "Silent" %} {{ silent_count }}
</a>
<a class="flex h-9 flex-1 items-center justify-center rounded-full font-display text-xs font-extrabold tracking-wide uppercase {% if filter_param == "goalies" %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?filter=goalies">
{% trans "Goalies" %}
</a>
</div>
<form method="post" action="{% url "mobile:coach_attendance" event.pk %}" hx-boost="false">
{% csrf_token %}
<div class="m-card flex flex-col overflow-hidden">
{% for row in rows %}
<div class="flex items-center gap-3 px-4 py-2.5 {% if not forloop.last %}border-b border-rule{% endif %} {% if row.is_silent %}bg-warn-bg{% endif %}" {% if can_manage_active_team %}x-data="{ state: '{{ row.showed_up|yesno:'true,false,' }}' }"{% endif %}>
<div class="w-8 shrink-0 text-center font-display text-lg font-extrabold text-ink tabular-nums">{{ row.membership.jersey_number|default:"—" }}</div>
<div class="min-w-0 flex-1">
<div class="text-sm font-semibold text-ink">{{ row.member.get_full_name }}</div>
<div class="text-xs text-muted">{{ row.membership.position|default:"—" }}</div>
{% if row.note %}
{# Only ever set alongside absent/maybe -- EventDetailView.post clears it for present. #}
<div class="mt-0.5 truncate text-xs text-club-dark italic">&ldquo;{{ row.note }}&rdquo;</div>
{% endif %}
</div>
{% if can_manage_active_team %}
<input type="hidden" name="showed_up_{{ row.pk }}" :value="state">
<div class="flex h-11 w-23 shrink-0 overflow-hidden rounded-lg">
<button type="button" class="flex-1" :class="state === 'true' ? 'bg-ok text-white' : 'bg-rule text-dim'" @click="state = 'true'" aria-label="{% trans "In" %}">
{% lucide "check" size=18 stroke_width=2.4 class="mx-auto" %}
</button>
<button type="button" class="flex-1" :class="state === 'false' ? 'bg-club text-white' : 'bg-rule text-dim'" @click="state = 'false'" aria-label="{% trans "Out" %}">
{% lucide "x" size=18 stroke_width=2.4 class="mx-auto" %}
</button>
</div>
{% elif row.showed_up is not None %}
<span class="pill {% if row.showed_up %}pill-ok{% else %}pill-danger{% endif %} shrink-0">{% if row.showed_up %}{% trans "In" %}{% else %}{% trans "Out" %}{% endif %}</span>
{% endif %}
</div>
{% empty %}
<div class="px-4 py-6 text-center text-sm text-muted">{% trans "No one matches this filter." %}</div>
{% endfor %}
</div>
{% if can_manage_active_team %}
<button class="btn btn-dark mt-4 w-full" type="submit">{% trans "Save attendance" %}</button>
{% endif %}
</form>
{% endblock content %}