Terminplaner: Portal-Ansicht, Kachel, Termine

Kachel in Funktionen (Kommt bald / Aus / Aktiv mit Anfragen-Zahl / Abgeschaltet),
Ansicht #termine als Unterseite von Funktionen: Projektauswahl, Vorlagenauswahl
beim ersten Oeffnen, Reiter-Geruest (Platzhalter fuer Leistungen, Team & Plaetze,
Einstellungen) und der Reiter Termine mit Tag/Woche/Monat, Filtern, Aktionen mit
Rueckfragen in der Seite, Termin eintragen und Zeit sperren.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-03 08:17:44 +00:00
1 parent 70940e02c8
commit 43e4826ae3
10 files changed
+2552 -8

No files matched your search

+97
View File
@@ -49,6 +49,103 @@ test('Kacheln rendern ohne Fehler: News, Analyse comingSoon/aus/aktiv/gesperrt',
assert.doesNotMatch(disabled, /Dev-Modus/)
})
// ---- Terminplaner-Kachel: nur mit Schalter (termine.verfuegbar), Zustand je Projekt aus features.termine
const withTermine = (id, state) => ({ projectId: id, projectName: `P ${id}`, liveUrl: '', features: { news, termine: { state } } })
const noAnalyse = { comingSoon: true }
async function renderTermine(projects, { verfuegbar, anfragen = {} } = {}) {
const calls = []
globalThis.fetch = async (url) => {
calls.push(String(url))
if (String(url).startsWith('/api/termine/einstellungen')) {
const id = new URL(url, 'http://x').searchParams.get('projectId')
return { ok: true, status: 200, json: async () => ({ anfragenOffen: anfragen[id] ?? 0 }) }
}
return { ok: true, status: 200, json: async () => ({ projects, ...(verfuegbar === undefined ? {} : { termine: { verfuegbar } }) }) }
}
const count = { textContent: '', hidden: true }
const root = { innerHTML: '', querySelector: (sel) => (sel === '[data-termine-count]' ? count : null) }
await initFunktionen(root)
return { html: root.innerHTML, count, calls }
}
test('Terminplaner-Kachel: fehlt termine.verfuegbar oder ist false ⇒ „Kommt bald“ ohne Knopf', async () => {
for (const verfuegbar of [undefined, false]) {
const { html, calls } = await renderTermine([{ ...withTermine('a', 'active'), analyse: noAnalyse }], { verfuegbar })
assert.doesNotMatch(html, /data-fn-tile="termine"/)
assert.match(html, /fn-tile-soon[\s\S]*Terminplaner/)
assert.match(html, /Kommt bald/)
assert.doesNotMatch(html, /data-view="termine"/)
assert.deepEqual(calls, ['/api/funktionen'], 'ohne Schalter keine Abfrage an /api/termine')
}
})
test('Terminplaner-Kachel: Aus ⇒ „Wir schalten den Terminplaner für dich frei.“ + Chat', async () => {
for (const projects of [[{ ...project('a', noAnalyse), features: { news } }], [withTermine('a', 'off')], [withTermine('a', 'ordered')]]) {
const { html } = await renderTermine(projects, { verfuegbar: true })
const kachel = tile(html, 'termine')
assert.match(kachel, /Terminplaner/)
assert.match(kachel, /fn-pill-off">Aus</)
assert.match(kachel, /Wir schalten den Terminplaner für dich frei\./)
assert.match(kachel, /data-view="chat"[^>]*>Chat öffnen</)
assert.doesNotMatch(kachel, /data-view="termine"/)
}
})
test('Terminplaner-Kachel: Aktiv ⇒ „Termine öffnen“ mit Projekt, Anfragen aus anfragenOffen', async () => {
const { html, count, calls } = await renderTermine([withTermine('a', 'off'), withTermine('b', 'active')], { verfuegbar: true, anfragen: { b: 3 } })
const kachel = tile(html, 'termine')
assert.match(kachel, /fn-pill-on">Aktiv</)
assert.match(kachel, /data-view="termine" data-project="b"[^>]*>Termine öffnen</)
assert.deepEqual(calls, ['/api/funktionen', '/api/termine/einstellungen?projectId=b'], 'nur aktive Projekte werden gefragt')
assert.equal(count.textContent, '3 Anfragen warten')
assert.equal(count.hidden, false)
})
test('Terminplaner-Kachel: Anfragen zählen über alle aktiven Websites, 1 ⇒ Einzahl, 0 ⇒ nichts', async () => {
const zwei = await renderTermine([withTermine('a', 'active'), withTermine('b', 'active')], { verfuegbar: true, anfragen: { a: 1, b: 2 } })
assert.equal(zwei.count.textContent, '3 Anfragen warten')
const eine = await renderTermine([withTermine('a', 'active')], { verfuegbar: true, anfragen: { a: 1 } })
assert.equal(eine.count.textContent, '1 Anfrage wartet')
const keine = await renderTermine([withTermine('a', 'active')], { verfuegbar: true, anfragen: {} })
assert.equal(keine.count.textContent, '')
assert.equal(keine.count.hidden, true)
})
test('Terminplaner-Kachel: Fehler bei der Anfragen-Zahl lässt die Kachel stehen', async () => {
globalThis.fetch = async (url) => {
if (String(url).startsWith('/api/termine/')) throw new Error('offline')
return { ok: true, status: 200, json: async () => ({ projects: [withTermine('a', 'active')], termine: { verfuegbar: true } }) }
}
const count = { textContent: '', hidden: true }
const root = { innerHTML: '', querySelector: (sel) => (sel === '[data-termine-count]' ? count : null) }
await initFunktionen(root)
assert.match(tile(root.innerHTML, 'termine'), /Termine öffnen/)
assert.equal(count.textContent, '')
assert.equal(count.hidden, true)
})
test('Terminplaner-Kachel: Abgeschaltet ⇒ Pille, Hinweis, Termine nur ansehen', async () => {
const { html, calls } = await renderTermine([withTermine('a', 'disabled')], { verfuegbar: true })
const kachel = tile(html, 'termine')
assert.match(kachel, /fn-pill-off">Abgeschaltet</)
assert.match(kachel, /abgeschaltet/)
assert.match(kachel, /data-view="termine" data-project="a"[^>]*>Termine ansehen</)
assert.deepEqual(calls, ['/api/funktionen'], 'abgeschaltet: keine Anfragen-Zahl')
})
test('Terminplaner-Kachel: mehrere Websites, Hinweis „aktiv für n von m“, Aktive zuerst', async () => {
const { html } = await renderTermine([withTermine('a', 'disabled'), withTermine('b', 'off'), withTermine('c', 'active')], { verfuegbar: true })
const kachel = tile(html, 'termine')
assert.match(kachel, /aktiv für 1 von 3 Websites/)
assert.match(kachel, /data-project="c"/)
})
test('Terminplaner-Kachel: Projektname und Zustand kommen nur maskiert in die Kachel', async () => {
const { html } = await renderTermine([{ ...withTermine('a<b>', 'active'), projectName: '<img src=x onerror=1>' }], { verfuegbar: true })
assert.doesNotMatch(html, /<img/)
assert.match(tile(html, 'termine'), /data-project="a&lt;b&gt;"/)
})
// ---- Analyse-Einschalten und Ausschalten (ohne DOM: Attrappen, die innerHTML und Klick-Handler festhalten)
function fakeEl() {
const handlers = {}