Terminplaner: Kurzfassung der Einbauanleitung fuer das Einrichtungs-Ticket, T16-Gesundheitsblock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
90692b646b
commit
2db3eca357
9 files changed
+1181
-49
No files matched your search
@@ -4,7 +4,7 @@ import { createDocument, getDocument, listDocuments, updateDocument, Query } fro
|
||||
import { withLocks } from './keyedLock.js'
|
||||
import { sendAdminNotice, sendCustomerMail } from './mailer.js'
|
||||
import { getStripe, retrieveCheckoutSession } from './stripeBilling.js'
|
||||
import { anleitungsLink, featureConfig, featureHaken, featureKeys, ticketFreischaltungKeys, ticketTexte } from './featureRegistry.js'
|
||||
import { anleitungsLink, featureConfig, featureHaken, featureKeys, kuerze, ticketFreischaltungKeys, ticketTexte } from './featureRegistry.js'
|
||||
import { zustimmungFuerSitzung } from './featureConsents.js'
|
||||
import { aboLage, laeuft } from './aboStatus.js'
|
||||
import { loeschTag } from './termine/frist.js'
|
||||
@@ -711,7 +711,7 @@ async function ticketWoidOf(doc) {
|
||||
/**
|
||||
* T16 an das Team (Funktion bestellt): alle Platzhalter und Bloecke der Vorlage; was nicht genannt wird, bleibt leer bzw. aus.
|
||||
* Ohne bekannte Funktion heisst sie in Betreff und Text "Funktion". Liefert das Ergebnis von sendAdminNotice ({ sent, reason? }); der
|
||||
* Block gesundheit bleibt hier aus (D4 fuellt ihn).
|
||||
* Der Block gesundheit (Terminplaner) steht nur, wenn der Aufrufer ihn mit gesundheit_text einschaltet (teamNotice).
|
||||
*/
|
||||
function sendT16({ funktion, ...werte }, bloecke) {
|
||||
return sendAdminNotice({
|
||||
@@ -733,11 +733,48 @@ function sendT16({ funktion, ...werte }, bloecke) {
|
||||
})
|
||||
}
|
||||
|
||||
/** Der Warntext des T16-Blocks gesundheit hat hoechstens so viele Zeichen (ganze Meldungen, danach "… und N weitere"). */
|
||||
const GESUNDHEIT_TEXT_MAX = 500
|
||||
|
||||
/** Meldungen als ein Text: durch "; " getrennt; passt es nicht, so viele ganze Meldungen wie moeglich und "… und N weitere" (nie mitten in einer Meldung). */
|
||||
function gesundheitText(meldungen) {
|
||||
const alle = meldungen.join('; ')
|
||||
if (alle.length <= GESUNDHEIT_TEXT_MAX) return alle
|
||||
let text = ''
|
||||
let n = 0
|
||||
for (const meldung of meldungen) {
|
||||
const naechster = text ? `${text}; ${meldung}` : meldung
|
||||
if (`${naechster} … und ${meldungen.length - n - 1} weitere`.length > GESUNDHEIT_TEXT_MAX) break
|
||||
text = naechster
|
||||
n += 1
|
||||
}
|
||||
return text ? `${text} … und ${meldungen.length - n} weitere` : kuerze(meldungen[0], GESUNDHEIT_TEXT_MAX, { endzeile: '…' })
|
||||
}
|
||||
|
||||
/**
|
||||
* T16-Hinweis "Gesundheitsbereich pruefen" (nur Terminplaner): die Treffer der Stichwortpruefung ueber die gespeicherte Einstellung des Betriebs
|
||||
* (gesundheitsMeldungen: wohlgeformt, einzeilig, Klartext; renderMail maskiert sie fuer HTML), hoechstens 500 Zeichen. '' ohne Treffer und bei
|
||||
* jedem Lesefehler: T16 geht dann ohne Block raus (nie Geld ohne Spur); das Log nennt keine Daten des Betriebs. Dynamische Importe, weil
|
||||
* termine/kontext.js dieses Modul importiert.
|
||||
*/
|
||||
async function gesundheitHinweis(doc) {
|
||||
if (doc.featureKey !== 'termine') return ''
|
||||
try {
|
||||
const [{ ladeEinstellung }, { gesundheitsMeldungen }] = await Promise.all([import('./termine/kontext.js'), import('./termine/einbauanleitung.js')])
|
||||
const meldungen = gesundheitsMeldungen((await ladeEinstellung(doc.$id))?.einstellung)
|
||||
return meldungen.length ? gesundheitText(meldungen) : ''
|
||||
} catch (err) {
|
||||
console.error(`[funktionen] Gesundheitshinweis fuer T16 (Funktion ${doc.$id}) nicht lesbar, der Block entfaellt:`, err.message)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/** keinAbo: die Zahlung ist da, der Kunde hat aber kein laufendes Wartungs-Abo (Block kein_abo, nur bei Funktionen mit requiresWartungsAbo). */
|
||||
async function teamNotice(doc, customer, { failed = false, keinAbo = false } = {}) {
|
||||
const mails = featureConfig(doc.featureKey)?.mails
|
||||
// Knopf zur Einbauanleitung nur bei Funktionen mit Anleitung und nur, wenn bestellt und ein Ticket angelegt wurde
|
||||
const anleitung = Boolean(mails?.anleitung) && !failed
|
||||
const gesundheit = failed ? '' : await gesundheitHinweis(doc)
|
||||
await sendT16(
|
||||
{
|
||||
kunde_name: customer?.name || customer?.email || doc.customerId,
|
||||
@@ -747,8 +784,9 @@ async function teamNotice(doc, customer, { failed = false, keinAbo = false } = {
|
||||
woid: failed ? '' : await ticketWoidOf(doc),
|
||||
sitekey: failed ? '' : doc.siteKey || '',
|
||||
anleitung_url: anleitung ? anleitungsLink(doc.customerId, doc.projectId) : '',
|
||||
gesundheit_text: gesundheit,
|
||||
},
|
||||
{ zahlung_fehlgeschlagen: failed, anleitung, kein_abo: keinAbo }
|
||||
{ zahlung_fehlgeschlagen: failed, anleitung, kein_abo: keinAbo, gesundheit: gesundheit !== '' }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { config } from '../../config.js'
|
||||
import { anleitungsLink } from '../featureRegistry.js'
|
||||
import { TICKET_DETAILS_MAX, TICKET_GEKUERZT, anleitungsLink, kuerze } from '../featureRegistry.js'
|
||||
import { gesundheitsWarnungen } from './fragebogen.js'
|
||||
import {
|
||||
VORGABE_FARBE, fehlendePflichtangaben, hatPreis, preisHinweis, preisText, sichtbareLeistungen, text as bereinigterText, zahlungText, zeigtWiderruf,
|
||||
@@ -133,7 +133,7 @@ export function cssVorschlag(farbe) {
|
||||
|
||||
// ================================================================ Generator einbauanleitung()
|
||||
// Task D3a: Modell, Leistungen, Ressourcen, Regeln, Gastfelder, Benachrichtigungen. Task D3b: Einbau, Snippet, CSS, Datenschutz-Absaetze,
|
||||
// Checkliste, Abrechnung, die Warnungen und der Volltext. Task D4 haengt die Kurzfassung (`kurz`) an.
|
||||
// Checkliste, Abrechnung, die Warnungen und der Volltext. Task D4: die Kurzfassung fuer das Einrichtungs-Ticket (`kurz`), ticketStand und driftWarnung.
|
||||
//
|
||||
// Feldweissliste: gelesen werden nur ausdruecklich benannte Felder, nie ganze Objekte. Nie ausgegeben werden feedToken (Kalender-
|
||||
// Abo der Ressource), die Betriebs-E-Mail (sie steht nur in den Datenschutz-Absaetzen), Beschreibungen der Leistungen und alles,
|
||||
@@ -555,17 +555,188 @@ const WARNUNG_OHNE_DATENSCHUTZ = 'Datenschutz-Absatz nicht erzeugbar: Name oder
|
||||
*/
|
||||
const meldungText = (wert) => zeile(typeof wert === 'string' ? wert.toWellFormed() : '')
|
||||
|
||||
/** Warnungen fuer das Team in fester Reihenfolge: Banner, Pflichtangaben, Impressum, Datenschutz-URL, Gesundheit, Kontrast, Datenschutz-Absatz. */
|
||||
function warnungenAus(d) {
|
||||
const warnungen = []
|
||||
if (d.entwurf) warnungen.push(ENTWURF_BANNER)
|
||||
for (const feld of fehlendePflichtangaben(d.e)) warnungen.push(WARNUNG_PFLICHT[feld] ?? `Pflichtangabe fehlt: ${feld}`)
|
||||
if (!zeile(d.betrieb.impressumUrl)) warnungen.push(`Impressum-URL ${WARNUNG_OHNE_URL}`)
|
||||
if (!zeile(d.betrieb.datenschutzUrl)) warnungen.push(`Datenschutz-URL ${WARNUNG_OHNE_URL}`)
|
||||
for (const meldung of gesundheitsWarnungen(d.e)) warnungen.push(meldungText(meldung))
|
||||
warnungen.push(...cssVorschlag(d.farbe).hinweise)
|
||||
if (!d.datenschutz) warnungen.push(WARNUNG_OHNE_DATENSCHUTZ)
|
||||
return warnungen
|
||||
/**
|
||||
* Die Gesundheitsmeldungen einer Einstellung (hoechstens 10, Pruefung aus Block C) in der Form, in der sie im Panel, im Ticket und in T16 stehen:
|
||||
* wohlgeformt, einzeilig, Klartext. Ohne Einstellung (null) und ohne Treffer leer.
|
||||
*/
|
||||
export function gesundheitsMeldungen(einstellung) {
|
||||
return gesundheitsWarnungen(einstellung).map(meldungText)
|
||||
}
|
||||
|
||||
/**
|
||||
* Warnungen fuer das Team in fester Reihenfolge, in drei Teilen: davor (Banner, Pflichtangaben, Impressum, Datenschutz-URL), die Gesundheits-
|
||||
* meldungen, danach (Kontrast, Datenschutz-Absatz). warnungen = davor + gesundheit + danach; die Kurzfassung (Ticket) setzt die Gesundheit
|
||||
* in einen eigenen Absatz und zaehlt sie nicht zu ihren fuenf Warnungen.
|
||||
*/
|
||||
function warnungsTeile(d) {
|
||||
const davor = []
|
||||
if (d.entwurf) davor.push(ENTWURF_BANNER)
|
||||
for (const feld of fehlendePflichtangaben(d.e)) davor.push(WARNUNG_PFLICHT[feld] ?? `Pflichtangabe fehlt: ${feld}`)
|
||||
if (!zeile(d.betrieb.impressumUrl)) davor.push(`Impressum-URL ${WARNUNG_OHNE_URL}`)
|
||||
if (!zeile(d.betrieb.datenschutzUrl)) davor.push(`Datenschutz-URL ${WARNUNG_OHNE_URL}`)
|
||||
const danach = [...cssVorschlag(d.farbe).hinweise]
|
||||
if (!d.datenschutz) danach.push(WARNUNG_OHNE_DATENSCHUTZ)
|
||||
return { davor, gesundheit: gesundheitsMeldungen(d.e), danach }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Kurzfassung fuer das Einrichtungs-Ticket (Task D4)
|
||||
|
||||
// `kurz` steht in ticket.details (Klartext, das WOMS zeigt es mit pre-wrap) und ist die einzige Fassung, die ins Ticket geht: ohne Datenschutz-Absatz
|
||||
// (4,8 k Zeichen je Variante), ohne Token. Absaetze durch eine Leerzeile; der erste ist der Kopf und endet mit der Konfigurationsstand-Zeile, die
|
||||
// ticketStand liest. Platz: TICKET_DETAILS_MAX. Statt mitten im Satz abzuschneiden, ist es eine Budgetliste: Kopf, Gesundheitsabsatz, Snippet,
|
||||
// Schritte und Verweis bleiben immer (eine Gesundheitsmeldung verschwindet nie unter Platzdruck); passt die Summe nicht, fallen erst die Zaehlungen
|
||||
// weg, dann die uebrigen Warnungen (die letzte zuerst), dann der Kundenwunsch-Block. Nur wenn selbst die Pflichtteile nicht passen (ein unge-
|
||||
// pruefter Wert sprengt sie), kuerzt kuerze() am Ende an einer Zeilengrenze mit der Schlusszeile des Tickets.
|
||||
|
||||
const KURZ_NAMEN = 8 // so viele Namen je Zaehlung, danach "… und N weitere"
|
||||
const KURZ_NAME_MAX = 60
|
||||
const KURZ_KOPF_MAX = 120
|
||||
const KURZ_WARNUNGEN = 5
|
||||
const GESUNDHEIT_ABSATZ = 'Gesundheitsbereich prüfen:'
|
||||
const KURZ_OHNE_LINK = 'Verweis: im WOMS unter Kunden → Funktionen, Anleitung des Projekts'
|
||||
|
||||
const kopfWert = (wert) => kuerze(zeile(wert), KURZ_KOPF_MAX, { endzeile: '…' })
|
||||
const kurzName = (name) => kuerze(zeile(name) || 'ohne Namen', KURZ_NAME_MAX, { endzeile: '…' })
|
||||
|
||||
/** 'Konfigurationsstand: Version N, TT.MM.JJJJ' | '… Version N' (ohne Datum) | '… Version 0 (noch keine Einstellungen gespeichert)' | '… (Einstellungen nicht lesbar)'. */
|
||||
function standZeile(version, geaendert, hatEinstellung) {
|
||||
const n = Number.isInteger(version) && version >= 1 ? version : 0
|
||||
if (n === 0) return 'Konfigurationsstand: Version 0 (noch keine Einstellungen gespeichert)'
|
||||
if (!hatEinstellung) return `Konfigurationsstand: Version ${n} (Einstellungen nicht lesbar)`
|
||||
return `Konfigurationsstand: Version ${n}${geaendert ? `, ${geaendert}` : ''}`
|
||||
}
|
||||
|
||||
/** Kopf: Funktion, Projekt, Domain (wie createFeatureTicket: liveDomain, sonst previewUrl, sonst subdomain), Kunde, siteKey, Stand. */
|
||||
function kurzKopf(k) {
|
||||
const domain = zeile(k.project?.liveDomain) || zeile(k.project?.previewUrl) || zeile(k.project?.subdomain)
|
||||
return [
|
||||
'Funktion: Terminplaner',
|
||||
`Projekt: ${kopfWert(k.project?.projectName) || kopfWert(k.project?.subdomain)}`,
|
||||
`Domain: ${kopfWert(domain)}`,
|
||||
`Kunde: ${kopfWert(k.kunde)}`,
|
||||
`siteKey: ${k.siteKey}`,
|
||||
standZeile(k.version, k.geaendert, k.hatEinstellung),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** ' Stelle: erste Zeile' und die weiteren Zeilen um vier Leerzeichen eingerueckt; Leerzeilen fallen weg (nie ein Absatzumbruch im Kundentext). */
|
||||
function kurzWunschZeilen(bezeichnung, wert) {
|
||||
const [erste, ...weitere] = wert.split('\n').filter((z) => z !== '')
|
||||
return erste === undefined ? [] : [` ${bezeichnung}: ${erste}`, ...weitere.map((z) => ` ${z}`)]
|
||||
}
|
||||
|
||||
/** Kundenwunsch: Modus (ohne Wahl: Standard) und die vier Wunschtexte des Kunden (Text des Kunden, nie im Snippet ausser dem Knopf-Text). */
|
||||
function kurzWunsch(w) {
|
||||
const zeilen = [
|
||||
...kurzWunschZeilen('Knopf-Text', w.knopfText),
|
||||
...kurzWunschZeilen('Stelle', w.stelle),
|
||||
...kurzWunschZeilen('Gestaltung', w.gestaltung),
|
||||
...kurzWunschZeilen('Bemerkung', w.bemerkung),
|
||||
]
|
||||
return [
|
||||
'Kundenwunsch (Text des Kunden):',
|
||||
` Modus: ${w.modus}${w.modusGewaehlt ? '' : ' (Standard)'}`,
|
||||
...(zeilen.length ? zeilen : [' Sonst keine Angaben im Fragebogen']),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** '- 12 Leistungen (11 aktiv): A, B, … und 4 weitere': Anzahl, aktive (nur wenn es welche gibt und gezaehlt wird) und die ersten 8 Namen. */
|
||||
function kurzZaehlung(eintraege, einzahl, mehrzahl, { mitAktiv = false } = {}) {
|
||||
const n = eintraege.length
|
||||
const aktiv = mitAktiv && n > 0 ? ` (${eintraege.filter((x) => x.aktiv).length} aktiv)` : ''
|
||||
const namen = kuerzeListe(eintraege.map((x) => kurzName(x.name)), KURZ_NAMEN).join(', ')
|
||||
return `- ${anzahl(n, einzahl, mehrzahl)}${aktiv}${n > 0 ? `: ${namen}` : ''}`
|
||||
}
|
||||
|
||||
function kurzUmfang(k) {
|
||||
return [
|
||||
'Umfang:',
|
||||
kurzZaehlung(k.leistungen, 'Leistung', 'Leistungen', { mitAktiv: true }),
|
||||
kurzZaehlung(k.ressourcen, 'Person oder Platz', 'Personen und Plätze', { mitAktiv: true }),
|
||||
kurzZaehlung(k.zusatzfelder.map((z) => ({ name: z.label })), 'Zusatzfeld', 'Zusatzfelder'),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** Die sechs Schritte des Teams; Schritt 6 nennt den Preis nur, wenn die Einrichtung bezahlt ist (unlockedByPurchase === true). */
|
||||
function kurzSchritte(k) {
|
||||
const form = !k.hatEinstellung ? 'Sie- oder Du-Form nach der Anrede der Gäste' : k.anrede === 'du' ? 'Du-Form' : 'Sie-Form'
|
||||
const bezahlt = k.feature?.unlockedByPurchase === true ? ` (Einrichtung bezahlt, ${einrichtungsPreis()})` : ''
|
||||
return [
|
||||
'Schritte:',
|
||||
'1. WOMS-Panel öffnen (Kunde → Reiter Funktionen → Anleitung des Projekts): dort stehen alle Einstellungen, die Kopierknöpfe und die Abnahme-Checkliste.',
|
||||
'2. Einbau-Code einfügen: den Code oben an der Stelle einsetzen, die der Kunde nennt (siehe Kundenwunsch), das Skript einmal pro Seite.',
|
||||
'3. Gestaltung und Kontrast: das CSS aus der Anleitung (Abschnitt „Aussehen (CSS)“) in die Website übernehmen und den Kontrast prüfen.',
|
||||
`4. Datenschutz-Absatz (${form}) aus der Anleitung in die Datenschutzerklärung der Website aufnehmen, bevor freigeschaltet wird (${DATENSCHUTZ_ZWISCHENREGEL}).`,
|
||||
'5. „Freischalten (Abnahme)“ im WOMS auslösen, danach eine Probebuchung durchspielen und den Testtermin wieder stornieren (Checkliste in der Anleitung).',
|
||||
`6. Ticket erledigen und die Zeit im Arbeitsblatt buchen. Nicht auf das Stundenkontingent anrechnen${bezahlt}.`,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Kurzfassung aus den vorbereiteten Teilen. k: feature, siteKey, project, kunde, version, geaendert, hatEinstellung, anrede, wunsch, leistungen,
|
||||
* ressourcen, zusatzfelder, warnungen (ohne Gesundheit), gesundheit (Meldungen), snippet, anleitungsLink.
|
||||
*/
|
||||
function kurzfassung(k) {
|
||||
const pflicht = {
|
||||
kopf: kurzKopf(k),
|
||||
gesundheit: k.gesundheit.length ? [GESUNDHEIT_ABSATZ, ...k.gesundheit.map((m) => `- ${m}`)].join('\n') : '',
|
||||
snippet: `Einbau-Code (Snippet):\n${k.snippet}`,
|
||||
schritte: kurzSchritte(k),
|
||||
verweis: k.anleitungsLink ? `Verweis: ${k.anleitungsLink}` : KURZ_OHNE_LINK,
|
||||
}
|
||||
const umfang = k.hatEinstellung ? kurzUmfang(k) : ''
|
||||
const wunsch = kurzWunsch(k.wunsch)
|
||||
const warnungen = k.warnungen.slice(0, KURZ_WARNUNGEN).map((w) => `- ${w}`)
|
||||
let mitUmfang = Boolean(umfang)
|
||||
let mitWunsch = true
|
||||
|
||||
const zusammen = () =>
|
||||
[
|
||||
pflicht.kopf,
|
||||
mitWunsch ? wunsch : '',
|
||||
mitUmfang ? umfang : '',
|
||||
warnungen.length ? ['Warnungen:', ...warnungen].join('\n') : '',
|
||||
pflicht.gesundheit,
|
||||
pflicht.snippet,
|
||||
pflicht.schritte,
|
||||
pflicht.verweis,
|
||||
].filter(Boolean).join('\n\n').toWellFormed()
|
||||
|
||||
let text = zusammen()
|
||||
if (text.length > TICKET_DETAILS_MAX) {
|
||||
mitUmfang = false
|
||||
text = zusammen()
|
||||
}
|
||||
while (text.length > TICKET_DETAILS_MAX && warnungen.length > 0) {
|
||||
warnungen.pop()
|
||||
text = zusammen()
|
||||
}
|
||||
if (text.length > TICKET_DETAILS_MAX) {
|
||||
mitWunsch = false
|
||||
text = zusammen()
|
||||
}
|
||||
return text.length > TICKET_DETAILS_MAX ? kuerze(text, TICKET_DETAILS_MAX, { anZeile: true, endzeile: TICKET_GEKUERZT }) : text
|
||||
}
|
||||
|
||||
const STAND_RE = /^Konfigurationsstand: Version (\d{1,9})(?!\d)/m
|
||||
|
||||
/**
|
||||
* Die Version der Einstellungen, auf der ein Ticket beruht: die erste Zeile "Konfigurationsstand: Version N" am Zeilenanfang (die Wuensche des
|
||||
* Kunden stehen eingerueckt und koennen sie nicht nachahmen). Ohne die Zeile, mit mehr als neun Stellen oder bei Nicht-Text null; Version 0 ist ein Stand.
|
||||
*/
|
||||
export function ticketStand(details) {
|
||||
if (typeof details !== 'string') return null
|
||||
const treffer = STAND_RE.exec(details)
|
||||
return treffer ? Number(treffer[1]) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Hinweis fuers Team, wenn die Einstellungen seit der Bestellung gespeichert wurden: ticketVersion aus ticketStand, jetzt die aktuelle Version.
|
||||
* Nur bei zwei ganzen Zahlen und jetzt > ticketVersion, sonst null (auch ohne Stand im Ticket).
|
||||
*/
|
||||
export function driftWarnung(ticketVersion, jetzt) {
|
||||
if (!Number.isInteger(ticketVersion) || !Number.isInteger(jetzt) || jetzt <= ticketVersion) return null
|
||||
return `Einstellungen seit der Bestellung geändert (Ticket Version ${ticketVersion}, jetzt ${jetzt}).`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Zusammenbau
|
||||
@@ -603,27 +774,45 @@ function projektText(project) {
|
||||
* @returns {{stand: {version: number, updatedAt: string|null}, warnungen: string[], abschnitte: Array<{id: string, titel: string, text: string, kopierbar: boolean}>, text: string, kurz: string}}
|
||||
* abschnitte: 13 Stueck in fester Reihenfolge (modell bis abrechnung), kopierbar nur snippet, css und die beiden Datenschutz-Absaetze.
|
||||
* text: alle Abschnitte mit Ueberschrift, aber nur die Datenschutz-Fassung, die zu einstellung.anrede passt ('du', sonst Sie).
|
||||
* Ohne Einstellung (null): nur der Abschnitt snippet und die Warnung "Keine Einstellungen gespeichert". `kurz` ist '' (kommt mit D4).
|
||||
* Ohne Einstellung (null): nur der Abschnitt snippet und die Warnung "Keine Einstellungen gespeichert".
|
||||
* kurz: die Kurzfassung fuer ticket.details (Klartext, hoechstens TICKET_DETAILS_MAX Zeichen, erster Absatz = Kopf mit der Konfigurationsstand-Zeile,
|
||||
* ohne Datenschutz-Absatz und ohne Token); auch ohne Einstellung gefuellt (Kopf mit Version 0, Snippet, Schritte, Verweis).
|
||||
*/
|
||||
export function einbauanleitung({ feature, project, einstellung, fragebogen, version, now, updatedAt = null, kunde = null, entwurf = RECHTSTEXTE_ENTWURF }) {
|
||||
if (!(now instanceof Date) || Number.isNaN(now.getTime())) throw new TypeError('einbauanleitung: now fehlt')
|
||||
const stand = { version, updatedAt }
|
||||
const banner = entwurf ? [ENTWURF_BANNER] : []
|
||||
const geaendertAm = updatedAt ? new Date(updatedAt) : null
|
||||
const gemeinsam = {
|
||||
feature,
|
||||
project,
|
||||
siteKey: zeile(feature?.siteKey),
|
||||
portal: zeile(config.publicBaseUrl),
|
||||
wunsch: kundenwunsch(fragebogen),
|
||||
anleitungsLink: feature && zeile(feature.customerId) && zeile(feature.projectId) ? anleitungsLink(zeile(feature.customerId), zeile(feature.projectId)) : '',
|
||||
version,
|
||||
geaendert: geaendertAm && !Number.isNaN(geaendertAm.getTime()) ? datumKurz(geaendertAm) : '',
|
||||
}
|
||||
|
||||
if (!istObjekt(einstellung)) {
|
||||
const abschnitte = [abschnittSnippet({ ...gemeinsam, telefon: '' })]
|
||||
return { stand, warnungen: [...banner, 'Keine Einstellungen gespeichert'], abschnitte, text: volltext(abschnitte), kurz: '' }
|
||||
const warnungen = [...banner, 'Keine Einstellungen gespeichert']
|
||||
const kurz = kurzfassung({
|
||||
...gemeinsam,
|
||||
kunde: zeile(kunde?.name),
|
||||
hatEinstellung: false,
|
||||
anrede: null,
|
||||
leistungen: [],
|
||||
ressourcen: [],
|
||||
zusatzfelder: [],
|
||||
warnungen,
|
||||
gesundheit: [],
|
||||
snippet: abschnitte[0].text,
|
||||
})
|
||||
return { stand, warnungen, abschnitte, text: volltext(abschnitte), kurz }
|
||||
}
|
||||
|
||||
const ressourcen = liste(einstellung.ressourcen).filter(istObjekt)
|
||||
const geaendert = updatedAt ? new Date(updatedAt) : null
|
||||
const betrieb = istObjekt(einstellung.betrieb) ? einstellung.betrieb : {}
|
||||
const daten = {
|
||||
...gemeinsam,
|
||||
@@ -631,8 +820,6 @@ export function einbauanleitung({ feature, project, einstellung, fragebogen, ver
|
||||
now,
|
||||
entwurf: Boolean(entwurf),
|
||||
heuteTag: heute(now),
|
||||
version,
|
||||
geaendert: geaendert && !Number.isNaN(geaendert.getTime()) ? datumKurz(geaendert) : '',
|
||||
betrieb,
|
||||
telefon: zeile(betrieb.telefon),
|
||||
kunde: zeile(kunde?.name),
|
||||
@@ -647,11 +834,21 @@ export function einbauanleitung({ feature, project, einstellung, fragebogen, ver
|
||||
const abschnitte = ABSCHNITTE.map((baue) => baue(daten))
|
||||
// der Volltext fuehrt nur die Datenschutz-Fassung, die zur Anrede der Gaeste passt
|
||||
const andere = einstellung.anrede === 'du' ? 'datenschutz_sie' : 'datenschutz_du'
|
||||
const teile = warnungsTeile(daten)
|
||||
const kurz = kurzfassung({
|
||||
...daten,
|
||||
hatEinstellung: true,
|
||||
anrede: einstellung.anrede,
|
||||
zusatzfelder: liste(istObjekt(einstellung.gastFelder) ? einstellung.gastFelder.zusatz : null).filter(istObjekt),
|
||||
warnungen: [...teile.davor, ...teile.danach],
|
||||
gesundheit: teile.gesundheit,
|
||||
snippet: abschnitte.find((a) => a.id === 'snippet').text,
|
||||
})
|
||||
return {
|
||||
stand,
|
||||
warnungen: warnungenAus(daten),
|
||||
warnungen: [...teile.davor, ...teile.gesundheit, ...teile.danach],
|
||||
abschnitte,
|
||||
text: volltext(abschnitte.filter((a) => a.id !== andere)),
|
||||
kurz: '',
|
||||
kurz,
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,44 @@ export async function ladeEinstellung(featureId) {
|
||||
return gelesen?.einstellung ? { ...gelesen, fragebogen: fragebogenAus(doc) } : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Fragebogen aus einem termineConfig-Dokument, so wie er gespeichert ist (geparstes Objekt) oder null. Anders als fragebogenAus kein
|
||||
* leerer Fragebogen als Ersatz: fehlt das Attribut, ist es leer oder kein JSON-Objekt, ist das Ergebnis null (kaputtes JSON mit einer Logzeile).
|
||||
*/
|
||||
function fragebogenRoh(doc) {
|
||||
const roh = doc.fragebogen
|
||||
if (roh === undefined || roh === null || roh === '') return null
|
||||
try {
|
||||
const fragebogen = JSON.parse(roh)
|
||||
if (!istObjekt(fragebogen)) throw new Error('kein Objekt')
|
||||
return fragebogen
|
||||
} catch (err) {
|
||||
console.error(`[termine-kontext] Fragebogen ${doc.$id} ist unlesbar: ${err.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Der gespeicherte Stand einer Funktion fuer das Einrichtungs-Ticket und die Anleitung (TP2 D4): Einstellung, Version, Zeitpunkt der letzten
|
||||
* Aenderung und Fragebogen, ohne Modus- und Zustandspruefung. Anders als ladeEinstellung unterscheidet er "noch nie gespeichert" von "unlesbar":
|
||||
* null nur ohne termineConfig-Dokument; unlesbares JSON macht nur das jeweilige Feld null (die Version des Dokuments bleibt). Fehlt Collection
|
||||
* oder Datenbank, wird der Fehler weitergereicht.
|
||||
* @param {string} featureId ID des portalFeatures-Dokuments
|
||||
* @returns {Promise<{ einstellung: object|null, version: number, updatedAt: string|null, fragebogen: object|null }|null>}
|
||||
*/
|
||||
export async function ladeStand(featureId) {
|
||||
pruefeText(featureId, 'featureId')
|
||||
const doc = await holeOderNull(config.collections.termineConfig, featureId)
|
||||
if (!doc) return null
|
||||
const { einstellung, version } = liesDokument(doc)
|
||||
return {
|
||||
einstellung,
|
||||
version,
|
||||
updatedAt: typeof doc.updatedAt === 'string' && doc.updatedAt !== '' ? doc.updatedAt : null,
|
||||
fragebogen: fragebogenRoh(doc),
|
||||
}
|
||||
}
|
||||
|
||||
async function baueKontext(feature) {
|
||||
const [project, doc] = await Promise.all([
|
||||
feature.projectId ? holeOderNull(config.collections.websiteProjects, feature.projectId) : null,
|
||||
|
||||
@@ -1,10 +1,48 @@
|
||||
// STUB (Block B) – ersetzt Task D4
|
||||
import { minimaleTicketDetails } from '../featureRegistry.js'
|
||||
import { TICKET_DETAILS_MAX, kuerze, minimaleTicketDetails } from '../featureRegistry.js'
|
||||
import { einbauanleitung } from './einbauanleitung.js'
|
||||
import { ladeStand } from './kontext.js'
|
||||
|
||||
/**
|
||||
* Details des Einrichtungs-Tickets fuer den Terminplaner (config.features.termine.ticket.details).
|
||||
* ctx = { feature, doc, project, customer }. Block D4 ersetzt den Inhalt durch einbauanleitung(...).kurz; Signatur und ctx bleiben.
|
||||
* Details des Einrichtungs-Tickets fuer den Terminplaner (config.features.termine.ticket.details, TP2 Task D4).
|
||||
* ctx = { feature, doc, project, customer } wie ihn markPaid uebergibt: feature = Registereintrag (der Generator braucht ihn nicht),
|
||||
* doc = portalFeatures-Dokument (siteKey, $id, customerId, projectId, unlockedByPurchase kommen von dort), project = websiteProjects-Dokument,
|
||||
* customer = Kunde oder null (best effort).
|
||||
*
|
||||
* Inhalt: einbauanleitung(...).kurz, gebaut aus dem gespeicherten Stand der Einstellungen (ladeStand). Die Funktion wirft nie: bezahlt ist bezahlt,
|
||||
* das Ticket entsteht immer. Scheitert das Lesen oder der Generator, stehen die Kopfzeilen, der Hinweis, dass die Anleitung nicht erzeugt werden
|
||||
* konnte, und der Verweis auf das WOMS im Ticket (notfallDetails). Das Log nennt keine Daten des Betriebs.
|
||||
*/
|
||||
export async function termineTicketDetails(ctx) {
|
||||
return `${minimaleTicketDetails(ctx)}\nEinrichtung bezahlt (149 €), Zeit nicht auf das Stundenkontingent anrechnen.`
|
||||
try {
|
||||
const { doc, project, customer } = ctx
|
||||
const stand = await ladeStand(doc.$id)
|
||||
const { kurz } = einbauanleitung({
|
||||
feature: doc,
|
||||
project,
|
||||
einstellung: stand?.einstellung ?? null,
|
||||
fragebogen: stand?.fragebogen ?? null,
|
||||
version: stand?.version ?? 0,
|
||||
updatedAt: stand?.updatedAt ?? null,
|
||||
kunde: { name: customer?.name || doc.customerId || '' },
|
||||
now: new Date(),
|
||||
})
|
||||
if (typeof kurz !== 'string' || kurz.trim() === '') throw new Error('Kurzfassung leer')
|
||||
return kurz
|
||||
} catch (err) {
|
||||
console.error(`[termine-ticket] Kurzfassung der Einbauanleitung nicht erzeugbar, Notfall-Details: ${err?.message}`)
|
||||
return notfallDetails(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
const ANLEITUNG_FEHLT = 'Anleitung konnte nicht erzeugt werden, im WOMS öffnen (Verweis oben).'
|
||||
const KONTINGENT = 'Nicht auf das Stundenkontingent anrechnen.'
|
||||
|
||||
/** Kopfzeilen und Verweis (minimaleTicketDetails, Funktion wie im Kopf der Kurzfassung) plus Hinweis; wirft nie, auch nicht bei einem unbrauchbaren ctx. */
|
||||
function notfallDetails(ctx) {
|
||||
try {
|
||||
const kopf = minimaleTicketDetails({ ...ctx, feature: { key: 'Terminplaner' } })
|
||||
return kuerze(`${kopf}\n${ANLEITUNG_FEHLT}\n${KONTINGENT}`.toWellFormed(), TICKET_DETAILS_MAX)
|
||||
} catch {
|
||||
return `Funktion: Terminplaner\nAnleitung konnte nicht erzeugt werden, im WOMS öffnen (Kunden → Funktionen).\n${KONTINGENT}`
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user