Module: im Abo-Modell Reiter mit Kurzlabels an die Etage melden (C2)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 18:37:12 +00:00
1 parent b91ba1d5cb
commit a50c5ef6c9
6 files changed
+366 -17

No files matched your search

+20 -6
View File
@@ -7,6 +7,7 @@
import { fehlerKarte, h, infoKarte, request } from './shop-dom.js'
import { statusKarte } from './funktion-bestellen.js'
import { anfrageKarte, renderAnfrageFormular } from './buchungen-anfrage.js'
import { meldeReiter, reiterEintraege } from './fn-reiter.js'
/** Die Reiter: jeder ein Modul mit render(panel, ctx), erst beim ersten Oeffnen geladen. */
export const REITER = [
@@ -27,6 +28,10 @@ export function vergissAuswahl() {
let S = null // aktuelle Sitzung; eine alte Antwort darf eine neuere nicht ueberschreiben
const textOr = (x) => (typeof x === 'string' && x.trim() ? x : '')
// Abo-Modell (Antwort von GET /api/funktionen, strikt): die Reiter zeichnet die Etage, die Ansicht meldet sie (fn-reiter.js)
const istAbo = (s) => s.modell === 'abo'
/** Eine Ansicht ohne Reiter meldet die leere Liste, damit die Etage nicht die Reiter der vorigen stehen laesst. */
const keineReiter = (s) => { if (istAbo(s)) meldeReiter(s.root, { eintraege: [] }) }
const zustandVon = (projekt) => projekt?.features?.unterkunft?.state || 'off'
export function initBuchungen(root, { projectId } = {}) {
@@ -38,7 +43,7 @@ export function initBuchungen(root, { projectId } = {}) {
async function starte(root, projectId) {
if (projectId) auswahl.projectId = projectId
const s = { root, projects: [], projekt: null, status: null, lauf: 0, buehne: null, meldungEl: null }
const s = { root, projects: [], modell: undefined, projekt: null, status: null, lauf: 0, buehne: null, meldungEl: null }
S = s
let funktionen
try {
@@ -48,12 +53,15 @@ async function starte(root, projectId) {
return
}
if (S !== s) return
s.modell = funktionen?.modell
if (!funktionen?.unterkunft?.verfuegbar) {
keineReiter(s)
root.replaceChildren(infoKarte(NICHT_VERFUEGBAR, 'Sobald er für dich bereit ist, findest du ihn hier.'))
return
}
s.projects = Array.isArray(funktionen.projects) ? funktionen.projects : []
if (!s.projects.length) {
keineReiter(s)
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
return
}
@@ -99,6 +107,7 @@ const ladeStatus = (projectId) => request(`/api/unterkunft/status?projectId=${en
async function zeigeProjekt(s) {
const lauf = ++s.lauf
const aktuell = () => S === s && s.lauf === lauf
keineReiter(s)
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
let status
try {
@@ -113,6 +122,7 @@ async function zeigeProjekt(s) {
function zeichneAnsicht(s, { status, aktuell }) {
s.status = status
keineReiter(s) // die Ansicht mit Reitern (zeigeReiter) meldet ihre Liste gleich danach
const zustand = status?.state || zustandVon(s.projekt)
if (zustand === 'setup' || zustand === 'active' || zustand === 'disabled') zeigeReiter(s, { status, aktuell, zustand })
else if (zustand === 'ordered') zeigeBestellung(s)
@@ -171,15 +181,16 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
zeigeMeldung: (meldung, art) => { if (aktuell()) zeigeMeldung(s, meldung, art) },
neuLaden,
}
const abo = istAbo(s) // Abo-Modell: keine eigene Leiste, die Etage zeichnet sie (kn:reiter)
const panel = h('div', { class: 'sh-panel', role: 'tabpanel', id: 'bk-panel', tabindex: '0' })
let aktiv = null
let zaehler = 0
const knoepfe = REITER.map((r) => h('button', {
const knoepfe = abo ? [] : REITER.map((r) => h('button', {
type: 'button', role: 'tab', class: 'sh-reiter', id: `bk-tab-${r.key}`, 'aria-controls': 'bk-panel', 'data-reiter': r.key,
onclick: () => waehleReiter(r.key),
}, r.label))
const leiste = h('div', { class: 'sh-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Buchungskalenders' }, knoepfe)
leiste.addEventListener('keydown', (ev) => {
const leiste = abo ? null : h('div', { class: 'sh-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Buchungskalenders' }, knoepfe)
leiste?.addEventListener('keydown', (ev) => {
const i = REITER.findIndex((r) => r.key === aktiv)
const ziel = { ArrowRight: (i + 1) % REITER.length, ArrowLeft: (i + REITER.length - 1) % REITER.length, Home: 0, End: REITER.length - 1 }[ev.key]
if (ziel === undefined) return
@@ -197,7 +208,10 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
k.setAttribute('aria-selected', String(an))
k.tabIndex = an ? 0 : -1
}
panel.setAttribute('aria-labelledby', `bk-tab-${reiter.key}`)
if (abo) {
panel.setAttribute('aria-label', reiter.label)
meldeReiter(s.root, { eintraege: reiterEintraege('buchungen', REITER), aktiv: reiter.key, bei: (neu) => waehleReiter(neu) })
} else panel.setAttribute('aria-labelledby', `bk-tab-${reiter.key}`)
zeigeMeldung(s, '')
panel.replaceChildren()
reiter.laden()
@@ -207,6 +221,6 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
const hinweis = zustand === 'setup' ? 'Wir bauen deinen Buchungskalender gerade ein. Du kannst schon Unterkünfte, Preise und Einstellungen vorbereiten.'
: zustand === 'disabled' ? 'Dein Buchungskalender ist abgeschaltet. Du kannst bestehende Buchungen noch ansehen und bearbeiten.' : ''
const hinweisBox = h('div', { class: 'sh-hinweise' }, hinweis ? h('div', { class: `sh-box ${zustand === 'disabled' ? 'sh-box-warn' : 'sh-box-info'}`, role: 'note', 'data-hinweis': zustand }, hinweis) : null)
s.buehne.replaceChildren(hinweisBox, leiste, panel)
s.buehne.replaceChildren(...[hinweisBox, leiste, panel].filter(Boolean))
waehleReiter(auswahl.reiter)
}
+24
View File
@@ -0,0 +1,24 @@
/* Reiter der Funktionsmodule in der neuen Ansicht (Abo-Modell). Im Abo-Modell zeichnen Terminplaner, Webshop, Buchungskalender und
Analyse keine eigene Leiste, sondern melden ihre Reiter an die Etage 2 (public/neu/etage.js): CustomEvent 'kn:reiter' mit
bubbles und detail { eintraege: [{ key, label, kurz? }], aktiv, bei(key) }; eine leere Liste heisst „keine Reiter“.
Dieses Modul hat keine Importe und kennt weder DOM noch die Huelle. */
/** Kurzlabels fuer schmale Zeilen (C2): modul → { reiterKey: kurzes Wort }. Die Analyse braucht keine (Übersicht, Heatmap, Einstellungen). */
export const KURZLABELS = {
termine: { team: 'Team' },
shop: { uebersicht: 'Start', rechtliches: 'Recht' },
buchungen: { unterkuenfte: 'Preise' },
}
/** [{ key, label, … }] des Moduls → [{ key, label, kurz? }]; `kurz` nur mit Eintrag in KURZLABELS, alle anderen Felder fallen weg. */
export function reiterEintraege(modul, reiterListe) {
const kurz = Object.hasOwn(KURZLABELS, modul) ? KURZLABELS[modul] : {}
return reiterListe.map(({ key, label }) => (Object.hasOwn(kurz, key) ? { key, label, kurz: kurz[key] } : { key, label }))
}
/** Sendet 'kn:reiter' aus el. Ohne CustomEvent oder ohne el.dispatchEvent: false, sonst true. */
export function meldeReiter(el, { eintraege, aktiv = '', bei = () => {} } = {}) {
if (typeof CustomEvent !== 'function' || typeof el?.dispatchEvent !== 'function') return false
el.dispatchEvent(new CustomEvent('kn:reiter', { bubbles: true, detail: { eintraege, aktiv, bei } }))
return true
}
+18 -5
View File
@@ -16,6 +16,7 @@
import { fehlerKarte, h, infoKarte, request } from './shop-dom.js'
import { anfrageKarte, anfrageZeilen, renderAnfrageFormular } from './shop-anfrage.js'
import { renderBestellen, statusKarte } from './funktion-bestellen.js'
import { meldeReiter, reiterEintraege } from './fn-reiter.js'
/** Die Reiter: jeder ein Modul mit render(panel, ctx), erst beim ersten Oeffnen geladen. */
export const REITER = [
@@ -48,6 +49,10 @@ let S = null // aktuelle Sitzung der Ansicht; eine alte Antwort darf eine neuere
const zustandVon = (projekt) => projekt?.features?.shop?.state || 'off'
const text = (wert) => (typeof wert === 'string' ? wert : '')
// Abo-Modell (Antwort von GET /api/funktionen, strikt): die Reiter zeichnet die Etage, die Ansicht meldet sie (fn-reiter.js)
const istAbo = (s) => s.funktionen?.modell === 'abo'
/** Eine Ansicht ohne Reiter meldet die leere Liste, damit die Etage nicht die Reiter der vorigen stehen laesst. */
const keineReiter = (s) => { if (istAbo(s)) meldeReiter(s.root, { eintraege: [] }) }
/* --------------------------------------------------------- Einstieg --- */
/** Rueckkehr aus der Adresse lesen und diese bereinigen: #webshop?bestellt=1&projekt=<id> | #webshop?stripe=zurueck|neu&projekt=<id>. */
@@ -81,10 +86,12 @@ async function starte(root, projectId) {
}
if (S !== s) return
if (!s.funktionen?.shop?.verfuegbar) {
keineReiter(s)
root.replaceChildren(infoKarte(NICHT_VERFUEGBAR, 'Sobald er für dich bereit ist, findest du ihn hier.'))
return
}
if (!s.projects.length) {
keineReiter(s)
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
return
}
@@ -151,6 +158,7 @@ async function zeigeProjekt(s, optionen = {}) {
const projekt = s.projekt
const lauf = ++s.lauf
const aktuell = () => S === s && s.lauf === lauf
keineReiter(s)
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
let status
try {
@@ -179,6 +187,7 @@ async function ladeNeu(s, optionen = {}) {
/** Die Ansicht zum Zustand aus dem frisch geladenen Stand (status.state). */
async function zeichneAnsicht(s, { status, aktuell, bestellformular = false, meldung = null }) {
s.status = status
keineReiter(s) // die Ansicht mit Reitern (zeigeReiter) meldet ihre Liste gleich danach
if (!status.verfuegbar) {
s.buehne.replaceChildren(infoKarte(NICHT_VERFUEGBAR, 'Sobald er für dich bereit ist, findest du ihn hier.'))
return
@@ -318,15 +327,16 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
neuLaden,
}
const abo = istAbo(s) // Abo-Modell: keine eigene Leiste, die Etage zeichnet sie (kn:reiter)
const panel = h('div', { class: 'sh-panel', role: 'tabpanel', id: 'sh-panel', tabindex: '0' })
let aktiv = null // Schluessel des gezeigten Reiters
let zaehler = 0 // nur der zuletzt gewaehlte Reiter zeichnet in das Panel
const knoepfe = REITER.map((r) => h('button', {
const knoepfe = abo ? [] : REITER.map((r) => h('button', {
type: 'button', role: 'tab', class: 'sh-reiter', id: `sh-tab-${r.key}`, 'aria-controls': 'sh-panel', 'data-reiter': r.key,
onclick: () => waehleReiter(r.key),
}, r.label))
const leiste = h('div', { class: 'sh-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Webshops' }, knoepfe)
leiste.addEventListener('keydown', (ev) => {
const leiste = abo ? null : h('div', { class: 'sh-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Webshops' }, knoepfe)
leiste?.addEventListener('keydown', (ev) => {
const i = REITER.findIndex((r) => r.key === aktiv)
const ziel = { ArrowRight: (i + 1) % REITER.length, ArrowLeft: (i + REITER.length - 1) % REITER.length, Home: 0, End: REITER.length - 1 }[ev.key]
if (ziel === undefined) return
@@ -345,7 +355,10 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
k.setAttribute('aria-selected', String(an))
k.tabIndex = an ? 0 : -1
}
panel.setAttribute('aria-labelledby', `sh-tab-${reiter.key}`)
if (abo) {
panel.setAttribute('aria-label', reiter.label)
meldeReiter(s.root, { eintraege: reiterEintraege('shop', REITER), aktiv: reiter.key, bei: (neu) => waehleReiter(neu) })
} else panel.setAttribute('aria-labelledby', `sh-tab-${reiter.key}`)
zeigeMeldung(s, '')
panel.replaceChildren()
reiter.laden()
@@ -353,7 +366,7 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
.catch((err) => { if (nr === zaehler && aktuell()) panel.replaceChildren(fehlerKarte(err.message, () => waehleReiter(reiter.key))) })
}
s.buehne.replaceChildren(hinweisBox, leiste, panel)
s.buehne.replaceChildren(...[hinweisBox, leiste, panel].filter(Boolean))
// vom Onboarding bei Stripe zurueck: die Uebersicht zeigt den Stand der Stripe-Karte
waehleReiter(s.stripe && zustand !== 'disabled' ? 'uebersicht' : auswahl.reiter, { merken: false })
if (s.danke && zustand === 'setup') zeigeMeldung(s, DANKE_SETUP)
+21 -6
View File
@@ -23,6 +23,7 @@ import { render as renderEinstellungen } from './termine-tab-einstellungen.js'
import { renderFragebogen } from './termine-fragebogen.js'
import { renderBestellen, statusKarte } from './funktion-bestellen.js'
import { ZU_VERTRAEGE } from './vertraege-link.js'
import { meldeReiter, reiterEintraege } from './fn-reiter.js'
// Auswahl bleibt beim Wechsel zwischen Funktionen und Terminplaner erhalten (der Tag nicht: er beginnt immer bei heute)
const auswahl = { projectId: '', reiter: 'termine', ansicht: 'tag', filter: 'alle' }
@@ -41,6 +42,11 @@ function leseRueckkehr() {
return { bestellt: query.get('bestellt') === '1', projekt: query.get('projekt') || '' }
}
// Abo-Modell (Antwort von GET /api/funktionen, strikt): die Reiter zeichnet die Etage, die Ansicht meldet sie (fn-reiter.js)
const istAbo = (s) => s.modell === 'abo'
/** Eine Ansicht ohne Reiter meldet die leere Liste, damit die Etage nicht die Reiter der vorigen stehen laesst. */
const keineReiter = (s) => { if (istAbo(s)) meldeReiter(s.root, { eintraege: [] }) }
const zustandVon = (projekt) => projekt?.features?.termine?.state || 'off'
const istBestellt = (zustand) => zustand === 'ordered' || zustand === 'setup'
// Entwurf: Funktionsdokument im Zustand Aus mit siteKey (wie die Kachel in funktionen.js)
@@ -52,14 +58,16 @@ export async function initTermine(root, { projectId } = {}) {
const wunsch = rueckkehr.projekt || projectId || root.dataset.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab
delete root.dataset.wunschProjekt
if (wunsch) auswahl.projectId = wunsch
const { projects = [], termine } = await request('/api/funktionen')
const s = { root, projects, termine: termine || null, projekt: null, daten: null, tag: heuteBerlin(), nurLesen: false, lauf: 0, buehne: null, meldungEl: null, danke: false }
const { projects = [], termine, modell } = await request('/api/funktionen')
const s = { root, projects, termine: termine || null, modell, projekt: null, daten: null, tag: heuteBerlin(), nurLesen: false, lauf: 0, buehne: null, meldungEl: null, danke: false }
S = s
if (!termine?.verfuegbar) {
keineReiter(s)
root.replaceChildren(infoKarte('Der Terminplaner ist noch nicht verfügbar', 'Sobald er für dich bereit ist, findest du ihn hier.'))
return
}
if (!projects.length) {
keineReiter(s)
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
return
}
@@ -102,6 +110,7 @@ async function ladeFunktionen(s) {
const projects = Array.isArray(antwort.projects) ? antwort.projects : []
s.projects = projects
s.termine = antwort.termine || null
s.modell = antwort.modell
s.projekt = projects.find((p) => p.projectId === s.projekt?.projectId) || s.projekt
}
@@ -123,6 +132,7 @@ async function zeigeProjekt(s, optionen = {}) {
const projekt = s.projekt
const lauf = ++s.lauf
const aktuell = () => S === s && s.lauf === lauf
keineReiter(s)
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
let daten
try {
@@ -155,6 +165,7 @@ async function zeichneAnsicht(s, { daten, aktuell, bestellformular = false, meld
s.daten = daten
const zustand = daten.state || zustandVon(projekt)
s.nurLesen = zustand !== 'active' && zustand !== 'off'
keineReiter(s) // die Ansichten mit Reitern (zeigeReiter) melden ihre Liste gleich danach
if (zustand === 'off') {
if (daten.abo?.ok === true) zeigeFragebogen(s, { projekt, aktuell })
else await zeigeAboHinweis(s, { aktuell })
@@ -386,14 +397,15 @@ function zeigeReiter(s, { projekt, aktuell, zustand = s.nurLesen ? 'disabled' :
return
}
const abo = istAbo(s) // Abo-Modell: keine eigene Leiste, die Etage zeichnet sie (kn:reiter)
const panel = h('div', { class: 'tm-panel', role: 'tabpanel', id: 'tm-panel', tabindex: '0' })
let aktiv = null // Schluessel des gezeigten Reiters
const knoepfe = reiterListe.map((r) => h('button', {
const knoepfe = abo ? [] : reiterListe.map((r) => h('button', {
type: 'button', role: 'tab', class: 'tm-reiter', id: `tm-tab-${r.key}`, 'aria-controls': 'tm-panel', 'data-reiter': r.key,
onclick: () => waehleReiter(r.key),
}, r.label))
const leiste = h('div', { class: 'tm-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Terminplaners' }, knoepfe)
leiste.addEventListener('keydown', (ev) => {
const leiste = abo ? null : h('div', { class: 'tm-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Terminplaners' }, knoepfe)
leiste?.addEventListener('keydown', (ev) => {
const i = reiterListe.findIndex((r) => r.key === aktiv)
const ziel = { ArrowRight: (i + 1) % reiterListe.length, ArrowLeft: (i + reiterListe.length - 1) % reiterListe.length, Home: 0, End: reiterListe.length - 1 }[ev.key]
if (ziel === undefined) return
@@ -412,7 +424,10 @@ function zeigeReiter(s, { projekt, aktuell, zustand = s.nurLesen ? 'disabled' :
k.setAttribute('aria-selected', String(an))
k.tabIndex = an ? 0 : -1
}
panel.setAttribute('aria-labelledby', `tm-tab-${reiter.key}`)
if (abo) {
panel.setAttribute('aria-label', reiter.label)
meldeReiter(s.root, { eintraege: reiterEintraege('termine', reiterListe), aktiv: reiter.key, bei: (neu) => waehleReiter(neu) })
} else panel.setAttribute('aria-labelledby', `tm-tab-${reiter.key}`)
zeigeMeldung(s, '')
panel.replaceChildren()
Promise.resolve()