WOMS: Aktionen und Anleitungs-Logik fuer den Terminplaner

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 01:54:24 +00:00
1 parent 6f0371283e
commit c0015544c4
3 files changed
+284 -5

No files matched your search

+85 -3
View File
@@ -1,7 +1,7 @@
// 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'
import { fmtYmd, fmtBerlin } from './format'
/** Funktionen in Anzeigereihenfolge; key = Schluessel im Portal (features[key], POST /funktionen/:customerId/:key). */
export const FUNKTIONEN = [
@@ -19,6 +19,8 @@ export const FUNKTIONEN = [
bestaetigen: {
activate: 'Terminplaner ohne Zahlung aktivieren?',
disable: 'Terminplaner abschalten? Auf der Website sind dann keine Buchungen mehr möglich.',
abnahme: 'Terminplaner freischalten? Der Kunde bekommt die Mail „bereit“ und Gäste können buchen.',
ticket: 'Fehlendes Einrichtungs-Ticket neu anlegen?',
},
},
]
@@ -63,14 +65,94 @@ export function siteKeyOf(feature, key) {
return key === 'termine' && typeof wert === 'string' ? wert.trim() : ''
}
/** Moegliche Aktionen je Zustand: Aktivieren (ohne Zahlung) ausser bei "active", Abschalten solange bestellt, in Einrichtung oder aktiv. */
export function aktionen(state) {
/** Beschriftung der Aktionsknoepfe (Aktion = action im POST /funktionen/:customerId/:key). */
export const AKTION_LABEL = {
activate: 'Aktivieren (ohne Zahlung)',
disable: 'Abschalten',
abnahme: 'Freischalten (Abnahme)',
ticket: 'Ticket nachlegen',
}
/**
* Moegliche Aktionen je Zustand. News (und ohne key): Aktivieren (ohne Zahlung) ausser bei "active", Abschalten solange bestellt, in
* Einrichtung oder aktiv. Terminplaner: "ordered" nur Abschalten (Aktivieren ohne Zahlung wuerde eine spaetere Zahlung im Portal
* verschlucken), "setup" Freischalten nach der Abnahme (mit ticketMissing zuerst "Ticket nachlegen"), "active" nur Abschalten.
*/
export function aktionen(state, key, { ticketMissing = false } = {}) {
if (key === 'termine') {
if (state === 'ordered' || state === 'active') return ['disable']
if (state === 'setup') return ticketMissing ? ['ticket', 'abnahme', 'disable'] : ['abnahme', 'disable']
return ['activate']
}
const list = []
if (state !== 'active') list.push('activate')
if (['ordered', 'setup', 'active'].includes(state)) list.push('disable')
return list
}
/** Warum der Terminplaner in "setup" noch nicht freigeschaltet werden kann (Portal: feature.hindernis) -> Hinweistext; sonst ''. */
const HINDERNIS_TEXT = {
kein_abo: 'Kein laufendes Wartungs-Abo: Freischalten ist gesperrt.',
keine_einstellung: 'Noch keine Einstellungen gespeichert.',
abo_unbekannt: 'Abo gerade nicht prüfbar.',
}
export const hindernisText = (code) => (Object.prototype.hasOwnProperty.call(HINDERNIS_TEXT, code) ? HINDERNIS_TEXT[code] : '')
/** Feste Beschriftungen der Teilkopien (Abschnitt-id -> Knopftext); jeder andere kopierbare Abschnitt heisst "<Titel> kopieren". */
const KOPIE_LABEL = {
snippet: 'Einbau-Code kopieren',
css: 'CSS-Variablen kopieren',
datenschutz_sie: 'Datenschutz-Absatz (Sie) kopieren',
datenschutz_du: 'Datenschutz-Absatz (Du) kopieren',
}
const istText = (wert) => typeof wert === 'string' && wert !== ''
/** Kopierknoepfe des Anleitungs-Panels: zuerst "alles" (antwort.text), dann jeder kopierbare Abschnitt mit Text. Ohne Antwort oder ohne Text []. */
export function anleitungKopien(antwort) {
if (!istText(antwort?.text)) return []
const kopien = [{ id: 'alles', label: 'Alles kopieren', text: antwort.text }]
for (const a of Array.isArray(antwort.abschnitte) ? antwort.abschnitte : []) {
if (!a?.kopierbar || !istText(a.text)) continue
const label = Object.prototype.hasOwnProperty.call(KOPIE_LABEL, a.id) ? KOPIE_LABEL[a.id] : `${a.titel || a.id} kopieren`
kopien.push({ id: a.id, label, text: a.text })
}
return kopien
}
/** Pfad der Team-Route mit der Einbauanleitung des Terminplaners (relativ zu /api/team). */
export const anleitungPfad = (customerId, projectId) =>
`/funktionen/${encodeURIComponent(customerId)}/termine/anleitung?projectId=${encodeURIComponent(projectId)}`
/** "Stand Version N, TT.MM.JJJJ" (Berliner Kalendertag) bzw. "Stand Version N" ohne brauchbares Datum; ohne Stand ''. */
function standKopf(stand) {
if (!Number.isInteger(stand?.version)) return ''
const datum = stand.updatedAt && !Number.isNaN(Date.parse(stand.updatedAt)) ? fmtBerlin(stand.updatedAt) : ''
return `Stand Version ${stand.version}${datum ? `, ${datum}` : ''}`
}
/**
* Alles, was das Anleitungs-Panel anzeigt, aus der Portal-Antwort (GET .../termine/anleitung) und der Funktionsansicht (feature.hindernis):
* { kopf, ticketPfad (/tickets?woid=… | null), warnungen, hinweis (Hindernis-Text), kopien, abschnitte [{ id, titel, text }] }. Ohne Antwort alles leer.
*/
export function panelModell(antwort, feature) {
if (!antwort) return { kopf: '', ticketPfad: null, warnungen: [], hinweis: '', kopien: [], abschnitte: [] }
return {
kopf: standKopf(antwort.stand),
ticketPfad: antwort.ticketWoid ? `/tickets?woid=${encodeURIComponent(antwort.ticketWoid)}` : null,
warnungen: Array.isArray(antwort.warnungen) ? antwort.warnungen : [],
hinweis: hindernisText(feature?.hindernis),
kopien: anleitungKopien(antwort),
abschnitte: (Array.isArray(antwort.abschnitte) ? antwort.abschnitte : []).map(({ id, titel, text }) => ({ id, titel, text })),
}
}
/** true, wenn die Adresse (location.search) ?anleitung=<projectId> traegt (kodiert oder nicht): das Panel dieses Projekts startet offen. */
export function anleitungOffen(search, projectId) {
if (typeof search !== 'string' || typeof projectId !== 'string' || projectId === '') return false
return new URLSearchParams(search).get('anleitung') === projectId
}
/**
* Antwort des Portals (das eine geaenderte Projekt) in die Projektliste einarbeiten: uebernommen wird nur
* aktualisiert.features[key]; Projektfelder und die anderen Funktionen bleiben. Gibt eine neue Liste zurueck.