diff --git a/src/components/customer/CustomerFunktionen.jsx b/src/components/customer/CustomerFunktionen.jsx index 2b4219c..f14a8ae 100644 --- a/src/components/customer/CustomerFunktionen.jsx +++ b/src/components/customer/CustomerFunktionen.jsx @@ -4,7 +4,7 @@ import { FaCheck, FaCopy, FaSpinner } from 'react-icons/fa6' import { Card, Badge, Button, EmptyState } from '../ui' import { copyText } from '../CopyableCredential' import { getFunktionen, setFeature } from '../../lib/teamApi' -import { FUNKTIONEN, ZUSTAND, funktionOf, aktionen, mergeFunktion, siteKeyOf } from '../../lib/funktionenView' +import { FUNKTIONEN, ZUSTAND, funktionOf, aktionen, mergeFunktion, siteKeyOf, abgeschaltetText } from '../../lib/funktionenView' import '../../styles/admin-areas.css' // Reiter "Funktionen": News-Pinnwand und Terminplaner je Projekt an/aus wie im frueheren Admin-Panel (Kundenliste, Zeile "Funktionen") @@ -94,6 +94,7 @@ export default function CustomerFunktionen({ customerId }) { {f.label} {label} + {abgeschaltetText(feature) && <>
{abgeschaltetText(feature)}} {feature.adminNote && <>
{short(feature.adminNote)}} {siteKeyOf(feature, f.key) && <>
}
diff --git a/src/lib/funktionenView.js b/src/lib/funktionenView.js index 5fb88de..37627e6 100644 --- a/src/lib/funktionenView.js +++ b/src/lib/funktionenView.js @@ -1,6 +1,8 @@ // Kunden-Reiter "Funktionen": Register der Funktionen je Projekt (News, Terminplaner), Aktionen je Zustand und // Einarbeiten der Portal-Antwort. Rein, unter Node testbar. +import { fmtYmd } from './format' + /** Funktionen in Anzeigereihenfolge; key = Schluessel im Portal (features[key], POST /funktionen/:customerId/:key). */ export const FUNKTIONEN = [ { @@ -30,6 +32,25 @@ export const ZUSTAND = { disabled: ['muted', 'Abgeschaltet'], } +/** Abschaltgrund (Portal: features[key].disabledReason) -> Zusatz hinter "Abgeschaltet"; unbekannte Gruende gelten als "ohne Grund". */ +const ABGESCHALTET_GRUND = { + abo_ende: ' (Abo beendet)', + abo_ende_vor_einrichtung: ' (Abo endete vor Einrichtung, Erstattung prüfen)', + team: ' (Team)', + kunde_geloescht: ' (Kunde gelöscht)', +} + +/** + * Zeile unter dem Abzeichen "Abgeschaltet": Grund und, falls das Portal ein Loeschdatum nennt (feature.loeschungAm, Berliner + * Kalendertag; nur Terminplaner), "Löschung am TT.MM.JJJJ". Ausserhalb von "disabled" ''. + */ +export function abgeschaltetText(feature) { + if (feature?.state !== 'disabled') return '' + const grund = Object.prototype.hasOwnProperty.call(ABGESCHALTET_GRUND, feature.disabledReason) ? ABGESCHALTET_GRUND[feature.disabledReason] : '' + const loeschung = feature.loeschungAm ? `, Löschung am ${fmtYmd(feature.loeschungAm)}` : '' + return `Abgeschaltet${grund}${loeschung}` +} + /** Ansicht einer Funktion im Projekt (Portal: project.features[key]); ohne Eintrag gilt "off". */ export const funktionOf = (project, key) => project?.features?.[key] || { state: 'off' } diff --git a/tests/unit/funktionenView.test.mjs b/tests/unit/funktionenView.test.mjs index 2437272..63ed0f5 100644 --- a/tests/unit/funktionenView.test.mjs +++ b/tests/unit/funktionenView.test.mjs @@ -3,7 +3,7 @@ import assert from 'node:assert/strict' import { loadModule } from './load.mjs' // Kunden-Reiter "Funktionen": Register der Funktionen (News, Terminplaner), Aktionen je Zustand, Antwort einarbeiten -const { FUNKTIONEN, ZUSTAND, funktionOf, aktionen, mergeFunktion, siteKeyOf } = await loadModule('src/lib/funktionenView.js') +const { FUNKTIONEN, ZUSTAND, funktionOf, aktionen, mergeFunktion, siteKeyOf, abgeschaltetText } = await loadModule('src/lib/funktionenView.js') test('FUNKTIONEN: News und Terminplaner mit den Bestätigungstexten', () => { assert.deepEqual(FUNKTIONEN, [ @@ -101,3 +101,37 @@ test('siteKeyOf: nur der Terminplaner zeigt seinen Site-Key, nur wenn das Portal assert.equal(siteKeyOf(feature, 'termine'), '', JSON.stringify(feature)) } }) + +test('abgeschaltetText: Grund und Löschdatum im Zustand "disabled"', () => { + const text = (disabledReason, loeschungAm) => abgeschaltetText({ state: 'disabled', disabledReason, loeschungAm }) + assert.equal(text('abo_ende', '2026-11-04'), 'Abgeschaltet (Abo beendet), Löschung am 04.11.2026') + assert.equal(text('abo_ende', undefined), 'Abgeschaltet (Abo beendet)') + assert.equal(text('abo_ende_vor_einrichtung', '2026-11-04'), 'Abgeschaltet (Abo endete vor Einrichtung, Erstattung prüfen), Löschung am 04.11.2026') + assert.equal(text('abo_ende_vor_einrichtung', null), 'Abgeschaltet (Abo endete vor Einrichtung, Erstattung prüfen)') + assert.equal(text('team', '2026-12-01'), 'Abgeschaltet (Team), Löschung am 01.12.2026') + assert.equal(text('team', null), 'Abgeschaltet (Team)') + assert.equal(text('kunde_geloescht', '2026-12-31'), 'Abgeschaltet (Kunde gelöscht), Löschung am 31.12.2026') + assert.equal(text('kunde_geloescht', ''), 'Abgeschaltet (Kunde gelöscht)') +}) + +test('abgeschaltetText: ohne Grund nur "Abgeschaltet", mit Datum "Abgeschaltet, Löschung am …"', () => { + assert.equal(abgeschaltetText({ state: 'disabled' }), 'Abgeschaltet') + assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: null, loeschungAm: null }), 'Abgeschaltet') + assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: '', loeschungAm: '' }), 'Abgeschaltet') + assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: null, loeschungAm: '2026-11-04' }), 'Abgeschaltet, Löschung am 04.11.2026') + assert.equal(abgeschaltetText({ state: 'disabled', loeschungAm: '2026-01-09' }), 'Abgeschaltet, Löschung am 09.01.2026') +}) + +test('abgeschaltetText: unbekannter Grund wird wie "ohne Grund" behandelt', () => { + assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: 'frist' }), 'Abgeschaltet') + assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: 'frist', loeschungAm: '2026-11-04' }), 'Abgeschaltet, Löschung am 04.11.2026') + assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: 'constructor' }), 'Abgeschaltet', 'kein Treffer über die Prototypenkette') + assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: 5 }), 'Abgeschaltet') +}) + +test('abgeschaltetText: außerhalb von "disabled" leer, auch mit Grund und Datum', () => { + for (const state of ['active', 'setup', 'ordered', 'off', undefined, null]) { + assert.equal(abgeschaltetText({ state, disabledReason: 'abo_ende', loeschungAm: '2026-11-04' }), '', String(state)) + } + for (const feature of [{}, null, undefined]) assert.equal(abgeschaltetText(feature), '', JSON.stringify(feature)) +})