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
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user