Erste Version: HCRB Spieltag-Kalender und Fahrtplaner

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-10 13:54:33 +02:00
commit 10400eee59
30 files changed
+3771

No files matched your search

+182
View File
@@ -0,0 +1,182 @@
import {
$, api, esc, sortByName, showError, renderHeader,
toIso, fromIso, formatDate, getMe, setMe,
} from './common.js';
renderHeader('kalender');
let members = [];
let events = [];
let me = getMe();
const todayIso = toIso(new Date());
let selectedDay = todayIso;
let viewMonth = fromIso(todayIso);
viewMonth.setDate(1);
const memberName = (id) => members.find((m) => m.id === id)?.name ?? '(gelöscht)';
// ---------- "Ich bin" ----------
function renderMe() {
if (me && !members.some((m) => m.id === me)) me = null;
$('#me-select').innerHTML = '<option value="">– bitte auswählen –</option>' +
sortByName(members).map((m) =>
`<option value="${m.id}" ${m.id === me ? 'selected' : ''}>${esc(m.name)}</option>`).join('');
$('#me-hint').hidden = !!me;
$('#event-form button[type=submit]').disabled = !me;
}
$('#me-select').addEventListener('change', (e) => {
me = e.target.value || null;
setMe(me);
renderAll();
});
// ---------- Monatsansicht ----------
function renderCalendar() {
const title = viewMonth.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
$('#month-title').textContent = title;
// Raster beginnt am Montag vor (oder am) Monatsersten
const start = new Date(viewMonth);
start.setDate(1 - ((start.getDay() + 6) % 7));
const byDate = new Map();
for (const ev of events) {
if (!byDate.has(ev.date)) byDate.set(ev.date, []);
byDate.get(ev.date).push(ev);
}
let html = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map((d) => `<div class="dow">${d}</div>`).join('');
const day = new Date(start);
for (let i = 0; i < 42; i++) {
const iso = toIso(day);
const classes = ['cal-day'];
if (day.getMonth() !== viewMonth.getMonth()) classes.push('other');
if (iso === todayIso) classes.push('today');
if (iso === selectedDay) classes.push('selected');
const dayEvents = byDate.get(iso) || [];
html += `
<button class="${classes.join(' ')}" data-day="${iso}">
<span class="num">${day.getDate()}</span>
${dayEvents.map((ev) => `<span class="ev ${ev.attendees.includes(me) ? 'mine' : ''}"
title="${esc(ev.title)} (${ev.attendees.length} zugesagt)">${esc(ev.title)} · ${ev.attendees.length}</span>`).join('')}
</button>`;
day.setDate(day.getDate() + 1);
}
$('#calendar').innerHTML = html;
}
$('#calendar').addEventListener('click', (e) => {
const cell = e.target.closest('[data-day]');
if (!cell) return;
selectedDay = cell.dataset.day;
const d = fromIso(selectedDay);
if (d.getMonth() !== viewMonth.getMonth()) viewMonth = new Date(d.getFullYear(), d.getMonth(), 1);
renderAll();
$('#day-title').scrollIntoView({ behavior: 'smooth', block: 'start' });
});
function shiftMonth(delta) {
viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() + delta, 1);
renderCalendar();
}
$('#prev-month').addEventListener('click', () => shiftMonth(-1));
$('#next-month').addEventListener('click', () => shiftMonth(1));
$('#today-btn').addEventListener('click', () => {
selectedDay = todayIso;
viewMonth = fromIso(todayIso);
viewMonth.setDate(1);
renderAll();
});
// ---------- Termine ----------
function eventHtml(ev) {
const going = ev.attendees.includes(me);
const attendees = sortByName(ev.attendees.map((id) => ({ id, name: memberName(id) })));
return `
<div class="event">
<h3>${esc(ev.title)}</h3>
<div class="when">${formatDate(ev.date, true)}${ev.createdBy ? ` · eingetragen von ${esc(memberName(ev.createdBy))}` : ''}</div>
${ev.note ? `<div class="note">${esc(ev.note)}</div>` : ''}
<div class="muted">${ev.attendees.length} zugesagt:</div>
<div class="chips">
${attendees.length
? attendees.map((a) => `<span class="chip ${a.id === me ? 'me' : ''}">${esc(a.name)}</span>`).join('')
: '<span class="muted">noch niemand</span>'}
</div>
<div class="row wrap">
${me ? (going
? `<button data-action="leave" data-id="${ev.id}">Absagen</button>`
: `<button class="primary" data-action="join" data-id="${ev.id}">Ich bin dabei</button>`) : ''}
<a class="button" href="/fahrt?termin=${ev.id}">Fahrt planen</a>
<button class="link danger" data-action="delete" data-id="${ev.id}">Löschen</button>
</div>
</div>`;
}
function renderDay() {
$('#day-title').textContent = fromIso(selectedDay).toLocaleDateString('de-DE', {
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric',
});
const dayEvents = events.filter((e) => e.date === selectedDay);
$('#day-events').innerHTML = dayEvents.length
? dayEvents.map(eventHtml).join('')
: '<p class="muted">An diesem Tag ist noch kein Spieltag eingetragen.</p>';
}
function renderUpcoming() {
const upcoming = events.filter((e) => e.date >= todayIso);
$('#upcoming').innerHTML = upcoming.length
? upcoming.map(eventHtml).join('')
: '<p class="muted">Keine kommenden Spieltage.</p>';
}
document.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const { action, id } = btn.dataset;
try {
if (action === 'join') await api('PUT', `/events/${id}/attendees/${me}`);
if (action === 'leave') await api('DELETE', `/events/${id}/attendees/${me}`);
if (action === 'delete') {
const ev = events.find((x) => x.id === id);
if (!confirm(`Spieltag „${ev.title}“ am ${formatDate(ev.date)} wirklich löschen?`)) return;
await api('DELETE', `/events/${id}`);
}
await load();
} catch (err) {
showError(err);
}
});
$('#event-form').addEventListener('submit', async (e) => {
e.preventDefault();
try {
await api('POST', '/events', {
date: selectedDay,
title: $('#event-title').value,
note: $('#event-note').value,
createdBy: me,
});
e.target.reset();
await load();
} catch (err) {
showError(err);
}
});
// ---------- Laden ----------
function renderAll() {
renderMe();
renderCalendar();
renderDay();
renderUpcoming();
}
async function load() {
[members, events] = await Promise.all([api('GET', '/members'), api('GET', '/events')]);
renderAll();
}
load().catch(showError);