Files
sufw-webseite-terminplanung/development-system/public/booking-admin/index.php
T

177 lines
26 KiB
PHP
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.
<?php
declare(strict_types=1);
require __DIR__.'/../booking/backend.php';
session_name('sufw_booking_admin');
session_start();
$csrf = $_SESSION['csrf'] ??= bin2hex(random_bytes(32));
$database = bookingDatabase();
$project = (string) ($_GET['project'] ?? $_POST['project'] ?? 'perspektivwerk');
if (!bookingProjectIsValid($project)) $project = 'perspektivwerk';
$view = (string) ($_GET['view'] ?? 'events');
if (!in_array($view, ['events', 'registrations', 'export', 'export-data'], true)) $view = 'events';
function h(mixed $value): string { return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function isQuarterHourDateTime(string $value): bool {
$timestamp = strtotime($value);
return $timestamp !== false && date('s', $timestamp) === '00' && ((int) date('i', $timestamp) % 15) === 0;
}
function adminBack(string $project, string $view, string $notice): never {
header('Location: ?project='.rawurlencode($project).'&view='.rawurlencode($view).'&notice='.rawurlencode($notice));
exit;
}
if ($view === 'export-data') {
$from = (string) ($_GET['from'] ?? '');
$to = (string) ($_GET['to'] ?? '');
$exportProject = (string) ($_GET['filterProject'] ?? 'all');
if (!in_array($exportProject, ['all', 'perspektivwerk', 'produktionsschule'], true)) $exportProject = 'all';
header('Content-Type: application/json; charset=utf-8');
if (!preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $from) || !preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $to) || strtotime($from) === false || strtotime($to) === false || strtotime($from) > strtotime($to)) {
echo json_encode(['error' => 'Bitte wählen Sie einen gültigen Zeitraum.'], JSON_UNESCAPED_UNICODE);
exit;
}
$sql = "SELECT e.title, DATE(e.starts_at) AS event_day, r.name FROM sufw_booking_registrations r JOIN sufw_booking_events e ON e.id = r.event_id WHERE r.status = 'confirmed' AND e.starts_at BETWEEN :fromDate AND :toDate";
$parameters = ['fromDate' => $from.' 00:00:00', 'toDate' => $to.' 23:59:59'];
if ($exportProject !== 'all') { $sql .= ' AND e.project = :exportProject'; $parameters['exportProject'] = $exportProject; }
$statement = $database->prepare($sql.' ORDER BY e.starts_at, r.name');
$statement->execute($parameters);
echo json_encode(['registrations' => $statement->fetchAll()], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!hash_equals($csrf, (string) ($_POST['csrf'] ?? ''))) { http_response_code(403); exit('Ungültige Sitzung.'); }
$action = (string) ($_POST['action'] ?? '');
if ($action === 'save_event') {
$id = (string) ($_POST['id'] ?? '') ?: bookingUuid();
$title = trim((string) ($_POST['title'] ?? ''));
$startsAt = str_replace('T', ' ', (string) ($_POST['starts_at'] ?? ''));
$endsAt = str_replace('T', ' ', (string) ($_POST['ends_at'] ?? ''));
$capacity = (int) ($_POST['capacity'] ?? 0);
$format = (string) ($_POST['format'] ?? 'vor_ort');
$contactName = trim((string) ($_POST['contact_name'] ?? ''));
$contactEmail = trim((string) ($_POST['contact_email'] ?? ''));
$contactPhone = trim((string) ($_POST['contact_phone'] ?? ''));
if ($title === '' || $capacity < 1 || $capacity > 100 || !in_array($format, ['vor_ort', 'telefonisch'], true) || $contactName === '' || $contactPhone === '' || !filter_var($contactEmail, FILTER_VALIDATE_EMAIL) || !isQuarterHourDateTime($startsAt) || !isQuarterHourDateTime($endsAt) || strtotime($endsAt) <= strtotime($startsAt)) adminBack($project, 'events', 'Bitte prüfen Sie die Termindaten. Uhrzeiten sind nur im 15-Minuten-Raster zulässig.');
$save = $database->prepare("INSERT INTO sufw_booking_events (id, project, title, starts_at, ends_at, capacity, format, introduction_required, contact_name, contact_email, contact_phone) VALUES (:id, :project, :title, :startsAt, :endsAt, :capacity, :format, :introductionRequired, :contactName, :contactEmail, :contactPhone) ON DUPLICATE KEY UPDATE title = VALUES(title), starts_at = VALUES(starts_at), ends_at = VALUES(ends_at), capacity = VALUES(capacity), format = VALUES(format), introduction_required = VALUES(introduction_required), contact_name = VALUES(contact_name), contact_email = VALUES(contact_email), contact_phone = VALUES(contact_phone)");
$save->execute(['id' => $id, 'project' => $project, 'title' => $title, 'startsAt' => $startsAt, 'endsAt' => $endsAt, 'capacity' => $capacity, 'format' => $format, 'introductionRequired' => isset($_POST['introduction_required']) ? 1 : 0, 'contactName' => $contactName, 'contactEmail' => $contactEmail, 'contactPhone' => $contactPhone]);
adminBack($project, 'events', 'Termin gespeichert.');
}
if ($action === 'toggle_event') {
$database->prepare('UPDATE sufw_booking_events SET is_active = 1 - is_active WHERE id = :id AND project = :project')->execute(['id' => $_POST['id'] ?? '', 'project' => $project]);
adminBack($project, 'events', 'Terminstatus geändert.');
}
if ($action === 'remove_registration') {
$database->prepare("UPDATE sufw_booking_registrations SET status = 'removed', removed_at = UTC_TIMESTAMP() WHERE id = :id AND status IN ('pending_confirmation', 'confirmed')")->execute(['id' => $_POST['id'] ?? '']);
adminBack($project, 'registrations', 'Teilnahme entfernt; die Kapazität ist wieder frei.');
}
if ($action === 'run_jobs') {
$result = bookingRunJobs($database);
adminBack($project, $view, "Jobs ausgeführt: {$result['expired']} abgelaufen, {$result['remindersConsidered']} Erinnerungen geprüft.");
}
}
$titles = $database->prepare('SELECT DISTINCT title FROM sufw_booking_events WHERE project = :project ORDER BY title');
$titles->execute(['project' => $project]);
$titles = $titles->fetchAll(PDO::FETCH_COLUMN);
$contacts = $database->prepare('SELECT DISTINCT contact_name, contact_email, contact_phone FROM sufw_booking_events WHERE project = :project ORDER BY contact_name, contact_email');
$contacts->execute(['project' => $project]);
$contactByName = [];
$contactByEmail = [];
foreach ($contacts as $contact) { $contactByName[$contact['contact_name']] ??= ['email' => $contact['contact_email'], 'phone' => $contact['contact_phone']]; $contactByEmail[$contact['contact_email']] ??= ['name' => $contact['contact_name'], 'phone' => $contact['contact_phone']]; }
$settings = $database->prepare('SELECT feed_token FROM sufw_booking_project_settings WHERE project = :project');
$settings->execute(['project' => $project]);
$feedToken = (string) $settings->fetchColumn();
$notice = (string) ($_GET['notice'] ?? '');
$events = [];
$edit = null;
if ($view === 'events') {
$statement = $database->prepare("SELECT e.*, COUNT(CASE WHEN r.status = 'confirmed' THEN 1 END) AS confirmed_count FROM sufw_booking_events e LEFT JOIN sufw_booking_registrations r ON r.event_id = e.id WHERE e.project = :project GROUP BY e.id ORDER BY e.starts_at");
$statement->execute(['project' => $project]);
$events = $statement->fetchAll();
foreach ($events as $event) if ($event['id'] === ($_GET['edit'] ?? '')) $edit = $event;
}
$registrations = [];
if ($view === 'registrations') {
$statement = $database->prepare("SELECT r.*, e.title, e.starts_at FROM sufw_booking_registrations r JOIN sufw_booking_events e ON e.id = r.event_id WHERE e.project = :project ORDER BY r.created_at DESC LIMIT 200");
$statement->execute(['project' => $project]);
$registrations = $statement->fetchAll();
}
$outbox = [];
if ($view === 'events') {
$statement = $database->prepare("SELECT o.*, r.name FROM sufw_booking_outbox o JOIN sufw_booking_registrations r ON r.id = o.registration_id JOIN sufw_booking_events e ON e.id = r.event_id WHERE e.project = :project ORDER BY o.created_at DESC LIMIT 20");
$statement->execute(['project' => $project]);
$outbox = $statement->fetchAll();
}
$nextMonth = new DateTimeImmutable('first day of next month');
$defaultFrom = $nextMonth->format('Y-m-01');
$defaultTo = $nextMonth->format('Y-m-t');
?>
<!doctype html>
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Terminbuchung · Testverwaltung</title>
<style>
:root{--green:#08752a;--bg:#f3f6f3;--line:#cad2cb;--ink:#202020}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.45 Arial,sans-serif}.shell{max-width:1180px;margin:auto;padding:1.5rem}header{display:flex;gap:1rem;justify-content:space-between;align-items:start;flex-wrap:wrap}h1,h2{color:var(--green)}h1{margin:0}.warning,.notice{padding:.75rem 1rem;border-left:4px solid #a36a00;background:#fff8e7}.notice{border-color:var(--green);background:#eaf6ed}.tabs{display:flex;flex-wrap:wrap;gap:.5rem;margin:1rem 0}.tabs a,.button,button{display:inline-block;padding:.65rem .85rem;border:1px solid var(--green);background:#fff;color:var(--green);font:inherit;text-decoration:none;cursor:pointer}.tabs a.active,button.primary{background:var(--green);color:#fff}.grid{display:grid;gap:1.25rem;grid-template-columns:1.25fr .75fr;margin-top:1.25rem}.card{background:#fff;border:1px solid var(--line);padding:1rem;overflow:auto}table{border-collapse:collapse;width:100%;min-width:680px}th,td{padding:.6rem;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}small{color:#555}.status{font-weight:bold}.confirmed{color:#08752a}.pending_confirmation{color:#a36a00}.removed,.expired{color:#8a1515}form.inline{display:inline}label{display:grid;gap:.25rem;font-weight:bold}input,select{font:inherit;padding:.55rem;border:1px solid #68736a;width:100%}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.8rem}.wide{grid-column:1/-1}pre{white-space:pre-wrap;max-width:42rem;font:13px/1.35 ui-monospace,monospace}.date-time-trigger{width:100%;min-height:2.75rem;text-align:left}.picker-dialog{width:min(25rem,calc(100% - 2rem));border:0;padding:0;box-shadow:0 .75rem 2rem #0006}.picker-dialog::backdrop{background:#0008}.picker-content{padding:1rem}.picker-head,.picker-actions{display:flex;justify-content:space-between;gap:.5rem;align-items:center}.picker-month{font-weight:bold;text-transform:capitalize}.picker-week,.picker-days{display:grid;grid-template-columns:repeat(7,1fr);gap:.25rem;text-align:center}.picker-week{margin:1rem 0 .25rem;font-size:.8rem;font-weight:bold}.picker-days button{min-height:2.4rem;padding:.25rem;border:1px solid transparent}.picker-days button.selected{background:var(--green);color:#fff}.picker-days button:disabled{visibility:hidden}.picker-time{display:grid;grid-template-columns:1fr 1fr;gap:.75rem;margin:1rem 0}.export-results{margin-top:1rem}.export-results li{margin:.5rem 0;white-space:nowrap;overflow-x:auto}@media(max-width:760px){.grid,.form-grid{grid-template-columns:1fr}.shell{padding:1rem}}
select[data-dropdown-indicator]{appearance:none;padding-right:2.5rem;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%2308752a' d='m3 6 5 5 5-5z'/%3E%3C/svg%3E") no-repeat right .75rem center/1rem}</style></head><body><main class="shell">
<header><div><h1>Terminbuchung · Testverwaltung</h1></div><form method="post"><input type="hidden" name="csrf" value="<?=h($csrf)?>"><input type="hidden" name="project" value="<?=h($project)?>"><input type="hidden" name="action" value="run_jobs"><button>Jobs ausführen</button></form></header>
<?php if ($notice !== ''): ?><p class="notice"><?=h($notice)?></p><?php endif; ?>
<nav class="tabs"><a class="<?= $view === 'events' && $project === 'perspektivwerk' ? 'active' : '' ?>" href="?project=perspektivwerk&view=events">PerspektivWerk</a><a class="<?= $view === 'events' && $project === 'produktionsschule' ? 'active' : '' ?>" href="?project=produktionsschule&view=events">Produktionsschule</a><a class="<?= $view === 'registrations' ? 'active' : '' ?>" href="?project=<?=h($project)?>&view=registrations">Teilnahmen</a><a class="<?= $view === 'export' ? 'active' : '' ?>" href="?project=<?=h($project)?>&view=export">Export</a></nav>
<?php if ($view === 'events'): ?>
<section class="grid"><article class="card"><h2><?= $edit ? 'Termin bearbeiten' : 'Termin anlegen' ?></h2><form method="post"><input type="hidden" name="csrf" value="<?=h($csrf)?>"><input type="hidden" name="action" value="save_event"><input type="hidden" name="project" value="<?=h($project)?>"><input type="hidden" name="id" value="<?=h($edit['id'] ?? '')?>"><div class="form-grid">
<label class="wide">Terminart<select name="title" required data-dropdown-indicator><option value="">Terminart wählen …</option><?php foreach ($titles as $title): ?><option value="<?=h($title)?>" <?=($edit['title'] ?? '') === $title ? 'selected' : ''?>><?=h($title)?></option><?php endforeach; ?></select></label>
<label>Beginn<input type="hidden" name="starts_at" value="<?=h($edit['starts_at'] ?? '')?>"><button type="button" class="date-time-trigger" data-datetime-picker data-target="starts_at">Datum &amp; Uhrzeit wählen</button></label><label>Ende<input type="hidden" name="ends_at" value="<?=h($edit['ends_at'] ?? '')?>"><button type="button" class="date-time-trigger" data-datetime-picker data-target="ends_at">Datum &amp; Uhrzeit wählen</button></label>
<label>Kapazität (1–100)<input type="number" min="1" max="100" name="capacity" required value="<?=h($edit['capacity'] ?? 1)?>"></label><label>Format<select name="format"><option value="vor_ort" <?=($edit['format'] ?? '') === 'vor_ort' ? 'selected' : ''?>>Vor Ort</option><option value="telefonisch" <?=($edit['format'] ?? '') === 'telefonisch' ? 'selected' : ''?>>Telefonisch</option></select></label>
<label>Ansprechperson<select id="contact_name" name="contact_name" required data-dropdown-indicator><option value="">Ansprechperson wählen …</option><?php foreach ($contactByName as $name => $contact): ?><option value="<?=h($name)?>" <?=($edit['contact_name'] ?? '') === $name ? 'selected' : ''?>><?=h($name)?></option><?php endforeach; ?></select></label><label>Kontakt-E-Mail<select id="contact_email" name="contact_email" required data-dropdown-indicator><option value="">Kontakt-E-Mail wählen …</option><?php foreach ($contactByEmail as $email => $contact): ?><option value="<?=h($email)?>" <?=($edit['contact_email'] ?? '') === $email ? 'selected' : ''?>><?=h($email)?></option><?php endforeach; ?></select></label>
<label>Telefon Ansprechperson<input id="contact_phone" name="contact_phone" type="tel" required readonly value="<?=h($edit['contact_phone'] ?? '')?>"></label>
<label class="wide"><span><input type="checkbox" name="introduction_required" <?=!empty($edit['introduction_required']) ? 'checked' : ''?>> Einführungsveranstaltung erforderlich (PS-Probearbeiten)</span></label></div><p><button class="primary">Termin speichern</button><?php if ($edit): ?> <a class="button" href="?project=<?=h($project)?>&view=events">Abbrechen</a><?php endif; ?></p></form></article>
<aside class="card"><h2>Kalenderfeed</h2><p>Nur bestätigte Termine, ohne Teilnehmendendaten:</p><p><a href="<?=h(bookingBaseUrl().'/booking/ical.php?project='.$project.'&token='.$feedToken)?>">ICS-Feed öffnen</a></p></aside></section>
<section class="card"><h2>Termine</h2><table><thead><tr><th>Termin</th><th>Zeit</th><th>Kapazität</th><th>Status</th><th></th></tr></thead><tbody><?php foreach ($events as $event): ?><tr><td><strong><?=h($event['title'])?></strong><br><small><?=h($event['format'])?><?=!empty($event['introduction_required']) ? ' · Einführung erforderlich' : ''?></small></td><td><?=h($event['starts_at'])?><br>bis <?=h($event['ends_at'])?></td><td><?=h($event['confirmed_count'])?> / <?=h($event['capacity'])?> bestätigt</td><td><?= $event['is_active'] ? 'Aktiv' : 'Inaktiv' ?></td><td><a class="button" href="?project=<?=h($project)?>&view=events&edit=<?=h($event['id'])?>">Bearbeiten</a> <form class="inline" method="post"><input type="hidden" name="csrf" value="<?=h($csrf)?>"><input type="hidden" name="project" value="<?=h($project)?>"><input type="hidden" name="action" value="toggle_event"><input type="hidden" name="id" value="<?=h($event['id'])?>"><button><?= $event['is_active'] ? 'Deaktivieren' : 'Aktivieren' ?></button></form></td></tr><?php endforeach; ?></tbody></table></section>
<section class="card"><h2>Lokaler Mailausgang</h2><?php foreach ($outbox as $mail): ?><article><strong><?=h($mail['kind'])?></strong> · <?=h($mail['recipient_email'])?><br><small><?=h($mail['subject'])?></small><pre><?=h($mail['body'])?></pre></article><?php endforeach; ?></section>
<?php elseif ($view === 'registrations'): ?>
<section class="card"><h2>Teilnahmen · <?=h(bookingProjectTitle($project))?></h2><table><thead><tr><th>Person</th><th>Termin</th><th>Status</th><th></th></tr></thead><tbody><?php foreach ($registrations as $registration): ?><tr><td><?=h($registration['name'])?><br><small><?=h($registration['email'])?><br><?=h($registration['phone'])?></small></td><td><?=h($registration['title'])?><br><small><?=h($registration['starts_at'])?></small></td><td class="status <?=h($registration['status'])?>"><?=h($registration['status'])?></td><td><?php if (in_array($registration['status'], ['pending_confirmation','confirmed'], true)): ?><form method="post"><input type="hidden" name="csrf" value="<?=h($csrf)?>"><input type="hidden" name="project" value="<?=h($project)?>"><input type="hidden" name="action" value="remove_registration"><input type="hidden" name="id" value="<?=h($registration['id'])?>"><button>Entfernen</button></form><?php endif; ?></td></tr><?php endforeach; ?></tbody></table></section>
<?php elseif ($view === 'export'): ?>
<section class="card"><h2>Bestätigte Teilnahmen anzeigen</h2><p>Der Zeitraum umfasst jeweils vollständige Kalendertage (00:00 bis 23:59 Uhr) und aktualisiert die Liste unmittelbar.</p><div class="form-grid"><label>Projekt<select id="export-project" data-dropdown-indicator><option value="all">PerspektivWerk und Produktionsschule</option><option value="perspektivwerk">Nur PerspektivWerk</option><option value="produktionsschule">Nur Produktionsschule</option></select></label>
<label></label>
<label>Von<input type="hidden" id="export-from" value="<?=h($defaultFrom)?>"><button type="button" class="date-time-trigger" data-date-picker data-target="export-from">Datum wählen</button></label>
<label>Bis<input type="hidden" id="export-to" value="<?=h($defaultTo)?>"><button type="button" class="date-time-trigger" data-date-picker data-target="export-to">Datum wählen</button></label>
</div><div id="export-results" class="export-results" role="status" aria-live="polite">Lade Teilnahmen …</div></section>
<?php endif; ?>
</main>
<dialog class="picker-dialog" id="date-time-dialog" aria-labelledby="date-time-title"><div class="picker-content"><div class="picker-head"><button type="button" data-picker-prev aria-label="Vorheriger Monat">‹</button><strong class="picker-month" id="date-time-title"></strong><button type="button" data-picker-next aria-label="Nächster Monat">›</button></div><div class="picker-week" aria-hidden="true"><span>Mo</span><span>Di</span><span>Mi</span><span>Do</span><span>Fr</span><span>Sa</span><span>So</span></div><div class="picker-days" data-picker-days></div><?php if ($view === 'events'): ?><div class="picker-time" data-picker-time-controls><label><select data-picker-hour></select></label><label><select data-picker-quarter></select></label></div><?php endif; ?><div class="picker-actions"><button type="button" data-picker-close>Schließen</button><button type="button" class="primary" data-picker-apply>Übernehmen</button></div></div></dialog>
<script>
const contactByName = <?=json_encode($contactByName, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT)?>;
const contactByEmail = <?=json_encode($contactByEmail, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT)?>;
const contactName = document.querySelector('#contact_name'), contactEmail = document.querySelector('#contact_email'), contactPhone = document.querySelector('#contact_phone');
contactName?.addEventListener('change', () => { const contact=contactByName[contactName.value]; if (contact) { contactEmail.value=contact.email; contactPhone.value=contact.phone; } });
contactEmail?.addEventListener('change', () => { const contact=contactByEmail[contactEmail.value]; if (contact) { contactName.value=contact.name; contactPhone.value=contact.phone; } });
const dialog = document.querySelector('#date-time-dialog'), days = dialog.querySelector('[data-picker-days]'), monthLabel = dialog.querySelector('.picker-month'), timeControls = dialog.querySelector('[data-picker-time-controls]'), hourSelect = dialog.querySelector('[data-picker-hour]'), quarterSelect = dialog.querySelector('[data-picker-quarter]');
if (timeControls) { const hours = Array.from({length:24}, (_,hour) => String(hour).padStart(2,'0')); hourSelect.innerHTML = hours.map(hour => '<option value="' + hour + '">' + hour + ' Uhr</option>').join(''); quarterSelect.innerHTML = ['00','15','30','45'].map(minute => '<option value="' + minute + '">' + minute + ' Min.</option>').join(''); }
let pickerTarget, pickerDate, pickerMonth, pickerMode;
const asDate = value => { const parts = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}))?/); if (parts) return new Date(Number(parts[1]), Number(parts[2])-1, Number(parts[3]), Number(parts[4] || 0), Number(parts[5] || 0)); const defaultDate = new Date(); defaultDate.setHours(9,0,0,0); return defaultDate; };
const formatDateValue = date => String(date.getFullYear())+'-'+String(date.getMonth()+1).padStart(2,'0')+'-'+String(date.getDate()).padStart(2,'0');
const formatValue = date => formatDateValue(date)+' '+String(date.getHours()).padStart(2,'0')+':'+String(date.getMinutes()).padStart(2,'0')+':00';
const formatLabel = (value, includeTime) => value ? new Intl.DateTimeFormat('de-DE', includeTime ? {dateStyle:'medium',timeStyle:'short'} : {dateStyle:'medium'}).format(asDate(value)) : (includeTime ? 'Datum & Uhrzeit wählen' : 'Datum wählen');
const getTarget = name => document.querySelector('[name="'+name+'"],#'+name);
const syncPickerLabels = () => document.querySelectorAll('[data-datetime-picker],[data-date-picker]').forEach(button => { const input = getTarget(button.dataset.target); button.textContent = formatLabel(input?.value, button.hasAttribute('data-datetime-picker')); });
function renderPicker() {
monthLabel.textContent = new Intl.DateTimeFormat('de-DE',{month:'long',year:'numeric'}).format(pickerMonth);
const first = new Date(pickerMonth.getFullYear(),pickerMonth.getMonth(),1), offset = (first.getDay()+6)%7, last = new Date(pickerMonth.getFullYear(),pickerMonth.getMonth()+1,0).getDate();
days.innerHTML = Array.from({length:offset+last}, (_,index) => { if(index<offset) return '<button type="button" disabled></button>'; const day=index-offset+1, selected=pickerDate.getFullYear()===pickerMonth.getFullYear()&&pickerDate.getMonth()===pickerMonth.getMonth()&&pickerDate.getDate()===day; return '<button type="button" data-picker-day="'+day+'" class="'+(selected?'selected':'')+'" aria-pressed="'+selected+'">'+day+'</button>'; }).join('');
days.querySelectorAll('[data-picker-day]').forEach(button => button.addEventListener('click', () => { pickerDate = new Date(pickerMonth.getFullYear(),pickerMonth.getMonth(),Number(button.dataset.pickerDay),pickerDate.getHours(),pickerDate.getMinutes()); renderPicker(); }));
}
document.querySelectorAll('[data-datetime-picker],[data-date-picker]').forEach(button => button.addEventListener('click', () => { pickerTarget=getTarget(button.dataset.target); pickerMode=button.hasAttribute('data-date-picker') ? 'date' : 'datetime'; pickerDate=asDate(pickerTarget.value); pickerMonth=new Date(pickerDate.getFullYear(),pickerDate.getMonth(),1); if (timeControls) { hourSelect.value=String(pickerDate.getHours()).padStart(2,'0'); quarterSelect.value=String(Math.floor(pickerDate.getMinutes()/15)*15).padStart(2,'0'); } renderPicker(); dialog.showModal(); }));
dialog.querySelector('[data-picker-prev]').addEventListener('click', () => { pickerMonth=new Date(pickerMonth.getFullYear(),pickerMonth.getMonth()-1,1); renderPicker(); });
dialog.querySelector('[data-picker-next]').addEventListener('click', () => { pickerMonth=new Date(pickerMonth.getFullYear(),pickerMonth.getMonth()+1,1); renderPicker(); });
dialog.querySelector('[data-picker-close]').addEventListener('click', () => dialog.close());
dialog.querySelector('[data-picker-apply]').addEventListener('click', () => { if (pickerMode === 'datetime' && timeControls) pickerDate.setHours(Number(hourSelect.value),Number(quarterSelect.value),0,0); pickerTarget.value=pickerMode === 'date' ? formatDateValue(pickerDate) : formatValue(pickerDate); syncPickerLabels(); pickerTarget.dispatchEvent(new Event('booking-datetime-change')); dialog.close(); });
syncPickerLabels();
const exportResults=document.querySelector('#export-results');
const escapeHtml = value => String(value).replace(/[&<>"']/g, character => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[character]));
async function updateExport() { if(!exportResults)return; const from=document.querySelector('#export-from').value,to=document.querySelector('#export-to').value,filterProject=document.querySelector('#export-project').value; exportResults.textContent='Lade Teilnahmen …'; const result=await fetch('?view=export-data&filterProject='+encodeURIComponent(filterProject)+'&from='+encodeURIComponent(from)+'&to='+encodeURIComponent(to)).then(response=>response.json()); if(result.error){exportResults.textContent=result.error;return;} exportResults.innerHTML=result.registrations.length?'<p>'+result.registrations.length+' bestätigte Teilnahme(n)</p><ul>'+result.registrations.map(item=>'<li><strong>'+escapeHtml(item.name)+'</strong> · '+escapeHtml(item.title)+' · '+escapeHtml(item.event_day)+'</li>').join('')+'</ul>':'<p>Für diesen Zeitraum liegen keine bestätigten Teilnahmen vor.</p>'; }
document.querySelectorAll('#export-from,#export-to').forEach(input=>input.addEventListener('booking-datetime-change',updateExport)); document.querySelector('#export-project')?.addEventListener('change', updateExport); updateExport();
</script></body></html>