/* 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 (
);
}
return (
);
}
window.EventBookingForm = EventBookingForm;
window.EB_TYPES = EB_TYPES;