Verdrahtet P5 Task 15 und P7 Task 0 (N33): consentAbo liest Linkzeile und Hinweis aus WORTLAUT_V6, unterlagenFuer die fünf Funktionen aus FUNKTIONEN_UNTERLAGEN_V6 (Analyse bleibt bei den Alt-Unterlagen bis zur Freigabe), config.aboFunktionen.texte.hinweis entfällt, pdfAnhaenge liest PDFs der Fassung 6, sichereAdresse lässt /neu/rechtliches/ zu. Der Analyse-Kundenweg setzt wie der Team-Weg den Abo-Marker (unlockedBy und einrichtungAbrechnung abo). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
441 lines
21 KiB
JavaScript
441 lines
21 KiB
JavaScript
/* Kundenansicht neu (P3): Einschalten einer Funktion im Abo-Modell.
|
|
Dieser Teil ist rein (ohne DOM, ohne Netz) und läuft unter Node: Wege nach der Vorprüfung, Fristen, Körper des Absendens, Fehlerfolgen.
|
|
Die Vorprüfung spiegelt aboDeckt (server/services/aboFunktionen.js) nur, um den richtigen Ort zu wählen; entschieden wird immer vom Server
|
|
(POST /api/funktionen/<key>/einschalten, 409 mit Code). Kundentexte sind Festwerte (TEXTE), Fremdtext kommt nie als HTML in die Seite. */
|
|
|
|
import { h, leeren } from './dom.js'
|
|
import { api as apiStandard } from './api.js'
|
|
import { geheZu as geheZuStandard } from './router.js'
|
|
|
|
/** Etage 2 je Funktion (Schlüssel der Hülle in FUNKTIONS_MODULE, Hash `#<etage>?projekt=<id>`). */
|
|
export const ETAGE_FUER = Object.freeze({ news: 'news', termine: 'termine', shop: 'webshop', unterkunft: 'buchungen', zeiten: 'zeiten', analyse: 'analyse' })
|
|
|
|
/** Festtexte der Oberfläche (du-Form, kein Preis). */
|
|
export const TEXTE = Object.freeze({
|
|
stundenkonto: 'Die Einrichtung geht von deinem Stundenkonto ab, auch wenn das Guthaben noch nicht reicht. Extrakosten entstehen keine.',
|
|
nichtVerfuegbar: 'Diese Funktion ist für dich gerade nicht verfügbar.',
|
|
gekuendigt: 'Dein Abo ist gekündigt. Solange die Kündigung läuft, kannst du nichts Neues einschalten.',
|
|
nichtAktiv: 'Dein Abo ist noch nicht aktiv. Sobald die erste Zahlung da ist, kannst du einschalten.',
|
|
falschesAbo: 'Dein Abo enthält diese Funktion nicht.',
|
|
beginntSpaeter: (datum) => `Dein Abo beginnt am ${datum}. Ab dann kannst du einschalten.`,
|
|
fassungVeraltet: 'Die Bedingungen wurden aktualisiert. Bitte lies die Zeile noch einmal.',
|
|
allgemein: 'Das Einschalten hat gerade nicht geklappt. Versuch es gleich noch einmal oder schreib uns im Chat.',
|
|
nurBetrieb: (name) => `${name} gibt es für Betriebe. Stimmt das nicht? Schreib uns im Chat.`,
|
|
registerFehlt: 'Einschalten ist gerade nicht möglich. Schreib uns im Chat.',
|
|
kundenansicht: 'In der Kundenansicht kann nichts eingeschaltet werden.',
|
|
kundenansichtKnopf: 'In der Kundenansicht nicht möglich.', // Hinweis neben dem gesperrten Knopf in Entdecken
|
|
laden: 'Die Angaben konnten gerade nicht geladen werden. Bitte versuch es gleich noch einmal.',
|
|
})
|
|
|
|
/** Die Erklärung der Funktion aus dem Katalog der Antwort (einzige Stelle für das Register); fehlt sie, null. */
|
|
export function consentVon(daten, key) {
|
|
const eintrag = Array.isArray(daten?.katalog) ? daten.katalog.find((e) => e?.key === key) : null
|
|
const consent = eintrag?.consent
|
|
return consent && typeof consent === 'object' && Array.isArray(consent.items) ? consent : null
|
|
}
|
|
|
|
/** Anfragestatus von Webshop oder Buchungskalender an einer Website (N18, einzige Stelle); sonst ''. */
|
|
export function anfrageStatusVon(projekt, key) {
|
|
const status = projekt?.features?.[key]?.anfrageStatus
|
|
return typeof status === 'string' ? status : ''
|
|
}
|
|
|
|
/**
|
|
* Wohin führt „Einschalten“? Prüfreihenfolge wie der Server (V5): verfügbar, gekündigt, kein Abo, nicht aktiv, Hosting bei einer
|
|
* Wartungs-Funktion, nur Betrieb; danach Etage (Analyse, Fragebogen, Anfrage ohne Zusage) oder die Bestätigungsseite.
|
|
* Alles außer 'unternehmen' gilt als privat (E4).
|
|
*/
|
|
export function einschaltWeg({ eintrag, projekt, abo, kundenart }) {
|
|
if (eintrag?.verfuegbar !== true) return { art: 'meldung', text: TEXTE.nichtVerfuegbar }
|
|
if (abo?.gekuendigt === true) return { art: 'meldung', text: TEXTE.gekuendigt }
|
|
if (!abo || abo.produkt === null || abo.produkt === undefined) return { art: 'bestellen', produkt: eintrag.enthaltenIn }
|
|
if (abo.laeuft !== true) return { art: 'meldung', text: TEXTE.nichtAktiv }
|
|
if (eintrag.enthaltenIn === 'wartung' && abo.produkt === 'hosting') return { art: 'wechsel' }
|
|
if (eintrag.nurBetrieb === true && kundenart !== 'unternehmen') return { art: 'meldung', text: TEXTE.nurBetrieb(eintrag.name) }
|
|
if (eintrag.key === 'analyse') return { art: 'etage', etage: ETAGE_FUER.analyse }
|
|
if (eintrag.ablauf === 'fragebogen') return { art: 'etage', etage: ETAGE_FUER[eintrag.key] }
|
|
if (eintrag.ablauf === 'anfrage' && anfrageStatusVon(projekt, eintrag.key) !== 'angenommen') return { art: 'etage', etage: ETAGE_FUER[eintrag.key] }
|
|
return { art: 'seite' }
|
|
}
|
|
|
|
const TAG = /^\d{4}-\d{2}-\d{2}$/
|
|
|
|
/** Berliner Kalendertag 'JJJJ-MM-TT' eines Zeitpunkts (ISO-Text, Date) oder eines Tagestextes; unlesbar ergibt ''. */
|
|
function berlinTag(wert) {
|
|
if (typeof wert === 'string' && TAG.test(wert)) return wert
|
|
const ms = wert instanceof Date ? wert.getTime() : (typeof wert === 'string' && wert !== '' ? Date.parse(wert) : Number.NaN)
|
|
if (Number.isNaN(ms)) return ''
|
|
return new Date(ms).toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' })
|
|
}
|
|
|
|
/** Der Berliner Tag nach dem Fristende (Zeitpunkt) als 'JJJJ-MM-TT'; unlesbar ergibt ''. */
|
|
export function tagNachFrist(wert) {
|
|
const tag = berlinTag(wert)
|
|
if (!tag) return ''
|
|
const [j, m, t] = tag.split('-').map(Number)
|
|
return new Date(Date.UTC(j, m - 1, t + 1)).toISOString().slice(0, 10)
|
|
}
|
|
|
|
/** 'JJJJ-MM-TT' → 'TT.MM.'; sonst ''. */
|
|
export function datumKurz(tag) {
|
|
if (typeof tag !== 'string' || !TAG.test(tag)) return ''
|
|
const [, m, t] = tag.split('-')
|
|
return `${t}.${m}.`
|
|
}
|
|
|
|
/** 'JJJJ-MM-TT' → 'TT.MM.JJJJ' wie datumDe des Servers; sonst ''. */
|
|
function datumLang(tag) {
|
|
if (typeof tag !== 'string' || !TAG.test(tag)) return ''
|
|
const [j, m, t] = tag.split('-')
|
|
return `${t}.${m}.${j}`
|
|
}
|
|
|
|
/**
|
|
* Braucht der Kunde das Häkchen „sofortiger Beginn“, und ab wann ginge es ohne? Nur Privatkunden (leer zählt als privat) und nur bis
|
|
* zum Fristende (am Ende selbst noch Frist). Eine Antwort des Servers (serverAb: ISO oder Tag) gewinnt.
|
|
* @returns {{ noetig: boolean, ab: string }} ab = 'JJJJ-MM-TT' oder ''
|
|
*/
|
|
export function beginnLage({ kundenart, abo, jetzt, serverAb = '' }) {
|
|
const vomServer = berlinTag(serverAb)
|
|
if (vomServer) return { noetig: true, ab: vomServer }
|
|
const bis = abo?.widerrufsfristBis
|
|
const ende = typeof bis === 'string' ? Date.parse(bis) : Number.NaN
|
|
if (kundenart !== 'unternehmen' && ende >= jetzt.getTime()) return { noetig: true, ab: tagNachFrist(bis) }
|
|
return { noetig: false, ab: '' }
|
|
}
|
|
|
|
/** Körper des Absendens: Fassung aus dem Register, Haken nur 'beginn'; die Kundenart sendet der Client nie. */
|
|
export function einschaltKoerper({ projectId, consent, beginnGesetzt }) {
|
|
return { projectId, zustimmung: { version: consent.version, haken: beginnGesetzt ? ['beginn'] : [] } }
|
|
}
|
|
|
|
/** Nur https ohne Zugangsdaten oder Pfade unter /rechtliches/, /vertragsunterlagen/ und /neu/rechtliches/ (Fassung 6); sonst ''. */
|
|
export function sichereAdresse(url) {
|
|
if (typeof url !== 'string' || url === '') return ''
|
|
if (/[\s\u0000-\u001f\u007f\\]/.test(url) || url.includes('..') || /%(2e|2f|5c)/i.test(url)) return ''
|
|
if (url.startsWith('/rechtliches/') || url.startsWith('/vertragsunterlagen/') || url.startsWith('/neu/rechtliches/')) return url
|
|
if (!url.startsWith('https://')) return ''
|
|
try {
|
|
const u = new URL(url)
|
|
return u.protocol === 'https:' && u.hostname !== '' && u.username === '' && u.password === '' ? url : ''
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
const UNTERLAGEN_WOERTER = [['Leistungsbeschreibung', 'leistungsbeschreibung'], ['Auftragsverarbeitung', 'avv']]
|
|
|
|
/**
|
|
* Die Zeile aus dem Register als Stücke [{ text, href? }]: das erste Vorkommen von „Leistungsbeschreibung“ und „Auftragsverarbeitung“
|
|
* wird zum Link, wenn die Unterlage eine sichere Adresse hat. Fehlt das Wort bei einer sicheren Unterlage, hängt ihr Link hinten an
|
|
* (zwei Links mit „ · “ dazwischen). Benachbarte Textstücke sind zusammengefasst.
|
|
*/
|
|
export function linkzeile(text, unterlagen) {
|
|
const basis = typeof text === 'string' ? text : ''
|
|
const treffer = []
|
|
const anhang = []
|
|
for (const [wort, schluessel] of UNTERLAGEN_WOERTER) {
|
|
const href = sichereAdresse(unterlagen?.[schluessel]?.url)
|
|
if (!href) continue
|
|
const pos = basis.indexOf(wort)
|
|
if (pos >= 0) treffer.push({ pos, wort, href })
|
|
else anhang.push({ wort, href })
|
|
}
|
|
treffer.sort((a, b) => a.pos - b.pos)
|
|
const stuecke = []
|
|
const textStueck = (t) => {
|
|
if (t === '') return
|
|
const letztes = stuecke.at(-1)
|
|
if (letztes && letztes.href === undefined) letztes.text += t
|
|
else stuecke.push({ text: t })
|
|
}
|
|
let ab = 0
|
|
for (const t of treffer) {
|
|
textStueck(basis.slice(ab, t.pos))
|
|
stuecke.push({ text: t.wort, href: t.href })
|
|
ab = t.pos + t.wort.length
|
|
}
|
|
textStueck(basis.slice(ab))
|
|
anhang.forEach((a, i) => {
|
|
textStueck(i === 0 ? ' ' : ' · ')
|
|
stuecke.push({ text: a.wort, href: a.href })
|
|
})
|
|
return stuecke
|
|
}
|
|
|
|
/**
|
|
* Was folgt aus einem Fehler des Absendens? fehler = ApiFehler ({ status, code?, message, body }).
|
|
* wechsel/bestellen/etage führen weg, beginn und spaeter sperren den Knopf bis zum Datum (beginn: Häkchen möglich), neu-laden lädt die
|
|
* Liste neu, schon heißt: ist schon eingeschaltet, meldung zeigt den Text. Alles ohne eigene Regel zeigt den Text des Servers.
|
|
*/
|
|
export function fehlerAktion(fehler, { eintrag }) {
|
|
const body = fehler?.body && typeof fehler.body === 'object' ? fehler.body : {}
|
|
const code = fehler?.code || body.code || ''
|
|
const servertext = typeof fehler?.message === 'string' && fehler.message !== '' ? fehler.message : TEXTE.allgemein
|
|
const meldung = (text) => ({ art: 'meldung', text })
|
|
switch (code) {
|
|
case 'nicht_verfuegbar':
|
|
return meldung(TEXTE.nichtVerfuegbar)
|
|
case 'abo_fehlt': {
|
|
if (body.wechsel === true) return { art: 'wechsel' }
|
|
if (body.grund === 'kein_abo') return { art: 'bestellen', produkt: eintrag.enthaltenIn }
|
|
if (body.grund === 'gekuendigt') return meldung(TEXTE.gekuendigt)
|
|
if (body.grund === 'nicht_aktiv') return meldung(TEXTE.nichtAktiv)
|
|
if (body.grund === 'falsches_abo') return meldung(TEXTE.falschesAbo)
|
|
if (body.grund === 'beginnt_spaeter') {
|
|
const tag = berlinTag(body.ab)
|
|
return tag ? { art: 'spaeter', ab: body.ab, text: TEXTE.beginntSpaeter(datumLang(tag)) } : meldung(servertext)
|
|
}
|
|
return meldung(servertext)
|
|
}
|
|
case 'nur_betrieb':
|
|
return meldung(TEXTE.nurBetrieb(eintrag.name))
|
|
case 'bestellung_offen':
|
|
return { art: 'etage', etage: ETAGE_FUER[eintrag.key] }
|
|
case 'beginn_noetig':
|
|
return { art: 'beginn', ab: body.ab }
|
|
case 'bereits_eingeschaltet':
|
|
return { art: 'schon' }
|
|
case 'fassung_veraltet':
|
|
return { art: 'neu-laden', text: TEXTE.fassungVeraltet }
|
|
default:
|
|
return meldung(servertext)
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------------------------- Bestätigungsseite --- */
|
|
|
|
/**
|
|
* Die Bestätigungsseite zum Einschalten zeichnen (Aufbau und Zustand von Häkchen und Knopf; Senden steht weiter unten).
|
|
* @param {HTMLElement} behaelter Platz, in dem die Seite gezeichnet wird (sein Inhalt wird ersetzt)
|
|
* @param {object} o
|
|
* key, projectId; daten = Antwort von GET /api/funktionen (Abo-Modell); kundenartWirksam (leer oder unbekannt zählt als privat);
|
|
* viewAs (Kundenansicht der Mitarbeiter: nichts einschalten); imModul (ein Funktionsmodul bettet die Seite ein: kein „Abbrechen“);
|
|
* jetzt, api, geheZu, speicher (einspeisbar für Tests); bei = { fertig, weiter, abbrechen, neuLaden } (alle optional)
|
|
* @returns {HTMLElement} section.kn-fn-einschalten
|
|
*/
|
|
export function renderEinschalten(behaelter, o) {
|
|
const { key, projectId, viewAs = false, imModul = false, jetzt = () => new Date() } = o
|
|
let daten = o.daten
|
|
const api = o.api ?? apiStandard
|
|
const geheZu = o.geheZu ?? geheZuStandard
|
|
const kundenart = () => (o.kundenartWirksam === undefined ? daten?.kundenart : o.kundenartWirksam)
|
|
const z = { haken: false, serverAb: '', sperreAb: '', erzwingeHaken: false, sendet: false, meldung: '' }
|
|
// Standards der Rückmeldungen (Task 4): Erfolg merkt die Meldung und geht zu Meine, Abbrechen zu Entdecken
|
|
const bei = {
|
|
fertig: (antwort) => { merkeMeldung(erfolgText(String(eintragVon()?.name ?? key), antwort)); geheZu('#funktionen', { ersetzen: true }) },
|
|
weiter: (weg) => navigiere(weg, { key, projectId }, { geheZu, speicher: o.speicher }),
|
|
abbrechen: () => geheZu('#entdecken', { ersetzen: true }),
|
|
neuLaden: () => api('/api/funktionen'),
|
|
...o.bei,
|
|
}
|
|
const abschnitt = h('section', { class: 'kn-fn-einschalten', 'data-key': key, 'data-projekt': projectId })
|
|
let knopf = null
|
|
let meldungEl = null
|
|
|
|
const eintragVon = () => (Array.isArray(daten?.katalog) ? daten.katalog.find((e) => e?.key === key) ?? null : null)
|
|
const lageJetzt = () => {
|
|
const lage = beginnLage({ kundenart: kundenart(), abo: daten?.abo, jetzt: jetzt(), serverAb: z.serverAb })
|
|
return z.erzwingeHaken && !lage.noetig ? { noetig: true, ab: '' } : lage // beginn_noetig ohne lesbares Datum: Häkchen trotzdem zeigen
|
|
}
|
|
|
|
function aktualisiereKnopf() {
|
|
if (!knopf) return
|
|
const lage = lageJetzt()
|
|
const fehltHaken = lage.noetig && !z.haken
|
|
const ab = z.sperreAb || (fehltHaken ? lage.ab : '')
|
|
knopf.disabled = viewAs || z.sendet || fehltHaken || Boolean(z.sperreAb)
|
|
knopf.textContent = ab && datumKurz(ab) ? `Einschalten ab ${datumKurz(ab)}` : 'Einschalten'
|
|
}
|
|
|
|
function setzeMeldung(text) {
|
|
z.meldung = text
|
|
if (meldungEl) meldungEl.textContent = text
|
|
}
|
|
|
|
function fuelle() {
|
|
const eintrag = eintragVon()
|
|
const consent = consentVon(daten, key)
|
|
const items = consent?.items ?? []
|
|
const item = (id) => items.find((i) => i?.id === id) ?? null
|
|
const zeile = item('einschalten-zeile')
|
|
const hinweis = item('hinweis')
|
|
const beginn = item('beginn')
|
|
const lage = lageJetzt()
|
|
const register = Boolean(eintrag && zeile && (!lage.noetig || beginn))
|
|
|
|
const seiten = Array.isArray(daten?.projects) ? daten.projects : []
|
|
const websiteName = seiten.find((p) => p?.projectId === projectId)?.projectName
|
|
const abo = daten?.abo?.produkt ?? eintrag?.enthaltenIn
|
|
const fakten = [
|
|
seiten.length > 1 ? ['Website', String(websiteName ?? '')] : null,
|
|
['Leistung', 'Einrichtung durch WEBklar'],
|
|
['Dauer', String(eintrag?.einrichtung ?? '')],
|
|
['Laufzeit', `läuft, solange dein ${abo === 'hosting' ? 'Hosting-Abo' : 'Wartungs-Abo'} läuft`],
|
|
].filter(Boolean)
|
|
|
|
const box = h('input', { type: 'checkbox', 'data-haken': 'beginn', on: { change: () => { z.haken = box.checked; aktualisiereKnopf() } } })
|
|
box.checked = z.haken
|
|
meldungEl = h('p', { class: 'kn-fn-meldung', role: 'alert', 'data-meldung': true }, z.meldung)
|
|
knopf = register ? h('button', { type: 'button', class: 'kn-fn-knopf', 'data-aktion': 'einschalten', on: { click: () => senden() } }, 'Einschalten') : null
|
|
|
|
const kinder = [
|
|
h('h2', null, `${String(eintrag?.name ?? key)} einschalten`),
|
|
h('dl', { class: 'kn-fn-fakten' }, fakten.map(([name, wert]) => h('div', null, h('dt', null, name), h('dd', null, wert)))),
|
|
h('p', { class: 'kn-fn-konto' }, TEXTE.stundenkonto),
|
|
register
|
|
? [
|
|
h('p', { class: 'kn-fn-recht' }, linkzeile(zeile.text, eintrag.unterlagen).map((s) => (s.href ? h('a', { href: s.href, target: '_blank', rel: 'noopener noreferrer' }, s.text) : s.text))),
|
|
hinweis ? h('p', { class: 'kn-fn-hinweis' }, String(hinweis.text ?? '')) : null,
|
|
lage.noetig ? h('label', { class: 'kn-fn-haken' }, box, h('span', null, String(beginn.text ?? ''))) : null,
|
|
]
|
|
: h('p', { class: 'kn-fn-info' }, TEXTE.registerFehlt),
|
|
viewAs ? h('p', { class: 'kn-fn-info' }, TEXTE.kundenansicht) : null,
|
|
meldungEl,
|
|
h('div', { class: 'kn-fn-aktionen' },
|
|
knopf ?? h('button', { type: 'button', class: 'kn-fn-knopf', 'data-aktion': 'chat', on: { click: () => geheZu('#chat') } }, 'Im Chat schreiben'),
|
|
imModul ? null : h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', 'data-aktion': 'abbrechen', on: { click: () => bei.abbrechen() } }, 'Abbrechen')),
|
|
]
|
|
abschnitt.replaceChildren(...kinder.flat(Infinity).filter(Boolean))
|
|
aktualisiereKnopf()
|
|
}
|
|
|
|
async function senden() {
|
|
if (z.sendet || viewAs || z.sperreAb) return
|
|
const eintrag = eintragVon()
|
|
const consent = consentVon(daten, key)
|
|
const lage = lageJetzt()
|
|
if (!eintrag || !consent || (lage.noetig && !z.haken)) return
|
|
z.sendet = true
|
|
setzeMeldung('')
|
|
aktualisiereKnopf()
|
|
let antwort
|
|
try {
|
|
antwort = await api(`/api/funktionen/${encodeURIComponent(key)}/einschalten`, {
|
|
method: 'POST',
|
|
body: einschaltKoerper({ projectId, consent, beginnGesetzt: lage.noetig && z.haken }),
|
|
})
|
|
} catch (fehler) {
|
|
z.sendet = false
|
|
await behandleFehler(fehler, eintrag)
|
|
return
|
|
}
|
|
bei.fertig(antwort) // der Knopf bleibt gesperrt: nach Erfolg gibt es nichts mehr zu senden
|
|
}
|
|
|
|
async function behandleFehler(fehler, eintrag) {
|
|
const aktion = fehlerAktion(fehler, { eintrag })
|
|
switch (aktion.art) {
|
|
case 'wechsel':
|
|
case 'bestellen':
|
|
aktualisiereKnopf()
|
|
bei.weiter(aktion)
|
|
break
|
|
case 'etage': // im Modul steht der Servertext (die Etage ist schon offen), sonst führt die Seite hin
|
|
if (imModul) { setzeMeldung(fehler?.message || TEXTE.allgemein); aktualisiereKnopf() } else { aktualisiereKnopf(); bei.weiter(aktion) }
|
|
break
|
|
case 'schon':
|
|
bei.fertig(null)
|
|
break
|
|
case 'beginn': // Häkchen zeigen, Knopf bis dahin gesperrt mit Datum
|
|
z.serverAb = typeof aktion.ab === 'string' ? aktion.ab : ''
|
|
z.erzwingeHaken = true
|
|
z.haken = false
|
|
fuelle()
|
|
abschnitt.querySelector('input[data-haken="beginn"]')?.focus?.()
|
|
break
|
|
case 'spaeter': // das Abo beginnt später: Text und gesperrter Knopf mit Datum, kein Häkchen
|
|
z.sperreAb = berlinTag(aktion.ab)
|
|
setzeMeldung(aktion.text)
|
|
aktualisiereKnopf()
|
|
break
|
|
case 'neu-laden':
|
|
try {
|
|
daten = await bei.neuLaden()
|
|
z.haken = false
|
|
z.serverAb = ''
|
|
z.erzwingeHaken = false
|
|
setzeMeldung(aktion.text)
|
|
fuelle()
|
|
} catch {
|
|
setzeMeldung(TEXTE.allgemein)
|
|
aktualisiereKnopf()
|
|
}
|
|
break
|
|
default:
|
|
setzeMeldung(aktion.text)
|
|
aktualisiereKnopf()
|
|
}
|
|
}
|
|
|
|
leeren(behaelter).append(abschnitt)
|
|
fuelle()
|
|
return abschnitt
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------------------ Navigation und Meldungen --- */
|
|
|
|
/**
|
|
* Wohin nach einem Weg von einschaltWeg oder fehlerAktion? ziel = { key, projectId }; 'seite' behandelt die Ansicht selbst.
|
|
* etage → #<etage>?projekt=<id>, bestellen → #bestellen-<produkt>, wechsel → die gewünschte Funktion merken (V11), dann #wechsel.
|
|
* Der Wechsel geht auch, wenn sessionStorage fehlt oder voll ist.
|
|
*/
|
|
export async function navigiere(weg, ziel, { geheZu = geheZuStandard, speicher } = {}) {
|
|
switch (weg?.art) {
|
|
case 'etage':
|
|
geheZu(`#${weg.etage}?projekt=${encodeURIComponent(ziel.projectId)}`)
|
|
break
|
|
case 'bestellen':
|
|
geheZu(`#bestellen-${weg.produkt}`)
|
|
break
|
|
case 'wechsel':
|
|
try {
|
|
const ablage = speicher ?? globalThis.sessionStorage
|
|
ablage.setItem('kn-wechsel-funktion', JSON.stringify({ key: ziel.key, projectId: ziel.projectId }))
|
|
} catch { /* ohne Speicher geht der Wechsel trotzdem; nur die Rückkehr zur Funktion entfällt */ }
|
|
geheZu('#wechsel')
|
|
break
|
|
default:
|
|
}
|
|
}
|
|
|
|
/** Text nach dem Einschalten; antwort null heißt: war schon eingeschaltet. */
|
|
export function erfolgText(name, antwort) {
|
|
if (!antwort) return `${name} ist schon eingeschaltet.`
|
|
const woid = antwort.ticketWoid ?? antwort.feature?.ticketWoid // der Server nennt das Ticket an beiden Stellen
|
|
const ticket = woid ? ` Ticket #${woid}.` : ''
|
|
return `${name} ist eingeschaltet. Die Einrichtung läuft jetzt.${ticket}`
|
|
}
|
|
|
|
let gemerkteMeldung = ''
|
|
/** Merkt genau eine Meldung für den nächsten Aufbau von Meine oder Entdecken. */
|
|
export function merkeMeldung(text) { gemerkteMeldung = String(text ?? '') }
|
|
/** Gibt die gemerkte Meldung zurück und vergisst sie. */
|
|
export function nimmMeldung() {
|
|
const text = gemerkteMeldung
|
|
gemerkteMeldung = ''
|
|
return text
|
|
}
|
|
|
|
/**
|
|
* Vertrag mit den Funktionsmodulen (P4, VF1): die Bestätigungsseite in `platz`, ohne „Abbrechen“. Lädt GET /api/funktionen, wenn
|
|
* `daten` fehlt. onFertig({ feature, ticketWoid }) nach 200, onFertig({ feature: null, ticketWoid: null }) nach 409 bereits_eingeschaltet;
|
|
* alle anderen Fehler zeigt die Seite selbst. Liefert die gezeichnete Seite (oder die Fehlerzeile beim Laden).
|
|
*/
|
|
export async function zeigeEinschalten(platz, { key, projectId, onFertig, daten, kundenartWirksam, viewAs = false, api = apiStandard, geheZu, speicher, jetzt } = {}) {
|
|
let liste = daten
|
|
if (!liste) {
|
|
leeren(platz).append(h('p', { class: 'kn-leer' }, 'Wird geladen …'))
|
|
try {
|
|
liste = await api('/api/funktionen')
|
|
} catch {
|
|
return leeren(platz).appendChild(h('p', { class: 'kn-fn-info', role: 'alert' }, TEXTE.laden,
|
|
h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', 'data-aktion': 'nochmal', on: { click: () => zeigeEinschalten(platz, { key, projectId, onFertig, kundenartWirksam, viewAs, api, geheZu, speicher, jetzt }) } }, 'Nochmal versuchen')))
|
|
}
|
|
}
|
|
return renderEinschalten(platz, {
|
|
key, projectId, daten: liste, kundenartWirksam, viewAs, imModul: true, api, geheZu, speicher, jetzt,
|
|
bei: { fertig: (antwort) => onFertig?.(antwort ?? { feature: null, ticketWoid: null }) },
|
|
})
|
|
}
|