Buchungskalender: Bestellung auf Anfrage, Freischaltung und Ticket
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
827ee54875
commit
9d01d38a45
7 files changed
+799
-15
No files matched your search
@@ -1,7 +1,293 @@
|
||||
// Stub (Task 5): Anfrage-Funktion des Buchungskalenders. Task 11 ersetzt dieses Modul.
|
||||
export const ANFRAGE_OFFEN_TEXT = 'Anfrage noch nicht freigegeben'
|
||||
import { config } from '../../config.js'
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
import { sendAdminNotice, sendCustomerMail } from '../mailer.js'
|
||||
import { assertOwnProject, customerOf, ensureFeatureDoc, getFeatureDoc } from '../siteFeatures.js'
|
||||
import { unterkunftFuerKunde } from './schalter.js'
|
||||
import { ladeEinstellungen, speichereEinstellungen } from './speicher.js'
|
||||
|
||||
/** Bestellpruefung-Haken: offene Punkte der Bestellung (Stub: immer offen). */
|
||||
export async function anfrageOffenePunkte() {
|
||||
return [ANFRAGE_OFFEN_TEXT]
|
||||
/**
|
||||
* Anfrage der Funktion "Buchungskalender" (Muster services/shop/anfrage.js, Spezifikation 2026-10-07-buchungskalender-design.md,
|
||||
* Abschnitt 9). Der Kunde stellt eine Anfrage, das Team entscheidet (annehmen oder ablehnen); erst eine angenommene Anfrage laesst
|
||||
* die Bestellung zu (anfrageOffenePunkte, Haken bestellpruefung). Die Anfrage steht in ukEinstellungen (Dokument-ID = ID des
|
||||
* portalFeatures-Dokuments): anfrage (JSON), anfrageStatus ('' | offen | angenommen | abgelehnt | zurueckgezogen), anfrageAt,
|
||||
* anfrageEntschiedenAt, anfrageEntschiedenVon, anfrageBegruendung. Sperre je Projekt: uk-anfrage:<projectId>.
|
||||
* Fehler an den Kunden tragen status (4xx) und einen deutschen Text (Du-Form), optional err.body.code und err.fehler (je Feld).
|
||||
* Mails: T22 ans Team (Anfrage eingegangen), K34 an den Kunden (Entscheidung); die Vorlagen kommen aus Task 20.
|
||||
*/
|
||||
|
||||
export const ANFRAGE_OFFEN_TEXT = 'Deine Anfrage für den Buchungskalender ist noch nicht freigegeben.'
|
||||
|
||||
export const ARTEN = ['ferienwohnung', 'monteurzimmer', 'pension', 'sonstiges']
|
||||
export const ART_NAMEN = { ferienwohnung: 'Ferienwohnung oder Ferienhaus', monteurzimmer: 'Monteurwohnung', pension: 'Pension oder Gästezimmer', sonstiges: 'Sonstiges' }
|
||||
/** Das Portal sendet und liest 'monteur' (ui-erwartungen.md); gespeichert wird 'monteurzimmer'. */
|
||||
const ART_ALIAS = { monteur: 'monteurzimmer' }
|
||||
export const PORTALE = ['booking', 'airbnb']
|
||||
export const PORTAL_NAMEN = { booking: 'Booking.com', airbnb: 'Airbnb', andere: 'Portale (ohne Angabe)' }
|
||||
|
||||
const EINHEITEN_MAX = 999
|
||||
const NACHRICHT_MAX = 1000
|
||||
const BEGRUENDUNG_MAX = 1000
|
||||
|
||||
const MELDUNG = {
|
||||
projekt: 'Bitte wähle ein Projekt.',
|
||||
nichtVerfuegbar: 'Der Buchungskalender ist noch nicht verfügbar.',
|
||||
vorhanden: 'Für dieses Projekt gibt es schon einen Buchungskalender.',
|
||||
laeuft: 'Für dieses Projekt gibt es schon eine Anfrage für den Buchungskalender.',
|
||||
nichtOffen: 'Die Anfrage kann nur zurückgezogen werden, solange sie offen ist.',
|
||||
nichtEntscheidbar: 'Diese Anfrage ist nicht (mehr) offen und kann nicht entschieden werden.',
|
||||
entscheidung: 'Bitte wähle annehmen oder ablehnen.',
|
||||
begruendung: 'Bitte nenne eine Begründung für die Ablehnung (höchstens 1000 Zeichen).',
|
||||
ungueltig: 'Ungültige Eingabe.',
|
||||
}
|
||||
|
||||
const stop = (status, text, code) => Object.assign(new Error(text), { status, ...(code ? { body: { code } } : {}) })
|
||||
const istObjekt = (wert) => wert !== null && typeof wert === 'object' && !Array.isArray(wert)
|
||||
|
||||
/** Versand (nur fuer Tests austauschbar: die Vorlagen K34 und T22 liegen auf einem anderen Strang). */
|
||||
export const _versand = {
|
||||
admin: (nachricht) => sendAdminNotice(nachricht),
|
||||
kunde: (an, nachricht) => sendCustomerMail(an, nachricht),
|
||||
}
|
||||
|
||||
/** [] wenn die Anfrage des Projekts vom Team angenommen ist, sonst ein Punkt. Lesefehler werden weitergereicht. */
|
||||
export async function anfrageOffenePunkte({ customerId, projectId }) {
|
||||
const doc = await getFeatureDoc(customerId, projectId, 'unterkunft')
|
||||
if (!doc) return [ANFRAGE_OFFEN_TEXT]
|
||||
const einst = await ladeEinstellungen(doc.$id)
|
||||
return einst?.anfrageStatus === 'angenommen' ? [] : [ANFRAGE_OFFEN_TEXT]
|
||||
}
|
||||
|
||||
/**
|
||||
* Eingabe pruefen => { anfrage } (nur bekannte Felder, bereinigt) oder { fehler: { feld: text } }.
|
||||
* art (ARTEN, auch 'monteur'), einheiten (ganze Zahl 1-999), portale (Liste aus booking|airbnb, leer erlaubt; ein boolean wird
|
||||
* angenommen: true = 'andere'), nachricht (optional, bis 1000 Zeichen).
|
||||
*/
|
||||
export function normalisiereAnfrage(eingabe) {
|
||||
const fehler = {}
|
||||
const e = istObjekt(eingabe) ? eingabe : {}
|
||||
|
||||
const artRoh = typeof e.art === 'string' ? (Object.hasOwn(ART_ALIAS, e.art) ? ART_ALIAS[e.art] : e.art) : ''
|
||||
const art = ARTEN.includes(artRoh) ? artRoh : ''
|
||||
if (!art) fehler.art = 'Bitte wähle die Art deiner Unterkunft.'
|
||||
|
||||
const einheiten = e.einheiten
|
||||
if (!Number.isInteger(einheiten) || einheiten < 1 || einheiten > EINHEITEN_MAX) fehler.einheiten = `Bitte gib eine ganze Zahl von 1 bis ${EINHEITEN_MAX} an.`
|
||||
|
||||
let portale = []
|
||||
if (e.portale === undefined || e.portale === null) portale = []
|
||||
else if (typeof e.portale === 'boolean') portale = e.portale ? ['andere'] : []
|
||||
else if (Array.isArray(e.portale) && e.portale.every((p) => PORTALE.includes(p))) portale = PORTALE.filter((p) => e.portale.includes(p))
|
||||
else fehler.portale = MELDUNG.ungueltig
|
||||
|
||||
let nachricht = ''
|
||||
if (e.nachricht !== undefined && e.nachricht !== null && e.nachricht !== '') {
|
||||
if (typeof e.nachricht !== 'string') fehler.nachricht = MELDUNG.ungueltig
|
||||
else {
|
||||
nachricht = e.nachricht.toWellFormed().trim()
|
||||
if (nachricht.length > NACHRICHT_MAX) fehler.nachricht = `Die Nachricht darf höchstens ${NACHRICHT_MAX} Zeichen lang sein.`
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(fehler).length) return { fehler }
|
||||
return { anfrage: { art, einheiten, portale, nachricht } }
|
||||
}
|
||||
|
||||
/** Gespeicherte Anfrage (aus ukEinstellungen.anfrage, schon geparst) bereinigt oder null. */
|
||||
function leseAnfrage(einst) {
|
||||
const roh = einst?.anfrage
|
||||
if (!istObjekt(roh)) return null
|
||||
return {
|
||||
art: ARTEN.includes(roh.art) ? roh.art : '',
|
||||
einheiten: Number.isInteger(roh.einheiten) ? roh.einheiten : null,
|
||||
portale: Array.isArray(roh.portale) ? roh.portale.filter((p) => typeof p === 'string') : [],
|
||||
nachricht: typeof roh.nachricht === 'string' ? roh.nachricht : '',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anfrage fuer das Portal des Kunden: { status, anfrage: { art ('monteur' statt 'monteurzimmer'), einheiten, portale (Liste),
|
||||
* nachricht }, at, entschiedenAt, begruendung } oder null, solange es keine gab.
|
||||
*/
|
||||
export function anfrageAnsicht(einst) {
|
||||
const status = einst?.anfrageStatus || ''
|
||||
if (!status) return null
|
||||
const a = leseAnfrage(einst)
|
||||
return {
|
||||
status,
|
||||
anfrage: a ? { ...a, art: a.art === 'monteurzimmer' ? 'monteur' : a.art } : null,
|
||||
at: einst.anfrageAt || null,
|
||||
entschiedenAt: einst.anfrageEntschiedenAt || null,
|
||||
begruendung: einst.anfrageBegruendung || '',
|
||||
}
|
||||
}
|
||||
|
||||
/** Anfrage fuer das Team (WOMS): flach, art wie gespeichert, portale als boolean; ohne Anfrage status ''. */
|
||||
export function anfrageAnsichtTeam(einst) {
|
||||
const a = leseAnfrage(einst)
|
||||
return {
|
||||
status: einst?.anfrageStatus || '',
|
||||
at: einst?.anfrageAt || null,
|
||||
art: a?.art || '',
|
||||
einheiten: a?.einheiten ?? null,
|
||||
portale: (a?.portale.length ?? 0) > 0,
|
||||
nachricht: a?.nachricht || '',
|
||||
entschiedenAt: einst?.anfrageEntschiedenAt || null,
|
||||
entschiedenVon: einst?.anfrageEntschiedenVon || '',
|
||||
begruendung: einst?.anfrageBegruendung || '',
|
||||
}
|
||||
}
|
||||
|
||||
/** Projekt des Kunden (Appwrite-Fehler 400 bei unbrauchbarer ID zaehlt als fremdes Projekt) und Schalter des Kunden. */
|
||||
async function pruefeZugang({ customerId, projectId, person, neu }) {
|
||||
if (typeof projectId !== 'string' || !projectId) throw stop(400, MELDUNG.projekt)
|
||||
let project
|
||||
try {
|
||||
project = await assertOwnProject(customerId, projectId, { forNewOrder: neu })
|
||||
} catch (err) {
|
||||
throw err?.status === 400 && err.type ? stop(403, 'Kein Zugriff auf dieses Projekt') : err
|
||||
}
|
||||
if (!unterkunftFuerKunde(person?.email)) throw stop(409, MELDUNG.nichtVerfuegbar, 'nicht_verfuegbar')
|
||||
return project
|
||||
}
|
||||
|
||||
/**
|
||||
* Anfrage stellen. Reihenfolge: Projekt (403/409 archiviert), Schalter (409), Eingabe (400 mit `fehler`), dann unter Sperre je
|
||||
* Projekt: Funktion muss `off` sein (409), Status '' | abgelehnt | zurueckgezogen (409 sonst). Speichert, loescht eine fruehere
|
||||
* Entscheidung und schickt T22 ans Team (ein Fehler dabei wird nur geloggt).
|
||||
* @returns {Promise<object>} anfrageAnsicht des neuen Standes
|
||||
*/
|
||||
export async function stelleAnfrage({ customerId, projectId, eingabe, person = {}, now = new Date() }) {
|
||||
const project = await pruefeZugang({ customerId, projectId, person, neu: true })
|
||||
const { anfrage, fehler } = normalisiereAnfrage(eingabe)
|
||||
if (fehler) throw Object.assign(stop(400, Object.values(fehler)[0]), { fehler })
|
||||
|
||||
const gespeichert = await withLocks([`uk-anfrage:${projectId}`], async () => {
|
||||
const feature = await ensureFeatureDoc(customerId, projectId, 'unterkunft')
|
||||
if (feature.state !== 'off') throw stop(409, MELDUNG.vorhanden, 'vorhanden')
|
||||
return withLocks([`uk-einst:${feature.$id}`], async () => {
|
||||
const einst = await ladeEinstellungen(feature.$id)
|
||||
if (!['', 'abgelehnt', 'zurueckgezogen'].includes(einst?.anfrageStatus || '')) throw stop(409, MELDUNG.laeuft, 'anfrage_laeuft')
|
||||
return speichereEinstellungen(feature.$id, {
|
||||
anfrage,
|
||||
anfrageStatus: 'offen',
|
||||
anfrageAt: now.toISOString(),
|
||||
anfrageEntschiedenAt: null,
|
||||
anfrageEntschiedenVon: '',
|
||||
anfrageBegruendung: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
await meldeTeam({ customerId, projectId, project, anfrage, person })
|
||||
return anfrageAnsicht(gespeichert)
|
||||
}
|
||||
|
||||
/** T22 "Neue Anfrage Buchungskalender" ans Team; sendAdminNotice wirft nie, ein Fehlschlag steht nur im Log. */
|
||||
async function meldeTeam({ customerId, projectId, project, anfrage, person }) {
|
||||
try {
|
||||
let kundeName = String(person?.name || '').trim()
|
||||
if (!kundeName) kundeName = String((await customerOf(customerId))?.name || '').trim()
|
||||
const result = await _versand.admin({
|
||||
template: 'T22',
|
||||
values: {
|
||||
kunde_name: kundeName || 'Kunde',
|
||||
email: person?.email || '',
|
||||
projekt: project?.projectName || projectId,
|
||||
art: ART_NAMEN[anfrage.art],
|
||||
einheiten: String(anfrage.einheiten),
|
||||
portale: anfrage.portale.length ? anfrage.portale.map((p) => PORTAL_NAMEN[p]).join(', ') : 'keine',
|
||||
nachricht: anfrage.nachricht || '–',
|
||||
woms_url: `${config.womsBaseUrl}/customers/${encodeURIComponent(customerId)}?tab=funktionen`,
|
||||
},
|
||||
})
|
||||
if (!result?.sent) console.warn(`[unterkunft] Team-Mail T22 nicht gesendet: ${result?.reason || 'unbekannt'}`)
|
||||
} catch (err) {
|
||||
console.warn(`[unterkunft] Team-Mail T22 nicht gesendet: ${err?.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Offene Anfrage zurueckziehen (Status `zurueckgezogen`); aus jedem anderen Zustand 409. @returns {Promise<object>} anfrageAnsicht */
|
||||
export async function zieheZurueck({ customerId, projectId, person = {} }) {
|
||||
await pruefeZugang({ customerId, projectId, person, neu: false })
|
||||
const gespeichert = await withLocks([`uk-anfrage:${projectId}`], async () => {
|
||||
const feature = await getFeatureDoc(customerId, projectId, 'unterkunft')
|
||||
if (!feature) throw stop(409, MELDUNG.nichtOffen, 'nicht_offen')
|
||||
return withLocks([`uk-einst:${feature.$id}`], async () => {
|
||||
const einst = await ladeEinstellungen(feature.$id)
|
||||
if (!einst || einst.anfrageStatus !== 'offen') throw stop(409, MELDUNG.nichtOffen, 'nicht_offen')
|
||||
return speichereEinstellungen(feature.$id, { anfrageStatus: 'zurueckgezogen' })
|
||||
})
|
||||
})
|
||||
return anfrageAnsicht(gespeichert)
|
||||
}
|
||||
|
||||
/** 'Name <email>' (wie createdBy der Angebote); ohne Name die Adresse, ohne beides 'Admin'. */
|
||||
const personLabel = (person) => (person?.name ? `${person.name}${person.email ? ` <${person.email}>` : ''}` : person?.email || 'Admin')
|
||||
|
||||
/**
|
||||
* Das Team entscheidet eine offene Anfrage: annehmen oder ablehnen (Begruendung Pflicht, 1-1000 Zeichen). Unter der Sperre je
|
||||
* Projekt; das Projekt muss dem Kunden gehoeren (403), der Status muss `offen` sein (409). Danach geht K34 an den Kunden; scheitert
|
||||
* die Mail, bleibt die Entscheidung bestehen, wird geloggt und die Antwort traegt `mailFehler: true`.
|
||||
* @returns {Promise<{ anfrage: object, mailFehler: boolean }>} anfrageAnsicht des neuen Standes
|
||||
*/
|
||||
export async function entscheideAnfrage({ customerId, projectId, entscheidung, begruendung, person = {}, now = new Date() }) {
|
||||
if (entscheidung !== 'annehmen' && entscheidung !== 'ablehnen') throw stop(400, MELDUNG.entscheidung)
|
||||
let grund = ''
|
||||
if (entscheidung === 'ablehnen') {
|
||||
grund = typeof begruendung === 'string' ? begruendung.toWellFormed().trim() : ''
|
||||
if (!grund || grund.length > BEGRUENDUNG_MAX) throw stop(400, MELDUNG.begruendung)
|
||||
}
|
||||
if (typeof projectId !== 'string' || !projectId) throw stop(400, MELDUNG.projekt)
|
||||
|
||||
const { gespeichert, project } = await withLocks([`uk-anfrage:${projectId}`], async () => {
|
||||
let besitz
|
||||
try {
|
||||
besitz = await assertOwnProject(customerId, projectId)
|
||||
} catch (err) {
|
||||
throw err?.status === 400 && err.type ? stop(403, 'Kein Zugriff auf dieses Projekt') : err
|
||||
}
|
||||
const feature = await getFeatureDoc(customerId, projectId, 'unterkunft')
|
||||
if (!feature) throw stop(409, MELDUNG.nichtEntscheidbar, 'nicht_offen')
|
||||
const neu = await withLocks([`uk-einst:${feature.$id}`], async () => {
|
||||
const einst = await ladeEinstellungen(feature.$id)
|
||||
if (!einst || einst.anfrageStatus !== 'offen') throw stop(409, MELDUNG.nichtEntscheidbar, 'nicht_offen')
|
||||
return speichereEinstellungen(feature.$id, {
|
||||
anfrageStatus: entscheidung === 'annehmen' ? 'angenommen' : 'abgelehnt',
|
||||
anfrageEntschiedenAt: now.toISOString(),
|
||||
anfrageEntschiedenVon: personLabel(person).slice(0, 120),
|
||||
anfrageBegruendung: grund,
|
||||
})
|
||||
})
|
||||
return { gespeichert: neu, project: besitz }
|
||||
})
|
||||
|
||||
const mailFehler = !(await sendeEntscheidung({ customerId, projectId, project, angenommen: entscheidung === 'annehmen', begruendung: grund }))
|
||||
return { anfrage: anfrageAnsicht(gespeichert), mailFehler }
|
||||
}
|
||||
|
||||
/** K34 an den Kunden; true = gesendet. Jeder Fehler wird nur geloggt (die Entscheidung ist gespeichert). */
|
||||
async function sendeEntscheidung({ customerId, projectId, project, angenommen, begruendung }) {
|
||||
try {
|
||||
const kunde = await customerOf(customerId)
|
||||
if (!kunde?.email) {
|
||||
console.warn(`[unterkunft] K34 nicht gesendet: Kunde ${customerId} ohne E-Mail-Adresse`)
|
||||
return false
|
||||
}
|
||||
await _versand.kunde(kunde.email, {
|
||||
template: 'K34',
|
||||
values: {
|
||||
kunde_name: String(kunde.name || '').trim() || 'Kunde',
|
||||
projekt: project?.projectName || projectId,
|
||||
preis: config.features.unterkunft.priceLabel,
|
||||
ust_hinweis: config.ustHinweis,
|
||||
begruendung,
|
||||
portal_url: `${config.publicBaseUrl}/dashboard.html#buchungen?projekt=${encodeURIComponent(projectId)}`,
|
||||
},
|
||||
blocks: { angenommen, abgelehnt: !angenommen },
|
||||
})
|
||||
return true
|
||||
} catch (err) {
|
||||
console.warn(`[unterkunft] K34 nicht gesendet: ${err?.message}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,33 @@
|
||||
// Stub (Task 5): Freischaltpruefung des Buchungskalenders. Task 11 ersetzt dieses Modul.
|
||||
import { preiseVollstaendig } from './preis.js'
|
||||
import { ladeEinstellungen, ladePreise, listeUnterkuenfte } from './speicher.js'
|
||||
|
||||
/** freischaltbar-Haken (Stub, fail-closed: nie freigeben). */
|
||||
export async function freischaltbar() {
|
||||
return false
|
||||
/**
|
||||
* freischaltbar-Haken des Buchungskalenders (featureRegistry, Zustand 'setup', Ticket geschlossen): darf die Funktion jetzt aktiv
|
||||
* werden? Ja, wenn der Kunde alles hinterlegt hat, was Rechnungen und Buchungen brauchen:
|
||||
* - Einstellungen: firma, strasse, plz, ort, email und (steuernummer oder ustId), dazu rechnungAuftragAt (Zustimmung zur
|
||||
* Rechnungsstellung im Namen des Vermieters);
|
||||
* - mindestens eine aktive Unterkunft mit vollstaendigen Preisen (preiseVollstaendig) und Stornobedingungen.
|
||||
* Fail-closed: bei jedem Lesefehler false.
|
||||
* @param {{ $id: string }} doc portalFeatures-Dokument
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function freischaltbar(doc) {
|
||||
try {
|
||||
const featureId = doc?.$id
|
||||
if (typeof featureId !== 'string' || !featureId) return false
|
||||
const einst = await ladeEinstellungen(featureId)
|
||||
if (!einst) return false
|
||||
const text = (wert) => typeof wert === 'string' && wert.trim() !== ''
|
||||
if (!['firma', 'strasse', 'plz', 'ort', 'email'].every((k) => text(einst[k]))) return false
|
||||
if (!text(einst.steuernummer) && !text(einst.ustId)) return false
|
||||
if (!einst.rechnungAuftragAt) return false
|
||||
const unterkuenfte = await listeUnterkuenfte(featureId, { nurAktive: true })
|
||||
for (const u of unterkuenfte) {
|
||||
const preise = await ladePreise(u.$id)
|
||||
if (preiseVollstaendig(preise) && text(preise.stornobedingungen)) return true
|
||||
}
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { TICKET_DETAILS_MAX, anleitungsLink, kuerze, minimaleTicketDetails } from '../featureRegistry.js'
|
||||
import { ART_NAMEN, PORTAL_NAMEN } from './anfrageFunktion.js'
|
||||
import { ladeEinstellungen } from './speicher.js'
|
||||
|
||||
/**
|
||||
* Details des Einrichtungs-Tickets fuer den Buchungskalender (config.features.unterkunft.ticket.details, Spezifikation Abschnitt 9).
|
||||
* ctx = { feature, doc, project, customer } wie ihn markPaid uebergibt (doc = portalFeatures-Dokument). Deterministischer Text:
|
||||
* Kopfzeilen, Kurzfassung der Anfrage (best effort aus ukEinstellungen; Lesefehler => ohne), Snippet und Schritte. Wirft nie.
|
||||
*/
|
||||
|
||||
/** Kurzfassung der Anfrage als Zeile, oder ''. */
|
||||
async function anfrageZeile(doc) {
|
||||
try {
|
||||
const a = (await ladeEinstellungen(doc.$id))?.anfrage
|
||||
if (a === null || typeof a !== 'object' || Array.isArray(a)) return ''
|
||||
const teile = []
|
||||
if (Object.hasOwn(ART_NAMEN, a.art)) teile.push(`Art: ${ART_NAMEN[a.art]}`)
|
||||
if (Number.isInteger(a.einheiten)) teile.push(`Einheiten: ${a.einheiten}`)
|
||||
const portale = (Array.isArray(a.portale) ? a.portale : []).filter((p) => Object.hasOwn(PORTAL_NAMEN, p)).map((p) => PORTAL_NAMEN[p])
|
||||
teile.push(`Portale: ${portale.length ? portale.join(', ') : 'keine'}`)
|
||||
return `Anfrage: ${teile.join(', ')}`
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export async function unterkunftTicketDetails(ctx) {
|
||||
try {
|
||||
const { doc } = ctx
|
||||
const siteKey = doc?.siteKey || ''
|
||||
const kopf = minimaleTicketDetails({ ...ctx, feature: { key: 'Buchungskalender' } }).split('\n').filter((zeile) => !zeile.startsWith('Verweis: '))
|
||||
const anfrage = doc?.$id ? await anfrageZeile(doc) : ''
|
||||
const zeilen = [
|
||||
...kopf,
|
||||
...(anfrage ? [anfrage] : []),
|
||||
'',
|
||||
'Snippet (an der Stelle des Kalender-Bereichs einsetzen):',
|
||||
`<div data-webklar-unterkunft="${siteKey}" data-api="https://project.webklar.com"></div>`,
|
||||
'<script src="https://project.webklar.com/embed/unterkunft.js" defer></script>',
|
||||
'',
|
||||
'Schritte:',
|
||||
'1. Seite oder Bereich für den Kalender anlegen und Menüpunkt setzen, Snippet einsetzen.',
|
||||
'2. CSS-Variablen (--wk-uk-akzent, --wk-uk-text, --wk-uk-flaeche, --wk-uk-linie, --wk-uk-radius, --wk-uk-schrift) an die Seite anpassen.',
|
||||
'3. Datenschutz-Absatz Buchungskalender in die Datenschutzerklärung einfügen.',
|
||||
'4. Impressum prüfen.',
|
||||
'5. Testanfrage über den Kalender stellen und im Portal (Buchungen) prüfen.',
|
||||
'6. Ticket schließen: das schaltet die Funktion frei (sobald der Kunde Unterkunft, Preise, Stornobedingungen und Firmendaten hinterlegt hat).',
|
||||
'',
|
||||
'Zeit im Arbeitsblatt erfassen, nicht auf Abo-Stunden anrechnen.',
|
||||
'Einbauanleitung: docs/funktionen/unterkunft-einbau.md',
|
||||
`Verweis: ${anleitungsLink(doc?.customerId, doc?.projectId)}`,
|
||||
]
|
||||
return kuerze(zeilen.join('\n').toWellFormed(), TICKET_DETAILS_MAX, { anZeile: true })
|
||||
} catch (err) {
|
||||
console.error(`[unterkunft-ticket] Details nicht erzeugbar, Notfall-Details: ${err?.message}`)
|
||||
try {
|
||||
return kuerze(minimaleTicketDetails({ ...ctx, feature: { key: 'Buchungskalender' } }).toWellFormed(), TICKET_DETAILS_MAX)
|
||||
} catch {
|
||||
return 'Funktion: Buchungskalender\nDetails konnten nicht erzeugt werden, im WOMS öffnen (Kunden → Funktionen).'
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user