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:
@@ -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 %}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user