Funktionen neu: Ansicht mit Meine und Entdecken, Website-Auswahl, Hinweise je Abo-Lage, Rechnungszeile
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
57112cc51f
commit
f30e72ebbb
2 files changed
+370
-5
No files matched your search
@@ -2,8 +2,12 @@
|
||||
Der erste Teil ist rein (Modell aus der Antwort von GET /api/funktionen im Abo-Modell, V5); die Ansicht darunter zeichnet es.
|
||||
Keine Preise: Der Katalog trägt keinen. Fremdtext (Funktions- und Websitenamen) wird nur als Text gesetzt. */
|
||||
|
||||
import { h } from './dom.js'
|
||||
import { ETAGE_FUER, anfrageStatusVon } from './funktion-einschalten.js'
|
||||
import { api as apiStandard } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { baueReiter } from './reiter.js'
|
||||
import { geheZu as geheZuStandard, oeffneOverlay as oeffneOverlayStandard, beendeOverlay as beendeOverlayStandard } from './router.js'
|
||||
import { ladeOffeneRechnungen, rechnungOffenZeile } from './rechnung-offen.js'
|
||||
import { ETAGE_FUER, TEXTE, anfrageStatusVon, einschaltWeg, navigiere, nimmMeldung, renderEinschalten } from './funktion-einschalten.js'
|
||||
|
||||
const ANFRAGE_SICHTBAR = ['offen', 'angenommen', 'abgelehnt'] // zurückgezogen zählt nicht
|
||||
|
||||
@@ -118,7 +122,168 @@ export function zeilenWeg(zeile) {
|
||||
return { art: 'etage', etage: ETAGE_FUER[zeile.key] }
|
||||
}
|
||||
|
||||
// Platzhalter bis Task 5 (zeichnet die Ansicht)
|
||||
export async function zeigeFunktionen(wurzel, { route, me, nav }) {
|
||||
wurzel.replaceChildren(h('p', { class: 'kn-leer' }, 'Funktionen (folgt)'))
|
||||
/* ------------------------------------------------------------------------------------------------ Ansicht --- */
|
||||
|
||||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs: eine ältere Antwort zeichnet nichts mehr
|
||||
let reiterGewaehlt = false // Tipp auf „Meine“: der nächste Aufruf zeigt den Leerzustand statt zu Entdecken zu springen
|
||||
let vorgabe = null // nach einer eigenen Navigation (Reiter, Sprung zu Entdecken) nimmt der nächste Aufruf die geladenen Antworten
|
||||
|
||||
/** Setzt die Merker des Moduls zurück (nur für Tests). */
|
||||
export function vergissMerker() {
|
||||
reiterGewaehlt = false
|
||||
vorgabe = null
|
||||
}
|
||||
|
||||
const BILD = /^\/neu\/bilder\/[a-z0-9-]+\.webp$/
|
||||
|
||||
/** Stück für die Pille einer Zeile. */
|
||||
const pille = (p) => h('span', { class: `kn-fn-pille kn-fn-pille--${p.ton}` }, p.text)
|
||||
|
||||
export async function zeigeFunktionen(wurzel, { route, me, nav } = {}, abh = {}) {
|
||||
const A = {
|
||||
api: abh.api ?? apiStandard,
|
||||
geheZu: abh.geheZu ?? geheZuStandard,
|
||||
oeffneOverlay: abh.oeffneOverlay ?? oeffneOverlayStandard,
|
||||
beendeOverlay: abh.beendeOverlay ?? beendeOverlayStandard,
|
||||
speicher: abh.speicher,
|
||||
jetzt: abh.jetzt ?? (() => new Date()),
|
||||
}
|
||||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||
laeufe.set(wurzel, nr)
|
||||
const aktuell = () => laeufe.get(wurzel) === nr
|
||||
const nochmal = () => zeigeFunktionen(wurzel, { route, me, nav }, abh)
|
||||
const reiterWahl = route?.reiter === 'entdecken' ? 'entdecken' : 'meine'
|
||||
const ausdruecklich = reiterGewaehlt
|
||||
reiterGewaehlt = false
|
||||
const mitgebracht = vorgabe && vorgabe.wurzel === wurzel && vorgabe.bis > Date.now() ? vorgabe : null
|
||||
vorgabe = null
|
||||
|
||||
let daten
|
||||
let offen
|
||||
if (mitgebracht) {
|
||||
({ daten, offen } = mitgebracht)
|
||||
} else {
|
||||
leeren(wurzel).append(h('p', { class: 'kn-leer' }, 'Wird geladen …'))
|
||||
try {
|
||||
[daten, offen] = await Promise.all([A.api('/api/funktionen'), ladeOffeneRechnungen(A.api)])
|
||||
} catch {
|
||||
if (!aktuell()) return
|
||||
leeren(wurzel).append(
|
||||
h('p', { class: 'kn-fehler', role: 'alert' }, 'Die Funktionen konnten gerade nicht geladen werden.'),
|
||||
h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', 'data-aktion': 'nochmal', on: { click: () => nochmal() } }, 'Nochmal versuchen'))
|
||||
return
|
||||
}
|
||||
if (!aktuell()) return
|
||||
}
|
||||
if (daten?.modell !== 'abo') {
|
||||
leeren(wurzel).append(h('p', { class: 'kn-fehler', role: 'alert' }, 'Die Funktionen sind gerade nicht verfügbar. Lade die Seite neu.'))
|
||||
return
|
||||
}
|
||||
|
||||
const kundenart = me?.customer?.kundenartWirksam ?? daten.kundenart
|
||||
const viewAs = me?.viewAs === true
|
||||
const zeilen = meineZeilen(daten)
|
||||
if (reiterWahl === 'meine' && zeilen.length === 0 && !ausdruecklich) {
|
||||
vorgabe = { wurzel, daten, offen, bis: Date.now() + 5000 }
|
||||
A.geheZu('#entdecken', { ersetzen: true })
|
||||
return
|
||||
}
|
||||
|
||||
const projektVon = (projectId) => daten.projects.find((p) => p?.projectId === projectId)
|
||||
const weiter = (weg, ziel) => navigiere(weg, ziel, { geheZu: A.geheZu, speicher: A.speicher })
|
||||
|
||||
/** Die Bestätigungsseite für diese Funktion und Website. */
|
||||
function oeffneSeite(key, projectId) {
|
||||
renderEinschalten(wurzel, { key, projectId, daten, kundenartWirksam: kundenart, viewAs, jetzt: A.jetzt, api: A.api, geheZu: A.geheZu, speicher: A.speicher })
|
||||
}
|
||||
|
||||
/** „Einschalten“ für eine Website: die Vorprüfung wählt den Ort; eine Meldung steht im Eintrag. */
|
||||
function einschaltTipp(eintrag, kandidat, meldungEl) {
|
||||
const weg = einschaltWeg({ eintrag, projekt: projektVon(kandidat.projectId), abo: daten.abo, kundenart })
|
||||
if (weg.art === 'meldung') { meldungEl.textContent = weg.text; return }
|
||||
meldungEl.textContent = ''
|
||||
if (weg.art === 'seite') oeffneSeite(eintrag.key, kandidat.projectId)
|
||||
else weiter(weg, { key: eintrag.key, projectId: kandidat.projectId })
|
||||
}
|
||||
|
||||
function meineListe() {
|
||||
if (zeilen.length === 0) {
|
||||
return [
|
||||
h('p', { class: 'kn-leer' }, 'Du hast noch keine Funktion eingeschaltet.'),
|
||||
h('button', { type: 'button', class: 'kn-fn-knopf', 'data-aktion': 'entdecken', on: { click: () => wechsleReiter('entdecken') } }, 'Entdecken'),
|
||||
]
|
||||
}
|
||||
return h('ul', { class: 'kn-fn-liste' }, zeilen.map((z) => h('li', { class: 'kn-fn-zeile', 'data-fn': z.key, 'data-projekt': z.projectId },
|
||||
h('button', {
|
||||
type: 'button',
|
||||
class: 'kn-fn-zeile-knopf',
|
||||
on: {
|
||||
click: () => {
|
||||
const weg = zeilenWeg(z)
|
||||
if (weg.art === 'seite') oeffneSeite(z.key, z.projectId)
|
||||
else weiter(weg, { key: z.key, projectId: z.projectId })
|
||||
},
|
||||
},
|
||||
},
|
||||
h('span', { class: 'kn-fn-text' }, h('span', { class: 'kn-fn-name' }, z.name), h('span', { class: 'kn-fn-zeile2' }, z.zeile2)),
|
||||
pille(z.pille)))))
|
||||
}
|
||||
|
||||
function eintragKarte({ eintrag, kandidaten }) {
|
||||
const nurBetriebHinweis = eintrag.nurBetrieb === true && kundenart !== 'unternehmen'
|
||||
const meldungEl = h('p', { class: 'kn-fn-meldung', role: 'alert' })
|
||||
let wahl = null
|
||||
const knopf = h('button', {
|
||||
type: 'button',
|
||||
class: 'kn-fn-knopf',
|
||||
'data-aktion': 'einschalten',
|
||||
on: {
|
||||
click: () => {
|
||||
if (kandidaten.length === 1) { einschaltTipp(eintrag, kandidaten[0], meldungEl); return }
|
||||
if (wahl) { wahl.remove(); wahl = null; return }
|
||||
wahl = h('div', { class: 'kn-fn-wahl', role: 'group', 'aria-label': 'Für welche Website?' },
|
||||
kandidaten.map((k) => h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', on: { click: () => einschaltTipp(eintrag, k, meldungEl) } }, k.projektName)))
|
||||
knopf.parentNode.insertBefore(wahl, meldungEl)
|
||||
},
|
||||
},
|
||||
}, 'Einschalten')
|
||||
const bild = typeof eintrag.bild === 'string' && BILD.test(eintrag.bild)
|
||||
? h('img', { class: 'kn-fn-bild', src: eintrag.bild, alt: `Beispiel: ${eintrag.name}`, width: 960, height: 600, loading: 'lazy', on: { error: (ev) => ev.target.remove() } })
|
||||
: null
|
||||
return h('section', { class: 'kn-fn-eintrag', 'data-fn': eintrag.key },
|
||||
bild,
|
||||
h('div', { class: 'kn-fn-eintrag-text' },
|
||||
h('h3', null, String(eintrag.name ?? '')),
|
||||
h('p', { class: 'kn-fn-beschreibung' }, String(eintrag.beschreibung ?? '')),
|
||||
h('p', { class: 'kn-fn-enthalten' }, eintrag.enthaltenIn === 'hosting' ? 'Im Hosting-Abo enthalten' : 'Im Wartungs-Abo enthalten'),
|
||||
nurBetriebHinweis ? h('p', { class: 'kn-fn-hinweis' }, TEXTE.nurBetrieb(String(eintrag.name ?? ''))) : (kandidaten.length ? knopf : null),
|
||||
meldungEl))
|
||||
}
|
||||
|
||||
function entdeckenListe() {
|
||||
const eintraege = entdeckenEintraege(daten)
|
||||
if (eintraege.length === 0) return h('p', { class: 'kn-leer' }, 'Du hast schon alles eingeschaltet, was es gibt.')
|
||||
const ohneWebsite = daten.projects.length === 0
|
||||
return [
|
||||
ohneWebsite ? h('p', { class: 'kn-fn-info' }, 'Sobald deine Website angelegt ist, kannst du Funktionen einschalten.') : null,
|
||||
...eintraege.map(eintragKarte),
|
||||
]
|
||||
}
|
||||
|
||||
/** Reiterwechsel: Meine oder Entdecken; die geladenen Antworten bleiben für den folgenden Aufruf des Routers. */
|
||||
function wechsleReiter(key) {
|
||||
if (key === 'meine') reiterGewaehlt = true
|
||||
vorgabe = { wurzel, daten, offen, bis: Date.now() + 5000 }
|
||||
A.geheZu(key === 'meine' ? '#funktionen' : '#entdecken', { ersetzen: true })
|
||||
}
|
||||
|
||||
const meldung = nimmMeldung()
|
||||
const reiter = baueReiter({ eintraege: [{ key: 'meine', label: 'Meine' }, { key: 'entdecken', label: 'Entdecken' }], aktiv: reiterWahl, art: 'unterstrich', ariaLabel: 'Funktionen', bei: wechsleReiter })
|
||||
leeren(wurzel).append(
|
||||
...[
|
||||
meldung ? h('p', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, meldung) : null,
|
||||
rechnungOffenZeile(offen),
|
||||
reiter,
|
||||
h('div', { class: 'kn-reiter-inhalt' }, reiterWahl === 'meine' ? meineListe() : entdeckenListe()),
|
||||
].filter(Boolean))
|
||||
}
|
||||
Reference in new issue
Block a user