Kundenansicht P5: Katalog, Abo-Stand und Anfragestatus in GET /api/funktionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e30318a470
commit
b6f2df0d4a
3 files changed
+263
-2
No files matched your search
@@ -26,6 +26,7 @@ import { einschaltenDeps, einschaltenZeiten, zeitenProjektAnsicht } from '../ser
|
||||
import { starteBehalten } from '../services/zeiten/behalten.js'
|
||||
import { aboMemo } from '../services/aboStatus.js'
|
||||
import { aboProdukt } from '../services/aboFunktionen.js'
|
||||
import { anfrageStatusJeProjekt, katalogFuerKunde } from '../services/aboKatalog.js'
|
||||
import { datumDe, einschalten, EINSCHALTEN_TEXTE, pruefeAboDeckung, pruefeEinschaltZustand } from '../services/funktionEinschalten.js'
|
||||
import { kundenartWirksam, NUR_BETRIEB_TEXT } from '../services/kundenart.js'
|
||||
import { neueAnsicht } from '../services/kundenansicht/schalter.js'
|
||||
@@ -226,7 +227,7 @@ router.get('/', requireSession, async (req, res) => {
|
||||
)
|
||||
// Der Abo-Stand wird fuer Terminplaner und Oeffnungszeiten nur einmal gelesen
|
||||
const abos = aboMemo()
|
||||
res.json({
|
||||
const antwort = {
|
||||
projects,
|
||||
// Preise und Bestelltexte je Funktion aus dem Register (nur die oeffentliche Ansicht, auch wenn der Terminplaner
|
||||
// dem Kunden noch nicht freigegeben ist); setupEstimate und consentText bleiben als News-Alias fuer aeltere Clients
|
||||
@@ -248,7 +249,23 @@ router.get('/', requireSession, async (req, res) => {
|
||||
// Oeffnungszeiten live: Schalter ZEITEN_ENABLED (off | pilot | on); nur wenn verfuegbar zeigt das Portal die Kachel, dann mit
|
||||
// dem Abo-Stand (inklusive, grund), dem Preis und den Texten des Gratisweges "Einschalten"
|
||||
zeiten: await zeitenAngebot(req.session.email, customerId, abos),
|
||||
})
|
||||
}
|
||||
// Kundenansicht neu (V5): Abo-Modell mit Kundenart, Abo-Stand, Katalog und Anfragestatus von Webshop und Buchungskalender je Projekt.
|
||||
// Ohne neue Ansicht bleibt die Antwort wie sie war. Ein Lesefehler beim Abo-Stand ist ein Fehler der Liste (nie ein erfundenes "kein Abo").
|
||||
if (neueAnsicht(req)) {
|
||||
const [kunde, abo, katalog, anfrageStatus] = await Promise.all([
|
||||
kundenDokument(customerId),
|
||||
aboProdukt(customerId, { now: jetzt }),
|
||||
katalogFuerKunde({ email: req.session.email }),
|
||||
anfrageStatusJeProjekt({ docs, projects }),
|
||||
])
|
||||
for (const p of projects) {
|
||||
p.features.shop.anfrageStatus = anfrageStatus.get(p.projectId)?.shop ?? ''
|
||||
p.features.unterkunft.anfrageStatus = anfrageStatus.get(p.projectId)?.unterkunft ?? ''
|
||||
}
|
||||
Object.assign(antwort, { modell: 'abo', kundenart: kundenartWirksam(kunde), abo, katalog })
|
||||
}
|
||||
res.json(antwort)
|
||||
} catch (err) {
|
||||
fail(res, err, 'Die Funktionen konnten gerade nicht geladen werden. Bitte lade die Seite gleich noch einmal.')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { config } from '../config.js'
|
||||
import { ENTHALTEN_IN } from './aboFunktionen.js'
|
||||
import { consentAbo, featureConfig, featureSperre, katalogFelder, katalogReihenfolge } from './featureRegistry.js'
|
||||
import { unterlagenFuer } from './aboUnterlagen.js'
|
||||
import { leseShopConfig } from './shop/anfrage.js'
|
||||
import { ladeEinstellungen } from './unterkunft/speicher.js'
|
||||
|
||||
/**
|
||||
* Katalog der Funktionen im Abo-Modell (V5) fuer GET /api/funktionen mit neuer Ansicht. Reine Lesefunktionen; der Katalog enthaelt
|
||||
* keinen Preis. Alle Texte kommen vom Server (Register, config.aboFunktionen), nie vom Client.
|
||||
*/
|
||||
|
||||
/** Oeffentliche Sicht der Erklaerung: version, knopf und die Items in Anzeigereihenfolge (nurPrivatInFrist nur, wo es gesetzt ist). */
|
||||
function oeffentlicheErklaerung(consent) {
|
||||
if (!consent) return null
|
||||
return {
|
||||
version: consent.version,
|
||||
knopf: consent.knopf,
|
||||
items: consent.items.map((item) => ({
|
||||
id: item.id,
|
||||
text: item.text,
|
||||
haken: Boolean(item.haken),
|
||||
...(item.nurPrivatInFrist ? { nurPrivatInFrist: true } : {}),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Katalogeintraege in der Reihenfolge von katalogReihenfolge(); die Analyse nur bei config.analyse.enabled.
|
||||
* @param {{ email?: string }} p email: Konto des Kunden (Schalter und Pilotlisten der Funktionen)
|
||||
* @returns {Promise<Array<{ key: string, name: string, beschreibung: string, bild: string, enthaltenIn: 'hosting'|'wartung', verfuegbar: boolean,
|
||||
* ablauf: string, einrichtung: string, unterlagen: object, nurBetrieb: boolean, consent: object|null, hinweis: string|null }>>}
|
||||
*/
|
||||
export async function katalogFuerKunde({ email } = {}) {
|
||||
const eintraege = []
|
||||
for (const key of katalogReihenfolge()) {
|
||||
if (key === 'analyse' && !config.analyse.enabled) continue
|
||||
const felder = katalogFelder(key)
|
||||
const consent = oeffentlicheErklaerung(consentAbo(key)) // null bei analyse
|
||||
eintraege.push({
|
||||
key,
|
||||
name: key === 'analyse' ? config.aboFunktionen.analyse.name : featureConfig(key).label.name,
|
||||
beschreibung: felder.beschreibung,
|
||||
bild: felder.bild,
|
||||
enthaltenIn: ENTHALTEN_IN[key][0], // das kleinste Abo, das die Funktion enthaelt
|
||||
verfuegbar: key === 'analyse' ? true : featureSperre(key, email) === null,
|
||||
ablauf: felder.ablauf,
|
||||
einrichtung: felder.einrichtung,
|
||||
unterlagen: unterlagenFuer(key),
|
||||
nurBetrieb: felder.nurBetrieb,
|
||||
consent,
|
||||
hinweis: consent?.items.find((item) => item.id === 'hinweis')?.text ?? null,
|
||||
})
|
||||
}
|
||||
return eintraege
|
||||
}
|
||||
|
||||
/** Anfragestatus einer Funktion zum Dokument: shopConfig (shop) bzw. ukEinstellungen (unterkunft). Lesefehler ergeben '' und einen Logeintrag. */
|
||||
async function leseAnfrageStatus(key, doc) {
|
||||
if (!doc) return ''
|
||||
try {
|
||||
const gelesen = key === 'shop' ? await leseShopConfig(doc.$id) : await ladeEinstellungen(doc.$id)
|
||||
return typeof gelesen?.anfrageStatus === 'string' ? gelesen.anfrageStatus : ''
|
||||
} catch (err) {
|
||||
console.error(`[funktionen] Anfragestatus (${key}) nicht lesbar: ${err?.name ?? 'Fehler'} ${err?.status ?? ''}`.trim())
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anfragestatus von Webshop und Buchungskalender je Projekt: Map<projectId, { shop, unterkunft }> mit '' | 'offen' | 'angenommen' |
|
||||
* 'abgelehnt' | 'zurueckgezogen'. Je Projekt mit Funktionsdokument wird genau eine Konfiguration gelesen, ohne Dokument gilt ''.
|
||||
* @param {{ docs: object[], projects: Array<{ projectId: string }> }} p docs: portalFeatures-Dokumente des Kunden; projects: Projekte der Liste
|
||||
*/
|
||||
export async function anfrageStatusJeProjekt({ docs, projects }) {
|
||||
const ergebnis = new Map()
|
||||
await Promise.all(
|
||||
projects.map(async (projekt) => {
|
||||
const dokument = (key) => docs.find((d) => d.projectId === projekt.projectId && d.featureKey === key)
|
||||
const [shop, unterkunft] = await Promise.all([leseAnfrageStatus('shop', dokument('shop')), leseAnfrageStatus('unterkunft', dokument('unterkunft'))])
|
||||
ergebnis.set(projekt.projectId, { shop, unterkunft })
|
||||
})
|
||||
)
|
||||
return ergebnis
|
||||
}
|
||||
Reference in new issue
Block a user