Files
sufw-webseite-terminplanung/development-system/public/booking/booking.js
T

111 lines
7.4 KiB
JavaScript
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.
/* Browsernatives ES-Modul; wird ohne TypeScript-Compiler ausgeliefert. */
const demoSlots = [
{ id: 'pw-2026-08-25-1000', project: 'perspektivwerk', date: '2026-08-25', start: '10:00', title: 'Erstgespräch', status: 'available' },
{ id: 'pw-2026-08-25-1300', project: 'perspektivwerk', date: '2026-08-25', start: '13:00', title: 'Soz. Päd. Modul', status: 'booked' },
{ id: 'pw-2026-08-25-1500', project: 'perspektivwerk', date: '2026-08-25', start: '15:00', title: 'Erstgespräch', status: 'available' },
{ id: 'pw-2026-08-27-0900', project: 'perspektivwerk', date: '2026-08-27', start: '09:00', title: 'Psy Modul', status: 'available' },
{ id: 'pw-2026-08-31-0800', project: 'perspektivwerk', date: '2026-08-31', start: '08:00', title: 'Praxis Modul', status: 'available' },
{ id: 'ps-2026-08-25-1000', project: 'produktionsschule', date: '2026-08-25', start: '10:00', title: 'Kennenlerntermin', status: 'available' },
{ id: 'ps-2026-08-25-1300', project: 'produktionsschule', date: '2026-08-25', start: '13:00', title: 'Kennenlerntermin', status: 'booked' },
{ id: 'ps-2026-08-31-0800', project: 'produktionsschule', date: '2026-08-31', start: '08:00', title: 'Probearbeiten', status: 'available' }
];
export function getSlotsForDate(project, date) {
return demoSlots.filter((slot) => slot.project === project && slot.date === date);
}
const dateFormatter = new Intl.DateTimeFormat('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
const monthFormatter = new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' });
const weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
function parseDate(value) {
return new Date(`${value}T12:00:00`);
}
function formatDateKey(date) {
return date.toISOString().slice(0, 10);
}
function createMonthDates(month) {
const first = new Date(month.getFullYear(), month.getMonth(), 1);
const padding = (first.getDay() + 6) % 7;
const days = new Array(padding).fill(null);
const lastDay = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();
for (let day = 1; day <= lastDay; day += 1) {
days.push(new Date(month.getFullYear(), month.getMonth(), day));
}
return days;
}
function monthForProject(project) {
const firstSlot = demoSlots.find((slot) => slot.project === project);
return firstSlot ? new Date(parseDate(firstSlot.date).getFullYear(), parseDate(firstSlot.date).getMonth(), 1) : new Date();
}
function renderComponent(container) {
const configuredProject = container.dataset.project;
const project = configuredProject === 'produktionsschule' ? 'produktionsschule' : 'perspektivwerk';
let currentMonth = monthForProject(project);
let selectedDate = demoSlots.find((slot) => slot.project === project)?.date ?? '';
const render = () => {
const slots = selectedDate ? getSlotsForDate(project, selectedDate) : [];
const selectedLabel = selectedDate ? dateFormatter.format(parseDate(selectedDate)) : 'einen Tag';
container.innerHTML = `
<section class="sufw-booking" aria-labelledby="sufw-booking-title">
<header class="sufw-booking__header">
<p class="sufw-booking__eyebrow">Terminbuchung – Vorschau</p>
<h2 id="sufw-booking-title">${project === 'perspektivwerk' ? 'PerspektivWerk' : 'Produktionsschule'}</h2>
<p>Wählen Sie einen Tag. Freie Termine sind mit „frei“ gekennzeichnet.</p>
</header>
<div class="sufw-booking__legend" aria-label="Legende"><span><i class="sufw-booking__marker sufw-booking__marker--available"></i> Termin verfügbar</span><span><i class="sufw-booking__marker sufw-booking__marker--booked"></i> Termin belegt</span></div>
<div class="sufw-booking__month-nav">
<button class="sufw-booking__nav" type="button" data-action="previous" aria-label="Vorheriger Monat">‹</button>
<h3 aria-live="polite">${monthFormatter.format(currentMonth)}</h3>
<button class="sufw-booking__nav" type="button" data-action="next" aria-label="Nächster Monat">›</button>
</div>
<div class="sufw-booking__weekdays" aria-hidden="true">${weekdays.map((weekday) => `<span>${weekday}</span>`).join('')}</div>
<div class="sufw-booking__calendar" role="grid" aria-label="Kalender ${monthFormatter.format(currentMonth)}">
${createMonthDates(currentMonth).map((date) => {
if (!date)
return '<span class="sufw-booking__empty" aria-hidden="true"></span>';
const key = formatDateKey(date);
const daySlots = getSlotsForDate(project, key);
const available = daySlots.some((slot) => slot.status === 'available');
const booked = daySlots.length > 0 && !available;
const selected = key === selectedDate;
const state = available ? 'available' : booked ? 'booked' : 'none';
const label = daySlots.length ? `${dateFormatter.format(date)}: ${available ? 'freie Termine verfügbar' : 'nur belegte Termine'}` : dateFormatter.format(date);
return `<button type="button" role="gridcell" class="sufw-booking__day sufw-booking__day--${state}${selected ? ' is-selected' : ''}" data-date="${key}" aria-pressed="${selected}" aria-label="${label}"><time datetime="${key}">${date.getDate()}</time>${daySlots.length ? `<small>${available ? 'frei' : 'belegt'}</small>` : ''}</button>`;
}).join('')}
</div>
<section class="sufw-booking__slots" aria-labelledby="sufw-slot-title">
<h3 id="sufw-slot-title">Termine am ${selectedLabel}</h3>
${slots.length ? `<ul>${slots.map((slot) => `<li><button type="button" class="sufw-booking__slot" data-slot-id="${slot.id}" ${slot.status === 'booked' ? 'disabled aria-disabled="true"' : ''}><strong>${slot.start} Uhr</strong><span>${slot.title}</span><em>${slot.status === 'available' ? 'Verfügbar' : 'Belegt'}</em></button></li>`).join('')}</ul>` : '<p class="sufw-booking__empty-state">Für diesen Tag liegen in der Vorschau keine Termine vor.</p>'}
<p class="sufw-booking__status" role="status" aria-live="polite"></p>
</section>
</section>`;
container.querySelector('[data-action="previous"]')?.addEventListener('click', () => {
currentMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1);
render();
});
container.querySelector('[data-action="next"]')?.addEventListener('click', () => {
currentMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1);
render();
});
container.querySelectorAll('[data-date]').forEach((button) => button.addEventListener('click', () => {
selectedDate = button.dataset.date ?? '';
render();
}));
container.querySelectorAll('[data-slot-id]').forEach((button) => button.addEventListener('click', () => {
const status = container.querySelector('.sufw-booking__status');
if (status)
status.textContent = 'Vorschau: Die eigentliche Buchungsmaske wird im nächsten Schritt ergänzt.';
}));
};
render();
}
export function initializeBookingComponents(root = document) {
root.querySelectorAll('[data-sufw-booking]').forEach(renderComponent);
}
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => initializeBookingComponents());
}
else {
initializeBookingComponents();
}
}