Terminplaner-Banner und Meldung nach Abschaltgrund mit Löschdatum

GET /api/termine/einstellungen liefert bei abgeschalteter Funktion
abgeschaltet { grund, seit, loeschungAm }. Die 409-Meldung bei
Schreibversuchen (Code abgeschaltet) und der Banner im Portal
unterscheiden Wartungs-Abo beendet, Abo vor der Einrichtung beendet und
die übrigen Gründe und nennen das Löschdatum, wenn es eines gibt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 18:41:03 +00:00
1 parent d66dd60a76
commit 36b36bcc4d
7 files changed
+268 -26

No files matched your search

+24 -1
View File
@@ -10,6 +10,7 @@ import { verschiebeZeiten } from '../services/termine/ablauf.js'
import { aktion, entsperre, manuell, sperre } from '../services/termine/ablaufBetrieb.js'
import { EXPORT_SEITE, exportSeite, hole, imZeitraum, zaehleAnfragen, zaehleKuenftige } from '../services/termine/buchungen.js'
import { fehlendePflichtangaben, neuerFeedToken, normalisiereEinstellung } from '../services/termine/einstellungen.js'
import { loeschTag, loeschzeit } from '../services/termine/frist.js'
import { kontextPerFeature, speichereEinstellung } from '../services/termine/kontext.js'
import { zustellFehler } from '../services/termine/outbox.js'
import { termineFuerKunde } from '../services/termine/schalter.js'
@@ -57,6 +58,8 @@ const MELDUNG = {
nichtVerfuegbar: 'Der Terminplaner ist noch nicht verfügbar.',
nichtAktiv: 'Der Terminplaner ist für diese Website nicht aktiv.',
abgeschaltet: 'Der Terminplaner ist für diese Website abgeschaltet. Du kannst Termine nur noch ansehen und exportieren.',
abgeschaltetAboEnde: 'Der Terminplaner ist abgeschaltet, weil dein Wartungs-Abo beendet ist. Du kannst Termine nur noch ansehen und exportieren.',
abgeschaltetVorEinrichtung: 'Der Terminplaner ist abgeschaltet, weil dein Wartungs-Abo endete, bevor der Terminplaner eingerichtet war.',
keineEinstellung: 'Bitte richte den Terminplaner zuerst ein.',
zuViele: 'Zu viele Änderungen in kurzer Zeit. Bitte versuch es in einer Stunde wieder.',
vorlage: 'Bitte wähle eine Vorlage aus der Liste.',
@@ -125,6 +128,25 @@ export function resetTermineRoutesForTests() {
// ---------------------------------------------------------------- Projekt und Funktion
/**
* Was die Abschaltung dem Kunden sagt: Grund (disabledReason, ohne Grund = Altbestand => null), Zeitpunkt (disabledAt) und der Berliner
* Tag, ab dem die Daten gelöscht werden dürfen (loeschungAm, 30 Tage nach disabledAt, termine/frist.js; null ohne lesbaren Zeitpunkt).
* Diese Route gehört nur zum Terminplaner, deshalb gibt es das Löschdatum hier immer (R105: bei News nie, siehe siteFeatures.featureView).
*/
const abgeschaltetAnsicht = (feature) => ({
grund: feature.disabledReason || null,
seit: feature.disabledAt || null,
loeschungAm: loeschTag(feature.disabledAt),
})
/** Antwort auf einen Schreibversuch bei abgeschalteter Funktion (409, Code `abgeschaltet`): je Grund ein Satz, mit Löschdatum, wenn es eines gibt. */
function abgeschaltetMeldung(feature) {
const grund = feature.disabledReason
const satz = grund === 'abo_ende' ? MELDUNG.abgeschaltetAboEnde : grund === 'abo_ende_vor_einrichtung' ? MELDUNG.abgeschaltetVorEinrichtung : MELDUNG.abgeschaltet
const ende = loeschzeit(feature.disabledAt)
return ende ? `${satz} Ab dem ${datumKurz(ende)} löschen wir die Daten.` : satz
}
const projectIdOf = (req) => {
const raw = req.query.projectId ?? req.body?.projectId
return typeof raw === 'string' ? raw : ''
@@ -149,7 +171,7 @@ const withTermine = ({ schreiben = false } = {}) => route(async (req, _res, next
const feature = await getFeatureDoc(customerId, projectId, 'termine')
const ctx = feature && ['active', 'disabled'].includes(feature.state) ? await kontextPerFeature(feature.$id) : null
if (!ctx || ctx.feature.customerId !== customerId) throw stop(409, MELDUNG.nichtAktiv, 'nicht_aktiv')
if (schreiben && ctx.feature.state !== 'active') throw stop(409, MELDUNG.abgeschaltet, 'abgeschaltet')
if (schreiben && ctx.feature.state !== 'active') throw stop(409, abgeschaltetMeldung(ctx.feature), 'abgeschaltet')
req.ctx = ctx
return next()
})
@@ -203,6 +225,7 @@ router.get('/einstellungen', requireSession, withTermine(), route(async (req, re
const e = ctx.einstellung
res.json({
state: ctx.feature.state,
abgeschaltet: ctx.feature.state === 'disabled' ? abgeschaltetAnsicht(ctx.feature) : null,
aktivSeit: ctx.feature.activatedAt || null,
einstellung: e ? fuerKunde(e) : null,
version: ctx.version,