Adds the Week/Month/Season calendar to the Events page (team/group-scoped visibility, kind-colored blocks, a repeating-series indicator) alongside the existing table as a List view, and redesigns event/event-series creation around a kind-first picker with progressive disclosure instead of one long flat form. Also: an accessible radio-based kind picker (no redundant select-next-to-cards), a fixed week-grid layout bug, a filterable single-list redesign of the event detail page's RSVP/responses modal, a tidied-up recurrence field layout, and assorted button/field height and width fixes across the referee panel and event forms. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
272 lines
15 KiB
HTML
272 lines
15 KiB
HTML
{% extends "management/base.html" %}
|
|
{% load i18n lucide %}
|
|
|
|
{% block heading %}{{ event.title }}{% endblock heading %}
|
|
|
|
{% block topbar_context %}
|
|
<span class="badge {% if event.kind == "game" %}badge-error{% elif event.kind == "training" %}badge-info{% elif event.kind == "tournament" %}badge-warning{% elif event.kind == "meeting" %}badge-neutral{% elif event.kind == "social" %}border-violet/30 bg-violet/10 text-violet{% else %}badge-outline{% endif %}">
|
|
{{ event.get_kind_display }}
|
|
</span>
|
|
{% if event.is_live %}<span class="badge badge-error gap-1 animate-pulse">{% lucide "circle" size=10 %} {% trans "Live" %}</span>{% endif %}
|
|
{% endblock topbar_context %}
|
|
|
|
{% block actions %}
|
|
{% if can_manage %}
|
|
<a class="btn btn-outline gap-2" href="{% url 'management:event_update' event.pk %}">{% lucide "pencil" size=16 %} {% trans "Edit" %}</a>
|
|
{% if event.series_id and not event.detached %}
|
|
<form method="post" action="{% url 'management:event_detach' event.pk %}">
|
|
{% csrf_token %}
|
|
<button class="btn btn-outline gap-2" type="submit">{% lucide "unlink" size=16 %} {% trans "Detach from series" %}</button>
|
|
</form>
|
|
{% endif %}
|
|
<button class="btn btn-outline btn-error gap-2" type="button" onclick="document.getElementById('event_delete_modal').showModal()">
|
|
{% lucide "trash-2" size=16 %}
|
|
{% if event.series_id %}{% trans "Cancel occurrence" %}{% else %}{% trans "Delete" %}{% endif %}
|
|
</button>
|
|
{% endif %}
|
|
{% endblock actions %}
|
|
|
|
{% block panel %}
|
|
{% if event.series_id %}
|
|
<div class="alert alert-info">
|
|
{% lucide "repeat" size=20 %}
|
|
<span>
|
|
{% blocktrans with series=event.series %}Part of the recurring series “{{ series }}”.{% endblocktrans %}
|
|
<a class="link" href="{% url 'management:event_series_detail' event.series_id %}">{% trans "View series" %}</a>
|
|
{% if event.detached %}<span class="badge badge-sm badge-neutral ml-2">{% trans "Detached" %}</span>{% endif %}
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="grid gap-4 lg:grid-cols-2">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-base">{% lucide "info" size=18 %} {% trans "Details" %}</h2>
|
|
<dl>
|
|
<div class="flex items-center justify-between gap-4 py-2">
|
|
<dt class="text-sm text-muted">{% trans "Start" %}</dt>
|
|
<dd class="font-mono text-sm text-ink">{{ event.start|date:"j M Y H:i" }}</dd>
|
|
</div>
|
|
<div class="flex items-center justify-between gap-4 py-2">
|
|
<dt class="text-sm text-muted">{% trans "End" %}</dt>
|
|
<dd class="font-mono text-sm text-ink">{{ event.end|date:"j M Y H:i"|default:"—" }}</dd>
|
|
</div>
|
|
<div class="flex items-center justify-between gap-4 py-2">
|
|
<dt class="text-sm text-muted">{% trans "Gathering" %}</dt>
|
|
<dd class="font-mono text-sm text-ink">{{ event.gathering|date:"j M Y H:i"|default:"—" }}</dd>
|
|
</div>
|
|
<div class="flex items-center justify-between gap-4 py-2">
|
|
<dt class="text-sm text-muted">{% trans "Registration deadline" %}</dt>
|
|
<dd class="font-mono text-sm text-ink">{{ event.deadline|date:"j M Y H:i"|default:"—" }}</dd>
|
|
</div>
|
|
<div class="flex items-center justify-between gap-4 py-2">
|
|
<dt class="text-sm text-muted">{% trans "Location" %}</dt>
|
|
<dd class="text-sm text-ink">{{ event.location|default:"—" }}</dd>
|
|
</div>
|
|
<div class="flex items-center justify-between gap-4 py-2">
|
|
<dt class="text-sm text-muted">{% trans "Opponent" %}</dt>
|
|
<dd class="text-sm text-ink">{{ event.opponent|default:"—" }}</dd>
|
|
</div>
|
|
<div class="flex items-center justify-between gap-4 py-2">
|
|
<dt class="text-sm text-muted">{% trans "Teams" %}</dt>
|
|
<dd class="text-sm text-ink">{% for team in event.teams.all %}{{ team.name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}</dd>
|
|
</div>
|
|
<div class="flex items-center justify-between gap-4 py-2">
|
|
<dt class="text-sm text-muted">{% trans "Groups" %}</dt>
|
|
<dd class="text-sm text-ink">{% for group in event.groups.all %}{{ group.name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}</dd>
|
|
</div>
|
|
{% if event.club_wide %}
|
|
<div class="flex items-center justify-between gap-4 py-2">
|
|
<dt class="text-sm text-muted">{% trans "Audience" %}</dt>
|
|
<dd><span class="badge badge-neutral">{% trans "Whole club" %}</span></dd>
|
|
</div>
|
|
{% endif %}
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
<h2 class="card-title text-base">{% lucide "user-check" size=18 %} {% trans "RSVPs" %}</h2>
|
|
{% if has_attendance_rows %}
|
|
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('rsvp_modal').showModal()">
|
|
{% lucide "list" size=14 %} {% trans "View responses" %}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
<dl>
|
|
{% for group in attendance_groups %}
|
|
<div class="flex items-center justify-between gap-4 py-2">
|
|
<dt class="text-sm text-muted">{{ group.label }}</dt>
|
|
<dd class="font-mono text-sm font-semibold text-ink tabular-nums">{{ group.rows|length }}</dd>
|
|
</div>
|
|
{% endfor %}
|
|
</dl>
|
|
{% if not has_attendance_rows %}
|
|
<p class="text-sm text-muted">{% trans "No one invited yet." %}</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if event.kind == "game" %}
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
<h2 class="card-title text-base">{% lucide "trophy" size=18 %} {% trans "Game" %}</h2>
|
|
{% if can_manage and event.competition %}
|
|
<form method="post" action="{% url 'management:event_fetch_game_info' event.pk %}">
|
|
{% csrf_token %}
|
|
<button class="btn btn-outline btn-sm gap-2" type="submit">{% lucide "refresh-cw" size=14 %} {% trans "Fetch new game info" %}</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<div class="text-sm text-muted">{% trans "Score" %}</div>
|
|
<div class="font-mono text-sm font-semibold tabular-nums text-ink">
|
|
{% if event.score_for is not None and event.score_against is not None %}{{ event.score_for }} - {{ event.score_against }}{% else %}—{% endif %}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-muted">{% trans "Competition" %}</div>
|
|
<div class="text-sm text-ink">{{ event.competition|default:"—" }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-muted">{% trans "External game ID" %}</div>
|
|
<div class="font-mono text-sm text-ink">{{ event.external_game_id|default:"—" }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if event.is_home_game and not referee_management_needed %}
|
|
<div class="alert alert-info">
|
|
{% lucide "info" size=20 %}
|
|
<span>{% trans "Referees for this game are managed by the federation, not the club." %}</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if referee_management_needed %}
|
|
<div class="card">
|
|
<div class="card-body">
|
|
{% include "management/_referee_assignment_panel.html" %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock panel %}
|
|
|
|
{% block extra_body %}
|
|
{% if can_manage %}
|
|
<dialog id="event_delete_modal" class="modal">
|
|
<div class="modal-box">
|
|
<h3 class="text-lg font-bold text-ink">
|
|
{% if event.series_id %}{% trans "Cancel occurrence" %}{% else %}{% trans "Delete event" %}{% endif %}
|
|
</h3>
|
|
<form method="post" action="{% url 'management:event_delete' event.pk %}" id="event_delete_form">
|
|
{% csrf_token %}
|
|
{% if event.series_id %}
|
|
<p class="py-2 text-sm text-muted">{% trans "Removes this occurrence from the schedule. It won't be regenerated." %}</p>
|
|
<label class="flex cursor-pointer items-start gap-2 text-sm text-slate">
|
|
<input type="checkbox" name="keep_record" class="checkbox mt-0.5">
|
|
<span>{% trans "Keep a record of it (marks it cancelled instead of deleting it)" %}</span>
|
|
</label>
|
|
{% else %}
|
|
<p class="py-2 text-sm text-muted">{% trans "This cannot be undone." %}</p>
|
|
{% endif %}
|
|
</form>
|
|
<div class="modal-action">
|
|
<form method="dialog">
|
|
<button class="btn btn-outline gap-2">{% lucide "x" size=16 %} {% trans "Cancel" %}</button>
|
|
</form>
|
|
<button class="btn btn-error gap-2" type="submit" form="event_delete_form">{% lucide "trash-2" size=16 %} {% trans "Confirm" %}</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button>close</button>
|
|
</form>
|
|
</dialog>
|
|
{% endif %}
|
|
|
|
{% if has_attendance_rows %}
|
|
<dialog id="rsvp_modal" class="modal">
|
|
<div class="modal-box max-w-3xl">
|
|
<h3 class="text-lg font-bold text-ink">{% trans "Who responded" %}</h3>
|
|
|
|
{# One flat, filterable list beats a stack of collapsed accordions you have to open one at a time to see the full picture -- these pills just show/hide rows client-side, no reload. #}
|
|
<div class="mt-3 flex flex-wrap gap-1.5">
|
|
<button type="button" class="btn btn-primary btn-sm gap-1.5 rsvp-filter" data-status="">
|
|
{% trans "All" %} <span class="badge badge-sm badge-ghost">{{ attendance_row_count }}</span>
|
|
</button>
|
|
{% for group in attendance_groups %}
|
|
{% if group.rows %}
|
|
<button type="button" class="btn btn-outline btn-sm gap-1.5 rsvp-filter" data-status="{{ group.value }}">
|
|
{{ group.label }} <span class="badge badge-sm badge-ghost">{{ group.rows|length }}</span>
|
|
</button>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="mt-3 max-h-96 overflow-y-auto rounded-lg border border-line">
|
|
<table class="table">
|
|
<tbody>
|
|
{% for group in attendance_groups %}
|
|
{% for row in group.rows %}
|
|
<tr data-status="{{ group.value }}">
|
|
<td class="w-px">
|
|
<span class="avatar avatar-placeholder shrink-0">
|
|
<div class="h-7 w-7 rounded-full text-xs">{{ row.member.first_name|slice:":1" }}{{ row.member.last_name|slice:":1" }}</div>
|
|
</span>
|
|
</td>
|
|
<td class="font-semibold text-ink">{{ row.member }}</td>
|
|
<td>
|
|
<span class="badge badge-sm
|
|
{% if group.value == "present" %}badge-success
|
|
{% elif group.value == "absent" %}badge-error
|
|
{% elif group.value == "excused" %}badge-warning
|
|
{% elif group.value == "selected" %}badge-info
|
|
{% elif group.value == "maybe" %}badge-warning
|
|
{% elif group.value == "not_selected" %}badge-neutral
|
|
{% else %}badge-outline{% endif %}">
|
|
{{ group.label }}
|
|
</span>
|
|
</td>
|
|
<td class="text-sm text-muted">{{ row.note|default:"—" }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<form method="dialog">
|
|
<button class="btn btn-outline gap-2">{% lucide "x" size=16 %} {% trans "Close" %}</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button>close</button>
|
|
</form>
|
|
</dialog>
|
|
<script>
|
|
(() => {
|
|
const modal = document.getElementById("rsvp_modal");
|
|
const pills = modal.querySelectorAll(".rsvp-filter");
|
|
const rows = modal.querySelectorAll("tbody tr[data-status]");
|
|
pills.forEach((pill) => {
|
|
pill.addEventListener("click", () => {
|
|
pills.forEach((other) => {
|
|
other.classList.toggle("btn-primary", other === pill);
|
|
other.classList.toggle("btn-outline", other !== pill);
|
|
});
|
|
rows.forEach((row) => row.classList.toggle("hidden", pill.dataset.status !== "" && row.dataset.status !== pill.dataset.status));
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
{% endif %}
|
|
{% endblock extra_body %}
|