Files
Kurt_kalendar/ht_booking/assets/views/calendar/week.html
2026-05-16 19:17:12 +02:00

348 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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; }
/* Mobile: show a small window of days (2 on a narrow phone, 3 otherwise)
instead of the full week. JS marks the visible columns with .cal-day-on
and the day navigator slides the window; before JS runs, the first three
days stand in. Wider screens keep the full 7-day grid. */
@media (max-width: 767px) {
#cal thead { display: none; }
#cal .cal-day { display: none; }
#cal .cal-day-0,
#cal .cal-day-1,
#cal .cal-day-2 { display: table-cell; }
#cal.cal-js .cal-day { display: none; }
#cal.cal-js .cal-day.cal-day-on { display: table-cell; }
/* Slim the hour column to its compact label so bookings get the width. */
#cal tbody td:first-child {
width: 1%;
white-space: nowrap;
padding: 0.3rem 0.4rem;
font-size: 0.7rem;
line-height: 1.15;
}
}
</style>
{% if is_admin %}
<style>
/* Admin calendar — strict, content-independent column widths. A long
booking can never widen its column; it grows taller instead. (The public
calendar has merged free rows with a colspan, so it keeps auto layout.) */
#cal { table-layout: fixed; }
@media (max-width: 767px) {
#cal tbody td:first-child { width: 3.25rem; }
}
/* Detailed-view toggle. */
.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 { 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;
}
/* Detailed fields wrap to as many lines as needed — the full booking info
shows and the cell just grows taller, never wider. */
#cal.cal--detailed .cal-name,
#cal.cal--detailed .cal-detail > span {
white-space: normal;
overflow-wrap: anywhere;
}
/* A long note is capped with an ellipsis so one booking can't run away. */
#cal.cal--detailed .cal-note {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 6;
overflow: hidden;
}
</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">
{% if can_prev %}
<a href="{{ base_path }}?court={{ court_id }}&week={{ prev_week }}"
class="btn btn-sm join-item">«<span class="ml-1 hidden sm:inline">{{ t(key="prev-week", lang=lang) }}</span></a>
{% else %}
<span class="btn btn-sm join-item btn-disabled" aria-disabled="true">«<span
class="ml-1 hidden sm:inline">{{ t(key="prev-week", lang=lang) }}</span></span>
{% endif %}
<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"><span class="mr-1 hidden sm:inline">{{ t(key="next-week", lang=lang) }}</span>»</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>
<span class="hidden sm:inline">{{ t(key="view-details", lang=lang) }}</span>
</button>
<form method="get" action="{{ base_path }}" class="relative inline-flex">
<input type="hidden" name="court" value="{{ court_id }}">
<button type="button" id="cal-jump-btn" class="btn btn-sm gap-1"
title="{{ t(key='pick-week', 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="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />
</svg>
<span class="hidden sm:inline">{{ t(key="pick-week", lang=lang) }}</span>
</button>
<input type="date" name="week" id="cal-jump" value="{{ week }}"
onchange="this.form.submit()" tabindex="-1" aria-hidden="true"
class="pointer-events-none absolute inset-0 -z-10 opacity-0">
</form>
{% endif %}
</div>
<div class="text-sm font-medium opacity-60">{{ court_name }} · {{ week_label }}</div>
</div>
<div id="cal-daynav" class="mb-3 flex items-center gap-2 md:hidden">
<button type="button" id="cal-day-prev" class="btn btn-square"
aria-label="{{ t(key='prev-day', lang=lang) }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
</button>
<div id="cal-daynav-label" class="flex-1 text-center text-base font-semibold"></div>
<button type="button" id="cal-day-next" class="btn btn-square"
aria-label="{{ t(key='next-day', lang=lang) }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</button>
</div>
<div class="overflow-x-auto border border-base-300 bg-base-100 shadow-sm">
<table id="cal" data-day="{{ mobile_day }}" 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="cal-day cal-day-{{ loop.index0 }} border border-base-300 p-2 text-center"
data-d="{{ t(key=d.key, lang=lang) }} {{ d.num }}">
<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="cal-day cal-day-{{ loop.index0 }} 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="cal-note 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 %}
<script>
// Mobile: show a sliding window of days (2 on a narrow phone, 3 otherwise)
// rather than the whole week. The navigator buttons and a horizontal swipe
// slide the window; a resize re-fits it. Public and admin calendars alike.
(function () {
var cal = document.getElementById('cal');
var nav = document.getElementById('cal-daynav');
if (!cal || !nav) return;
var label = document.getElementById('cal-daynav-label');
var prevBtn = document.getElementById('cal-day-prev');
var nextBtn = document.getElementById('cal-day-next');
var dayLabels = [].slice.call(cal.querySelectorAll('thead th[data-d]'))
.map(function (th) { return th.getAttribute('data-d'); });
var start = parseInt(cal.getAttribute('data-day'), 10) || 0;
cal.classList.add('cal-js');
function windowSize() {
return window.innerWidth < 480 ? 2 : 3;
}
function apply() {
var size = windowSize();
var maxStart = 7 - size;
if (start > maxStart) start = maxStart;
if (start < 0) start = 0;
for (var d = 0; d < 7; d++) {
var on = d >= start && d < start + size;
var cells = cal.querySelectorAll('.cal-day-' + d);
for (var i = 0; i < cells.length; i++) {
cells[i].classList.toggle('cal-day-on', on);
}
}
var last = start + size - 1;
label.textContent = (dayLabels[start] || '') + ' ' + (dayLabels[last] || '');
prevBtn.disabled = start <= 0;
nextBtn.disabled = start >= maxStart;
}
function slide(delta) { start += delta; apply(); }
prevBtn.addEventListener('click', function () { slide(-1); });
nextBtn.addEventListener('click', function () { slide(1); });
window.addEventListener('resize', apply);
var x0 = null, y0 = null;
cal.addEventListener('touchstart', function (e) {
x0 = e.touches[0].clientX;
y0 = e.touches[0].clientY;
}, { passive: true });
cal.addEventListener('touchend', function (e) {
if (x0 === null) return;
var dx = e.changedTouches[0].clientX - x0;
var dy = e.changedTouches[0].clientY - y0;
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) slide(dx < 0 ? 1 : -1);
x0 = y0 = null;
}, { passive: true });
apply();
})();
</script>
{% 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>
<script>
// "Go to week" — open the native date picker; picking any day loads the
// week it belongs to. Admin only.
(function () {
var btn = document.getElementById('cal-jump-btn');
var input = document.getElementById('cal-jump');
if (!btn || !input) return;
btn.addEventListener('click', function () {
if (typeof input.showPicker === 'function') {
input.showPicker();
} else {
input.focus();
input.click();
}
});
})();
</script>
{% endif %}
{% endblock js %}