/* global React, TopBar, Header, Footer */ const { useState, useMemo, useEffect } = React; /* ============================================================ FIRMENFEIER-BUCHUNG Wiederverwendbare Booking-Engine, hier konfiguriert für Firmenfeier. Strings via window.t / window.tt (i18n.jsx). ============================================================ */ const BOOKING_ENDPOINT = "/api/booking"; const PRICING = { perPersonBase: 29.90, perPersonExtraHour: 14.95, minPersons: 10, baseHours: 2, 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, }; const fmtEUR = (n) => new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(n); function todayPlusDays(days) { const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() + days); return d.toISOString().slice(0, 10); } function calcTotal(state) { const persons = Math.max(PRICING.minPersons, state.persons || 0); const hours = Math.max(PRICING.baseHours, state.hours || PRICING.baseHours); const extraHours = hours - PRICING.baseHours; const lines = []; const personSubtotal = persons * PRICING.perPersonBase + persons * extraHours * PRICING.perPersonExtraHour; lines.push({ label: window.tt("ffb.line.base", { persons, price: fmtEUR(PRICING.perPersonBase), hours: PRICING.baseHours }), amount: persons * PRICING.perPersonBase, }); if (extraHours > 0) { lines.push({ label: window.tt("ffb.line.extra_hours", { n: extraHours, persons, price: fmtEUR(PRICING.perPersonExtraHour) }), amount: persons * extraHours * PRICING.perPersonExtraHour, }); } if (state.drinks) { const amt = Math.max(0, state.drinksBudget || 0); lines.push({ label: window.t("ffb.line.drinks"), amount: amt }); } if (state.music === "dj") { lines.push({ label: window.t("ffb.line.dj"), amount: PRICING.dj }); } else if (state.music === "own") { lines.push({ label: window.t("ffb.line.own_music"), amount: PRICING.ownMusic }); } if (state.customBalls && state.customBallsAmount >= PRICING.customBallsPackSize) { const packs = Math.ceil(state.customBallsAmount / PRICING.customBallsPackSize); const amt = packs * PRICING.customBallsPack; lines.push({ label: window.tt("ffb.line.custom_balls", { n: packs * PRICING.customBallsPackSize }), amount: amt, }); } if (state.giftBalls) { const amt = persons * PRICING.customBallsGiftPerPerson; lines.push({ label: window.tt("ffb.line.gift_balls", { persons }), amount: amt }); } if (state.coach) { const amt = hours * PRICING.coachPerHour; lines.push({ label: window.tt("ffb.line.coach", { hours }), amount: amt }); } const total = lines.reduce((s, l) => s + l.amount, 0); return { lines, total, persons, hours }; } function SubHero() { return (
{window.t("ff.crumb_book")} / {window.t("ff.crumb")} / {window.t("ffb.crumb_buchen")}

{window.t("ffb.title_pre")}
{window.t("ffb.title_alt")}

{window.t("ff_buchung.lede")}

); } function FormRow({ label, hint, error, children }) { return (
{label && } {children} {hint && !error &&
{hint}
} {error &&
{error}
}
); } function AddonToggle({ id, title, price, checked, onChange, disabled, children }) { return ( ); } function BookingForm() { const [persons, setPersons] = useState(PRICING.minPersons); const [hours, setHours] = useState(PRICING.baseHours); const [date, setDate] = useState(""); const [time, setTime] = useState("18:00"); const [drinks, setDrinks] = useState(false); const [drinksBudget, setDrinksBudget] = useState(PRICING.drinksBudgetDefault); const [music, setMusic] = useState("none"); const [customBalls, setCustomBalls] = useState(false); const [customBallsAmount, setCustomBallsAmount] = useState(PRICING.customBallsPackSize); const [customBallsLogo, setCustomBallsLogo] = useState(null); const [customBallsLogoName, setCustomBallsLogoName] = useState(""); const [customBallsNote, setCustomBallsNote] = useState(""); const [giftBalls, setGiftBalls] = useState(false); 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 needsCustomBallsLead = customBalls || giftBalls; const minDate = useMemo( () => todayPlusDays( needsCustomBallsLead ? Math.max(PRICING.minLeadDaysDefault, PRICING.minLeadDaysCustomBalls) : PRICING.minLeadDaysDefault, ), [needsCustomBallsLead], ); useEffect(() => { if (date && date < minDate) setDate(minDate); }, [minDate, date]); const showLogoUpload = customBalls || giftBalls; const calc = useMemo( () => calcTotal({ persons, hours, drinks, drinksBudget, music, customBalls, customBallsAmount, giftBalls, coach, }), [persons, hours, drinks, drinksBudget, music, customBalls, customBallsAmount, giftBalls, 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 < PRICING.minPersons) e.persons = window.tt("ffb.err.persons_min", { n: PRICING.minPersons }); if (hours < PRICING.baseHours) e.hours = window.tt("ffb.err.hours_min", { n: PRICING.baseHours }); if (customBalls && customBallsAmount < PRICING.customBallsPackSize) { e.customBallsAmount = window.tt("ffb.err.balls_min", { n: PRICING.customBallsPackSize }); } if (showLogoUpload && !customBallsLogo) { e.customBallsLogo = window.t("ffb.err.logo_required"); } 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 (!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; } function handleLogoUpload(file) { if (!file) { setCustomBallsLogo(null); setCustomBallsLogoName(""); return; } if (file.size > 5 * 1024 * 1024) { setErrors((p) => ({ ...p, customBallsLogo: window.t("ffb.err.logo_size") })); return; } const reader = new FileReader(); reader.onload = () => { setCustomBallsLogo({ filename: file.name, mimeType: file.type || "application/octet-stream", base64: String(reader.result).split(",")[1] || "", }); setCustomBallsLogoName(file.name); setErrors((p) => { const n = { ...p }; delete n.customBallsLogo; return n; }); }; reader.readAsDataURL(file); } 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: "firmenfeier", submittedAt: new Date().toISOString(), event: { date, time, persons, hours }, options: { drinks: drinks ? { budget: Math.max(0, drinksBudget || 0) } : false, music, customBalls: customBalls ? { amount: customBallsAmount, note: customBallsNote } : null, giftBalls, robot, coach, }, logo: customBallsLogo, contact: { firstName: contactFirst, lastName: contactLast, email: contactEmail, phone: contactPhone, }, billing: { company, street: billingStreet, zip: billingZip, city: billingCity, vatId, }, notes, pricing: { lines: calc.lines, totalNet: calc.total, }, lang: window.currentLang || "de", }; const res = await fetch(BOOKING_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: PRICING.baseHours, days: 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"} /> {customBalls && ( setCustomBallsAmount(parseInt(e.target.value || "0", 10)) } /> )}
{window.t("ffb.addon.replays.title")} {window.t("ffb.addon.free")}
{window.t("ffb.addon.replays.body")}
{showLogoUpload && (

{window.t("ffb.logo.h3")}

handleLogoUpload(e.target.files?.[0] || null)} />