/* Browsernatives ES-Modul; wird ohne TypeScript-Compiler ausgeliefert. */ const demoSlots = [ { id: 'pw-2026-08-24-0900', project: 'perspektivwerk', date: '2026-08-24', start: '09:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-08-24-1100', project: 'perspektivwerk', date: '2026-08-24', start: '11:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-08-24-1400', project: 'perspektivwerk', date: '2026-08-24', start: '14:00', title: 'Soz. Päd. Modul', status: 'booked' }, { id: 'pw-2026-08-18-1000', project: 'perspektivwerk', date: '2026-08-18', start: '10:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-08-18-1300', project: 'perspektivwerk', date: '2026-08-18', start: '13:00', title: 'Soz. Päd. Modul', status: 'booked' }, { id: 'pw-2026-08-18-1500', project: 'perspektivwerk', date: '2026-08-18', start: '15:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-08-26-0900', project: 'perspektivwerk', date: '2026-08-26', start: '09:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-08-26-1100', project: 'perspektivwerk', date: '2026-08-26', start: '11:00', title: 'Psy Modul', status: 'booked' }, { id: 'pw-2026-08-26-1400', project: 'perspektivwerk', date: '2026-08-26', start: '14:00', title: 'Soz. Päd. Modul', status: 'available' }, { id: 'pw-2026-08-20-0900', project: 'perspektivwerk', date: '2026-08-20', start: '09:00', title: 'Psy Modul', status: 'available' }, { id: 'pw-2026-08-20-1100', project: 'perspektivwerk', date: '2026-08-20', start: '11:00', title: 'Erstgespräch', status: 'booked' }, { id: 'pw-2026-08-20-1500', project: 'perspektivwerk', date: '2026-08-20', start: '15:00', title: 'Psy Modul', status: 'available' }, { id: 'pw-2026-08-28-0900', project: 'perspektivwerk', date: '2026-08-28', start: '09:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-08-28-1100', project: 'perspektivwerk', date: '2026-08-28', start: '11:00', title: 'Soz. Päd. Modul', status: 'available' }, { id: 'pw-2026-08-28-1400', project: 'perspektivwerk', date: '2026-08-28', start: '14:00', title: 'Erstgespräch', status: 'booked' }, { id: 'pw-2026-08-31-0800', project: 'perspektivwerk', date: '2026-08-31', start: '08:00', title: 'Praxis Modul', status: 'available' }, { id: 'pw-2026-09-02-0900', project: 'perspektivwerk', date: '2026-09-02', start: '09:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-09-02-1100', project: 'perspektivwerk', date: '2026-09-02', start: '11:00', title: 'Soz. Päd. Modul', status: 'booked' }, { id: 'pw-2026-09-02-1400', project: 'perspektivwerk', date: '2026-09-02', start: '14:00', title: 'Psy Modul', status: 'available' }, { id: 'pw-2026-09-07-0900', project: 'perspektivwerk', date: '2026-09-07', start: '09:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-09-07-1300', project: 'perspektivwerk', date: '2026-09-07', start: '13:00', title: 'Soz. Päd. Modul', status: 'available' }, { id: 'pw-2026-09-10-0900', project: 'perspektivwerk', date: '2026-09-10', start: '09:00', title: 'Psy Modul', status: 'available' }, { id: 'pw-2026-09-10-1100', project: 'perspektivwerk', date: '2026-09-10', start: '11:00', title: 'Erstgespräch', status: 'booked' }, { id: 'pw-2026-09-10-1500', project: 'perspektivwerk', date: '2026-09-10', start: '15:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-09-15-0900', project: 'perspektivwerk', date: '2026-09-15', start: '09:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-09-15-1100', project: 'perspektivwerk', date: '2026-09-15', start: '11:00', title: 'Soz. Päd. Modul', status: 'available' }, { id: 'pw-2026-09-15-1400', project: 'perspektivwerk', date: '2026-09-15', start: '14:00', title: 'Psy Modul', status: 'booked' }, { id: 'pw-2026-09-18-0900', project: 'perspektivwerk', date: '2026-09-18', start: '09:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-09-18-1300', project: 'perspektivwerk', date: '2026-09-18', start: '13:00', title: 'Soz. Päd. Modul', status: 'booked' }, { id: 'pw-2026-09-22-1000', project: 'perspektivwerk', date: '2026-09-22', start: '10:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-09-22-1400', project: 'perspektivwerk', date: '2026-09-22', start: '14:00', title: 'Psy Modul', status: 'available' }, { id: 'pw-2026-09-25-0900', project: 'perspektivwerk', date: '2026-09-25', start: '09:00', title: 'Soz. Päd. Modul', status: 'available' }, { id: 'pw-2026-09-25-1100', project: 'perspektivwerk', date: '2026-09-25', start: '11:00', title: 'Erstgespräch', status: 'booked' }, { id: 'pw-2026-09-29-0900', project: 'perspektivwerk', date: '2026-09-29', start: '09:00', title: 'Erstgespräch', status: 'available' }, { id: 'pw-2026-09-29-1100', project: 'perspektivwerk', date: '2026-09-29', start: '11:00', title: 'Psy Modul', status: 'available' }, { id: 'pw-2026-09-29-1400', project: 'perspektivwerk', date: '2026-09-29', start: '14:00', title: 'Soz. Päd. Modul', status: 'booked' }, { 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); } export function validateBookingField(fieldName, value) { const text = typeof value === 'string' ? value.trim() : ''; if (fieldName === 'name') return text ? '' : 'Bitte geben Sie Ihren Namen ein.'; if (fieldName === 'email') return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text) ? '' : 'Bitte geben Sie eine gültige E-Mail-Adresse ein.'; if (fieldName === 'phone') return !text || /^[0-9+()\-/\s]{6,}$/.test(text) ? '' : 'Bitte geben Sie eine gültige Telefonnummer ein.'; if (fieldName === 'age') return !text || (/^\d+$/.test(text) && Number(text) >= 12 && Number(text) <= 112) ? '' : 'Bitte geben Sie ein Alter zwischen 12 und 112 Jahren ein.'; if (fieldName === 'message') return text.length <= 2000 ? '' : 'Die Nachricht darf höchstens 2.000 Zeichen enthalten.'; if (fieldName === 'caseWorker') return text.length <= 160 ? '' : 'Die Angabe darf höchstens 160 Zeichen enthalten.'; if (fieldName === 'privacy') return value ? '' : 'Bitte bestätigen Sie die Datenschutzhinweise.'; return ''; } export function validateBookingForm({ name = '', email = '', phone = '', age = '', message = '', caseWorker = '', privacyAccepted = false }) { const values = { name, email, phone, age, message, caseWorker, privacy: privacyAccepted }; return Object.fromEntries(Object.entries(values) .map(([fieldName, value]) => [fieldName, validateBookingField(fieldName, value)]) .filter(([, error]) => error)); } export function canSubmitBookingForm(privacyAccepted) { return privacyAccepted; } export function bookingConfirmationMessage(appointmentTitle) { return `Ihre Terminanfrage für ${appointmentTitle} wurde erfolgreich übermittelt.`; } 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`); } export function formatDateKey(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } 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 escapeHtml(value) { return value.replace(/[&<>'"]/g, (character) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' })[character]); } function showFieldError(form, fieldName, error) { const field = form.elements.namedItem(fieldName); const errorId = `sufw-booking-${fieldName}-error`; let message = form.querySelector(`#${errorId}`); if (!message) { message = document.createElement('span'); message.id = errorId; message.className = 'sufw-booking__field-error'; message.setAttribute('role', 'alert'); message.style.display = 'block'; message.style.marginTop = '.35rem'; message.style.color = '#6c0808'; message.style.fontWeight = '400'; const host = fieldName === 'privacy' ? field.closest('label') : field; host.insertAdjacentElement('afterend', message); field.setAttribute('aria-describedby', errorId); } message.textContent = error; message.hidden = !error; field.setAttribute('aria-invalid', error ? 'true' : 'false'); } function openConfirmationDialog(container, slot) { const projectTitle = slot.project === 'perspektivwerk' ? 'PerspektivWerk' : 'Produktionsschule'; const dialog = document.createElement('dialog'); dialog.className = 'sufw-booking__dialog'; dialog.setAttribute('aria-labelledby', 'sufw-booking-confirmation-title'); dialog.innerHTML = `

Termin angefragt

Vielen Dank für Ihre Anfrage

${escapeHtml(bookingConfirmationMessage(slot.title))}

${escapeHtml(projectTitle)}
${escapeHtml(dateFormatter.format(parseDate(slot.date)))} · ${escapeHtml(slot.start)} Uhr · ${escapeHtml(slot.title)}

Wir senden Ihnen eine E-Mail zur Bestätigung Ihres Termins.

`; dialog.querySelector('[data-close-confirmation]').addEventListener('click', () => dialog.close()); dialog.addEventListener('close', () => dialog.remove()); container.append(dialog); if (typeof dialog.showModal === 'function') { dialog.showModal(); } else { dialog.setAttribute('open', ''); dialog.querySelector('[data-close-confirmation]').focus(); } } function openBookingDialog(container, slot) { const projectTitle = slot.project === 'perspektivwerk' ? 'PerspektivWerk' : 'Produktionsschule'; const dialog = document.createElement('dialog'); dialog.className = 'sufw-booking__dialog'; dialog.setAttribute('aria-labelledby', 'sufw-booking-form-title'); dialog.innerHTML = `

Termin anfragen

Ihre Angaben

${escapeHtml(projectTitle)}
${escapeHtml(dateFormatter.format(parseDate(slot.date)))} · ${escapeHtml(slot.start)} Uhr · ${escapeHtml(slot.title)}

Pflichtfeld

`; const form = dialog.querySelector('form'); const privacy = form.elements.namedItem('privacy'); const submitButton = dialog.querySelector('[data-submit-booking]'); const actions = dialog.querySelector('.sufw-booking__form-actions'); dialog.style.overflowY = 'auto'; actions.style.position = 'sticky'; actions.style.bottom = '0'; actions.style.zIndex = '2'; actions.style.display = 'flex'; actions.style.visibility = 'visible'; actions.style.opacity = '1'; actions.style.backgroundColor = '#fff'; actions.style.paddingTop = '.75rem'; const fieldNames = ['name', 'email', 'phone', 'age', 'message', 'caseWorker', 'privacy']; const validateField = (fieldName) => { const field = form.elements.namedItem(fieldName); const value = fieldName === 'privacy' ? field.checked : field.value; const error = validateBookingField(fieldName, value); showFieldError(form, fieldName, error); return error; }; const updateSubmitButton = () => { submitButton.disabled = !canSubmitBookingForm(privacy.checked); submitButton.hidden = false; submitButton.style.display = 'inline-block'; submitButton.style.visibility = 'visible'; submitButton.style.opacity = '1'; submitButton.style.position = 'relative'; submitButton.style.zIndex = '3'; submitButton.style.minHeight = '2.75rem'; }; fieldNames.forEach((fieldName) => { const field = form.elements.namedItem(fieldName); const eventName = fieldName === 'privacy' ? 'change' : 'input'; field.addEventListener(eventName, () => validateField(fieldName)); if (fieldName !== 'privacy') field.addEventListener('blur', () => validateField(fieldName)); }); privacy.addEventListener('change', updateSubmitButton); updateSubmitButton(); form.addEventListener('submit', (event) => { event.preventDefault(); const errors = Object.fromEntries(fieldNames .map((fieldName) => [fieldName, validateField(fieldName)]) .filter(([, error]) => error)); if (Object.keys(errors).length) { const firstInvalid = form.elements.namedItem(Object.keys(errors)[0]); firstInvalid.focus(); return; } dialog.close(); openConfirmationDialog(container, slot); }); dialog.querySelector('[data-close-dialog]').addEventListener('click', () => dialog.close()); dialog.addEventListener('close', () => dialog.remove()); container.append(dialog); if (typeof dialog.showModal === 'function') { dialog.showModal(); } else { dialog.setAttribute('open', ''); dialog.querySelector('input[name="name"]').focus(); } } 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 = `

Terminbuchung

${project === 'perspektivwerk' ? 'PerspektivWerk' : 'Produktionsschule'}

Wählen Sie einen Tag. Freie Termine sind mit „frei“ gekennzeichnet.

Termin verfügbar Termin belegt

${monthFormatter.format(currentMonth)}

${createMonthDates(currentMonth).map((date) => { if (!date) return ''; 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 ``; }).join('')}

Termine am ${selectedLabel}

${slots.length ? `` : '

Für diesen Tag liegen keine Termine vor.

'}

`; 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 slot = demoSlots.find((entry) => entry.id === button.dataset.slotId); if (slot) openBookingDialog(container, slot); })); }; 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(); } }