Funktionen neu: Modell für Meine und Entdecken (mehrere Websites, Anfragestatus)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
437947cdf6
commit
c72c9d9f5a
2 files changed
+200
-1
No files matched your search
@@ -1,7 +1,124 @@
|
||||
/* Kundenansicht neu: Platzhalter, den das nächste Paket füllt (Stub aus P1). */
|
||||
/* 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 { h } from './dom.js'
|
||||
import { ETAGE_FUER, anfrageStatusVon } 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.'
|
||||
}
|
||||
|
||||
/** Pille und Zeile einer Funktion an einer Website (nur für Websites, bei denen istInMeine gilt). */
|
||||
function statusVon(eintrag, projekt) {
|
||||
if (eintrag.key === 'analyse') {
|
||||
const a = projekt.analyse
|
||||
if (a.state === 'active') return { pille: { text: 'Aktiv', ton: 'gruen' }, status: 'Läuft auf deiner Website' }
|
||||
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: f.ticketWoid ? `Einrichtung läuft · Ticket #${f.ticketWoid}` : 'Einrichtung läuft' }
|
||||
case 'ordered':
|
||||
return { pille: { text: 'Zahlung offen', ton: 'orange' }, status: 'Wir warten auf deine Zahlung' }
|
||||
case 'active': {
|
||||
const seit = datumDe(f.activatedAt)
|
||||
return { pille: { text: 'Aktiv', ton: 'gruen' }, status: seit ? `Aktiv seit ${seit}` : 'Läuft auf deiner Website' }
|
||||
}
|
||||
case 'disabled':
|
||||
return { pille: { text: 'Abgeschaltet', ton: 'grau' }, status: f.disabledReason === 'abo_ende' ? aboEndeText(f) : 'Schreib uns im Chat, wenn du die Funktion wieder möchtest.' }
|
||||
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: 'Schreib uns im Chat' }
|
||||
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). zeile2 ist bei mehreren Websites der Websitename, sonst die Statuszeile.
|
||||
* @returns {Array<{ key: string, name: string, projectId: string, projektName: string, zustand: string, anfrageStatus: string, pille: { text: string, ton: string }, 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)
|
||||
zeilen.push({
|
||||
key: eintrag.key,
|
||||
name: String(eintrag.name ?? ''),
|
||||
projectId: projekt.projectId,
|
||||
projektName: String(projekt.projectName ?? ''),
|
||||
zustand: String(zustandVon(projekt, eintrag.key) ?? ''),
|
||||
anfrageStatus: eintrag.key === 'analyse' ? '' : anfrageStatusVon(projekt, eintrag.key),
|
||||
pille,
|
||||
status,
|
||||
zeile2: mehrere ? String(projekt.projectName ?? '') : 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] }
|
||||
}
|
||||
|
||||
// Platzhalter bis Task 5 (zeichnet die Ansicht)
|
||||
export async function zeigeFunktionen(wurzel, { route, me, nav }) {
|
||||
wurzel.replaceChildren(h('p', { class: 'kn-leer' }, 'Funktionen (folgt)'))
|
||||
}
|
||||
Reference in new issue
Block a user