From 76e45ac9ad10ee228349b5375e18454d2b880141 Mon Sep 17 00:00:00 2001 From: Robert Wartenberg Date: Thu, 20 Aug 2026 12:28:57 +0200 Subject: [PATCH] Erstellen einer ersten test-Komponente die in der Mockup-HTML-Seite und in Contao eingebunden werden kann --- AGENTS.md | 40 ++++--- development-system/README.md | 45 ++++++- .../integration/contao-html-element.html | 9 ++ development-system/package-lock.json | 54 +-------- development-system/package.json | 11 +- development-system/public/booking/booking.css | 1 + development-system/public/booking/booking.js | 110 ++++++++++++++++++ .../public/perspektivwerk/index.html | 5 + development-system/src/booking.ts | 21 ---- .../tests/PerspektivwerkRoute.Tests.ps1 | 8 ++ development-system/tests/booking.test.mjs | 16 +++ development-system/tsconfig.json | 11 -- 12 files changed, 217 insertions(+), 114 deletions(-) create mode 100644 development-system/integration/contao-html-element.html create mode 100644 development-system/public/booking/booking.css create mode 100644 development-system/public/booking/booking.js delete mode 100644 development-system/src/booking.ts create mode 100644 development-system/tests/booking.test.mjs delete mode 100644 development-system/tsconfig.json diff --git a/AGENTS.md b/AGENTS.md index ca769ac..2b55a42 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,15 @@ In dieses soll eine WebComponente integriert werden, die einen Terminbuchungskal Termininformationen kommen aus der Datenbank entweder mittels Contao oder direkt als MySQL Query. Es gibt zwei Kalender (je für PW/PS) und jeder Kalender enthält eigene Termine die von Besuchern gebucht werden können. +### Auslieferungswege der Terminbuchungskomponente + +Die Terminbuchungskomponente muss in zwei gleichwertigen Varianten funktionieren: + +- **Apache-Vorschau:** als Bestandteil der statischen Seite im Webpfad `public/perspektivwerk/`, erreichbar unter `http://localhost:8080/perspektivwerk/`. +- **Contao-Einbindung:** als einbettbare Komponente in einem Contao-Inhaltselement vom Typ HTML. + +Beide Varianten verwenden dieselben browsernativen JavaScript- und CSS-Assets aus `public/booking/`. Die Apache-Vorschau referenziert sie über `/booking/booking.js` und `/booking/booking.css`. Für Contao wird das kombinierte Snippet aus `integration/contao-html-element.html` in ein neues Inhaltselement vom Typ **HTML** eingefügt. Es muss als Roh-HTML im Feld „HTML-Code“ gespeichert werden, nicht als Textinhalt eines TinyMCE-Text-Elements. Ein zweites Kopieren in den Contao-Dateibereich `files/` ist nicht erforderlich. + ### Die Oberfläche / UI - ein Kalender-view aller Tage des aktuellen Monats @@ -70,19 +79,20 @@ Es gibt zwei Kalender (je für PW/PS) und jeder Kalender enthält eigene Termine ### Technische Fragen - 1. Die verwendete Contao-Version hat bekannte Sicherheitslücken, wann wird aktualisiert? - 2. Ich benötige eigene Datenbanken (oder Speicher direkt in Contao) für die Termine und E-Mails - 3. E-Mail Validierung bei Buchung: +1. Die verwendete Contao-Version hat bekannte Sicherheitslücken, wann wird aktualisiert? +2. Zum einbinden der Terminbuchung muss in den Sicherheitseinstellungen von Contao und diff --git a/development-system/package-lock.json b/development-system/package-lock.json index e84c283..e836c0e 100644 --- a/development-system/package-lock.json +++ b/development-system/package-lock.json @@ -6,13 +6,7 @@ "": { "name": "perspektivwerk-booking-frontend", "dependencies": { - "bootstrap": "^5.3.3", - "jquery": "^3.7.1", - "slick-carousel": "^1.8.1" - }, - "devDependencies": { - "@types/jquery": "^3.5.32", - "typescript": "^5.7.2" + "bootstrap": "^5.3.3" } }, "node_modules/@popperjs/core": { @@ -26,23 +20,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@types/jquery": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.34.tgz", - "integrity": "sha512-3m3939S3erqmTLJANS/uy0B6V7BorKx7RorcGZVjZ62dF5PAGbKEDZK1CuLtKombJkFA2T1jl8LAIIs7IV6gBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/sizzle": "*" - } - }, - "node_modules/@types/sizzle": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", - "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", - "dev": true, - "license": "MIT" - }, "node_modules/bootstrap": { "version": "5.3.8", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", @@ -61,35 +38,6 @@ "peerDependencies": { "@popperjs/core": "^2.11.8" } - }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT" - }, - "node_modules/slick-carousel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz", - "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==", - "license": "MIT", - "peerDependencies": { - "jquery": ">=1.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } } } } diff --git a/development-system/package.json b/development-system/package.json index e58947e..5429d1f 100644 --- a/development-system/package.json +++ b/development-system/package.json @@ -3,16 +3,9 @@ "private": true, "type": "module", "scripts": { - "build": "tsc", - "dev": "tsc --watch" + "test": "node --test tests/booking.test.mjs" }, "dependencies": { - "bootstrap": "^5.3.3", - "jquery": "^3.7.1", - "slick-carousel": "^1.8.1" - }, - "devDependencies": { - "@types/jquery": "^3.5.32", - "typescript": "^5.7.2" + "bootstrap": "^5.3.3" } } diff --git a/development-system/public/booking/booking.css b/development-system/public/booking/booking.css new file mode 100644 index 0000000..3508ea7 --- /dev/null +++ b/development-system/public/booking/booking.css @@ -0,0 +1 @@ +.sufw-booking{--sufw-booking-green:#08752a;--sufw-booking-text:#202020;--sufw-booking-muted:#5a5a5a;--sufw-booking-line:#c7c7c7;--sufw-booking-pale:#f3f6f3;box-sizing:border-box;max-width:860px;margin:2rem auto;padding:1.25rem;color:var(--sufw-booking-text);background:#fff;border:1px solid var(--sufw-booking-line);font:1rem/1.5 Arial,"Helvetica Neue",sans-serif}.sufw-booking *{box-sizing:border-box}.sufw-booking__header h2,.sufw-booking__slots h3{margin:.15rem 0 .5rem;color:var(--sufw-booking-green);font-size:1.4rem}.sufw-booking__header p{margin:.3rem 0}.sufw-booking__eyebrow{color:var(--sufw-booking-green);font-size:.875rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.sufw-booking__legend{display:flex;gap:1rem;flex-wrap:wrap;margin:1.25rem 0;font-size:.9rem}.sufw-booking__marker{display:inline-block;width:.8rem;height:.8rem;margin-right:.35rem;border-radius:50%}.sufw-booking__marker--available{background:var(--sufw-booking-green)}.sufw-booking__marker--booked{background:#535353}.sufw-booking__month-nav{display:grid;grid-template-columns:3rem 1fr 3rem;align-items:center;margin-top:1rem;text-align:center}.sufw-booking__month-nav h3{margin:0;font-size:1.1rem;text-transform:capitalize}.sufw-booking__nav,.sufw-booking__day,.sufw-booking__slot{font:inherit}.sufw-booking__nav{min-width:2.75rem;min-height:2.75rem;border:1px solid var(--sufw-booking-green);color:var(--sufw-booking-green);background:#fff;font-size:1.7rem;cursor:pointer}.sufw-booking__nav:hover,.sufw-booking__nav:focus-visible{background:var(--sufw-booking-pale)}.sufw-booking__weekdays,.sufw-booking__calendar{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:.3rem}.sufw-booking__weekdays{margin-top:1rem;text-align:center;font-weight:700;font-size:.8rem}.sufw-booking__day{display:grid;place-items:center;gap:.1rem;min-height:3.8rem;padding:.25rem;border:1px solid transparent;background:#fff;color:var(--sufw-booking-text);cursor:pointer}.sufw-booking__day:hover{border-color:var(--sufw-booking-green);background:var(--sufw-booking-pale)}.sufw-booking__day--available{background:#dff3e4;border-color:var(--sufw-booking-green)}.sufw-booking__day--booked{background:#ececec;border-color:#777}.sufw-booking__day.is-selected{outline:3px solid #1f4fbc;outline-offset:1px}.sufw-booking__day small{font-size:.7rem}.sufw-booking__empty{min-height:3.8rem}.sufw-booking__slots{margin-top:1.5rem;padding-top:1.25rem;border-top:1px solid var(--sufw-booking-line)}.sufw-booking__slots ul{padding:0;margin:0;list-style:none}.sufw-booking__slot{display:grid;grid-template-columns:5rem 1fr auto;gap:.7rem;width:100%;margin:.45rem 0;padding:.8rem;text-align:left;color:var(--sufw-booking-text);background:#fff;border:1px solid var(--sufw-booking-green);cursor:pointer}.sufw-booking__slot:hover,.sufw-booking__slot:focus-visible{background:var(--sufw-booking-pale)}.sufw-booking__slot em{color:var(--sufw-booking-green);font-style:normal;font-weight:700}.sufw-booking__slot:disabled{border-color:#777;background:#eee;color:#555;cursor:not-allowed}.sufw-booking__slot:disabled em{color:#555}.sufw-booking__empty-state,.sufw-booking__status{margin:.7rem 0}.sufw-booking__status{font-weight:700}@media (max-width:520px){.sufw-booking{margin:1rem 0;padding:1rem}.sufw-booking__weekdays{font-size:.7rem}.sufw-booking__day{min-height:3.4rem;padding:.15rem}.sufw-booking__slot{grid-template-columns:1fr;gap:.15rem}.sufw-booking__slot strong{font-size:1.1rem}} diff --git a/development-system/public/booking/booking.js b/development-system/public/booking/booking.js new file mode 100644 index 0000000..1e0ba03 --- /dev/null +++ b/development-system/public/booking/booking.js @@ -0,0 +1,110 @@ +/* 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 = ` +
+
+

Terminbuchung – Vorschau

+

${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 ? `
    ${slots.map((slot) => `
  • `).join('')}
` : '

Für diesen Tag liegen in der Vorschau 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 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(); + } +} diff --git a/development-system/public/perspektivwerk/index.html b/development-system/public/perspektivwerk/index.html index b28ca4c..73f46be 100644 --- a/development-system/public/perspektivwerk/index.html +++ b/development-system/public/perspektivwerk/index.html @@ -6,6 +6,7 @@ PerspektivWerk | SUFW Dresden + @@ -19,8 +20,12 @@

PerspektivWerk – Brücke zwischen Diagnostik, Begleitung und betrieblicher Praxis

Unser Fachpersonal arbeitet mit Ihnen gemeinsam daran, Ihre aktuelle Lebenssituation zu verstehen. Ziel ist es, aus den Informationen individuelle und zu Ihnen passende Wege ins Arbeitsleben sichtbar zu machen.

Dazu gehört es, herauszufinden, welche Herausforderungen Sie im Alltag bestreiten müssen und wo es Unterstützungsbedarf gibt. Vor allem aber auch, wo Ihre Fähigkeiten, Interessen und Entwicklungsmöglichkeiten liegen. Dabei arbeiten wir eng mit den beratenden Fachkräften der Agentur für Arbeit und des Jobcenters zusammen.

Wir sind für Sie da

Unser Team, bestehend aus sozialpädagogischen, pädagogischen und psychologischen Mitarbeiterinnen und Mitarbeitern, unterstützt Ihren Prozess der beruflichen Orientierung. Wir arbeiten eng mit den Bearbeiterinnen und Beratern der Agentur für Arbeit, dem Jobcenter und weiteren Beratungsstellen zusammen.

Die Teilnahme am PerspektivWerk ist kostenlos. Sie bestimmen, an welchen Angeboten Sie teilnehmen.

Beratungsgespräch im PerspektivWerk

Wer kann zu uns kommen?

Menschen zwischen 15 und 65 Jahren, die sich beruflich neu orientieren möchten, beim Einstieg Unterstützung suchen oder ihre Möglichkeiten gemeinsam klären wollen.

1. Orientierung & Ersteinschätzung

Sozialpädagogische und psychologische Module helfen dabei, persönliche Situation, Motivation, Belastbarkeit und Stärken einzuschätzen.

2. Praktische Erprobung & Belastungstest

Arbeitsproben und praktische Erprobungen machen fachliche, soziale und arbeitsbezogene Kompetenzen sichtbar.

3. Auswertung & Perspektiventwicklung

Wir führen die Ergebnisse zusammen und entwickeln konkrete, realistische nächste Schritte.

+
+
+

Sie haben Fragen zu dieser Maßnahme?

Haben Sie Interesse an unserem Angebot, melden Sie sich bei uns. Wir beraten Sie gern vorab zu den weiteren Schritten.

Kontakt: (0176) 60328929 · perspektivwerk@sufw.de · Dresdner Straße 16, 01326 Dresden

+ diff --git a/development-system/src/booking.ts b/development-system/src/booking.ts deleted file mode 100644 index 2bcf850..0000000 --- a/development-system/src/booking.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Dünne, frameworkfreie Buchungslogik. Sie kann später gegen eine Backend-API - * oder eine Kalender-Integration ausgetauscht werden, ohne das UI neu zu bauen. - */ -export type BookingProject = 'perspektivwerk' | 'produktionsschule'; - -export interface BookingOption { - id: string; - project: BookingProject; - title: string; - duration: string; -} - -export const bookingOptions: BookingOption[] = [ - { id: 'pw-erstgespraech', project: 'perspektivwerk', title: 'Erstgespräch Perspektivwerk', duration: '1 Stunde' }, - { id: 'pw-sozpaed', project: 'perspektivwerk', title: 'Soz. Päd. Modul Perspektivwerk', duration: '4 Stunden' }, - { id: 'pw-psy', project: 'perspektivwerk', title: 'Psy Modul Perspektivwerk', duration: '4 Stunden' }, - { id: 'pw-praxis', project: 'perspektivwerk', title: 'Praxis Modul Perspektivwerk', duration: '1 Woche, ab Montag 08:00 Uhr' }, - { id: 'ps-kennenlernen', project: 'produktionsschule', title: 'Voranmeldung Kennenlerntermin Produktionsschule', duration: '1 Stunde, dienstags' }, - { id: 'ps-probearbeiten', project: 'produktionsschule', title: 'Probearbeiten', duration: '1 Woche, ab Montag 08:00 Uhr' } -]; diff --git a/development-system/tests/PerspektivwerkRoute.Tests.ps1 b/development-system/tests/PerspektivwerkRoute.Tests.ps1 index 87a0398..e16fbc1 100644 --- a/development-system/tests/PerspektivwerkRoute.Tests.ps1 +++ b/development-system/tests/PerspektivwerkRoute.Tests.ps1 @@ -7,6 +7,14 @@ Describe 'Perspektivwerk static route' { (Test-Path -LiteralPath $mockupPath) | Should Be $true } + It 'embeds the appointment booking component with its public assets' { + $mockup = Get-Content -LiteralPath $mockupPath -Raw + + $mockup | Should Match 'href="/booking/booking\.css"' + $mockup | Should Match 'data-sufw-booking' + $mockup | Should Match 'src="/booking/booking\.js"' + } + It 'routes /perspektivwerk to the static entry point before the Contao fallback' { $htaccess = Get-Content -LiteralPath $htaccessPath -Raw $staticRouteIndex = $htaccess.IndexOf('RewriteRule ^perspektivwerk/?$ perspektivwerk/index.html [L]') diff --git a/development-system/tests/booking.test.mjs b/development-system/tests/booking.test.mjs new file mode 100644 index 0000000..e52ae20 --- /dev/null +++ b/development-system/tests/booking.test.mjs @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { getSlotsForDate } from '../public/booking/booking.js'; + +test('liefert für einen Beispieltag verfügbare und belegte Slots', () => { + const slots = getSlotsForDate('perspektivwerk', '2026-08-25'); + + assert.equal(slots.length, 3); + assert.equal(slots.filter((slot) => slot.status === 'available').length, 2); + assert.equal(slots.filter((slot) => slot.status === 'booked').length, 1); +}); + +test('liefert für einen Tag ohne Termin keine Slots', () => { + assert.deepEqual(getSlotsForDate('produktionsschule', '2026-08-20'), []); +}); diff --git a/development-system/tsconfig.json b/development-system/tsconfig.json deleted file mode 100644 index 030ba79..0000000 --- a/development-system/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ES2020", - "moduleResolution": "Bundler", - "strict": true, - "noEmitOnError": true, - "outDir": "public/perspektivwerk/js" - }, - "include": ["src/**/*.ts"] -}