/* Kundenansicht neu: Routentabelle. Reine Logik ohne DOM, in Node importierbar. Alle alten Hashes bleiben gültig (VIEW_HASHES, Aliase #wartung, #abo, #fortschritt); die Query nach ? bleibt Zeichen für Zeichen erhalten. Neue Hash-Namen nur aus [a-z] wegen der Login-Whitelist /^\/dashboard\.html(#[a-z]+)?$/. */ export const BEREICHE = ['website', 'funktionen', 'chat', 'vertraege'] export const FUNKTIONS_ETAGEN = ['news', 'termine', 'webshop', 'buchungen', 'zeiten', 'analyse'] const FELDER = ['bereich', 'reiter', 'etage', 'produkt', 'scroll'] // Name (Hash ohne #) → Ziel ohne Query. Je Ziel steht die kanonische Schreibweise zuerst (abo vor wartung). // vertraege hat bewusst keinen Reiter: nur dann wählt die Ansicht den Standardreiter; abo und wartung sind die ausdrückliche Wahl. export const ROUTEN = Object.freeze(Object.fromEntries(Object.entries({ '': { bereich: 'website' }, projekt: { bereich: 'website', etage: 'projekt' }, fortschritt: { bereich: 'website', scroll: 'aktivitaet' }, passwort: { bereich: 'website', etage: 'passwort' }, funktionen: { bereich: 'funktionen', reiter: 'meine' }, entdecken: { bereich: 'funktionen', reiter: 'entdecken' }, news: { bereich: 'funktionen', etage: 'news' }, termine: { bereich: 'funktionen', etage: 'termine' }, webshop: { bereich: 'funktionen', etage: 'webshop' }, buchungen: { bereich: 'funktionen', etage: 'buchungen' }, zeiten: { bereich: 'funktionen', etage: 'zeiten' }, analyse: { bereich: 'funktionen', etage: 'analyse' }, chat: { bereich: 'chat' }, vertraege: { bereich: 'vertraege' }, abo: { bereich: 'vertraege', reiter: 'abo' }, wartung: { bereich: 'vertraege', reiter: 'abo' }, rechnungen: { bereich: 'vertraege', reiter: 'rechnungen' }, stunden: { bereich: 'vertraege', reiter: 'stunden' }, speicher: { bereich: 'vertraege', reiter: 'email' }, gutschrift: { bereich: 'vertraege', etage: 'gutschrift' }, wechsel: { bereich: 'vertraege', etage: 'wechsel' }, 'bestellen-hosting': { bereich: 'vertraege', etage: 'bestellen', produkt: 'hosting' }, 'bestellen-wartung': { bereich: 'vertraege', etage: 'bestellen', produkt: 'wartung' }, }).map(([name, ziel]) => [name, Object.freeze(ziel)]))) // Hash eines Bereichs, wenn kein Eintrag der Tabelle genau passt const BEREICHS_HASH = { website: '', funktionen: 'funktionen', chat: 'chat', vertraege: 'vertraege' } const TITEL_BEREICH = { website: 'Website', funktionen: 'Funktionen', chat: 'Chat', vertraege: 'Verträge' } const TITEL_ETAGE = { projekt: 'Projekt', news: 'News', termine: 'Terminplaner', webshop: 'Webshop', buchungen: 'Buchungskalender', zeiten: 'Öffnungszeiten', analyse: 'Analyse', gutschrift: 'Übernahmegutschrift', wechsel: 'Abo wechseln', bestellen: 'Abo bestellen', passwort: 'Passwort ändern', } // Alter data-view-Name (VIEW_HASHES, app.js:1933) → Hash-Name const ANSICHT_ZU_HASH = { projects: '', funktionen: 'funktionen', news: 'news', analyse: 'analyse', termine: 'termine', invoices: 'rechnungen', chat: 'chat', vertraege: 'vertraege', stunden: 'stunden', 'bestellen-hosting': 'bestellen-hosting', 'bestellen-wartung': 'bestellen-wartung', wechsel: 'wechsel', takeover: 'gutschrift', projekt: 'projekt', passwort: 'passwort', webshop: 'webshop', buchungen: 'buchungen', zeiten: 'zeiten', } const gleich = (a, b) => FELDER.every((feld) => (a[feld] ?? '') === (b[feld] ?? '')) const mitQuery = (name, query) => `#${name}${query ? `?${query}` : ''}` /** '#termine?bestellt=1' → { bereich: 'funktionen', etage: 'termine', query: 'bestellt=1' }. Unbekannte Namen ergeben die Website. */ export function routeAusHash(hash) { const roh = typeof hash === 'string' ? hash.replace(/^#/, '') : '' const frage = roh.indexOf('?') const name = (frage === -1 ? roh : roh.slice(0, frage)).toLowerCase() const query = frage === -1 ? '' : roh.slice(frage + 1) if (!Object.hasOwn(ROUTEN, name)) return { ...ROUTEN[''], query: '' } return { ...ROUTEN[name], query } } /** Umkehrung von routeAusHash in der kanonischen Schreibweise (#wartung wird #abo, die Website ohne Query ist '#'). */ export function hashFuerRoute(route) { const query = typeof route?.query === 'string' ? route.query : '' const treffer = Object.entries(ROUTEN).find(([, ziel]) => gleich(ziel, route ?? {})) const name = treffer ? treffer[0] : (BEREICHS_HASH[route?.bereich] ?? '') return mitQuery(name, query) } /** Brücke für die gemeinsamen Module: alter data-view-Name → Hash, unbekannter Name → null. */ export function hashFuerAnsicht(name) { return Object.hasOwn(ANSICHT_ZU_HASH, name) ? `#${ANSICHT_ZU_HASH[name]}` : null } /** Hash beim Start der Seite. ?abo=success|cancel (Rückkehr von Stripe) öffnet den Reiter Abo; die Query bleibt in location.search. */ export function startHash({ search = '', hash = '' } = {}) { const abo = new URLSearchParams(search).get('abo') if (abo === 'success' || abo === 'cancel') return '#abo' return hashFuerRoute(routeAusHash(hash)) } /** Hashes von der Website bis zum Ziel (für den Eltern-Eintrag bei Einstieg per Link). */ export function ankerKette(route) { const ziel = hashFuerRoute(route) if (route.bereich === 'website') return route.etage ? ['#', ziel] : [ziel] if (!route.etage) return ['#', ziel] return ['#', hashFuerRoute({ ...ROUTEN[BEREICHS_HASH[route.bereich]], query: '' }), ziel] } /** Elternroute einer Etage: derselbe Bereich, bei passwort die Herkunft (sonst die Website). Ohne Etage null. */ export function elternVon(route, herkunft = '') { if (!route.etage) return null if (route.etage === 'passwort') { const vorher = routeAusHash(herkunft) return vorher.etage === 'passwort' ? { ...ROUTEN[''], query: '' } : vorher } return { ...ROUTEN[BEREICHS_HASH[route.bereich]], query: '' } } /** Titel für Kopfzeile und Browser: die Etage, sonst der Bereich. */ export function titelFuer(route) { return TITEL_ETAGE[route.etage] ?? TITEL_BEREICH[route.bereich] ?? '' }