/* 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("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 (
);
}
return (
);
}
function App() {
useEffect(() => {
document.title = window.t("page.title.firmenfeier");
}, []);
return (
<>
>
);
}
ReactDOM.createRoot(document.getElementById("root")).render();