Improve club detail layout: enhance logo display, adjust badge styles, refine card layouts, and optimize spacing

This commit is contained in:
2026-07-16 00:06:54 +02:00
parent 91270b0cf8
commit f403128f57
10 changed files with 5456 additions and 120 deletions

View File

@@ -122,7 +122,7 @@
</td>
<td>
<a class="btn btn-sm gap-2" href="{% url "controlpanel:club_detail" club.pk %}">{% lucide "pencil" size=14 %} Edit</a>
<a class="btn btn-sm btn-outline gap-2" href="{% url "controlpanel:club_detail" club.pk %}">{% lucide "pencil" size=14 %} Edit</a>
</td>
</tr>
{% empty %}

View File

@@ -28,8 +28,10 @@
<a class="btn btn-sm gap-2 btn-error btn-soft" href="{% url 'controlpanel:features' %}">{% lucide "unlock" size=16 %} Reopen platform</a>
</div>
{% endif %}
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
<div class="flex flex-col gap-2">
<div class="mb-6 flex flex-wrap flex-row items-center justify-between gap-3">
{% block logo %}{% endblock logo %}
<div class="flex flex-col gap-2 grow">
<h1 class="text-3xl font-bold">
{% block heading %}Control panel{% endblock heading %}
</h1>

View File

@@ -1,19 +1,29 @@
{% extends "controlpanel/base.html" %}
{% load static lucide %}
{% block logo %}
{% if club.logo %}
<img class="h-16 w-16 object-contain" src="{{ club.logo.url }}" alt="{{ club.name }}">
{% else %}
<div class="avatar avatar-placeholder">
<div class="w-16 text-xl rounded-full bg-neutral text-neutral-content">
<span>{{ club.initials }}</span>
</div>
</div>
{% endif %}
{% endblock logo %}
{% block heading %}{{ club.name }}{% endblock heading %}
{% block subheading %}
<p class="text-sm opacity-70">
{{ club.slug }}
{% if club.is_archived %}
<span class="badge badge-warning badge-sm ml-2">Archived</span>
{% endif %}
</p>
{{ club.slug }}.rosterchief.app
{% if club.is_archived %}
<span class="badge badge-warning badge-sm ml-2">Archived</span>
{% endif %}
{% endblock subheading %}
{% block actions %}
<a class="btn btn-ghost gap-2" href="{% url 'controlpanel:club_update' club.pk %}">{% lucide "pencil" size=16 %} Edit</a>
<a class="btn btn-outline gap-2" href="{% url 'controlpanel:club_update' club.pk %}">{% lucide "pencil" size=16 %} Edit</a>
{% if club.is_archived %}
<form method="post" action="{% url 'controlpanel:club_restore' club.pk %}">
{% csrf_token %}
@@ -30,6 +40,7 @@
{% block panel %}
{% if club.is_archived %}
<div class="alert alert-warning mb-6">
{% lucide "alert-triangle" size=20 %}
<span>This club is archived: its subdomain no longer resolves. Nothing has been deleted — restore it to bring it back.</span>
</div>
{% endif %}
@@ -47,96 +58,91 @@
the club's setup, not a statistic: with nobody in a management position the access
service grants no authority over that team, so nobody can pick the squad.
{% endcomment %}
<div class="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="card bg-base-100 shadow {% if attention.outstanding %}border-l-4 border-error{% endif %}">
<div class="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-6">
{% comment %}<div class="card bg-base-100 shadow {% if attention.outstanding %}border-l-4 border-error{% endif %}">
<div class="card-body p-4">
<div class="flex items-center gap-2 text-sm opacity-70">{% lucide "banknote" size=16 %} Outstanding</div>
<div class="text-3xl font-bold tabular-nums">€{{ attention.outstanding|floatformat:2 }}</div>
<div class="text-xs opacity-60">{{ attention.unpaid_members }} member{{ attention.unpaid_members|pluralize }} unpaid this season</div>
</div>
</div>
<div class="card bg-base-100 shadow {% if attention.teams_without_manager %}border-l-4 border-error{% endif %}">
</div>{% endcomment %}
<div class="card bg-base-100 shadow border-l-4 {% if attention.teams_without_manager %}border-error{% else %}border-success{% endif %}">
<div class="card-body p-4">
<div class="flex items-center gap-2 text-sm opacity-70">{% lucide "user-x" size=16 %} No coach</div>
<div class="text-3xl font-bold tabular-nums">{{ attention.teams_without_manager }}</div>
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "user-x" size=16 %} Teams without coach</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ attention.teams_without_manager }}</div>
<div class="text-xs opacity-60">Teams nobody can pick a squad for</div>
</div>
</div>
<div class="card bg-base-100 shadow {% if attention.unrostered %}border-l-4 border-warning{% endif %}">
<div class="card bg-base-100 shadow border-l-4 {% if attention.unrostered %}border-warning{% else %}border-success{% endif %}">
<div class="card-body p-4">
<div class="flex items-center gap-2 text-sm opacity-70">{% lucide "user-minus" size=16 %} Unrostered</div>
<div class="text-3xl font-bold tabular-nums">{{ attention.unrostered }}</div>
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "user-minus" size=16 %} Unrostered members</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ attention.unrostered }}</div>
<div class="text-xs opacity-60">Active members on no team</div>
</div>
</div>
<div class="card bg-base-100 shadow {% if attention.pending_approvals %}border-l-4 border-warning{% endif %}">
<div class="card bg-base-100 shadow border-l-4 {% if attention.pending_approvals %}border-warning{% else %}border-success{% endif %}">
<div class="card-body p-4">
<div class="flex items-center gap-2 text-sm opacity-70">{% lucide "clock" size=16 %} Pending</div>
<div class="text-3xl font-bold tabular-nums">{{ attention.pending_approvals }}</div>
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "clock" size=16 %} Pending</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ attention.pending_approvals }}</div>
<div class="text-xs opacity-60">Memberships awaiting approval</div>
</div>
</div>
</div>
<div class="mb-6 grid gap-4 lg:grid-cols-4">
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "sparkles" size=18 %} New members</h2>
<div class="text-4xl font-bold tabular-nums">{{ attention.new_members }}</div>
<div class="card bg-base-100 shadow border-l-4 border-info">
<div class="card-body p-4">
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "sparkles" size=16 %} New members</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ attention.new_members }}</div>
{# First season at this club — someone returning after a year away is a renewal. #}
<p class="text-sm opacity-70">first season at this club</p>
<div class="text-xs opacity-60">First season at this club</div>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "repeat" size=18 %} Renewal</h2>
{% if attention.renewal_rate is None %}
{# No prior season to compare against: a first-season club has not failed to renew anyone. #}
<p class="text-sm opacity-60">No previous season to compare against yet.</p>
{% else %}
<div class="text-4xl font-bold tabular-nums">{{ attention.renewal_rate }}%</div>
<p class="text-sm opacity-70">of last season's active members signed up again</p>
<progress class="progress progress-primary w-full" value="{{ attention.renewal_rate }}" max="100"></progress>
{% endif %}
<div class="card bg-base-100 shadow border-l-4 {% if attention.renewal_rate is None %}border-info{% elif attention.renewal_rate < 30 %}border-error{% elif attention.renewal_rate < 65 %}border-warning{% else %}border-success{% endif %}">
<div class="card-body p-4">
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "repeat" size=16 %} Renewal rate</div>
<div class="text-4xl font-bold tabular-nums font-mono">
{% if attention.renewal_rate is None %}
N/A
{% else %}
{{ attention.renewal_rate }}%
{% endif %}
</div>
<div class="text-xs opacity-60">
{% if attention.renewal_rate is None %}
No previous season
{% else %}
<progress class="progress w-full {% if attention.renewal_rate < 30 %}progress-error{% elif attention.renewal_rate < 65 %}progress-warning{% else %}progress-success{% endif %}" value="{{ attention.renewal_rate }}"
max="100"></progress>
{% endif %}
</div>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "user-check" size=18 %} Attendance</h2>
{% if attention.attendance.turnout is None %}
<p class="text-sm opacity-60">No past events with responses this season.</p>
{% else %}
<div class="text-4xl font-bold tabular-nums">{{ attention.attendance.turnout }}%</div>
<p class="text-sm opacity-70">turnout of those who answered</p>
<p class="mt-2 text-sm">
{# The leading indicator: it measures whether members use the app at all. #}
<span class="font-semibold {% if attention.attendance.no_response > 30 %}text-warning{% endif %}">{{ attention.attendance.no_response }}%</span>
<span class="opacity-70">never responded</span>
</p>
{% endif %}
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "hourglass" size=18 %} Unpaid, by age</h2>
<table class="table table-sm">
<tbody>
{% for bucket in attention.aging %}
<tr>
<td class="{% if bucket.overdue and bucket.total %}font-semibold text-error{% endif %}">{{ bucket.label }}</td>
<td class="text-right tabular-nums">€{{ bucket.total|floatformat:2 }}</td>
<td class="text-right opacity-60">{{ bucket.count }} order{{ bucket.count|pluralize }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="card bg-base-100 shadow border-l-4 {% if attention.attendance.turnout is None %}border-info{% elif attention.attendance.turnout < 30 %}border-error{% elif attention.attendance.turnout < 65 %}border-warning{% else %}border-success{% endif %}">
<div class="card-body p-4">
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "user-check" size=16 %} Attendance rate</div>
<div class="text-4xl font-bold tabular-nums font-mono">
{% if attention.attendance.turnout is None %}
N/A
{% else %}
{{ attention.attendance.turnout }}%
{% endif %}
</div>
<div class="text-xs opacity-60">
{% if attention.attendance.turnout is None %}
No events this season
{% else %}
<progress class="progress w-full {% if attention.attendance.turnout < 30 %}progress-error{% elif attention.attendance.turnout < 65 %}progress-warning{% else %}progress-success{% endif %}"
value="{{ attention.attendance.turnout }}" max="100"></progress>
{% endif %}
</div>
</div>
</div>
</div>
<div class="mb-6 grid gap-4 lg:grid-cols-2">
<div class="card bg-base-100 shadow">
<div class="card-body">
@@ -149,7 +155,7 @@
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "wallet" size=18 %} Fee status this season</h2>
<h2 class="card-title text-base">{% lucide "wallet" size=18 %} Club fee status this season</h2>
<div class="h-56">
<canvas id="fees-chart"></canvas>
</div>
@@ -157,7 +163,7 @@
</div>
</div>
<div class="mb-6 grid gap-4 md:grid-cols-2">
<div class="mb-6 grid gap-4 md:grid-cols-4">
{% for group in groups %}
<div class="card bg-base-100 shadow">
<div class="card-body">
@@ -166,7 +172,7 @@
{% for label, value in group.stats %}
<div class="flex items-center justify-between py-2">
<dt class="text-sm opacity-70">{{ label }}</dt>
<dd class="font-semibold tabular-nums">{{ value }}</dd>
<dd class="font-semibold tabular-nums font-mono">{% if group.title == "Shop" and label == "Outstanding" or label == "Revenue" %}&euro;{% endif %}{{ value }}</dd>
</div>
{% endfor %}
</dl>
@@ -178,7 +184,7 @@
<div class="card-body">
<div class="flex items-center justify-between">
<h2 class="card-title text-base">{% lucide "toggle-right" size=18 %} Features</h2>
<a class="btn btn-ghost btn-xs" href="{% url 'controlpanel:features' %}">Manage features</a>
<a class="btn btn-outline btn-sm gap-2" href="{% url 'controlpanel:features' %}">{% lucide "wrench" size=14 %} Manage features</a>
</div>
<div class="overflow-x-auto">
<table class="table">
@@ -233,7 +239,7 @@
<p class="text-sm opacity-70">This club is not billed for anything. Put it on a tier to start.</p>
{% else %}
<p class="text-sm opacity-70">
On <strong>{{ subscription.tier.name }}</strong>.
On plan <strong>{{ subscription.tier.name }}</strong>.
{% if subscription.auto_renew %}
Renews automatically 30 days before the period ends.
{% else %}
@@ -253,7 +259,7 @@
<th>Period</th>
<th class="text-right">Billed</th>
<th class="text-right">Paid</th>
<th>Status</th>
<th class="text-right">Status</th>
<th></th>
</tr>
</thead>
@@ -266,17 +272,17 @@
</td>
<td class="text-right tabular-nums">€{{ due.amount|floatformat:2 }}</td>
<td class="text-right tabular-nums">€{{ due.amount_paid|floatformat:2 }}</td>
<td>
<td class="text-right">
{% if due.status == "paid" %}
<span class="badge badge-success gap-1">{% lucide "check" size=12 %} Paid</span>
{% elif due.status == "waived" %}
<span class="badge badge-ghost">Waived</span>
<span class="badge badge-outline gap-1">{% lucide "check" size=12 %} Waived</span>
{% elif due.grace_until < today %}
<span class="badge badge-error gap-1">{% lucide "triangle-alert" size=12 %} Overdue</span>
{% elif due.period_end < today %}
<span class="badge badge-warning gap-1">{% lucide "hourglass" size=12 %} In grace</span>
{% else %}
<span class="badge badge-ghost">{{ due.get_status_display }}</span>
<span class="badge badge-outline">{{ due.get_status_display }}</span>
{% endif %}
</td>
<td class="text-right">
@@ -289,7 +295,7 @@
</form>
{% endif %}
{% endif %}
<a class="btn btn-ghost btn-xs gap-1" href="{% url 'controlpanel:due_invoice' due.pk %}">{% lucide "file-text" size=14 %} Invoice</a>
<a class="btn btn-accent btn-outline btn-sm gap-1" href="{% url 'controlpanel:due_invoice' due.pk %}">{% lucide "file-down" size=14 %} Download invoice</a>
</td>
</tr>
{% for payment in due.payments.all %}
@@ -317,7 +323,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="flex items-center justify-between">
<h2 class="card-title text-base">Club admins</h2>
<h2 class="card-title text-base">{% lucide "shield-user" size=18 %} Club admins</h2>
<a class="btn btn-primary btn-sm gap-2" href="{% url 'controlpanel:club_admin_add' club.pk %}">{% lucide "user-plus" size=16 %} Add admin</a>
</div>
<div class="overflow-x-auto">
@@ -337,7 +343,7 @@
<td class="text-right">
<form method="post" action="{% url 'controlpanel:club_admin_remove' club.pk role.pk %}">
{% csrf_token %}
<button class="btn btn-ghost btn-xs gap-1 text-error" type="submit">{% lucide "trash-2" size=14 %} Remove</button>
<button class="btn btn-error btn-outline btn-sm gap-1" type="submit">{% lucide "trash-2" size=14 %} Remove</button>
</form>
</td>
</tr>
@@ -372,17 +378,17 @@
data: {
labels: data.signups.map((point) => point.month),
datasets: [
{ label: "New", data: data.signups.map((point) => point.new), backgroundColor: css("--color-primary", "#4f46e5") },
{ label: "Returning", data: data.signups.map((point) => point.returning), backgroundColor: css("--color-accent", "#0ea5e9") },
{label: "New", data: data.signups.map((point) => point.new), backgroundColor: css("--color-primary", "#4f46e5")},
{label: "Returning", data: data.signups.map((point) => point.returning), backgroundColor: css("--color-accent", "#0ea5e9")},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: "bottom", labels: { color: ink } } },
plugins: {legend: {position: "bottom", labels: {color: ink}}},
scales: {
x: { stacked: true, ticks: { color: ink }, grid: { color: grid } },
y: { stacked: true, beginAtZero: true, ticks: { color: ink, precision: 0 }, grid: { color: grid } },
x: {stacked: true, ticks: {color: ink}, grid: {color: grid}},
y: {stacked: true, beginAtZero: true, ticks: {color: ink, precision: 0}, grid: {color: grid}},
},
},
});
@@ -403,7 +409,7 @@
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: "right", labels: { color: ink } } },
plugins: {legend: {position: "right", labels: {color: ink}}},
},
});
@@ -416,7 +422,7 @@
new MutationObserver(() => {
charts.forEach((chart) => chart.destroy());
charts = render();
}).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
}).observe(document.documentElement, {attributes: true, attributeFilter: ["data-theme"]});
})();
</script>
{% endblock extra_body %}

View File

@@ -4,28 +4,33 @@
{% block heading %}{% if object %}Edit {{ object }}{% else %}New club{% endif %}{% endblock heading %}
{% block panel %}
<div class="card max-w-xl bg-base-100 shadow">
<div class="card w-full bg-base-100 shadow">
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error my-2">
<span>{{ error }}</span>
</div>
{% endfor %}
{% for field in form %}
<div class="form-control my-3 w-full">
<label class="label" for="{{ field.id_for_label }}">
<span class="label-text">{{ field.label }}</span>
</label>
{{ field|daisy }}
{% if field.help_text %}<span class="label-text-alt mt-1 text-base-content/70">{{ field.help_text }}</span>{% endif %}
{% for error in field.errors %}<span class="label-text-alt mt-1 text-error">{{ error }}</span>{% endfor %}
</div>
{% endfor %}
<div class="card-actions justify-end pt-2">
<a class="btn btn-outline gap-2" href="{% url 'controlpanel:club_list' %}">{% lucide "arrow-left" size=16 %} Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for field in form %}
<div class="my-3 w-full">
<label class="label" for="{{ field.id_for_label }}">
<span class="label-text">{{ field.label }}</span>
</label>
{{ field|daisy }}
{% if field.help_text and not field.errors %}<span class="label-text-alt mt-1 text-base-content/70">{{ field.help_text }}</span>{% endif %}
{% for error in field.errors %}<span class="label-text-alt mt-1 text-error">{{ error }}</span>{% endfor %}
</div>
{% endfor %}
</div>
<div class="card-actions justify-start pt-2 mt-2">
<a class="btn btn-outline gap-2" href="{% if update_view %}{% url "controlpanel:club_detail" object.pk %}{% else %}{% url "controlpanel:club_list" %}{% endif %}">{% lucide "arrow-left" size=16 %} Cancel</a>
<button class="btn btn-primary gap-2" type="submit">{% lucide "save" size=16 %} Save</button>
</div>
</form>
</div>

View File

@@ -5,9 +5,9 @@
{% block actions %}
{% if show_archived %}
<a class="btn btn-ghost" href="{% url 'controlpanel:club_list' %}">Active clubs</a>
<a class="btn btn-outline" href="{% url 'controlpanel:club_list' %}">{% lucide "archive-x" size=16 %} Hide archived clubs</a>
{% else %}
<a class="btn btn-ghost" href="{% url 'controlpanel:club_list' %}?archived=1">Archived</a>
<a class="btn btn-outline" href="{% url 'controlpanel:club_list' %}?archived=1">{% lucide "archive" size=16 %} Show archived clubs</a>
{% endif %}
<a class="btn btn-primary gap-2" href="{% url 'controlpanel:club_create' %}">{% lucide "plus" size=16 %} New club</a>
{% endblock actions %}
@@ -15,12 +15,19 @@
{% block panel %}
<form method="get" class="mb-4 flex gap-2">
{% if show_archived %}<input type="hidden" name="archived" value="1">{% endif %}
<input type="search"
name="q"
value="{{ search }}"
placeholder="Search clubs…"
class="input input-bordered w-full max-w-xs">
<button class="btn gap-2" type="submit">{% lucide "search" size=16 %} Search</button>
<label class="input">
<span class="opacity-50">{% lucide "search" size=16 %}</span>
<input type="search"
name="q"
value="{{ search }}"
placeholder="Search clubs…"
class="input input-bordered w-full max-w-xs">
</label>
<button class="btn btn-outline gap-2" type="submit">{% lucide "search" size=16 %} Search</button>
{% if search %}
<a class="btn btn-primary gap-2" href="{% url "controlpanel:club_list" %}">{% lucide "x" size=16 %} Clear filter</a>
{% endif %}
</form>
<div class="card bg-base-100 shadow">
<div class="card-body">

View File

@@ -91,7 +91,7 @@
<div>
<div class="mb-1 flex items-center justify-between text-sm">
<span class="flex items-center gap-2">{% lucide step.icon size=14 %} {{ step.label }}</span>
<span class="font-semibold tabular-nums">{{ step.count }}</span>
<span class="font-semibold tabular-nums font-mono">{{ step.count }}</span>
</div>
<progress class="progress {% if step.count == funnel.0.count %}progress-success{% elif step.count == 0 %}progress-error{% else %}progress-warning{% endif %} w-full" value="{{ step.count }}"
max="{{ funnel.0.count }}"></progress>

View File

@@ -18,11 +18,11 @@ DEFAULT_WIDGET_CLASS = "input input-bordered w-full"
#: Icon, default heading and daisyUI colour per message level.
MESSAGE_ALERTS = {
"debug": ("bug", "Debug", "alert-info"),
"info": ("info", "Heads up", "alert-info"),
"success": ("circle-check", "Done", "alert-success"),
"warning": ("triangle-alert", "Careful", "alert-warning"),
"error": ("circle-x", "Something went wrong", "alert-error"),
"debug": ("bug", "Debug", "alert-info border-info"),
"info": ("info", "Heads up", "alert-info border-info"),
"success": ("circle-check", "Done", "alert-success border-success"),
"warning": ("triangle-alert", "Careful", "alert-warning border-warning"),
"error": ("circle-x", "Something went wrong", "alert-error border-error"),
}
DEFAULT_MESSAGE_ALERT = MESSAGE_ALERTS["info"]

View File

@@ -80,6 +80,9 @@ class ClubCreateView(PlatformStaffRequiredMixin, CreateView):
def get_success_url(self):
return reverse("controlpanel:club_detail", args=[self.object.pk])
def get_context_data(self, **kwargs):
return super().get_context_data(nav="clubs", **kwargs)
class ClubUpdateView(PlatformStaffRequiredMixin, UpdateView):
model = Club
@@ -94,6 +97,9 @@ class ClubUpdateView(PlatformStaffRequiredMixin, UpdateView):
def get_success_url(self):
return reverse("controlpanel:club_detail", args=[self.object.pk])
def get_context_data(self, **kwargs):
return super().get_context_data(nav="clubs", update_view=True, **kwargs)
class ClubDetailView(PlatformStaffRequiredMixin, DetailView):
model = Club

File diff suppressed because one or more lines are too long

View File

@@ -85,7 +85,7 @@
<div class="mb-6 w-full space-y-2">
{% for message in messages %}
{% with alert=message|as_alert %}
<div class="alert alert-soft {{ alert.css }}" role="alert">
<div class="alert alert-soft border-2 {{ alert.css }}" role="alert">
{% lucide alert.icon size=20 %}
<div>
<div class="font-bold">{{ alert.title }}</div>