Weitere Demo Daten und Default settings für DateTimePicker

This commit is contained in:
Robert Wartenberg committed 2026-08-28 17:02:23 +02:00
1 parent e122aa8650
commit 72c032a5ca
4 files changed
+48 -9

No files matched your search

+2
View File
@@ -63,3 +63,5 @@ lokale Entwicklungs- und Testdatenbank bestimmt und darf nicht auf dem
Produktivsystem ausgeführt werden. `004_booking_contact_phones.sql` ergänzt Produktivsystem ausgeführt werden. `004_booking_contact_phones.sql` ergänzt
die Telefonnummer der Ansprechperson und ersetzt die Kontakte der stabilen die Telefonnummer der Ansprechperson und ersetzt die Kontakte der stabilen
Demo-Termine durch Carina Hollederer und Sarah van den Heuvel. Demo-Termine durch Carina Hollederer und Sarah van den Heuvel.
`005_booking_demo_july_august.sql` ergänzt ausschließlich lokal weitere
bestätigte Demo-Termine und -Teilnahmen für Juli und August.
@@ -0,0 +1,26 @@
-- SUFW Terminbuchung: Zusätzliche lokale Demo-Termine für Juli und August
--
-- Nur für lokale Entwicklungs- und Testdatenbanken. Stabile IDs sorgen dafür,
-- dass die Beispieldaten bei erneutem Ausführen nicht dupliziert werden.
INSERT INTO `sufw_booking_events`
(`id`, `project`, `title`, `starts_at`, `ends_at`, `capacity`, `format`, `introduction_required`, `contact_name`, `contact_email`, `contact_phone`, `is_active`)
VALUES
('f5c7f216-2704-4c81-b17a-000000000001', 'perspektivwerk', 'Soz. Päd. Modul PerspektivWerk', CONCAT(YEAR(CURDATE()), '-07-10 09:00:00'), CONCAT(YEAR(CURDATE()), '-07-10 13:00:00'), 2, 'vor_ort', 0, 'Carina Hollederer', 'c.hollederer@sufw.de', '(0176) 17922244', 1),
('f5c7f216-2704-4c81-b17a-000000000002', 'produktionsschule', 'Voranmeldung Kennenlerntermin Produktionsschule', CONCAT(YEAR(CURDATE()), '-07-10 10:00:00'), CONCAT(YEAR(CURDATE()), '-07-10 11:00:00'), 3, 'vor_ort', 0, 'Sarah van den Heuvel', 's.vandenHeuvel@sufw.de', '(0176) 60328929', 1),
('f5c7f216-2704-4c81-b17a-000000000003', 'perspektivwerk', 'Erstgespräch PerspektivWerk', CONCAT(YEAR(CURDATE()), '-08-12 09:00:00'), CONCAT(YEAR(CURDATE()), '-08-12 10:00:00'), 2, 'vor_ort', 0, 'Carina Hollederer', 'c.hollederer@sufw.de', '(0176) 17922244', 1),
('f5c7f216-2704-4c81-b17a-000000000004', 'produktionsschule', 'Probearbeiten', CONCAT(YEAR(CURDATE()), '-08-12 08:00:00'), CONCAT(YEAR(CURDATE()), '-08-12 16:00:00'), 2, 'vor_ort', 1, 'Sarah van den Heuvel', 's.vandenHeuvel@sufw.de', '(0176) 60328929', 1)
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`), `is_active` = VALUES(`is_active`);
INSERT INTO `sufw_booking_registrations`
(`id`, `event_id`, `status`, `name`, `email`, `phone`, `privacy_accepted_at`, `confirmed_at`, `introduction_attended`, `introduction_date`)
VALUES
('g5c7f216-2704-4c81-b17a-000000000001', 'f5c7f216-2704-4c81-b17a-000000000001', 'confirmed', 'Demo Greta Juli', 'greta.juli@example.test', '+49 000 100006', DATE_SUB(NOW(), INTERVAL 50 DAY), DATE_SUB(NOW(), INTERVAL 50 DAY), 0, NULL),
('g5c7f216-2704-4c81-b17a-000000000002', 'f5c7f216-2704-4c81-b17a-000000000002', 'confirmed', 'Demo Hasan Juli', 'hasan.juli@example.test', '+49 000 100007', DATE_SUB(NOW(), INTERVAL 50 DAY), DATE_SUB(NOW(), INTERVAL 50 DAY), 0, NULL),
('g5c7f216-2704-4c81-b17a-000000000003', 'f5c7f216-2704-4c81-b17a-000000000003', 'confirmed', 'Demo Ines August', 'ines.august@example.test', '+49 000 100008', DATE_SUB(NOW(), INTERVAL 20 DAY), DATE_SUB(NOW(), INTERVAL 20 DAY), 0, NULL),
('g5c7f216-2704-4c81-b17a-000000000004', 'f5c7f216-2704-4c81-b17a-000000000004', 'confirmed', 'Demo Jakob August', 'jakob.august@example.test', '+49 000 100009', DATE_SUB(NOW(), INTERVAL 20 DAY), DATE_SUB(NOW(), INTERVAL 20 DAY), 1, CURDATE())
ON DUPLICATE KEY UPDATE
`event_id` = VALUES(`event_id`), `status` = VALUES(`status`), `name` = VALUES(`name`), `email` = VALUES(`email`), `phone` = VALUES(`phone`), `privacy_accepted_at` = VALUES(`privacy_accepted_at`), `confirmed_at` = VALUES(`confirmed_at`), `introduction_attended` = VALUES(`introduction_attended`), `introduction_date` = VALUES(`introduction_date`), `removed_at` = NULL;
INSERT IGNORE INTO `sufw_booking_schema_migrations` (`version`) VALUES ('005_booking_demo_july_august');
@@ -107,8 +107,9 @@ if ($view === 'events') {
$statement->execute(['project' => $project]); $statement->execute(['project' => $project]);
$outbox = $statement->fetchAll(); $outbox = $statement->fetchAll();
} }
$defaultFrom = date('Y-m-01'); $nextMonth = new DateTimeImmutable('first day of next month');
$defaultTo = date('Y-m-t'); $defaultFrom = $nextMonth->format('Y-m-01');
$defaultTo = $nextMonth->format('Y-m-t');
?> ?>
<!doctype html> <!doctype html>
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Terminbuchung · Testverwaltung</title> <html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Terminbuchung · Testverwaltung</title>
@@ -140,7 +141,7 @@ select[data-dropdown-indicator]{appearance:none;padding-right:2.5rem;background:
</div><div id="export-results" class="export-results" role="status" aria-live="polite">Lade Teilnahmen …</div></section> </div><div id="export-results" class="export-results" role="status" aria-live="polite">Lade Teilnahmen …</div></section>
<?php endif; ?> <?php endif; ?>
</main> </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><div class="picker-time" data-picker-time-controls><label>Stunde<select data-picker-hour></select></label><label>Viertelstunde<select data-picker-quarter></select></label></div><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> <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> <script>
const contactByName = <?=json_encode($contactByName, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT)?>; 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 contactByEmail = <?=json_encode($contactByEmail, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT)?>;
@@ -148,11 +149,9 @@ const contactName = document.querySelector('#contact_name'), contactEmail = docu
contactName?.addEventListener('change', () => { const contact=contactByName[contactName.value]; if (contact) { contactEmail.value=contact.email; contactPhone.value=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; } }); 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]'); 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]');
const hours = Array.from({length:24}, (_,hour) => String(hour).padStart(2,'0')); 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(''); }
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; let pickerTarget, pickerDate, pickerMonth, pickerMode;
const asDate = value => { const parts = String(value || '').match(/^(\\d{4})-(\\d{2})-(\\d{2})(?:[ T](\\d{2}):(\\d{2}))?/); return parts ? new Date(Number(parts[1]), Number(parts[2])-1, Number(parts[3]), Number(parts[4] || 0), Number(parts[5] || 0)) : new Date(); }; 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 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 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 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');
@@ -164,11 +163,11 @@ function renderPicker() {
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.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(); })); 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); timeControls.hidden=pickerMode === 'date'; hourSelect.value=String(pickerDate.getHours()).padStart(2,'0'); quarterSelect.value=String(Math.floor(pickerDate.getMinutes()/15)*15).padStart(2,'0'); renderPicker(); dialog.showModal(); })); 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-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-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-close]').addEventListener('click', () => dialog.close());
dialog.querySelector('[data-picker-apply]').addEventListener('click', () => { if (pickerMode === 'datetime') 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(); }); 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(); syncPickerLabels();
const exportResults=document.querySelector('#export-results'); const exportResults=document.querySelector('#export-results');
const escapeHtml = value => String(value).replace(/[&<>"']/g, character => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[character])); const escapeHtml = value => String(value).replace(/[&<>"']/g, character => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[character]));
@@ -51,6 +51,15 @@ test('erweitert Demo-Kontakte um die vorgesehenen Ansprechpartnerdaten', async (
assert.match(migration, /Sarah van den Heuvel/); assert.match(migration, /Sarah van den Heuvel/);
}); });
test('liefert zusätzliche lokale Demo-Termine für Juli und August', async () => {
const migration = await readFile(new URL('../database/migrations/005_booking_demo_july_august.sql', import.meta.url), 'utf8');
assert.match(migration, /Nur für lokale Entwicklungs- und Testdatenbanken/);
assert.match(migration, /-07-10 09:00:00/);
assert.match(migration, /-08-12 09:00:00/);
assert.match(migration, /sufw_booking_registrations/);
});
test('liefert die unabhängige Verwaltung unter booking-admin mit getrennten Bereichen', async () => { test('liefert die unabhängige Verwaltung unter booking-admin mit getrennten Bereichen', async () => {
const [routes, admin] = await Promise.all([ const [routes, admin] = await Promise.all([
readFile(new URL('../public/.htaccess', import.meta.url), 'utf8'), readFile(new URL('../public/.htaccess', import.meta.url), 'utf8'),
@@ -68,8 +77,10 @@ test('liefert die unabhängige Verwaltung unter booking-admin mit getrennten Ber
assert.doesNotMatch(admin, /datetime-local/); assert.doesNotMatch(admin, /datetime-local/);
assert.match(admin, /data-picker-hour/); assert.match(admin, /data-picker-hour/);
assert.match(admin, /data-picker-quarter/); assert.match(admin, /data-picker-quarter/);
assert.match(admin, /<\?php if \(\$view === 'events'\): \?><div class="picker-time" data-picker-time-controls>/);
assert.match(admin, /Array\.from\(\{length:24\}/); assert.match(admin, /Array\.from\(\{length:24\}/);
assert.match(admin, /\['00','15','30','45'\]/); assert.match(admin, /\['00','15','30','45'\]/);
assert.match(admin, /defaultDate\.setHours\(9,0,0,0\)/);
assert.match(admin, /data-picker-close/); assert.match(admin, /data-picker-close/);
assert.match(admin, /<select name="title"/); assert.match(admin, /<select name="title"/);
assert.match(admin, /data-dropdown-indicator/); assert.match(admin, /data-dropdown-indicator/);
@@ -80,5 +91,6 @@ test('liefert die unabhängige Verwaltung unter booking-admin mit getrennten Ber
assert.match(admin, /id="export-project"/); assert.match(admin, /id="export-project"/);
assert.match(admin, /DATE\(e\.starts_at\) AS event_day/); assert.match(admin, /DATE\(e\.starts_at\) AS event_day/);
assert.match(admin, /export-project/); assert.match(admin, /export-project/);
assert.match(admin, /new DateTimeImmutable\('first day of next month'\)/);
assert.doesNotMatch(admin, /text\/csv|fputcsv/i); assert.doesNotMatch(admin, /text\/csv|fputcsv/i);
}); });