diff --git a/scripts/setup-legal.mjs b/scripts/setup-legal.mjs
index 79e1e31..5e8cded 100644
--- a/scripts/setup-legal.mjs
+++ b/scripts/setup-legal.mjs
@@ -104,6 +104,9 @@ export const DOCUMENTS = [
{ key: "avv-webshop", title: "AVV Webshop", liveUrl: "https://project.webklar.com/rechtliches/avv-webshop.html" },
// Oeffnungszeiten live: Schluessel hoechstens 36 Zeichen (WOMS-Grenze fuer Dokument-IDs); die Fassungen (PDF/HTML) laedt der Inhaber hoch
{ key: "leistungsbeschreibung-zeiten", title: "Leistungsbeschreibung Öffnungszeiten live", liveUrl: "https://project.webklar.com/vertragsunterlagen/zeiten-leistungsbeschreibung.html" },
+ // Buchungskalender (Funktion unterkunft): Alias-Seiten ohne Fassungsdatum wie beim Webshop; die Fassungen laedt der Inhaber hoch
+ { key: "leistungsbeschreibung-unterkunft", title: "Leistungsbeschreibung Buchungskalender", liveUrl: "https://project.webklar.com/rechtliches/leistungsbeschreibung-unterkunft.html" },
+ { key: "avv-unterkunft", title: "AVV Buchungskalender", liveUrl: "https://project.webklar.com/rechtliches/avv-unterkunft.html" },
].map((d, i) => ({ ...d, sort: i + 1 }));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
diff --git a/src/components/customer/CustomerFunktionen.jsx b/src/components/customer/CustomerFunktionen.jsx
index b5c89a9..4c1dfb8 100644
--- a/src/components/customer/CustomerFunktionen.jsx
+++ b/src/components/customer/CustomerFunktionen.jsx
@@ -2,7 +2,7 @@ import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { Link } from 'react-router-dom'
import { Card, Badge, Button, EmptyState } from '../ui'
import { copyText } from '../CopyableCredential'
-import { getFunktionen, setFeature, getTermineAnleitung, getShop, entscheideShopAnfrage } from '../../lib/teamApi'
+import { getFunktionen, setFeature, getTermineAnleitung, getShop, entscheideShopAnfrage, getUnterkunft, entscheideUnterkunftAnfrage } from '../../lib/teamApi'
import {
FUNKTIONEN, ZUSTAND, AKTION_LABEL, funktionOf, aktionen, mergeFunktion, siteKeyOf, abgeschaltetText,
hindernisText, panelModell, anleitungOffen, textformNoetig, textformAnsicht, textformBereit, speicherStatus,
@@ -160,21 +160,18 @@ function TextformPanel({ funktion, feld, value, onChange, projectId, today, disa
)
}
-// Webshop je Projekt: Anfrage des Kunden (annehmen / ablehnen mit Begruendung), Stripe-Stand und Kennzahlen. Die Entscheidung geht ans Portal
-// (POST /shop/:customerId/anfrage); danach laden Funktionen und Shop neu (onDecided). Texte sind ausschliesslich React-Textknoten.
-function ShopPanel({ customerId, shop, onDecided }) {
+// Entscheidung ueber eine Anfrage (Webshop und Buchungskalender): Annehmen / Ablehnen mit Begruendung. Die Entscheidung geht ueber entscheide
+// ans Portal; danach laden Funktionen und Bereich neu (onDecided). Texte sind ausschliesslich React-Textknoten.
+function EntscheidungBlock({ projectId, anfrage, entscheide, onDecided }) {
const [frage, setFrage] = useState(null) // null | 'annehmen' | 'ablehnen'
const [begruendung, setBegruendung] = useState('')
const [busy, setBusy] = useState(false)
const [status, setStatus] = useState(null) // { ok, text }
- const a = shop.anfrage
- const [tone, label] = anfrageStatus(a?.status)
- const stripe = shop.stripe || { accountId: false, status: null }
- const entscheiden = async () => {
+ const run = async () => {
setBusy(true)
setStatus(null)
try {
- const antwort = await entscheideShopAnfrage(customerId, { projectId: shop.projectId, entscheidung: frage, begruendung })
+ const antwort = await entscheide({ projectId, entscheidung: frage, begruendung })
setFrage(null)
setBegruendung('')
setStatus(entscheidungStatus(antwort))
@@ -189,32 +186,15 @@ function ShopPanel({ customerId, shop, onDecided }) {
}
const ablehnenFehlt = frage === 'ablehnen' && !begruendungOk(begruendung)
return (
-
-
- Webshop: {shop.projectName}
- {a && Anfrage: {label}}
- {shop.geoeffnet ? 'Geöffnet' : 'Geschlossen'}
- Stripe: {stripeText(stripe.status, !!stripe.accountId)}
-
- {a && (
-
- {anfrageZeilen(a.anfrage).map((z) => (
- - {z.label}
- {z.text}
- ))}
- - Eingegangen
- {a.at ? fmtDateTime(a.at) : '–'}
- {a.entschiedenAt && <>- Entschieden
- {fmtDateTime(a.entschiedenAt)}
>}
- {a.begruendung && <>- Begründung
- {a.begruendung}
>}
-
- )}
-
Artikel: {shop.zahlen?.artikel ?? 0} · Offene Bestellungen: {shop.zahlen?.offeneBestellungen ?? 0}
- {a?.status === 'offen' && (
+ <>
+ {anfrage?.status === 'offen' && (
frage ? (
{frage === 'annehmen' ? 'Anfrage annehmen? Der Kunde bekommt eine Mail.' : 'Anfrage ablehnen? Der Kunde bekommt die Begründung per Mail.'}
{frage === 'ablehnen' && (
) : (
@@ -225,6 +205,61 @@ function ShopPanel({ customerId, shop, onDecided }) {
)
)}
{status &&
{status.text}
}
+ >
+ )
+}
+
+// Zeilen der Anfrage samt Eingang, Entscheidung und Begruendung
+function AnfrageListe({ a, art }) {
+ return (
+
+ {anfrageZeilen(a.anfrage, art).map((z) => (
+ - {z.label}
- {z.text}
+ ))}
+ - Eingegangen
- {a.at ? fmtDateTime(a.at) : '–'}
+ {a.entschiedenAt && <>- Entschieden
- {fmtDateTime(a.entschiedenAt)}
>}
+ {a.begruendung && <>- Begründung
- {a.begruendung}
>}
+
+ )
+}
+
+// Webshop je Projekt: Anfrage des Kunden (annehmen / ablehnen mit Begruendung), Stripe-Stand und Kennzahlen. Die Entscheidung geht ans Portal
+// (POST /shop/:customerId/anfrage).
+function ShopPanel({ customerId, shop, onDecided }) {
+ const a = shop.anfrage
+ const [tone, label] = anfrageStatus(a?.status)
+ const stripe = shop.stripe || { accountId: false, status: null }
+ return (
+
+
+ Webshop: {shop.projectName}
+ {a && Anfrage: {label}}
+ {shop.geoeffnet ? 'Geöffnet' : 'Geschlossen'}
+ Stripe: {stripeText(stripe.status, !!stripe.accountId)}
+
+ {a &&
}
+
Artikel: {shop.zahlen?.artikel ?? 0} · Offene Bestellungen: {shop.zahlen?.offeneBestellungen ?? 0}
+
entscheideShopAnfrage(customerId, body)} onDecided={onDecided} />
+
+ )
+}
+
+// Buchungskalender je Projekt: Anfrage (annehmen / ablehnen mit Begruendung), Stripe-Stand, Zahl der Unterkuenfte und offene Buchungsanfragen
+// (Portal: GET /unterkunft/:customerId, POST /unterkunft/:customerId/anfrage).
+function UnterkunftPanel({ customerId, eintrag, onDecided }) {
+ const a = eintrag.anfrage
+ const [tone, label] = anfrageStatus(a?.status)
+ const stripe = eintrag.stripe || { accountId: false, status: null }
+ return (
+
+
+ Buchungskalender: {eintrag.name}
+ {a && Anfrage: {label}}
+ Stripe: {stripeText(stripe.status, !!stripe.accountId)}
+
+ {a &&
}
+
Unterkünfte: {eintrag.unterkuenfte ?? 0} · Offene Buchungsanfragen: {eintrag.offeneAnfragen ?? 0}
+
entscheideUnterkunftAnfrage(customerId, body)} onDecided={onDecided} />
)
}
@@ -243,6 +278,8 @@ export default function CustomerFunktionen({ customerId }) {
const [panel, setPanel] = useState({}) // projectId -> true/false (Knopf "Anleitung"); ohne Eintrag entscheidet die Adresse (?anleitung=
)
const [shop, setShop] = useState([]) // GET /shop/:customerId => projects; Fehler nur im Webshop-Bereich, nicht in der Tabelle
const [shopFehler, setShopFehler] = useState(null)
+ const [unterkunft, setUnterkunft] = useState([]) // GET /unterkunft/:customerId => projects
+ const [unterkunftFehler, setUnterkunftFehler] = useState(null)
const [panelReload, setPanelReload] = useState({}) // projectId -> Zaehler, nach jeder Aktion hochgezaehlt
const load = useCallback(() => {
@@ -257,8 +294,14 @@ export default function CustomerFunktionen({ customerId }) {
.then(({ projects: list }) => setShop(Array.isArray(list) ? list : []))
.catch((err) => setShopFehler(err.message || 'Webshop-Daten konnten nicht geladen werden.'))
}, [customerId])
- useEffect(() => { load(); loadShop() }, [load, loadShop])
- const nachEntscheidung = () => Promise.all([getFunktionen(customerId).then(({ projects: list, termine, shop: shopFeld }) => { setProjects(list || []); setFelder({ termine: termine || null, shop: shopFeld || null }) }), loadShop()])
+ const loadUnterkunft = useCallback(() => {
+ setUnterkunftFehler(null)
+ return getUnterkunft(customerId)
+ .then(({ projects: list }) => setUnterkunft(Array.isArray(list) ? list : []))
+ .catch((err) => setUnterkunftFehler(err.message || 'Daten des Buchungskalenders konnten nicht geladen werden.'))
+ }, [customerId])
+ useEffect(() => { load(); loadShop(); loadUnterkunft() }, [load, loadShop, loadUnterkunft])
+ const nachEntscheidung = () => Promise.all([getFunktionen(customerId).then(({ projects: list, termine, shop: shopFeld }) => { setProjects(list || []); setFelder({ termine: termine || null, shop: shopFeld || null }) }), loadShop(), loadUnterkunft()])
const save = async () => {
const { projectId, key, action } = confirm
@@ -285,6 +328,7 @@ export default function CustomerFunktionen({ customerId }) {
setBusy(false)
// Version und Hindernis der Anleitung haengen vom Zustand ab: Panel dieser Zeile neu laden
if (key === 'shop') loadShop()
+ if (key === 'unterkunft') loadUnterkunft()
if (key === 'termine') setPanelReload((r) => ({ ...r, [projectId]: (r[projectId] || 0) + 1 }))
}
}
@@ -300,6 +344,7 @@ export default function CustomerFunktionen({ customerId }) {
if (!projects.length) return
const shopEintraege = shop.filter(shopHatEintrag)
+ const unterkunftEintraege = unterkunft.filter(shopHatEintrag)
return (
@@ -411,6 +456,10 @@ export default function CustomerFunktionen({ customerId }) {
{shopEintraege.map((s) => (
))}
+ {unterkunftFehler && {unterkunftFehler}
}
+ {unterkunftEintraege.map((u) => (
+
+ ))}
)
}
diff --git a/src/lib/funktionenView.js b/src/lib/funktionenView.js
index 2a6c58f..18e29d5 100644
--- a/src/lib/funktionenView.js
+++ b/src/lib/funktionenView.js
@@ -44,6 +44,16 @@ export const FUNKTIONEN = [
ticket: 'Fehlendes Einbau-Ticket neu anlegen?',
},
},
+ {
+ key: 'unterkunft',
+ label: 'Buchungskalender',
+ bestaetigen: {
+ activate: 'Buchungskalender ohne Zahlung aktivieren?',
+ disable: 'Buchungskalender abschalten? Auf der Website sind dann keine Buchungsanfragen mehr möglich.',
+ abnahme: 'Buchungskalender freischalten? Der Kunde bekommt die Mail „bereit“ und Gäste können anfragen.',
+ ticket: 'Fehlendes Einrichtungs-Ticket neu anlegen?',
+ },
+ },
]
/** Zustand einer Funktion -> [Ton des Abzeichens, Beschriftung]. */
@@ -83,7 +93,7 @@ export const funktionOf = (project, key) => project?.features?.[key] || { state:
*/
export function siteKeyOf(feature, key) {
const wert = feature?.siteKey
- return (key === 'termine' || key === 'shop' || key === 'zeiten') && typeof wert === 'string' ? wert.trim() : ''
+ return (key === 'termine' || key === 'shop' || key === 'zeiten' || key === 'unterkunft') && typeof wert === 'string' ? wert.trim() : ''
}
/** Beschriftung der Aktionsknoepfe (Aktion = action im POST /funktionen/:customerId/:key). */
@@ -104,7 +114,7 @@ export const AKTION_LABEL = {
* (hindernisText); bei Oeffnungszeiten live entfaellt sie, solange der Kunde noch keine Zeiten eingetragen hat ('zeiten_fehlen').
*/
export function aktionen(state, key, { ticketMissing = false, hindernis = null } = {}) {
- if (key === 'termine' || key === 'shop' || key === 'zeiten') {
+ if (key === 'termine' || key === 'shop' || key === 'zeiten' || key === 'unterkunft') {
if (state === 'ordered' || state === 'active') return ['disable']
if (state === 'setup') {
const abnahme = key === 'zeiten' && hindernis ? [] : ['abnahme']
@@ -313,9 +323,25 @@ export const anfrageStatus = (status) => (has(ANFRAGE_STATUS, status) ? ANFRAGE_
const jaNein = (wert) => (wert === true ? 'Ja' : wert === false ? 'Nein' : '–')
const textOder = (wert) => (typeof wert === 'string' && wert.trim() ? wert.trim() : '–')
-/** Zeilen der Anfrage fuer die Anzeige: [{ label, text }]. Ohne Anfrage []. */
-export function anfrageZeilen(a) {
+const UNTERKUNFT_ART_LABEL = { ferienwohnung: 'Ferienwohnung', monteurzimmer: 'Monteurzimmer', pension: 'Pension', sonstiges: 'Sonstiges' }
+/** Art der Unterkunft -> Beschriftung; unbekannt oder leer '–'. */
+export const unterkunftArtLabel = (code) => (has(UNTERKUNFT_ART_LABEL, code) ? UNTERKUNFT_ART_LABEL[code] : code ? String(code) : '–')
+const einheitenText = (n) => (Number.isFinite(n) && n > 0 ? String(n) : '–')
+const portaleText = (wert) => (wert === true ? 'nutzt Booking.com/Airbnb' : wert === false ? 'nutzt keine Portale' : '–')
+
+/**
+ * Zeilen der Anfrage fuer die Anzeige: [{ label, text }]. Ohne Anfrage []. art = 'shop' (Standard) oder 'unterkunft' (Buchungskalender).
+ */
+export function anfrageZeilen(a, art = 'shop') {
if (!a || typeof a !== 'object') return []
+ if (art === 'unterkunft') {
+ return [
+ { label: 'Art', text: unterkunftArtLabel(a.art) },
+ { label: 'Einheiten', text: einheitenText(a.einheiten) },
+ { label: 'Portale', text: portaleText(a.portale) },
+ { label: 'Nachricht', text: textOder(a.nachricht) },
+ ]
+ }
const wege = [a.versand === true ? 'Versand' : '', a.abholung === true ? 'Abholung' : ''].filter(Boolean)
return [
{ label: 'Warenart', text: warenartLabel(a.warenart) },
@@ -328,9 +354,13 @@ export function anfrageZeilen(a) {
]
}
-/** Kurzfassung der Anfrage in einer Zeile ("Waren, bis 20 Artikel, Versand und Abholung"); ohne Anfrage ''. */
-export function anfrageText(a) {
+/** Kurzfassung der Anfrage in einer Zeile ("Waren, bis 20 Artikel, Versand und Abholung"); ohne Anfrage ''. art wie bei anfrageZeilen. */
+export function anfrageText(a, art = 'shop') {
if (!a || typeof a !== 'object') return ''
+ if (art === 'unterkunft') {
+ const n = Number.isFinite(a.einheiten) && a.einheiten > 0 ? `${a.einheiten} ${a.einheiten === 1 ? 'Einheit' : 'Einheiten'}` : ''
+ return [unterkunftArtLabel(a.art), n, a.portale === true ? 'nutzt Booking.com/Airbnb' : ''].filter((t) => t && t !== '–').join(', ')
+ }
const wege = [a.versand === true ? 'Versand' : '', a.abholung === true ? 'Abholung' : ''].filter(Boolean).join(' und ')
const zahl = a.artikelzahl ? `${artikelzahlLabel(a.artikelzahl)} Artikel` : ''
return [warenartLabel(a.warenart), zahl, wege].filter((t) => t && t !== '–').join(', ')
@@ -359,14 +389,14 @@ export const stripeTon = (status, hatKonto = false) => {
return status || hatKonto ? 'warn' : 'muted'
}
-/** Gibt es zu diesem Projekt etwas anzuzeigen (Anfrage vorhanden oder Webshop nicht "off")? */
+/** Gibt es zu diesem Projekt etwas anzuzeigen (Anfrage vorhanden oder Funktion nicht "off")? Gilt fuer Webshop und Buchungskalender. */
export const shopHatEintrag = (shop) => !!shop && (shop.anfrage != null || (shop.state && shop.state !== 'off'))
/** Begruendung der Ablehnung: Pflicht, hoechstens 1000 Zeichen (ohne Rand). */
export const BEGRUENDUNG_MAX = 1000
export const begruendungOk = (text) => typeof text === 'string' && text.trim().length > 0 && text.trim().length <= BEGRUENDUNG_MAX
-/** Rumpf fuer POST /shop/:customerId/anfrage. Begruendung nur beim Ablehnen. */
+/** Rumpf fuer POST /shop/:customerId/anfrage und /unterkunft/:customerId/anfrage. Begruendung nur beim Ablehnen. */
export function entscheidungBody({ projectId, entscheidung, begruendung }) {
const body = { projectId, entscheidung }
if (entscheidung === 'ablehnen') body.begruendung = String(begruendung ?? '').trim()
diff --git a/src/lib/teamApi.js b/src/lib/teamApi.js
index d0c55c1..5d02ee5 100644
--- a/src/lib/teamApi.js
+++ b/src/lib/teamApi.js
@@ -95,7 +95,7 @@ export const markStorageDone = (id) => request(`/storage-tasks/${enc(id)}/done`,
export const getFunktionen = (customerId) => request(`/funktionen/${enc(customerId)}`)
/**
- * Aktivieren (ohne Zahlung) oder Abschalten einer Funktion (key: news | termine | shop); Antwort { projects: [das eine Projekt] }.
+ * Aktivieren (ohne Zahlung) oder Abschalten einer Funktion (key: news | termine | shop | zeiten | unterkunft); Antwort { projects: [das eine Projekt] }.
* Terminplaner und Webshop im Modus on: beim Aktivieren zusaetzlich textformDate (JJJJ-MM-TT) und textformChannel (chat | email).
*/
export const setFeature = (customerId, key, { projectId, action, note, textformDate, textformChannel }) =>
@@ -112,6 +112,14 @@ export const getShop = (customerId) => request(`/shop/${enc(customerId)}`)
export const entscheideShopAnfrage = (customerId, { projectId, entscheidung, begruendung }) =>
request(`/shop/${enc(customerId)}/anfrage`, { method: 'POST', body: entscheidungBody({ projectId, entscheidung, begruendung }) })
+// --- Buchungskalender (Anfrage des Kunden, Stripe-Stand, Unterkuenfte) ---
+
+/** { projects: [{ projectId, name, state, anfrage, stripe, unterkuenfte, offeneAnfragen }] } (Aufbereitung: funktionenView) */
+export const getUnterkunft = (customerId) => request(`/unterkunft/${enc(customerId)}`)
+/** Anfrage annehmen oder ablehnen: body { projectId, entscheidung: 'annehmen' | 'ablehnen', begruendung } => { ok, anfrage, mailFehler? } */
+export const entscheideUnterkunftAnfrage = (customerId, { projectId, entscheidung, begruendung }) =>
+ request(`/unterkunft/${enc(customerId)}/anfrage`, { method: 'POST', body: entscheidungBody({ projectId, entscheidung, begruendung }) })
+
// --- Oeffnungszeiten live (Funktion zeiten): Ersatz-HTML und Team-Einstellungen ---
/** Ersatz-HTML (Wochentabelle) eines Plans fuer den Einbau: { html }; 404 unbekannter Plan, 409 "Der Kunde hat noch keine Zeiten eingetragen." */
diff --git a/tests/setup-legal.test.mjs b/tests/setup-legal.test.mjs
index f769d22..e76d241 100644
--- a/tests/setup-legal.test.mjs
+++ b/tests/setup-legal.test.mjs
@@ -65,7 +65,7 @@ test('terminplaner_karten_vorhanden', () => {
assert.ok(!/\d{4}-\d{2}-\d{2}/.test(d.liveUrl), 'Alias-Seite ohne Fassungsdatum')
}
// Oeffnungszeiten live: Karte 12, hinter den Terminplaner- und Webshop-Karten; der Schluessel hat 28 Zeichen (WOMS-Grenze 36, Ruling R35)
- assert.equal(DOCUMENTS.length, 12, 'keine Analyse-Karten und keine weiteren')
+ assert.equal(DOCUMENTS.length, 14, 'keine Analyse-Karten und keine weiteren')
})
test('zeiten_karte_vorhanden_am_ende', () => {
@@ -78,10 +78,18 @@ test('zeiten_karte_vorhanden_am_ende', () => {
sort: 12,
})
assert.ok(d.key.length <= 36, 'Dokument-ID hoechstens 36 Zeichen')
- assert.equal(DOCUMENTS.at(-1), d, 'am Ende von DOCUMENTS, nach den Terminplaner- und Webshop-Karten')
+ assert.equal(DOCUMENTS.at(-3), d, 'vor den Buchungskalender-Karten, nach den Terminplaner- und Webshop-Karten')
assert.equal(DOCUMENTS.findIndex((x) => x.key === 'widerrufsbelehrung-funktionen'), 8, 'Terminplaner-Karten unveraendert davor')
})
+test('unterkunft_karten_am_ende', () => {
+ assert.deepEqual(DOCUMENTS.slice(-2), [
+ { key: 'leistungsbeschreibung-unterkunft', title: 'Leistungsbeschreibung Buchungskalender', liveUrl: 'https://project.webklar.com/rechtliches/leistungsbeschreibung-unterkunft.html', sort: 13 },
+ { key: 'avv-unterkunft', title: 'AVV Buchungskalender', liveUrl: 'https://project.webklar.com/rechtliches/avv-unterkunft.html', sort: 14 },
+ ])
+ assert.equal(DOCUMENTS.at(-1).key, 'avv-unterkunft')
+})
+
test('bestehende_karten_unveraendert', () => {
assert.deepEqual(DOCUMENTS.slice(0, 9), [
{ key: 'agb', title: 'AGB', liveUrl: 'https://webklar.com/agb', sort: 1 },
@@ -111,5 +119,7 @@ test('dry_run_listet_karten', () => {
assert.ok(zeilen.some((z) => z.startsWith('Rechtstext leistungsbeschreibung-webshop:')))
assert.ok(zeilen.some((z) => z.startsWith('Rechtstext avv-webshop:')))
assert.ok(zeilen.includes('Rechtstext leistungsbeschreibung-zeiten: Leistungsbeschreibung Öffnungszeiten live'))
+ assert.ok(zeilen.includes('Rechtstext leistungsbeschreibung-unterkunft: Leistungsbeschreibung Buchungskalender'))
+ assert.ok(zeilen.includes('Rechtstext avv-unterkunft: AVV Buchungskalender'))
assert.ok(!/angelegt|vorhanden|Fertig/.test(r.stdout), 'Trockenlauf legt nichts an')
})
diff --git a/tests/unit/funktionenView.test.mjs b/tests/unit/funktionenView.test.mjs
index 7916522..17b25b2 100644
--- a/tests/unit/funktionenView.test.mjs
+++ b/tests/unit/funktionenView.test.mjs
@@ -82,11 +82,21 @@ test('FUNKTIONEN: News, Terminplaner, Webshop und Öffnungszeiten live (in diese
ticket: 'Fehlendes Einbau-Ticket neu anlegen?',
},
},
+ {
+ key: 'unterkunft',
+ label: 'Buchungskalender',
+ bestaetigen: {
+ activate: 'Buchungskalender ohne Zahlung aktivieren?',
+ disable: 'Buchungskalender abschalten? Auf der Website sind dann keine Buchungsanfragen mehr möglich.',
+ abnahme: 'Buchungskalender freischalten? Der Kunde bekommt die Mail „bereit“ und Gäste können anfragen.',
+ ticket: 'Fehlendes Einrichtungs-Ticket neu anlegen?',
+ },
+ },
])
})
test('FUNKTIONEN: Schlüssel sind eindeutig und jede Funktion hat Aktivieren und Abschalten bestätigt', () => {
- assert.deepEqual(FUNKTIONEN.map((f) => f.key), ['news', 'termine', 'shop', 'zeiten'])
+ assert.deepEqual(FUNKTIONEN.map((f) => f.key), ['news', 'termine', 'shop', 'zeiten', 'unterkunft'])
for (const f of FUNKTIONEN) {
assert.ok(f.bestaetigen.activate && f.bestaetigen.disable, f.key)
}
@@ -754,3 +764,42 @@ test('entscheidungStatus: Warnung, wenn die Mail an den Kunden fehlschlug', () =
assert.deepEqual(entscheidungStatus({ ok: true, mailFehler: true }), { ok: false, text: 'Entscheidung gespeichert, Mail an den Kunden fehlgeschlagen.' })
assert.deepEqual(entscheidungStatus(null), { ok: true, text: 'Gespeichert.' })
})
+
+test('unterkunft: Aktionen wie beim Webshop, Site-Key wird gezeigt', () => {
+ for (const state of ['off', 'ordered', 'setup', 'active', 'disabled', undefined]) {
+ assert.deepEqual(aktionen(state, 'unterkunft'), aktionen(state, 'shop'), String(state))
+ assert.deepEqual(aktionen(state, 'unterkunft', { ticketMissing: true }), aktionen(state, 'shop', { ticketMissing: true }), `${state} mit Ticket fehlt`)
+ }
+ assert.equal(siteKeyOf({ state: 'active', siteKey: ' abcdefghijklmnop ' }, 'unterkunft'), 'abcdefghijklmnop')
+ assert.equal(siteKeyOf({ state: 'active', siteKey: null }, 'unterkunft'), '')
+})
+
+test('anfrageZeilen (unterkunft): Art, Einheiten, Portale, Nachricht', () => {
+ const a = { art: 'ferienwohnung', einheiten: 3, portale: true, nachricht: ' Hallo ' }
+ assert.deepEqual(anfrageZeilen(a, 'unterkunft'), [
+ { label: 'Art', text: 'Ferienwohnung' },
+ { label: 'Einheiten', text: '3' },
+ { label: 'Portale', text: 'nutzt Booking.com/Airbnb' },
+ { label: 'Nachricht', text: 'Hallo' },
+ ])
+ assert.equal(anfrageZeilen({ ...a, art: 'monteurzimmer' }, 'unterkunft')[0].text, 'Monteurzimmer')
+ assert.equal(anfrageZeilen({ ...a, art: 'pension' }, 'unterkunft')[0].text, 'Pension')
+ assert.equal(anfrageZeilen({ ...a, art: 'sonstiges' }, 'unterkunft')[0].text, 'Sonstiges')
+ assert.equal(anfrageZeilen({ ...a, art: 'xyz' }, 'unterkunft')[0].text, 'xyz')
+ const leer = anfrageZeilen({ portale: false }, 'unterkunft')
+ assert.deepEqual(leer.map((z) => z.text), ['–', '–', 'nutzt keine Portale', '–'])
+ assert.equal(anfrageZeilen({ ...a, einheiten: 'x' }, 'unterkunft')[1].text, '–')
+ assert.deepEqual(anfrageZeilen(null, 'unterkunft'), [])
+ // ohne art: unveraendert Webshop
+ assert.deepEqual(anfrageZeilen(a), anfrageZeilen(a, 'shop'))
+})
+
+test('anfrageText (unterkunft): Kurzfassung', () => {
+ assert.equal(anfrageText({ art: 'pension', einheiten: 4, portale: true }, 'unterkunft'), 'Pension, 4 Einheiten, nutzt Booking.com/Airbnb')
+ assert.equal(anfrageText({ art: 'pension', einheiten: 1 }, 'unterkunft'), 'Pension, 1 Einheit')
+ assert.equal(anfrageText(null, 'unterkunft'), '')
+})
+
+test('entscheidungBody und begruendungOk gelten unverändert für den Buchungskalender', () => {
+ assert.deepEqual(entscheidungBody({ projectId: 'p', entscheidung: 'ablehnen', begruendung: ' x ' }), { projectId: 'p', entscheidung: 'ablehnen', begruendung: 'x' })
+})