Der Server berechnet kurzinfo (Terminplaner, Webshop, News, Buchungskalender, Öffnungszeiten) nur noch, wenn die neue Ansicht ?kurzinfo=1 sendet. Das tut allein die Seite "Meine"; Etagen, Einschalt-Ablauf und Module fragen ohne den Parameter, dann fehlt das Feld und es gibt keine Abfrage auf Termine, Bestellungen, Beiträge und Anfragen. Mit dem Parameter bleibt die Antwort wie bisher (Text oder null je Funktion). Nur der Wert "1" gilt; doppelte oder als Liste gesendete Parameter heißen: ohne. Alte Ansicht unverändert. Tests: kundenansichtKurzinfo (neuer Fall ohne/mit Parameter, Abfragen und Feld), knFunktionenAnsicht (die Seite lädt genau einmal ?kurzinfo=1, auch nach "Nochmal versuchen"). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
345 lines
18 KiB
JavaScript
345 lines
18 KiB
JavaScript
/* Kundenansicht neu (P3): Bereich „Funktionen“ mit den Reitern Meine und Entdecken.
|
||
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 { 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
|
||
|
||
/** Zeitpunkt oder 'JJJJ-MM-TT' → 'TT.MM.JJJJ' (Europe/Berlin); unlesbar ergibt ''. Ein Tagestext wird als Tag um 12:00 UTC gelesen. */
|
||
export function datumDe(wert) {
|
||
if (typeof wert !== 'string' || wert === '') return ''
|
||
const ms = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(wert) ? `${wert}T12:00:00Z` : wert)
|
||
if (Number.isNaN(ms)) return ''
|
||
return new Date(ms).toLocaleDateString('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', year: 'numeric' })
|
||
}
|
||
|
||
const verfuegbare = (d) => (Array.isArray(d?.katalog) ? d.katalog.filter((e) => e && typeof e === 'object' && e.verfuegbar === true && typeof e.key === 'string') : [])
|
||
const websites = (d) => (Array.isArray(d?.projects) ? d.projects.filter((p) => p && typeof p === 'object' && typeof p.projectId === 'string') : [])
|
||
const zustandVon = (projekt, key) => (key === 'analyse' ? projekt.analyse?.state : projekt.features?.[key]?.state)
|
||
|
||
/** Hat diese Website die Funktion schon (Zeile in Meine)? Eine Zeile ist Zustand, Entwurf des Fragebogens oder sichtbare Anfrage. */
|
||
function istInMeine(eintrag, projekt) {
|
||
if (eintrag.key === 'analyse') return ['active', 'disabled'].includes(projekt.analyse?.state)
|
||
const f = projekt.features?.[eintrag.key]
|
||
if (!f || typeof f !== 'object') return false
|
||
if (f.state !== 'off') return typeof f.state === 'string'
|
||
if (eintrag.ablauf === 'fragebogen') return Boolean(f.siteKey)
|
||
if (eintrag.ablauf === 'anfrage') return ANFRAGE_SICHTBAR.includes(anfrageStatusVon(projekt, eintrag.key))
|
||
return false
|
||
}
|
||
|
||
/** „Dein Abo ist beendet.“ mit dem Löschtag, wenn einer da ist. */
|
||
function aboEndeText(f) {
|
||
const tag = datumDe(f.loeschungAm)
|
||
return tag ? `Dein Abo ist beendet. Deine Daten bleiben bis ${tag} gespeichert.` : 'Dein Abo ist beendet.'
|
||
}
|
||
|
||
const KURZINFO_MAX = 80 // wie der Server (kurzinfo.js): länger ist kein Detail mehr, sondern ein Fehler
|
||
|
||
/** Kurzinfo einer aktiven Funktion aus der Antwort des Servers: nur ein nicht leerer, kurzer Text; sonst '' (die Zeile zeigt dann „Aktiv“). */
|
||
function kurzinfoVon(quelle) {
|
||
const text = typeof quelle?.kurzinfo === 'string' ? quelle.kurzinfo.trim() : ''
|
||
return text !== '' && text.length <= KURZINFO_MAX ? text : ''
|
||
}
|
||
|
||
/**
|
||
* Pille und Detailzeile einer Funktion an einer Website (nur für Websites, bei denen istInMeine gilt). Läuft die Funktion, gibt es keine
|
||
* Pille: die Detailzeile ist die Kurzinfo des Servers („3 Termine heute · nächster 10:30“) oder „Aktiv“. Alles andere trägt eine Pille.
|
||
*/
|
||
function statusVon(eintrag, projekt) {
|
||
if (eintrag.key === 'analyse') {
|
||
const a = projekt.analyse
|
||
if (a.state === 'active') return { pille: null, status: kurzinfoVon(a) || 'Aktiv' }
|
||
return { pille: { text: 'Gesperrt', ton: 'grau' }, status: a.disabledReason === 'abo_ende' ? aboEndeText(a) : 'Schreib uns im Chat' }
|
||
}
|
||
const f = projekt.features[eintrag.key]
|
||
switch (f.state) {
|
||
case 'setup':
|
||
return { pille: { text: 'In Einrichtung', ton: 'blau' }, status: 'Wir richten sie gerade ein' }
|
||
case 'ordered':
|
||
return { pille: { text: 'Zahlung offen', ton: 'orange' }, status: 'Wir warten auf deine Zahlung' }
|
||
case 'active':
|
||
return { pille: null, status: kurzinfoVon(f) || 'Aktiv' }
|
||
case 'disabled':
|
||
return { pille: { text: 'Abgeschaltet', ton: 'grau' }, status: f.disabledReason === 'abo_ende' ? aboEndeText(f) : 'Im Chat wieder anfragen' }
|
||
default: {
|
||
if (eintrag.ablauf === 'fragebogen') return { pille: { text: 'Entwurf', ton: 'grau' }, status: 'Fragebogen fortsetzen' }
|
||
const anfrage = anfrageStatusVon(projekt, eintrag.key)
|
||
if (anfrage === 'angenommen') return { pille: { text: 'Angenommen', ton: 'blau' }, status: 'Jetzt einschalten' }
|
||
if (anfrage === 'abgelehnt') return { pille: { text: 'Abgelehnt', ton: 'rot' }, status: 'Anfrage abgelehnt · Im Chat nachfragen' }
|
||
return { pille: { text: 'Angefragt', ton: 'violett' }, status: 'Wir prüfen deine Anfrage' }
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Die Zeilen von „Meine“: je Funktion und Website eine, in Katalogreihenfolge, dann Websitereihenfolge. Funktionen mit
|
||
* verfuegbar !== true fehlen (C9). pille ist null, solange die Funktion läuft; status ist die Detailzeile (Kurzinfo oder Zustandstext),
|
||
* zeile2 ist bei mehreren Websites „Websitename · status“, sonst der Status allein.
|
||
* @returns {Array<{ key: string, name: string, projectId: string, projektName: string, zustand: string, anfrageStatus: string, pille: { text: string, ton: string }|null, status: string, zeile2: string }>}
|
||
*/
|
||
export function meineZeilen(daten) {
|
||
const seiten = websites(daten)
|
||
const mehrere = seiten.length > 1
|
||
const zeilen = []
|
||
for (const eintrag of verfuegbare(daten)) {
|
||
for (const projekt of seiten) {
|
||
if (!istInMeine(eintrag, projekt)) continue
|
||
const { pille, status } = statusVon(eintrag, projekt)
|
||
const projektName = String(projekt.projectName ?? '')
|
||
zeilen.push({
|
||
key: eintrag.key,
|
||
name: String(eintrag.name ?? ''),
|
||
projectId: projekt.projectId,
|
||
projektName,
|
||
zustand: String(zustandVon(projekt, eintrag.key) ?? ''),
|
||
anfrageStatus: eintrag.key === 'analyse' ? '' : anfrageStatusVon(projekt, eintrag.key),
|
||
pille,
|
||
status,
|
||
zeile2: mehrere && projektName !== '' ? `${projektName} · ${status}` : status,
|
||
})
|
||
}
|
||
}
|
||
return zeilen
|
||
}
|
||
|
||
/**
|
||
* Die Einträge von „Entdecken“ in Katalogreihenfolge: solange eine Website die Funktion nicht hat oder es keine Website gibt.
|
||
* kandidaten sind die Websites ohne die Funktion (Analyse: nur mit Zustand off; unknown und comingSoon sind kein Kandidat).
|
||
* @returns {Array<{ eintrag: object, kandidaten: Array<{ projectId: string, projektName: string }> }>}
|
||
*/
|
||
export function entdeckenEintraege(daten) {
|
||
const seiten = websites(daten)
|
||
const ergebnis = []
|
||
for (const eintrag of verfuegbare(daten)) {
|
||
const ohne = seiten.filter((p) => !istInMeine(eintrag, p))
|
||
if (seiten.length > 0 && ohne.length === 0) continue
|
||
const kandidaten = (eintrag.key === 'analyse' ? ohne.filter((p) => p.analyse?.state === 'off') : ohne)
|
||
.map((p) => ({ projectId: p.projectId, projektName: String(p.projectName ?? '') }))
|
||
ergebnis.push({ eintrag, kandidaten })
|
||
}
|
||
return ergebnis
|
||
}
|
||
|
||
/** Tipp auf eine Zeile in Meine: eine angenommene Anfrage geht zur Bestätigung, alles andere in die Etage der Funktion. */
|
||
export function zeilenWeg(zeile) {
|
||
if (zeile.zustand === 'off' && zeile.anfrageStatus === 'angenommen') return { art: 'seite' }
|
||
return { art: 'etage', etage: ETAGE_FUER[zeile.key] }
|
||
}
|
||
|
||
/* ------------------------------------------------------------------------------------------------ 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
|
||
const wechselErledigt = new Set() // 'key:projectId' der Funktionen, in die die Rückkehr vom Wechsel schon geführt hat (je Seitenaufruf einmal)
|
||
|
||
/** Setzt die Merker des Moduls zurück (nur für Tests). */
|
||
export function vergissMerker() {
|
||
reiterGewaehlt = false
|
||
vorgabe = null
|
||
wechselErledigt.clear()
|
||
}
|
||
|
||
const BILD = /^\/neu\/bilder\/[a-z0-9-]+\.webp$/
|
||
|
||
/** Nur diese Seite (Meine, Detailzeile) bittet den Server um die Kurzinfo; Etagen, Einschalt-Ablauf und Module laden GET /api/funktionen ohne den Parameter. */
|
||
const FUNKTIONEN_PFAD = '/api/funktionen?kurzinfo=1'
|
||
|
||
/** 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
|
||
const frisch = !mitgebracht
|
||
if (mitgebracht) {
|
||
({ daten, offen } = mitgebracht)
|
||
} else {
|
||
leeren(wurzel).append(h('p', { class: 'kn-leer' }, 'Wird geladen …'))
|
||
try {
|
||
[daten, offen] = await Promise.all([A.api(FUNKTIONEN_PFAD), 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)
|
||
const projektVon = (projectId) => daten.projects.find((p) => p?.projectId === projectId)
|
||
const weiter = (weg, ziel) => navigiere(weg, ziel, { geheZu: A.geheZu, speicher: A.speicher })
|
||
let gezeigteMeldung = ''
|
||
|
||
/** Die Bestätigungsseite für diese Funktion und Website als Overlay: die Zurück-Geste und „Abbrechen“ bringen die Liste ohne neuen Abruf zurück. */
|
||
function oeffneSeite(key, projectId) {
|
||
A.oeffneOverlay({ schliesse: () => { if (aktuell()) zeichneListe() } })
|
||
renderEinschalten(wurzel, {
|
||
key, projectId, daten, kundenartWirksam: kundenart, viewAs, jetzt: A.jetzt, api: A.api, geheZu: A.geheZu, speicher: A.speicher,
|
||
bei: { abbrechen: () => A.beendeOverlay() },
|
||
})
|
||
}
|
||
|
||
/** „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 })
|
||
}
|
||
|
||
/**
|
||
* Rückkehr vom Wechsel ins Wartungs-Abo (V11): hat der Kunde den Wechsel für eine Funktion begonnen und hat die Website sie noch nicht,
|
||
* öffnet die Ansicht ihren Ablauf, je Seitenaufruf einmal. Jeder Fehler und jede ungültige Angabe wird ignoriert.
|
||
* @returns {Promise<boolean>} true, wenn ein Ablauf geöffnet wurde
|
||
*/
|
||
async function wechselRueckkehr() {
|
||
if (daten.abo?.produkt !== 'wartung' || daten.abo.laeuft !== true) return false
|
||
let abo
|
||
try { abo = await A.api('/api/abo') } catch { return false }
|
||
if (!aktuell()) return false
|
||
const f = abo?.subscription?.wechselFunktion
|
||
if (!f || typeof f.key !== 'string' || typeof f.projectId !== 'string') return false
|
||
const merker = `${f.key}:${f.projectId}`
|
||
const eintrag = verfuegbare(daten).find((e) => e.key === f.key)
|
||
const projekt = projektVon(f.projectId)
|
||
if (!eintrag || !projekt || wechselErledigt.has(merker) || istInMeine(eintrag, projekt)) return false
|
||
const weg = einschaltWeg({ eintrag, projekt, abo: daten.abo, kundenart })
|
||
if (weg.art !== 'seite' && weg.art !== 'etage') return false
|
||
wechselErledigt.add(merker)
|
||
if (weg.art === 'seite') oeffneSeite(f.key, f.projectId)
|
||
else weiter(weg, { key: f.key, projectId: f.projectId })
|
||
return true
|
||
}
|
||
|
||
if (frisch && await wechselRueckkehr()) return
|
||
if (!aktuell()) return
|
||
if (reiterWahl === 'meine' && zeilen.length === 0 && !ausdruecklich) {
|
||
vorgabe = { wurzel, daten, offen, bis: Date.now() + 5000 }
|
||
A.geheZu('#entdecken', { ersetzen: true })
|
||
return
|
||
}
|
||
|
||
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)),
|
||
h('span', { class: 'kn-fn-rechts' }, z.pille ? pille(z.pille) : null, h('span', { class: 'kn-fn-pfeil', 'aria-hidden': 'true' }, '›'))))))
|
||
}
|
||
|
||
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')
|
||
if (viewAs) knopf.disabled = true // Mitarbeiter-Kundenansicht: gesperrt statt erst nach dem Tipp ein 403-Text
|
||
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),
|
||
!nurBetriebHinweis && kandidaten.length && viewAs ? h('p', { class: 'kn-fn-info' }, TEXTE.kundenansichtKnopf) : 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 })
|
||
}
|
||
|
||
/** Reiter, Rechnungszeile und Liste aus den geladenen Antworten zeichnen (auch nach dem Schließen der Bestätigung, ohne Abruf). */
|
||
function zeichneListe() {
|
||
const reiter = baueReiter({ eintraege: [{ key: 'meine', label: 'Meine' }, { key: 'entdecken', label: 'Entdecken' }], aktiv: reiterWahl, art: 'unterstrich', ariaLabel: 'Funktionen', bei: wechsleReiter })
|
||
leeren(wurzel).append(
|
||
...[
|
||
gezeigteMeldung ? h('p', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, gezeigteMeldung) : null,
|
||
rechnungOffenZeile(offen),
|
||
reiter,
|
||
h('div', { class: 'kn-reiter-inhalt' }, reiterWahl === 'meine' ? meineListe() : entdeckenListe()),
|
||
].filter(Boolean))
|
||
}
|
||
|
||
gezeigteMeldung = nimmMeldung() // genau einmal; nach dem Schließen der Bestätigung bleibt sie stehen
|
||
zeichneListe()
|
||
}
|