/* global React */ /* ============================================================ DDPP GENERISCHE BUCHUNGS-ENGINE Wiederverwendbare Booking-Form für alle Event-Typen. Exportiert window.EventBookingForm({ cfg }). cfg steuert Preis, Mindestpersonen, Personen-Bezeichnung (noun), Rechnungsmodus ('company' | 'private') und welche Add-ons gelten. Strings via window.t / window.tt (i18n.jsx) — neutrale ev.*-Keys für personenbezogene Zeilen, sonst Wiederverwendung der ffb.*-Keys. ============================================================ */ const EBOOKING_ENDPOINT = "/api/booking"; /* Gemeinsame Add-on-/Stundenpreise (Basispreis & Mindestpersonen aus cfg) */ const EB_PRICING = { perPersonExtraHour: 14.95, drinksBudgetDefault: 300, drinksBudgetMin: 100, drinksBudgetStep: 50, dj: 300, ownMusic: 49.90, customBallsPack: 150, customBallsPackSize: 200, customBallsGiftPerPerson: 14.90, coachPerHour: 79.90, minLeadDaysDefault: 2, minLeadDaysCustomBalls: 1, }; /* ---------------------------------------------------------------- PRO EVENT-TYP — hier kannst du Preise jederzeit anpassen. base = Grundpreis pro Person (Basis-Stunden) minPersons = Mindestpersonen baseHours = inkludierte Stunden billing = 'company' (Firma + USt-IdNr.) | 'private' (nur Person) noun = Personen-Bezeichnung in den Preiszeilen extras = true → Custom-Bälle / Geschenkbälle / Replay-Hinweis ---------------------------------------------------------------- */ const EB_TYPES = { firmenfeiern: { kind: "firmenfeier", base: 29.90, minPersons: 10, baseHours: 2, billing: "company", noun: window.t("ev.noun.firmenfeiern"), extras: true }, geburtstage: { kind: "geburtstag", base: 24.90, minPersons: 6, baseHours: 2, billing: "private", noun: window.t("ev.noun.geburtstage"), extras: false }, events: { kind: "event", base: 24.90, minPersons: 8, baseHours: 2, billing: "private", noun: window.t("ev.noun.events"), extras: true }, }; const ebFmtEUR = (n) => new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(n); function ebTodayPlusDays(days) { const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() + days); return d.toISOString().slice(0, 10); } function ebCalcTotal(cfg, state) { const persons = Math.max(cfg.minPersons, state.persons || 0); const hours = Math.max(cfg.baseHours, state.hours || cfg.baseHours); const extraHours = hours - cfg.baseHours; const noun = cfg.noun; const lines = []; lines.push({ label: window.tt("ev.line.base", { persons, noun, price: ebFmtEUR(cfg.base), hours: cfg.baseHours }), amount: persons * cfg.base, }); if (extraHours > 0) { lines.push({ label: window.tt("ev.line.extra", { n: extraHours, persons, noun, price: ebFmtEUR(EB_PRICING.perPersonExtraHour) }), amount: persons * extraHours * EB_PRICING.perPersonExtraHour, }); } if (state.drinks) { lines.push({ label: window.t("ffb.line.drinks"), amount: Math.max(0, state.drinksBudget || 0) }); } if (state.music === "dj") { lines.push({ label: window.t("ffb.line.dj"), amount: EB_PRICING.dj }); } else if (state.music === "own") { lines.push({ label: window.t("ffb.line.own_music"), amount: EB_PRICING.ownMusic }); } if (cfg.extras && state.customBalls && state.customBallsAmount >= EB_PRICING.customBallsPackSize) { const packs = Math.ceil(state.customBallsAmount / EB_PRICING.customBallsPackSize); lines.push({ label: window.tt("ffb.line.custom_balls", { n: packs * EB_PRICING.customBallsPackSize }), amount: packs * EB_PRICING.customBallsPack }); } if (state.coach) { lines.push({ label: window.tt("ffb.line.coach", { hours }), amount: hours * EB_PRICING.coachPerHour }); } const total = lines.reduce((s, l) => s + l.amount, 0); return { lines, total, persons, hours }; } function EbFormRow({ label, hint, error, children }) { return (
{label && } {children} {hint && !error &&
{hint}
} {error &&
{error}
}
); } function EbAddonToggle({ id, title, price, checked, onChange, disabled, children }) { return ( ); } function EventBookingForm({ cfg }) { const { useState, useMemo, useEffect } = React; const noun = cfg.noun; const [persons, setPersons] = useState(cfg.minPersons); const [hours, setHours] = useState(cfg.baseHours); const [date, setDate] = useState(""); const [time, setTime] = useState("18:00"); const [drinks, setDrinks] = useState(false); const [drinksBudget, setDrinksBudget] = useState(EB_PRICING.drinksBudgetDefault); const [music, setMusic] = useState("none"); const [customBalls, setCustomBalls] = useState(false); const [customBallsAmount, setCustomBallsAmount] = useState(EB_PRICING.customBallsPackSize); const [robot, setRobot] = useState(true); const [coach, setCoach] = useState(false); const [contactFirst, setContactFirst] = useState(""); const [contactLast, setContactLast] = useState(""); const [contactEmail, setContactEmail] = useState(""); const [contactPhone, setContactPhone] = useState(""); const [company, setCompany] = useState(""); const [billingStreet, setBillingStreet] = useState(""); const [billingZip, setBillingZip] = useState(""); const [billingCity, setBillingCity] = useState(""); const [vatId, setVatId] = useState(""); const [notes, setNotes] = useState(""); const [consent, setConsent] = useState(false); const [submitting, setSubmitting] = useState(false); const [submitResult, setSubmitResult] = useState(null); const [errors, setErrors] = useState({}); const isCompany = cfg.billing === "company"; const minDate = useMemo( () => ebTodayPlusDays(customBalls ? Math.max(EB_PRICING.minLeadDaysDefault, EB_PRICING.minLeadDaysCustomBalls) : EB_PRICING.minLeadDaysDefault), [customBalls], ); useEffect(() => { if (date && date < minDate) setDate(minDate); }, [minDate, date]); const calc = useMemo( () => ebCalcTotal(cfg, { persons, hours, drinks, drinksBudget, music, customBalls, customBallsAmount, coach }), [cfg, persons, hours, drinks, drinksBudget, music, customBalls, customBallsAmount, coach], ); function validate() { const e = {}; if (!date) e.date = window.t("ffb.err.date_required"); if (date && date < minDate) e.date = window.tt("ffb.err.date_min", { date: minDate }); if (!time) e.time = window.t("ffb.err.time_required"); if (persons < cfg.minPersons) e.persons = window.tt("ev.hint.persons", { n: cfg.minPersons, noun }); if (hours < cfg.baseHours) e.hours = window.tt("ffb.err.hours_min", { n: cfg.baseHours }); if (cfg.extras && customBalls && customBallsAmount < EB_PRICING.customBallsPackSize) { e.customBallsAmount = window.tt("ffb.err.balls_min", { n: EB_PRICING.customBallsPackSize }); } if (!contactFirst.trim()) e.contactFirst = window.t("ffb.err.required"); if (!contactLast.trim()) e.contactLast = window.t("ffb.err.required"); if (!contactEmail.trim() || !/.+@.+\..+/.test(contactEmail)) e.contactEmail = window.t("ffb.err.email_invalid"); if (isCompany && !company.trim()) e.company = window.t("ffb.err.required"); if (!billingStreet.trim()) e.billingStreet = window.t("ffb.err.required"); if (!billingZip.trim()) e.billingZip = window.t("ffb.err.required"); if (!billingCity.trim()) e.billingCity = window.t("ffb.err.required"); if (!consent) e.consent = window.t("ffb.err.consent"); setErrors(e); return Object.keys(e).length === 0; } async function handleSubmit(e) { e.preventDefault(); setSubmitResult(null); if (!validate()) { const firstErr = document.querySelector(".form-error"); if (firstErr) firstErr.scrollIntoView({ behavior: "smooth", block: "center" }); return; } setSubmitting(true); try { const payload = { kind: cfg.kind, submittedAt: new Date().toISOString(), event: { date, time, persons, hours }, options: { drinks: drinks ? { budget: Math.max(0, drinksBudget || 0) } : false, music, customBalls: cfg.extras && customBalls ? { amount: customBallsAmount } : null, robot, coach, }, contact: { firstName: contactFirst, lastName: contactLast, email: contactEmail, phone: contactPhone }, billing: { company: isCompany ? company : null, street: billingStreet, zip: billingZip, city: billingCity, vatId: isCompany ? vatId : null }, notes, pricing: { lines: calc.lines, totalNet: calc.total }, lang: window.currentLang || "de", }; const res = await fetch(EBOOKING_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); setSubmitResult("ok"); window.scrollTo({ top: 0, behavior: "smooth" }); } catch (err) { console.error(err); setSubmitResult("error"); } finally { setSubmitting(false); } } if (submitResult === "ok") { return (
{window.t("ffb.success.eyebrow")}

{window.t("ffb.success.h2")}

{window.t("ffb.success.body")}

{window.t("ffb.success.back")}
); } return (
{submitResult === "error" && (
{window.t("ffb.error_banner")}
)} {/* TERMIN */}

{window.t("ffb.s1.h2")}

{window.tt("ffb.s1.intro", { hours: cfg.baseHours, days: EB_PRICING.minLeadDaysDefault })}

setDate(e.target.value)} /> setTime(e.target.value)} /> setPersons(parseInt(e.target.value || "0", 10))} /> setHours(parseInt(e.target.value || "0", 10))} />
{/* OPTIONEN */}

{window.t("ffb.s2.h2")}

{drinks && ( setDrinksBudget(parseInt(e.target.value || "0", 10))} /> )} setMusic(v ? "dj" : "none")} disabled={music === "own"} /> setMusic(v ? "own" : "none")} disabled={music === "dj"} /> {cfg.extras && ( {customBalls && ( setCustomBallsAmount(parseInt(e.target.value || "0", 10))} /> )} )}
{/* KONTAKT + RECHNUNG */}

{window.t("ffb.s3.h2")}

{window.t("ffb.s3.contact_subhead")}

setContactFirst(e.target.value)} autoComplete="given-name" /> setContactLast(e.target.value)} autoComplete="family-name" /> setContactEmail(e.target.value)} /> setContactPhone(e.target.value)} />

{window.t(isCompany ? "ffb.s3.billing_subhead" : "ev.s3.billing_private")}

{isCompany && ( setCompany(e.target.value)} autoComplete="organization" /> )} {isCompany && ( setVatId(e.target.value)} /> )} setBillingStreet(e.target.value)} /> setBillingZip(e.target.value)} /> setBillingCity(e.target.value)} />