No-shows on the desktop member page, coach event screens, "+" popup polish

- management member_detail.html's Attendance card now shows the same
  No-shows count as the mobile player sheet, and the card's content (sparkline
  + tiles) fills the card's height/width instead of sitting in the top-left
  corner -- same flex-1/justify-center + stretched-tiles fix used there.
- Coach mode's Attendance and Line-up screens (opening an event) now
  highlight the Schedule tab instead of Today -- that's the calendar-icon
  tab, and where a coach reaches either from CoachScheduleView's own rows.
- The tab bar's "+" button: rotates 45° into an "×" while its popup is open
  (icon reused via CSS transform, no icon swap needed), the popup sits a
  little higher above the button (mb-3 -> mb-5), and a dark backdrop now
  covers the content behind it so the popup stands out.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 14:16:05 +02:00
parent 4af598500b
commit fbaf5a8799
8 changed files with 71 additions and 11 deletions

View File

@@ -176,25 +176,29 @@
{% if not attendance_sparkline %} {% if not attendance_sparkline %}
<p class="text-sm text-muted">{% trans "No events yet this season." %}</p> <p class="text-sm text-muted">{% trans "No events yet this season." %}</p>
{% else %} {% else %}
<div class="flex flex-wrap items-center gap-6"> <div class="flex flex-1 flex-col justify-center gap-4">
<div class="attendance-sparkline"> <div class="attendance-sparkline">
{% for bar in attendance_sparkline %} {% for bar in attendance_sparkline %}
<span class="attendance-bar attendance-bar-{{ bar.state }}" title="{{ bar.event.title }} — {{ bar.event.start|date:"j M" }}"></span> <span class="attendance-bar attendance-bar-{{ bar.state }}" title="{{ bar.event.title }} — {{ bar.event.start|date:"j M" }}"></span>
{% endfor %} {% endfor %}
</div> </div>
<div class="flex gap-4"> <div class="flex">
<div> <div class="flex-1">
<div class="font-display text-2xl leading-none font-extrabold text-ok tabular-nums">{{ attendance_counts.present }}</div> <div class="font-display text-2xl leading-none font-extrabold text-ok tabular-nums">{{ attendance_counts.present }}</div>
<div class="mt-1 font-display text-[11px] font-bold tracking-[.1em] text-muted uppercase">{% trans "Present" %}</div> <div class="mt-1 font-display text-[11px] font-bold tracking-[.1em] text-muted uppercase">{% trans "Present" %}</div>
</div> </div>
<div> <div class="flex-1">
<div class="font-display text-2xl leading-none font-extrabold text-club tabular-nums">{{ attendance_counts.absent }}</div> <div class="font-display text-2xl leading-none font-extrabold text-club tabular-nums">{{ attendance_counts.absent }}</div>
<div class="mt-1 font-display text-[11px] font-bold tracking-[.1em] text-muted uppercase">{% trans "Absent" %}</div> <div class="mt-1 font-display text-[11px] font-bold tracking-[.1em] text-muted uppercase">{% trans "Absent" %}</div>
</div> </div>
<div> <div class="flex-1">
<div class="font-display text-2xl leading-none font-extrabold text-dim tabular-nums">{{ attendance_counts.no_reply }}</div> <div class="font-display text-2xl leading-none font-extrabold text-dim tabular-nums">{{ attendance_counts.no_reply }}</div>
<div class="mt-1 font-display text-[11px] font-bold tracking-[.1em] text-muted uppercase">{% trans "No reply" %}</div> <div class="mt-1 font-display text-[11px] font-bold tracking-[.1em] text-muted uppercase">{% trans "No reply" %}</div>
</div> </div>
<div class="flex-1">
<div class="font-display text-2xl leading-none font-extrabold text-warn-text tabular-nums">{{ attendance_counts.no_shows }}</div>
<div class="mt-1 font-display text-[11px] font-bold tracking-[.1em] text-muted uppercase">{% trans "No-shows" %}</div>
</div>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@@ -418,6 +418,21 @@ class MemberAttendanceSparklineTests(ManagementTestBase):
self.assertEqual(response.context["attendance_counts"]["no_reply"], 1) self.assertEqual(response.context["attendance_counts"]["no_reply"], 1)
def test_a_present_rsvp_checked_in_as_absent_counts_as_a_no_show(self):
self.make_attendance(status=Attendance.AttendanceStatus.PRESENT, start=timezone.now() - datetime.timedelta(days=2), showed_up=False)
response = self.club_get("member_detail", self.player.pk)
self.assertEqual(response.context["attendance_counts"]["no_shows"], 1)
self.assertContains(response, "No-shows")
def test_an_unchecked_present_rsvp_is_not_a_no_show(self):
self.make_attendance(status=Attendance.AttendanceStatus.PRESENT, start=timezone.now() - datetime.timedelta(days=2))
response = self.club_get("member_detail", self.player.pk)
self.assertEqual(response.context["attendance_counts"]["no_shows"], 0)
def test_the_sparkline_is_capped_at_twelve_bars_oldest_first(self): def test_the_sparkline_is_capped_at_twelve_bars_oldest_first(self):
for day in range(15, 0, -1): for day in range(15, 0, -1):
self.make_attendance(status=Attendance.AttendanceStatus.PRESENT, start=timezone.now() - datetime.timedelta(days=day)) self.make_attendance(status=Attendance.AttendanceStatus.PRESENT, start=timezone.now() - datetime.timedelta(days=day))

View File

@@ -187,7 +187,7 @@ class CoachAttendanceView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
template_name = "mobile/coach/attendance.html" template_name = "mobile/coach/attendance.html"
screen_title = _("Attendance") screen_title = _("Attendance")
active_tab = "coach_today" active_tab = "coach_schedule"
#: ?filter= values this screen understands -- anything else (including no #: ?filter= values this screen understands -- anything else (including no
#: param) means "Responded" (IN_STATUSES: present/selected/maybe), the #: param) means "Responded" (IN_STATUSES: present/selected/maybe), the
@@ -606,7 +606,7 @@ class CoachLineupView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
template_name = "mobile/coach/lineup.html" template_name = "mobile/coach/lineup.html"
screen_title = _("Line-up") screen_title = _("Line-up")
active_tab = "coach_today" active_tab = "coach_schedule"
def get_event(self): def get_event(self):
if self.active_team is None: if self.active_team is None:

View File

@@ -128,10 +128,11 @@
{% if can_manage_active_team %} {% if can_manage_active_team %}
<div class="coach-tab-bar-add" x-data="{ open: false }"> <div class="coach-tab-bar-add" x-data="{ open: false }">
<button class="coach-tab-bar-add-button" type="button" @click="open = !open" aria-label="{% trans "Add" %}"> <div class="fixed inset-0 z-10 bg-black/60" x-show="open" x-cloak @click="open = false"></div>
<button class="coach-tab-bar-add-button relative z-20 transition-transform duration-200" :class="open ? 'rotate-45' : ''" type="button" @click="open = !open" aria-label="{% trans "Add" %}">
{% lucide "plus" size=24 %} {% lucide "plus" size=24 %}
</button> </button>
<div class="absolute inset-x-3 bottom-full z-20 mb-3 flex flex-col gap-1 rounded-xl border border-line bg-white p-2 shadow-lg" x-show="open" x-cloak @click.outside="open = false"> <div class="absolute inset-x-3 bottom-full z-20 mb-5 flex flex-col gap-1 rounded-xl border border-line bg-white p-2 shadow-lg" x-show="open" x-cloak @click.outside="open = false">
<a class="flex items-center gap-3 rounded-lg p-3 text-sm font-semibold text-ink" href="{% url "mobile:coach_create_event" %}"> <a class="flex items-center gap-3 rounded-lg p-3 text-sm font-semibold text-ink" href="{% url "mobile:coach_create_event" %}">
{% lucide "calendar-plus" size=18 class="text-club" %} {% trans "New event" %} {% lucide "calendar-plus" size=18 class="text-club" %} {% trans "New event" %}
</a> </a>

View File

@@ -3179,6 +3179,13 @@ class CoachAttendanceViewTests(TestCase):
self.assertContains(response, "Anna Player") self.assertContains(response, "Anna Player")
self.assertContains(response, "9") self.assertContains(response, "9")
def test_the_schedule_tab_is_highlighted(self):
self.client.force_login(self.user)
response = self._get()
self.assertEqual(response.context["active_tab"], "coach_schedule")
def test_shows_a_maybe_reason_alongside_an_absent_one(self): def test_shows_a_maybe_reason_alongside_an_absent_one(self):
self.attendance.status = Attendance.AttendanceStatus.MAYBE self.attendance.status = Attendance.AttendanceStatus.MAYBE
self.attendance.note = "Might be a few minutes late" self.attendance.note = "Might be a few minutes late"
@@ -3726,6 +3733,13 @@ class CoachLineupViewTests(TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
def test_the_schedule_tab_is_highlighted(self):
self.client.force_login(self.user)
response = self.client.get(reverse("mobile:coach_lineup", kwargs={"event_id": self.event.pk}), HTTP_HOST="ajax-united.rosterchief.app")
self.assertEqual(response.context["active_tab"], "coach_schedule")
def test_get_creates_a_lineup_lazily(self): def test_get_creates_a_lineup_lazily(self):
self.client.force_login(self.user) self.client.force_login(self.user)

View File

@@ -3525,6 +3525,9 @@
.mb-4 { .mb-4 {
margin-bottom: calc(var(--spacing) * 4); margin-bottom: calc(var(--spacing) * 4);
} }
.mb-5 {
margin-bottom: calc(var(--spacing) * 5);
}
.mb-6 { .mb-6 {
margin-bottom: calc(var(--spacing) * 6); margin-bottom: calc(var(--spacing) * 6);
} }
@@ -4243,6 +4246,9 @@
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1); --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
} }
.rotate-45 {
rotate: 45deg;
}
.transform { .transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
} }
@@ -4645,6 +4651,12 @@
background-color: color-mix(in oklab, var(--color-black) 20%, transparent); background-color: color-mix(in oklab, var(--color-black) 20%, transparent);
} }
} }
.bg-black\/60 {
background-color: color-mix(in srgb, #000 60%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-black) 60%, transparent);
}
}
.bg-info { .bg-info {
background-color: var(--color-info); background-color: var(--color-info);
} }
@@ -5401,6 +5413,15 @@
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration)); transition-duration: var(--tw-duration, var(--default-transition-duration));
} }
.transition-transform {
transition-property: transform, translate, scale, rotate;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-200 {
--tw-duration: 200ms;
transition-duration: 200ms;
}
.ease-in-out { .ease-in-out {
--tw-ease: var(--ease-in-out); --tw-ease: var(--ease-in-out);
transition-timing-function: var(--ease-in-out); transition-timing-function: var(--ease-in-out);
@@ -6489,6 +6510,10 @@
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
} }
@property --tw-duration {
syntax: "*";
inherits: false;
}
@property --tw-ease { @property --tw-ease {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@@ -6557,6 +6582,7 @@
--tw-backdrop-opacity: initial; --tw-backdrop-opacity: initial;
--tw-backdrop-saturate: initial; --tw-backdrop-saturate: initial;
--tw-backdrop-sepia: initial; --tw-backdrop-sepia: initial;
--tw-duration: initial;
--tw-ease: initial; --tw-ease: initial;
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long