195 lines
7.7 KiB
HTML
195 lines
7.7 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ t(key="calendar-title", lang=lang) }}{% endblock title %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
/* Sharpen the calendar grid. The daisyUI base tones give very low-contrast
|
|
hairline borders and dimmed text in both themes. Tying the borders and
|
|
the header tint to base-content keeps the table legible and easy to
|
|
follow on light and dark alike (public + admin calendars). */
|
|
#cal th,
|
|
#cal td {
|
|
border-color: hsl(var(--bc) / 0.25);
|
|
}
|
|
#cal thead tr,
|
|
#cal tbody td:first-child {
|
|
background-color: hsl(var(--bc) / 0.1);
|
|
}
|
|
#cal tbody td:first-child { opacity: 1; }
|
|
#cal thead .opacity-50 { opacity: 0.8; }
|
|
#cal td .opacity-30 { opacity: 0.7; }
|
|
#cal td a.opacity-30:hover { opacity: 1; }
|
|
</style>
|
|
{% if is_admin %}
|
|
<style>
|
|
/* Admin calendar — detailed-view toggle. The public calendar is unaffected. */
|
|
.cal-detail { display: none; }
|
|
#cal.cal--detailed .cal-chip {
|
|
position: static;
|
|
inset: auto;
|
|
margin: 0.25rem;
|
|
min-height: 3rem;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
justify-content: center;
|
|
}
|
|
#cal.cal--detailed .cal-name {
|
|
white-space: normal;
|
|
font-weight: 600;
|
|
}
|
|
#cal.cal--detailed .cal-detail {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1px;
|
|
margin-top: 2px;
|
|
font-weight: 400;
|
|
line-height: 1.25;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
</style>
|
|
{% endif %}
|
|
{% endblock head %}
|
|
|
|
{% block content %}
|
|
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
|
<h1 class="text-2xl font-bold">
|
|
{% if is_admin %}{{ t(key="admin-title", lang=lang) }}{% else %}{{ t(key="calendar-title", lang=lang) }}{% endif %}
|
|
</h1>
|
|
{% if has_courts %}
|
|
<form method="get" action="{{ base_path }}" class="flex items-center gap-2">
|
|
<label class="text-sm font-medium opacity-70">{{ t(key="court-label", lang=lang) }}</label>
|
|
<input type="hidden" name="week" value="{{ week }}">
|
|
<select name="court" onchange="this.form.submit()" class="select select-bordered select-sm">
|
|
{% for c in courts %}
|
|
<option value="{{ c.id }}" {% if c.selected %}selected{% endif %}>{{ c.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if has_courts %}
|
|
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<div class="join">
|
|
<a href="{{ base_path }}?court={{ court_id }}&week={{ prev_week }}"
|
|
class="btn btn-sm join-item">« {{ t(key="prev-week", lang=lang) }}</a>
|
|
<a href="{{ base_path }}?court={{ court_id }}&week={{ this_week }}"
|
|
class="btn btn-sm join-item">{{ t(key="this-week", lang=lang) }}</a>
|
|
<a href="{{ base_path }}?court={{ court_id }}&week={{ next_week }}"
|
|
class="btn btn-sm join-item">{{ t(key="next-week", lang=lang) }} »</a>
|
|
</div>
|
|
{% if is_admin %}
|
|
<button type="button" id="cal-detail-toggle" class="btn btn-sm gap-1" aria-pressed="false"
|
|
title="{{ t(key='view-details', lang=lang) }}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
|
stroke="currentColor" class="h-4 w-4">
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
|
</svg>
|
|
{{ t(key="view-details", lang=lang) }}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
<div class="text-sm font-medium opacity-60">{{ court_name }} · {{ week_label }}</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto border border-base-300 bg-base-100 shadow-sm">
|
|
<table id="cal" class="w-full border-collapse text-sm">
|
|
<thead>
|
|
<tr class="bg-base-200">
|
|
<th class="w-16 border border-base-300 p-2"></th>
|
|
{% for d in days %}
|
|
<th class="border border-base-300 p-2 text-center">
|
|
<div class="font-semibold">{{ t(key=d.key, lang=lang) }}</div>
|
|
<div class="text-xs font-normal opacity-50">{{ d.num }}</div>
|
|
</th>
|
|
{% endfor %}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in rows %}
|
|
{% if row.free_group %}
|
|
<tr>
|
|
<td class="border border-base-300 bg-base-200 px-2 py-1 text-center text-xs font-medium">{{ row.hour_label }}</td>
|
|
<td colspan="7" class="border border-base-300 px-2 py-1">
|
|
<div class="text-center text-xs uppercase tracking-wide opacity-30">{{ t(key="free", lang=lang) }}</div>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td class="border border-base-300 bg-base-200 p-2 text-center font-medium opacity-70">{{ row.hour_label }}</td>
|
|
{% for cell in row.cells %}
|
|
<td class="relative h-14 border border-base-300">
|
|
{% if cell.booked %}
|
|
{% if is_admin %}
|
|
<a href="/admin/booking/{{ cell.booking_id }}"
|
|
class="cal-chip absolute inset-1 flex items-center overflow-hidden rounded px-1.5 text-xs font-medium text-white shadow-sm transition hover:opacity-90"
|
|
style="background-color: {{ cell.color }}">
|
|
<span class="cal-name min-w-0 truncate">{{ cell.name }}</span>
|
|
{% if cell.title or cell.contact or cell.note %}
|
|
<span class="cal-detail">
|
|
{%- if cell.title %}<span class="truncate opacity-95">{{ cell.title }}</span>{% endif -%}
|
|
{%- if cell.contact %}<span class="truncate opacity-80">{{ cell.contact }}</span>{% endif -%}
|
|
{%- if cell.note %}<span class="opacity-80">{{ cell.note }}</span>{% endif -%}
|
|
</span>
|
|
{% endif %}
|
|
</a>
|
|
{% else %}
|
|
<div class="absolute inset-1 flex items-center overflow-hidden rounded px-1.5 text-xs font-medium text-white shadow-sm"
|
|
style="background-color: {{ cell.color }}"{% if cell.title %} title="{{ cell.title }}"{% endif %}>
|
|
<span class="min-w-0 truncate">
|
|
{%- if cell.title %}{{ cell.title }}{% else %}{{ t(key="booked", lang=lang) }}{% endif -%}
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
{% if is_admin %}
|
|
<a href="/admin/booking?court={{ court_id }}&date={{ cell.date }}&hour={{ cell.hour }}"
|
|
class="absolute inset-0 flex items-center justify-center text-lg opacity-30 transition hover:bg-base-200 hover:opacity-100">+</a>
|
|
{% else %}
|
|
<div class="absolute inset-0 flex items-center justify-center text-xs opacity-30">{{ t(key="free", lang=lang) }}</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</td>
|
|
{% endfor %}
|
|
</tr>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
|
<div class="card-body items-center text-center opacity-60">{{ t(key="no-courts", lang=lang) }}</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock content %}
|
|
|
|
{% block js %}
|
|
{% if is_admin %}
|
|
<script>
|
|
// Detailed-view toggle for the admin calendar. The choice is remembered
|
|
// per browser; the public calendar never loads this script.
|
|
(function () {
|
|
var KEY = 'cal_detailed';
|
|
var tbl = document.getElementById('cal');
|
|
var btn = document.getElementById('cal-detail-toggle');
|
|
if (!tbl || !btn) return;
|
|
function apply(on) {
|
|
tbl.classList.toggle('cal--detailed', on);
|
|
btn.classList.toggle('btn-active', on);
|
|
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
}
|
|
apply(localStorage.getItem(KEY) === '1');
|
|
btn.addEventListener('click', function () {
|
|
var on = !tbl.classList.contains('cal--detailed');
|
|
localStorage.setItem(KEY, on ? '1' : '0');
|
|
apply(on);
|
|
});
|
|
})();
|
|
</script>
|
|
{% endif %}
|
|
{% endblock js %}
|