@bigz-app/booking-widget 0.3.5 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React__default from 'react';
2
- import React__default__default, { createContext, useState, useEffect, useMemo, useContext, useRef } from 'react';
2
+ import React__default__default, { createContext, useState, useEffect, useMemo, useContext, useRef, useCallback, Fragment as Fragment$1 } from 'react';
3
3
  import { createRoot } from 'react-dom/client';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import ReactDOM, { createPortal } from 'react-dom';
@@ -6888,7 +6888,7 @@ const preprocessMarkdown$1 = (markdown) => {
6888
6888
  // Convert double underscores to HTML underline tags for React Markdown
6889
6889
  return markdown.replace(/__([^_]+)__/g, "<u>$1</u>");
6890
6890
  };
6891
- const IconCheck$1 = ({ size = 16, color = "#10b981" }) => (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "20 6 9 17 4 12" }) }));
6891
+ const IconCheck$2 = ({ size = 16, color = "#10b981" }) => (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "20 6 9 17 4 12" }) }));
6892
6892
  const IconWave$1 = ({ size = 20, color = "#0ea5e9" }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M2 18c2-2 6-2 8 0s6 2 8 0 6-2 8 0" }), jsx("path", { d: "M2 12c2-2 6-2 8 0s6 2 8 0 6-2 8 0" }), jsx("path", { d: "M2 6c2-2 6-2 8 0s6 2 8 0 6-2 8 0" })] }));
6893
6893
  function EventTypeDetailsDialog({ isOpen, onClose, eventType, onEventTypeSelect, }) {
6894
6894
  if (!isOpen || !eventType)
@@ -6935,7 +6935,7 @@ function EventTypeDetailsDialog({ isOpen, onClose, eventType, onEventTypeSelect,
6935
6935
  fontSize: "16px",
6936
6936
  lineHeight: "1.6",
6937
6937
  color: "var(--bw-text-color)",
6938
- }, children: [jsx("div", { style: { marginTop: "4px", flexShrink: 0 }, children: jsx(IconCheck$1, { size: 16, color: "var(--bw-success-color)" }) }), jsx("span", { children: highlight.trim() })] }, index))) }) }) }))] }), eventType.description && (jsxs("div", { style: {
6938
+ }, children: [jsx("div", { style: { marginTop: "4px", flexShrink: 0 }, children: jsx(IconCheck$2, { size: 16, color: "var(--bw-success-color)" }) }), jsx("span", { children: highlight.trim() })] }, index))) }) }) }))] }), eventType.description && (jsxs("div", { style: {
6939
6939
  marginBottom: "24px",
6940
6940
  color: "var(--bw-text-muted)",
6941
6941
  fontSize: "16px",
@@ -7244,7 +7244,7 @@ const preprocessMarkdown = (markdown) => {
7244
7244
  // Custom minimal SVG icons (Lucide-style)
7245
7245
  const IconClock = ({ size = 16, color = "#10b981" }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("circle", { cx: "12", cy: "12", r: "10" }), jsx("polyline", { points: "12 6 12 12 16 14" })] }));
7246
7246
  const IconCalendar = ({ size = 16, color = "#3b82f6" }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }), jsx("line", { x1: "16", y1: "2", x2: "16", y2: "6" }), jsx("line", { x1: "8", y1: "2", x2: "8", y2: "6" }), jsx("line", { x1: "3", y1: "10", x2: "21", y2: "10" })] }));
7247
- const IconCheck = ({ size = 16, color = "#10b981" }) => (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "20 6 9 17 4 12" }) }));
7247
+ const IconCheck$1 = ({ size = 16, color = "#10b981" }) => (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "20 6 9 17 4 12" }) }));
7248
7248
  // Wave icon for booking action
7249
7249
  const IconWave = ({ size = 20, color = "#0ea5e9" }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M2 18c2-2 6-2 8 0s6 2 8 0 6-2 8 0" }), jsx("path", { d: "M2 12c2-2 6-2 8 0s6 2 8 0 6-2 8 0" }), jsx("path", { d: "M2 6c2-2 6-2 8 0s6 2 8 0 6-2 8 0" })] }));
7250
7250
  // Loading skeleton component that matches the actual design
@@ -7708,7 +7708,7 @@ function EventTypeSelection({ eventTypes, onEventTypeSelect, isLoading = false,
7708
7708
  color: "var(--bw-text-muted)",
7709
7709
  position: "relative",
7710
7710
  maxWidth: "100%",
7711
- }, children: [jsx("div", { style: { marginTop: "4px", flexShrink: 0 }, children: jsx(IconCheck, { size: 16, color: "var(--bw-success-color)" }) }), jsx("span", { style: {
7711
+ }, children: [jsx("div", { style: { marginTop: "4px", flexShrink: 0 }, children: jsx(IconCheck$1, { size: 16, color: "var(--bw-success-color)" }) }), jsx("span", { style: {
7712
7712
  textOverflow: "ellipsis",
7713
7713
  overflow: "hidden",
7714
7714
  whiteSpace: "nowrap",
@@ -9111,6 +9111,110 @@ var reactStripe_umd = {exports: {}};
9111
9111
 
9112
9112
  var reactStripe_umdExports = reactStripe_umd.exports;
9113
9113
 
9114
+ // Component for bookings fully covered by gift cards (no Stripe payment needed)
9115
+ function GiftCardOnlyBooking({ config, eventDetails, formData, discountCode, giftCards, onSuccess, onError, }) {
9116
+ const [isLoading, setIsLoading] = useState(false);
9117
+ const [error, setError] = useState(null);
9118
+ const handleBooking = async () => {
9119
+ setIsLoading(true);
9120
+ setError(null);
9121
+ try {
9122
+ // Create booking directly without Stripe payment
9123
+ const requestData = {
9124
+ eventInstanceId: config.eventInstanceId || eventDetails.id,
9125
+ organizationId: config.organizationId,
9126
+ participants: formData.participants.filter((p) => p.name?.trim()),
9127
+ discountCode: discountCode?.code,
9128
+ giftCardCodes: giftCards.map((gc) => gc.code),
9129
+ customerName: formData.customerName?.trim(),
9130
+ customerEmail: formData.customerEmail?.trim(),
9131
+ customerPhone: formData.customerPhone?.trim(),
9132
+ comment: formData.comment?.trim(),
9133
+ paymentMethod: "gift_card",
9134
+ };
9135
+ const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/create-gift-card-booking"), {
9136
+ method: "POST",
9137
+ headers: createApiHeaders(config),
9138
+ body: JSON.stringify(createRequestBody(config, requestData)),
9139
+ });
9140
+ const data = await response.json();
9141
+ if (response.ok) {
9142
+ onSuccess({
9143
+ booking: data.booking,
9144
+ order: data.order,
9145
+ giftCardRedemptions: data.giftCardRedemptions,
9146
+ });
9147
+ }
9148
+ else {
9149
+ setError(data.error || "Fehler beim Erstellen der Buchung");
9150
+ onError(data.error || "Fehler beim Erstellen der Buchung");
9151
+ }
9152
+ }
9153
+ catch (err) {
9154
+ setError(err.message || "Fehler beim Erstellen der Buchung");
9155
+ onError(err.message || "Fehler beim Erstellen der Buchung");
9156
+ }
9157
+ finally {
9158
+ setIsLoading(false);
9159
+ }
9160
+ };
9161
+ const totalGiftCardAmount = giftCards.reduce((sum, gc) => sum + (gc.balanceToUse || gc.discountAmount || 0), 0);
9162
+ return (jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "var(--bw-spacing)" }, children: [jsxs("div", { style: {
9163
+ backgroundColor: "var(--bw-success-color)15",
9164
+ border: "1px solid var(--bw-success-color)40",
9165
+ borderRadius: "var(--bw-border-radius)",
9166
+ padding: "var(--bw-spacing)",
9167
+ }, children: [jsx("div", { style: {
9168
+ display: "flex",
9169
+ alignItems: "center",
9170
+ gap: "8px",
9171
+ marginBottom: "8px",
9172
+ color: "var(--bw-success-color)",
9173
+ fontFamily: "var(--bw-font-family)",
9174
+ fontWeight: "600",
9175
+ }, children: "\uD83C\uDF81 Vollst\u00E4ndig durch Gutschein(e) gedeckt" }), jsxs("div", { style: {
9176
+ fontSize: "var(--bw-font-size)",
9177
+ color: "var(--bw-text-muted)",
9178
+ fontFamily: "var(--bw-font-family)",
9179
+ }, children: ["Gutschein-Guthaben: ", formatCurrency(totalGiftCardAmount)] })] }), error && (jsxs("div", { style: {
9180
+ backgroundColor: "var(--bw-error-color)15",
9181
+ border: "1px solid var(--bw-error-color)40",
9182
+ borderRadius: "var(--bw-border-radius)",
9183
+ padding: "var(--bw-spacing)",
9184
+ color: "var(--bw-error-color)",
9185
+ fontSize: "var(--bw-font-size)",
9186
+ fontFamily: "var(--bw-font-family)",
9187
+ }, children: ["\u26A0\uFE0F ", error] })), jsx("button", { type: "button", onClick: handleBooking, disabled: isLoading, style: {
9188
+ width: "100%",
9189
+ padding: "12px 24px",
9190
+ backgroundColor: "var(--bw-highlight-color)",
9191
+ color: "#fff",
9192
+ border: "none",
9193
+ borderRadius: "var(--bw-border-radius)",
9194
+ fontSize: "var(--bw-font-size)",
9195
+ fontWeight: "600",
9196
+ cursor: isLoading ? "not-allowed" : "pointer",
9197
+ opacity: isLoading ? 0.6 : 1,
9198
+ transition: "all 0.2s ease",
9199
+ fontFamily: "var(--bw-font-family)",
9200
+ display: "flex",
9201
+ alignItems: "center",
9202
+ justifyContent: "center",
9203
+ gap: "8px",
9204
+ }, children: isLoading ? (jsxs(Fragment, { children: [jsx("div", { style: {
9205
+ width: "16px",
9206
+ height: "16px",
9207
+ border: "2px solid #fff",
9208
+ borderTopColor: "transparent",
9209
+ borderRadius: "50%",
9210
+ animation: "spin 1s linear infinite",
9211
+ } }), "Buchung wird erstellt..."] })) : ("Mit Gutschein buchen") }), jsx("style", { children: `
9212
+ @keyframes spin {
9213
+ from { transform: rotate(0deg); }
9214
+ to { transform: rotate(360deg); }
9215
+ }
9216
+ ` })] }));
9217
+ }
9114
9218
  const spinner$1 = (borderColor) => (jsx("div", { style: {
9115
9219
  width: "auto",
9116
9220
  height: "auto",
@@ -9127,7 +9231,7 @@ const spinner$1 = (borderColor) => (jsx("div", { style: {
9127
9231
  borderRadius: "50%",
9128
9232
  } }) }));
9129
9233
  // Inner component that uses the Stripe hooks
9130
- function PaymentFormInner({ config, eventDetails, formData, totalAmount, discountCode, onSuccess, onError, }) {
9234
+ function PaymentFormInner({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess, onError, }) {
9131
9235
  const stripe = reactStripe_umdExports.useStripe();
9132
9236
  const elements = reactStripe_umdExports.useElements();
9133
9237
  const [isLoading, setIsLoading] = useState(false);
@@ -9258,7 +9362,7 @@ function PaymentFormInner({ config, eventDetails, formData, totalAmount, discoun
9258
9362
  ` })] }));
9259
9363
  }
9260
9364
  // Main PaymentForm component that handles payment intent creation and Elements wrapper
9261
- function PaymentForm({ config, eventDetails, formData, totalAmount, discountCode, onSuccess, onError, systemConfig, stripePromise, stripeAppearance, }) {
9365
+ function PaymentForm({ config, eventDetails, formData, totalAmount, discountCode, giftCards, onSuccess, onError, systemConfig, stripePromise, stripeAppearance, }) {
9262
9366
  const [clientSecret, setClientSecret] = useState(null);
9263
9367
  const [paymentIntentId, setPaymentIntentId] = useState(null);
9264
9368
  const [isCreatingPaymentIntent, setIsCreatingPaymentIntent] = useState(false);
@@ -9354,6 +9458,7 @@ function PaymentForm({ config, eventDetails, formData, totalAmount, discountCode
9354
9458
  currency: "eur",
9355
9459
  participants: formData.participants.filter((p) => p.name?.trim()),
9356
9460
  discountCode: discountCode?.code,
9461
+ giftCardCodes: giftCards?.map((gc) => gc.code) || [],
9357
9462
  customerName: formData.customerName?.trim(),
9358
9463
  customerEmail: formData.customerEmail?.trim(),
9359
9464
  customerPhone: formData.customerPhone?.trim(),
@@ -9415,8 +9520,21 @@ function PaymentForm({ config, eventDetails, formData, totalAmount, discountCode
9415
9520
  formData.customerName,
9416
9521
  totalAmount,
9417
9522
  discountCode,
9523
+ giftCards,
9418
9524
  config,
9419
9525
  ]);
9526
+ // Calculate total gift card coverage
9527
+ const totalGiftCardAmount = giftCards?.reduce((sum, gc) => sum + (gc.balanceToUse || gc.discountAmount || 0), 0) || 0;
9528
+ const baseTotal = eventDetails?.price
9529
+ ? eventDetails.price * (formData.participants?.filter((p) => p.name?.trim()).length || 0)
9530
+ : 0;
9531
+ const discountAmount = discountCode?.discountAmount || 0;
9532
+ const amountAfterDiscount = Math.max(0, baseTotal - discountAmount);
9533
+ const isFullyCoveredByGiftCards = totalGiftCardAmount >= amountAfterDiscount && amountAfterDiscount > 0;
9534
+ // If gift cards fully cover the payment, show a simplified booking button
9535
+ if (isFullyCoveredByGiftCards && totalAmount <= 0) {
9536
+ return (jsx(GiftCardOnlyBooking, { config: config, eventDetails: eventDetails, formData: formData, discountCode: discountCode, giftCards: giftCards || [], onSuccess: onSuccess, onError: onError }));
9537
+ }
9420
9538
  // Show loading state while creating payment intent
9421
9539
  if (isCreatingPaymentIntent || !clientSecret) {
9422
9540
  return (jsxs("div", { style: {
@@ -9450,7 +9568,7 @@ function PaymentForm({ config, eventDetails, formData, totalAmount, discountCode
9450
9568
  clientSecret,
9451
9569
  appearance: stripeAppearance || { theme: "stripe" },
9452
9570
  locale: config.locale || "de",
9453
- }, children: jsx(PaymentFormInner, { config: config, eventDetails: eventDetails, formData: formData, totalAmount: totalAmount, discountCode: discountCode, onSuccess: (result) => {
9571
+ }, children: jsx(PaymentFormInner, { config: config, eventDetails: eventDetails, formData: formData, totalAmount: totalAmount, discountCode: discountCode, giftCards: giftCards, onSuccess: (result) => {
9454
9572
  // Clear persisted PI data on successful payment
9455
9573
  clearPersistedPaymentIntent();
9456
9574
  setPaymentIntentId(null);
@@ -9655,6 +9773,222 @@ function Accordion({ title, priceInfo, children, isOpen, onToggle }) {
9655
9773
  }, children: children }))] }));
9656
9774
  }
9657
9775
 
9776
+ // Icons
9777
+ const IconTicket = ({ size = 20, color = "currentColor" }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z" }), jsx("path", { d: "M13 5v2" }), jsx("path", { d: "M13 17v2" }), jsx("path", { d: "M13 11v2" })] }));
9778
+ const IconGift = ({ size = 20, color = "currentColor" }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("rect", { x: "3", y: "8", width: "18", height: "4", rx: "1" }), jsx("path", { d: "M12 8v13" }), jsx("path", { d: "M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7" }), jsx("path", { d: "M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5" })] }));
9779
+ const IconCheck = ({ size = 16, color = "currentColor" }) => (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "20 6 9 17 4 12" }) }));
9780
+ const IconX = ({ size = 16, color = "currentColor" }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }));
9781
+ const IconSpinner = ({ size = 16, color = "currentColor" }) => (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { animation: "spin 1s linear infinite" }, children: jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }));
9782
+ function VoucherInput({ config, orderValue, eventInstanceId, customerEmail, onVoucherValidated, appliedVouchers, onRemoveVoucher, disabled = false, }) {
9783
+ const [inputValue, setInputValue] = useState("");
9784
+ const [isLoading, setIsLoading] = useState(false);
9785
+ const [error, setError] = useState(null);
9786
+ const [isExpanded, setIsExpanded] = useState(false);
9787
+ // Check if a discount code is already applied (only one allowed)
9788
+ const hasDiscountCode = appliedVouchers.some((v) => v.type === "discount");
9789
+ const validateVoucher = useCallback(async (code) => {
9790
+ if (!code.trim()) {
9791
+ setError(null);
9792
+ return;
9793
+ }
9794
+ // Check if code is already applied
9795
+ if (appliedVouchers.some((v) => v.code.toUpperCase() === code.toUpperCase())) {
9796
+ setError("Dieser Code wurde bereits angewendet");
9797
+ return;
9798
+ }
9799
+ setIsLoading(true);
9800
+ setError(null);
9801
+ try {
9802
+ const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/validate-voucher"), {
9803
+ method: "POST",
9804
+ headers: createApiHeaders(config),
9805
+ body: JSON.stringify(createRequestBody(config, {
9806
+ code: code.trim(),
9807
+ orderValue: orderValue,
9808
+ eventInstanceId: eventInstanceId,
9809
+ customerEmail: customerEmail,
9810
+ })),
9811
+ });
9812
+ const data = await response.json();
9813
+ if (data.valid && data.voucher) {
9814
+ // Check if trying to add a second discount code
9815
+ if (data.voucher.type === "discount" && hasDiscountCode) {
9816
+ setError("Es kann nur ein Rabattcode verwendet werden");
9817
+ onVoucherValidated(null, "Es kann nur ein Rabattcode verwendet werden");
9818
+ return;
9819
+ }
9820
+ onVoucherValidated(data.voucher);
9821
+ setInputValue("");
9822
+ setError(null);
9823
+ }
9824
+ else {
9825
+ setError(data.error || "Code nicht gefunden oder ungültig");
9826
+ onVoucherValidated(null, data.error);
9827
+ }
9828
+ }
9829
+ catch (err) {
9830
+ const errorMsg = "Fehler beim Validieren des Codes";
9831
+ setError(errorMsg);
9832
+ onVoucherValidated(null, errorMsg);
9833
+ }
9834
+ finally {
9835
+ setIsLoading(false);
9836
+ }
9837
+ }, [
9838
+ config,
9839
+ orderValue,
9840
+ eventInstanceId,
9841
+ customerEmail,
9842
+ appliedVouchers,
9843
+ hasDiscountCode,
9844
+ onVoucherValidated,
9845
+ ]);
9846
+ const handleSubmit = () => {
9847
+ if (inputValue.trim() && !isLoading && !disabled) {
9848
+ validateVoucher(inputValue);
9849
+ }
9850
+ };
9851
+ const handleKeyDown = (e) => {
9852
+ if (e.key === "Enter") {
9853
+ e.preventDefault();
9854
+ if (inputValue.trim() && !isLoading && !disabled) {
9855
+ validateVoucher(inputValue);
9856
+ }
9857
+ }
9858
+ };
9859
+ const inputStyle = {
9860
+ flex: 1,
9861
+ padding: "10px 12px",
9862
+ backgroundColor: "var(--bw-background-color)",
9863
+ border: "1px solid var(--bw-border-color)",
9864
+ borderRadius: "var(--bw-border-radius)",
9865
+ color: "var(--bw-text-color)",
9866
+ fontSize: "var(--bw-font-size)",
9867
+ fontFamily: "var(--bw-font-family)",
9868
+ outline: "none",
9869
+ transition: "all 0.2s ease",
9870
+ textTransform: "uppercase",
9871
+ };
9872
+ const buttonStyle = {
9873
+ padding: "10px 16px",
9874
+ backgroundColor: "var(--bw-highlight-color)",
9875
+ border: "none",
9876
+ borderRadius: "var(--bw-border-radius)",
9877
+ color: "#fff",
9878
+ fontSize: "var(--bw-font-size)",
9879
+ fontFamily: "var(--bw-font-family)",
9880
+ fontWeight: "600",
9881
+ cursor: disabled || isLoading ? "not-allowed" : "pointer",
9882
+ opacity: disabled || isLoading ? 0.6 : 1,
9883
+ transition: "all 0.2s ease",
9884
+ display: "flex",
9885
+ alignItems: "center",
9886
+ justifyContent: "center",
9887
+ gap: "6px",
9888
+ minWidth: "100px",
9889
+ };
9890
+ const appliedVoucherStyle = {
9891
+ display: "flex",
9892
+ alignItems: "center",
9893
+ justifyContent: "space-between",
9894
+ padding: "10px 12px",
9895
+ backgroundColor: "var(--bw-surface-color)",
9896
+ border: "1px solid var(--bw-border-color)",
9897
+ borderRadius: "var(--bw-border-radius)",
9898
+ marginBottom: "8px",
9899
+ };
9900
+ const removeButtonStyle = {
9901
+ background: "none",
9902
+ border: "none",
9903
+ padding: "4px",
9904
+ cursor: "pointer",
9905
+ color: "var(--bw-error-color)",
9906
+ display: "flex",
9907
+ alignItems: "center",
9908
+ justifyContent: "center",
9909
+ borderRadius: "50%",
9910
+ transition: "background-color 0.2s ease",
9911
+ };
9912
+ return (jsxs("div", { style: {
9913
+ backgroundColor: "var(--bw-surface-color)",
9914
+ border: "1px solid var(--bw-border-color)",
9915
+ borderRadius: "var(--bw-border-radius)",
9916
+ overflow: "hidden",
9917
+ }, children: [jsxs("button", { type: "button", onClick: () => setIsExpanded(!isExpanded), style: {
9918
+ width: "100%",
9919
+ padding: "var(--bw-spacing)",
9920
+ backgroundColor: "transparent",
9921
+ border: "none",
9922
+ cursor: "pointer",
9923
+ display: "flex",
9924
+ alignItems: "center",
9925
+ justifyContent: "space-between",
9926
+ color: "var(--bw-text-color)",
9927
+ fontFamily: "var(--bw-font-family)",
9928
+ fontSize: "var(--bw-font-size)",
9929
+ fontWeight: "500",
9930
+ }, children: [jsxs("span", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [jsx(IconTicket, { size: 18, color: "var(--bw-highlight-color)" }), "Rabattcode oder Gutschein", appliedVouchers.length > 0 && (jsx("span", { style: {
9931
+ backgroundColor: "var(--bw-highlight-color)",
9932
+ color: "#fff",
9933
+ padding: "2px 8px",
9934
+ borderRadius: "12px",
9935
+ fontSize: "12px",
9936
+ fontWeight: "600",
9937
+ }, children: appliedVouchers.length }))] }), jsx("span", { style: {
9938
+ transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
9939
+ transition: "transform 0.2s ease",
9940
+ }, children: "\u25BC" })] }), isExpanded && (jsxs("div", { style: { padding: "0 var(--bw-spacing) var(--bw-spacing)" }, children: [appliedVouchers.length > 0 && (jsx("div", { style: { marginBottom: "12px" }, children: appliedVouchers.map((voucher) => (jsxs("div", { style: appliedVoucherStyle, children: [jsxs("div", { style: { display: "flex", alignItems: "center", gap: "10px" }, children: [voucher.type === "discount" ? (jsx(IconTicket, { size: 18, color: "var(--bw-success-color)" })) : (jsx(IconGift, { size: 18, color: "var(--bw-success-color)" })), jsxs("div", { children: [jsxs("div", { style: {
9941
+ fontFamily: "var(--bw-font-family)",
9942
+ fontSize: "var(--bw-font-size)",
9943
+ fontWeight: "600",
9944
+ color: "var(--bw-text-color)",
9945
+ display: "flex",
9946
+ alignItems: "center",
9947
+ gap: "6px",
9948
+ }, children: [jsx("span", { style: { fontFamily: "monospace" }, children: voucher.code }), jsx(IconCheck, { size: 14, color: "var(--bw-success-color)" })] }), jsxs("div", { style: {
9949
+ fontFamily: "var(--bw-font-family)",
9950
+ fontSize: "12px",
9951
+ color: "var(--bw-success-color)",
9952
+ }, children: [voucher.type === "discount"
9953
+ ? `−${formatCurrency(voucher.discountAmount)} Rabatt`
9954
+ : `−${formatCurrency(voucher.balanceToUse || voucher.discountAmount)} Gutschein`, voucher.type === "giftCard" &&
9955
+ voucher.remainingBalance !== undefined &&
9956
+ voucher.remainingBalance > 0 && (jsxs("span", { style: { color: "var(--bw-text-muted)", marginLeft: "8px" }, children: ["(Rest: ", formatCurrency(voucher.remainingBalance), ")"] }))] })] })] }), jsx("button", { type: "button", onClick: () => onRemoveVoucher(voucher.code), style: removeButtonStyle, title: "Entfernen", children: jsx(IconX, { size: 16 }) })] }, voucher.code))) })), jsxs("div", { style: { display: "flex", gap: "8px" }, children: [jsx("input", { type: "text", value: inputValue, onChange: (e) => {
9957
+ setInputValue(e.target.value.toUpperCase());
9958
+ setError(null);
9959
+ }, onKeyDown: handleKeyDown, placeholder: hasDiscountCode
9960
+ ? "Gutscheincode eingeben..."
9961
+ : "Rabatt- oder Gutscheincode eingeben...", style: inputStyle, disabled: disabled || isLoading, onFocus: (e) => {
9962
+ e.target.style.borderColor = "var(--bw-highlight-color)";
9963
+ e.target.style.boxShadow = "0 0 0 2px var(--bw-highlight-color)33";
9964
+ }, onBlur: (e) => {
9965
+ e.target.style.borderColor = "var(--bw-border-color)";
9966
+ e.target.style.boxShadow = "none";
9967
+ } }), jsx("button", { type: "button", onClick: handleSubmit, style: buttonStyle, disabled: disabled || isLoading || !inputValue.trim(), children: isLoading ? (jsx(IconSpinner, { size: 16, color: "#fff" })) : (jsxs(Fragment, { children: [jsx(IconCheck, { size: 16 }), "Einl\u00F6sen"] })) })] }), error && (jsxs("div", { style: {
9968
+ marginTop: "8px",
9969
+ padding: "8px 12px",
9970
+ backgroundColor: "var(--bw-error-color)15",
9971
+ border: "1px solid var(--bw-error-color)40",
9972
+ borderRadius: "var(--bw-border-radius)",
9973
+ color: "var(--bw-error-color)",
9974
+ fontSize: "var(--bw-font-size)",
9975
+ fontFamily: "var(--bw-font-family)",
9976
+ display: "flex",
9977
+ alignItems: "center",
9978
+ gap: "8px",
9979
+ }, children: [jsx(IconX, { size: 16 }), error] })), hasDiscountCode && (jsx("div", { style: {
9980
+ marginTop: "8px",
9981
+ fontSize: "12px",
9982
+ color: "var(--bw-text-muted)",
9983
+ fontFamily: "var(--bw-font-family)",
9984
+ }, children: "\uD83D\uDCA1 Es wurde bereits ein Rabattcode angewendet. Du kannst weitere Gutscheine hinzuf\u00FCgen." }))] })), jsx("style", { children: `
9985
+ @keyframes spin {
9986
+ from { transform: rotate(0deg); }
9987
+ to { transform: rotate(360deg); }
9988
+ }
9989
+ ` })] }));
9990
+ }
9991
+
9658
9992
  // Form schemas
9659
9993
  const participantSchema = objectType({
9660
9994
  name: stringType().min(1, "Name ist erforderlich"),
@@ -9674,6 +10008,10 @@ const bookingFormSchema = objectType({
9674
10008
  const IconWarning = ({ size = 48, color = "var(--bw-error-color)" }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("circle", { cx: "12", cy: "12", r: "10" }), jsx("line", { x1: "12", y1: "8", x2: "12", y2: "12" }), jsx("circle", { cx: "12", cy: "16", r: "1" })] }));
9675
10009
  const IconMoney = ({ size = 20, color = "var(--bw-text-muted)" }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("rect", { x: "2", y: "6", width: "20", height: "12", rx: "2" }), jsx("circle", { cx: "12", cy: "12", r: "4" }), jsx("line", { x1: "2", y1: "10", x2: "2", y2: "14" }), jsx("line", { x1: "22", y1: "10", x2: "22", y2: "14" })] }));
9676
10010
  function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError, onBackToEventInstances, onBackToEventTypes, selectedEventType, selectedEventInstance, isOpen, onClose, systemConfig, }) {
10011
+ // New voucher system - supports multiple gift cards + one discount code
10012
+ const [appliedVouchers, setAppliedVouchers] = useState([]);
10013
+ const [voucherError, setVoucherError] = useState(null);
10014
+ // Legacy state for backward compatibility
9677
10015
  const [discountCode, setDiscountCode] = useState(null);
9678
10016
  const [discountLoading, setDiscountLoading] = useState(false);
9679
10017
  const [discountError, setDiscountError] = useState(null);
@@ -9687,28 +10025,66 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
9687
10025
  },
9688
10026
  });
9689
10027
  const watchedParticipants = form.watch("participants");
9690
- const watchedDiscountCode = form.watch("discountCode");
10028
+ form.watch("discountCode");
9691
10029
  const watchedCustomerName = form.watch("customerName");
9692
10030
  const watchedCustomerEmail = form.watch("customerEmail");
9693
10031
  const customerNameError = form.formState.errors.customerName;
9694
10032
  const customerEmailError = form.formState.errors.customerEmail;
9695
10033
  const watchedAcceptTerms = form.watch("acceptTerms");
9696
- // Calculate total amount and deposit amount
9697
- const calculateTotal = () => {
10034
+ // Calculate base total before any discounts
10035
+ const calculateBaseTotal = useCallback(() => {
9698
10036
  if (!eventDetails)
9699
10037
  return 0;
9700
- const baseTotal = eventDetails.price * watchedParticipants.filter((p) => p.name.trim()).length;
9701
- return discountCode ? discountCode.newTotal : baseTotal;
9702
- };
10038
+ return eventDetails.price * watchedParticipants.filter((p) => p.name.trim()).length;
10039
+ }, [eventDetails, watchedParticipants]);
10040
+ // Calculate total discount from all applied vouchers
10041
+ const calculateTotalDiscount = useCallback(() => {
10042
+ return appliedVouchers.reduce((total, voucher) => {
10043
+ if (voucher.type === "discount") {
10044
+ return total + voucher.discountAmount;
10045
+ }
10046
+ else if (voucher.type === "giftCard") {
10047
+ return total + (voucher.balanceToUse || voucher.discountAmount);
10048
+ }
10049
+ return total;
10050
+ }, 0);
10051
+ }, [appliedVouchers]);
10052
+ // Calculate total amount after discounts
10053
+ const calculateTotal = useCallback(() => {
10054
+ const baseTotal = calculateBaseTotal();
10055
+ const totalDiscount = calculateTotalDiscount();
10056
+ return Math.max(0, baseTotal - totalDiscount);
10057
+ }, [calculateBaseTotal, calculateTotalDiscount]);
9703
10058
  const calculateDeposit = () => {
9704
10059
  if (!eventDetails || !eventDetails.deposit)
9705
10060
  return 0;
9706
10061
  const participantCount = watchedParticipants.filter((p) => p.name.trim()).length;
9707
10062
  return eventDetails.deposit * participantCount;
9708
10063
  };
10064
+ const baseTotal = calculateBaseTotal();
10065
+ const totalDiscount = calculateTotalDiscount();
9709
10066
  const totalAmount = calculateTotal();
9710
10067
  const depositAmount = calculateDeposit();
9711
- const paymentAmount = depositAmount > 0 ? depositAmount : totalAmount;
10068
+ // If there's a deposit, we pay the deposit; otherwise we pay the total after discounts
10069
+ const paymentAmount = depositAmount > 0 ? Math.max(0, depositAmount - totalDiscount) : totalAmount;
10070
+ // Get discount code for legacy compatibility
10071
+ const appliedDiscountCode = appliedVouchers.find((v) => v.type === "discount");
10072
+ // Get gift cards
10073
+ const appliedGiftCards = appliedVouchers.filter((v) => v.type === "giftCard");
10074
+ // Voucher handlers
10075
+ const handleVoucherValidated = useCallback((voucher, error) => {
10076
+ if (error) {
10077
+ setVoucherError(error);
10078
+ return;
10079
+ }
10080
+ if (voucher) {
10081
+ setAppliedVouchers((prev) => [...prev, voucher]);
10082
+ setVoucherError(null);
10083
+ }
10084
+ }, []);
10085
+ const handleRemoveVoucher = useCallback((code) => {
10086
+ setAppliedVouchers((prev) => prev.filter((v) => v.code !== code));
10087
+ }, []);
9712
10088
  // Form validation helper
9713
10089
  const isFormValid = () => {
9714
10090
  const participantCount = watchedParticipants.filter((p) => p.name.trim()).length;
@@ -9722,48 +10098,51 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
9722
10098
  const hasAcceptedTerms = watchedAcceptTerms === true;
9723
10099
  return validParticipants && participantsWithinLimit && hasName && hasEmail && hasAcceptedTerms;
9724
10100
  };
9725
- // Validate discount codes
10101
+ // Re-validate vouchers when participant count changes (affects order value)
9726
10102
  useEffect(() => {
9727
- const validateDiscountCode = async (code) => {
9728
- if (!code.trim() || !eventDetails) {
9729
- setDiscountCode(null);
9730
- setDiscountError(null);
9731
- return;
9732
- }
9733
- setDiscountLoading(true);
9734
- setDiscountError(null);
9735
- try {
9736
- const baseTotal = eventDetails.price * watchedParticipants.filter((p) => p.name.trim()).length;
9737
- const response = await fetch(getApiUrl(config.apiBaseUrl, "/booking/validate-discount"), {
9738
- method: "POST",
9739
- headers: createApiHeaders(config),
9740
- body: JSON.stringify(createRequestBody(config, {
9741
- code: code.trim(),
9742
- orderValue: baseTotal,
9743
- })),
9744
- });
9745
- const data = await response.json();
9746
- if (data.valid) {
9747
- setDiscountCode(data.discountCode);
10103
+ // When participants change, we need to recalculate voucher amounts
10104
+ // For now, we'll clear vouchers if the order value changes significantly
10105
+ // In a production app, you might want to re-validate each voucher
10106
+ if (appliedVouchers.length > 0) {
10107
+ // Recalculate discount amounts based on new order value
10108
+ const newBaseTotal = eventDetails?.price
10109
+ ? eventDetails.price * watchedParticipants.filter((p) => p.name.trim()).length
10110
+ : 0;
10111
+ // Update voucher amounts (simplified - in production, re-validate via API)
10112
+ setAppliedVouchers((prev) => prev.map((voucher) => {
10113
+ if (voucher.type === "discount") {
10114
+ let newDiscountAmount = 0;
10115
+ if (voucher.discountType === "percentage") {
10116
+ newDiscountAmount = Math.round((newBaseTotal * (voucher.discountValue || 0)) / 10000);
10117
+ }
10118
+ else {
10119
+ newDiscountAmount = voucher.discountValue || 0;
10120
+ }
10121
+ newDiscountAmount = Math.min(newDiscountAmount, newBaseTotal);
10122
+ return {
10123
+ ...voucher,
10124
+ discountAmount: newDiscountAmount,
10125
+ newTotal: newBaseTotal - newDiscountAmount,
10126
+ };
9748
10127
  }
9749
- else {
9750
- setDiscountCode(null);
9751
- setDiscountError(data.error);
10128
+ else if (voucher.type === "giftCard") {
10129
+ // Gift card balance stays the same, but amount to use might change
10130
+ const remainingAfterDiscount = newBaseTotal -
10131
+ prev
10132
+ .filter((v) => v.type === "discount")
10133
+ .reduce((sum, v) => sum + v.discountAmount, 0);
10134
+ const balanceToUse = Math.min(voucher.currentBalance || 0, Math.max(0, remainingAfterDiscount));
10135
+ return {
10136
+ ...voucher,
10137
+ balanceToUse,
10138
+ remainingBalance: (voucher.currentBalance || 0) - balanceToUse,
10139
+ discountAmount: balanceToUse,
10140
+ };
9752
10141
  }
9753
- }
9754
- catch (err) {
9755
- setDiscountError("Fehler beim Validieren des Rabattcodes");
9756
- setDiscountCode(null);
9757
- }
9758
- finally {
9759
- setDiscountLoading(false);
9760
- }
9761
- };
9762
- const timer = setTimeout(() => {
9763
- validateDiscountCode(watchedDiscountCode || "");
9764
- }, 500);
9765
- return () => clearTimeout(timer);
9766
- }, [watchedDiscountCode, watchedParticipants, eventDetails, config]);
10142
+ return voucher;
10143
+ }));
10144
+ }
10145
+ }, [watchedParticipants, eventDetails]);
9767
10146
  // Helper functions
9768
10147
  const addParticipant = () => {
9769
10148
  const currentParticipants = form.getValues("participants");
@@ -9918,7 +10297,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
9918
10297
  color: "var(--bw-text-color)",
9919
10298
  fontWeight: "500",
9920
10299
  fontFamily: "var(--bw-font-family)",
9921
- }, children: [formatCurrency(eventDetails.price), " pro Person"] })] })] })] }), jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "var(--bw-spacing-large)" }, children: [jsxs("form", { style: { display: "flex", flexDirection: "column", gap: "var(--bw-spacing-large)" }, children: [jsxs("div", { style: {
10300
+ }, children: [formatCurrency(eventDetails.price), " pro Person"] })] })] })] }), jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "var(--bw-spacing-large)" }, children: [jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "var(--bw-spacing-large)" }, children: [jsxs("div", { style: {
9922
10301
  backgroundColor: "var(--bw-surface-color)",
9923
10302
  border: `1px solid var(--bw-border-color)`,
9924
10303
  backdropFilter: "blur(4px)",
@@ -10063,7 +10442,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
10063
10442
  color: "var(--bw-error-color)",
10064
10443
  fontSize: "var(--bw-font-size)",
10065
10444
  fontFamily: "var(--bw-font-family)",
10066
- }, children: ["Maximal ", eventDetails.availableSpots, " Pl\u00E4tze verf\u00FCgbar."] }))] })] }), jsxs("div", { style: {
10445
+ }, children: ["Maximal ", eventDetails.availableSpots, " Pl\u00E4tze verf\u00FCgbar."] }))] })] }), jsx(VoucherInput, { config: config, orderValue: baseTotal, eventInstanceId: eventDetails?.id, customerEmail: watchedCustomerEmail, onVoucherValidated: handleVoucherValidated, appliedVouchers: appliedVouchers, onRemoveVoucher: handleRemoveVoucher, disabled: !eventDetails }), jsxs("div", { style: {
10067
10446
  backgroundColor: "var(--bw-surface-color)",
10068
10447
  border: `1px solid var(--bw-border-color)`,
10069
10448
  backdropFilter: "blur(4px)",
@@ -10162,7 +10541,7 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
10162
10541
  color: "var(--bw-text-color)",
10163
10542
  fontWeight: "500",
10164
10543
  fontFamily: "var(--bw-font-family)",
10165
- }, children: formatCurrency(eventDetails.deposit || 0) })] })), discountCode && (jsxs(Fragment, { children: [jsxs("div", { style: {
10544
+ }, children: formatCurrency(eventDetails.deposit || 0) })] })), appliedVouchers.length > 0 && (jsxs(Fragment, { children: [jsxs("div", { style: {
10166
10545
  display: "flex",
10167
10546
  justifyContent: "space-between",
10168
10547
  alignItems: "center",
@@ -10171,20 +10550,31 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
10171
10550
  fontFamily: "var(--bw-font-family)",
10172
10551
  }, children: "Zwischensumme:" }), jsx("span", { style: {
10173
10552
  color: "var(--bw-text-muted)",
10174
- textDecoration: "line-through",
10553
+ textDecoration: totalDiscount > 0 ? "line-through" : "none",
10175
10554
  fontFamily: "var(--bw-font-family)",
10176
- }, children: formatCurrency(eventDetails.price *
10177
- watchedParticipants.filter((p) => p.name.trim()).length) })] }), jsxs("div", { style: {
10555
+ }, children: formatCurrency(baseTotal) })] }), appliedDiscountCode && (jsxs("div", { style: {
10178
10556
  display: "flex",
10179
10557
  justifyContent: "space-between",
10180
10558
  alignItems: "center",
10181
- }, children: [jsx("span", { style: {
10559
+ }, children: [jsxs("span", { style: {
10560
+ color: "var(--bw-success-color)",
10561
+ fontFamily: "var(--bw-font-family)",
10562
+ fontSize: "var(--bw-font-size)",
10563
+ }, children: ["Rabatt (", appliedDiscountCode.code, "):"] }), jsxs("span", { style: {
10182
10564
  color: "var(--bw-success-color)",
10183
10565
  fontFamily: "var(--bw-font-family)",
10184
- }, children: "Rabatt:" }), jsxs("span", { style: {
10566
+ }, children: ["-", formatCurrency(appliedDiscountCode.discountAmount)] })] })), appliedGiftCards.map((giftCard) => (jsxs("div", { style: {
10567
+ display: "flex",
10568
+ justifyContent: "space-between",
10569
+ alignItems: "center",
10570
+ }, children: [jsxs("span", { style: {
10185
10571
  color: "var(--bw-success-color)",
10186
10572
  fontFamily: "var(--bw-font-family)",
10187
- }, children: ["-", formatCurrency(discountCode.discountAmount)] })] })] })), jsxs("div", { style: {
10573
+ fontSize: "var(--bw-font-size)",
10574
+ }, children: ["Gutschein (", giftCard.code, "):"] }), jsxs("span", { style: {
10575
+ color: "var(--bw-success-color)",
10576
+ fontFamily: "var(--bw-font-family)",
10577
+ }, children: ["-", formatCurrency(giftCard.balanceToUse || giftCard.discountAmount)] })] }, giftCard.code)))] })), jsxs("div", { style: {
10188
10578
  borderTop: `1px solid var(--bw-border-color)`,
10189
10579
  paddingTop: "12px",
10190
10580
  }, children: [depositAmount > 0 && (jsxs("div", { style: {
@@ -10257,7 +10647,15 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
10257
10647
  fontFamily: "var(--bw-font-family)",
10258
10648
  borderBottom: "2px solid var(--bw-highlight-color)",
10259
10649
  paddingBottom: 4,
10260
- }, children: "Zahlung" }), jsx(PaymentForm, { config: config, eventDetails: eventDetails, formData: form.getValues(), totalAmount: paymentAmount, discountCode: discountCode, onSuccess: onSuccess, onError: onError, systemConfig: systemConfig, stripePromise: stripePromise, stripeAppearance: stripeAppearance })] }));
10650
+ }, children: "Zahlung" }), jsx(PaymentForm, { config: config, eventDetails: eventDetails, formData: form.getValues(), totalAmount: paymentAmount, discountCode: appliedDiscountCode ? {
10651
+ id: appliedDiscountCode.id,
10652
+ code: appliedDiscountCode.code,
10653
+ description: appliedDiscountCode.description || undefined,
10654
+ type: appliedDiscountCode.discountType || "percentage",
10655
+ value: appliedDiscountCode.discountValue || 0,
10656
+ discountAmount: appliedDiscountCode.discountAmount,
10657
+ newTotal: appliedDiscountCode.newTotal,
10658
+ } : null, giftCards: appliedGiftCards, onSuccess: onSuccess, onError: onError, systemConfig: systemConfig, stripePromise: stripePromise, stripeAppearance: stripeAppearance })] }));
10261
10659
  })()] })] }), jsx("style", { children: `
10262
10660
  .booking-widget-container *,
10263
10661
  .booking-widget-container *::before,
@@ -10908,16 +11306,7 @@ const spinner = (borderColor) => (jsx("div", { style: {
10908
11306
  } }) }));
10909
11307
  function EventInstanceSelection({ eventInstances, selectedEventType, onEventInstanceSelect, onBackToEventTypes, isOpen, onClose, isLoadingEventInstances = false, isLoadingEventDetails = false, }) {
10910
11308
  const [selectedEventInstanceId, setSelectedEventInstanceId] = useState(null);
10911
- const [openMonths, setOpenMonths] = useState(new Set());
10912
- const getEventsForMonth = (monthIndex) => {
10913
- return eventInstances.filter((instance) => new Date(instance.startTime).getMonth() === monthIndex);
10914
- };
10915
- const getMinPriceForMonth = (monthIndex) => {
10916
- const monthEvents = getEventsForMonth(monthIndex);
10917
- if (monthEvents.length === 0)
10918
- return 0;
10919
- return Math.min(...monthEvents.map((event) => event.price));
10920
- };
11309
+ const [openGroups, setOpenGroups] = useState(new Set());
10921
11310
  const getMonthPriceDisplayInfo = (minPrice) => {
10922
11311
  return getPriceDisplayInfo(minPrice, yearPrices);
10923
11312
  };
@@ -10927,27 +11316,48 @@ function EventInstanceSelection({ eventInstances, selectedEventType, onEventInst
10927
11316
  .filter((instance) => new Date(instance.startTime).getFullYear() === currentYear)
10928
11317
  .map((instance) => instance.price);
10929
11318
  const today = new Date();
10930
- // Get months that have events
10931
- const monthsWithEvents = months
10932
- .map((month, index) => ({
10933
- month,
10934
- index,
10935
- events: getEventsForMonth(index).sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime()),
10936
- minPrice: getMinPriceForMonth(index),
10937
- }))
10938
- .filter((monthData) => monthData.events.length > 0);
11319
+ // Group events by month and year, then sort chronologically (e.g., Aug 2025 before Apr 2026)
11320
+ const monthYearGroups = (() => {
11321
+ const groupsMap = new Map();
11322
+ for (const instance of eventInstances) {
11323
+ const date = new Date(instance.startTime);
11324
+ const year = date.getFullYear();
11325
+ const monthIndex = date.getMonth();
11326
+ const key = `${year}-${monthIndex}`;
11327
+ if (!groupsMap.has(key)) {
11328
+ groupsMap.set(key, {
11329
+ key,
11330
+ year,
11331
+ monthIndex,
11332
+ label: `${months[monthIndex]} ${year}`,
11333
+ events: [],
11334
+ minPrice: Number.POSITIVE_INFINITY,
11335
+ });
11336
+ }
11337
+ const group = groupsMap.get(key);
11338
+ group.events.push(instance);
11339
+ if (instance.price < group.minPrice)
11340
+ group.minPrice = instance.price;
11341
+ }
11342
+ return Array.from(groupsMap.values())
11343
+ .map((group) => ({
11344
+ ...group,
11345
+ events: group.events.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime()),
11346
+ }))
11347
+ .sort((a, b) => (a.year === b.year ? a.monthIndex - b.monthIndex : a.year - b.year));
11348
+ })();
10939
11349
  const handleEventInstanceSelect = (eventInstance) => {
10940
11350
  setSelectedEventInstanceId(eventInstance.id);
10941
11351
  onEventInstanceSelect(eventInstance);
10942
11352
  };
10943
- const toggleMonth = (monthIndex) => {
10944
- if (openMonths.has(monthIndex)) {
11353
+ const toggleGroup = (key) => {
11354
+ if (openGroups.has(key)) {
10945
11355
  // Close if already open
10946
- setOpenMonths(new Set());
11356
+ setOpenGroups(new Set());
10947
11357
  }
10948
11358
  else {
10949
11359
  // Open only this one, close others
10950
- setOpenMonths(new Set([monthIndex]));
11360
+ setOpenGroups(new Set([key]));
10951
11361
  }
10952
11362
  };
10953
11363
  const handleClose = () => {
@@ -11204,10 +11614,7 @@ function EventInstanceSelection({ eventInstances, selectedEventType, onEventInst
11204
11614
  minHeight: "400px",
11205
11615
  textAlign: "center",
11206
11616
  padding: "var(--bw-spacing)",
11207
- }, children: jsxs("div", { children: [jsx("div", { style: {
11208
- margin: "0 auto 16px",
11209
- fontSize: "48px",
11210
- }, children: "\uD83D\uDCC5" }), jsx("h3", { style: {
11617
+ }, children: jsxs("div", { children: [jsx("h3", { style: {
11211
11618
  marginBottom: "8px",
11212
11619
  fontWeight: "600",
11213
11620
  fontSize: "var(--bw-font-size-large)",
@@ -11275,136 +11682,154 @@ function EventInstanceSelection({ eventInstances, selectedEventType, onEventInst
11275
11682
  font-size: 1.1rem !important;
11276
11683
  }
11277
11684
  }
11278
- ` }), jsx(Sidebar, { isOpen: isOpen, onClose: handleClose, title: `Terminauswahl - ${selectedEventType?.name || "Event"}`, children: jsx("div", { className: "bw-event-instance-list", style: { padding: "24px" }, children: jsx("div", { style: {
11685
+ ` }), jsx(Sidebar, { isOpen: isOpen, onClose: handleClose, title: `${selectedEventType?.name}`, children: jsx("div", { className: "bw-event-instance-list", style: { padding: "24px" }, children: jsx("div", { style: {
11279
11686
  display: "flex",
11280
11687
  flexDirection: "column",
11281
11688
  gap: "20px",
11282
- }, children: monthsWithEvents.map(({ month, index, events, minPrice }) => {
11689
+ }, children: monthYearGroups.map(({ key, label, events, minPrice, year }, idx) => {
11283
11690
  const monthPriceDisplayInfo = getMonthPriceDisplayInfo(minPrice);
11284
- return (jsx(Accordion, { title: month, priceInfo: jsx("div", { style: {
11285
- fontSize: "1rem",
11286
- backgroundColor: monthPriceDisplayInfo
11287
- ? monthPriceDisplayInfo.backgroundColor
11288
- : "#14532d",
11289
- color: monthPriceDisplayInfo ? monthPriceDisplayInfo.textColor : "#4ade80",
11290
- fontWeight: 500,
11291
- marginLeft: "auto",
11292
- padding: "4px 8px",
11293
- borderRadius: "var(--bw-border-radius-small)",
11294
- border: monthPriceDisplayInfo ? "none" : undefined,
11295
- boxShadow: monthPriceDisplayInfo
11296
- ? "0 2px 4px rgba(0, 0, 0, 0.2)"
11297
- : undefined,
11298
- }, children: `ab ${formatCurrency(minPrice)}` }), isOpen: openMonths.has(index), onToggle: () => toggleMonth(index), children: jsx("div", { style: {
11299
- display: "flex",
11300
- flexDirection: "column",
11301
- gap: "12px",
11302
- paddingTop: "12px",
11303
- }, children: events.map((event) => {
11304
- const availableSpots = event.maxParticipants - event.participantCount;
11305
- const isFullyBooked = availableSpots === 0;
11306
- const startDate = new Date(event.startTime);
11307
- const isPastEvent = today.toISOString() >= startDate.toISOString();
11308
- return (jsxs("div", { className: "bw-event-instance-card", style: {
11309
- position: "relative",
11310
- cursor: !isFullyBooked && !isPastEvent && event.bookingOpen
11311
- ? "pointer"
11312
- : "not-allowed",
11313
- border: "1px solid var(--bw-border-color)",
11314
- backgroundColor: "var(--bw-surface-color)",
11315
- borderRadius: "var(--bw-border-radius)",
11316
- padding: "16px 20px",
11317
- transition: "all 0.2s ease",
11318
- opacity: isFullyBooked || isPastEvent ? 0.3 : 1,
11319
- filter: isFullyBooked || isPastEvent ? "grayscale(40%)" : "none",
11320
- fontFamily: "var(--bw-font-family)",
11321
- }, onClick: () => {
11322
- if (!isFullyBooked && !isPastEvent && event.bookingOpen) {
11323
- handleEventInstanceSelect(event);
11324
- }
11325
- }, onMouseEnter: (e) => {
11326
- if (!isFullyBooked && !isPastEvent && event.bookingOpen) {
11327
- e.currentTarget.style.transform = "scale(1.02)";
11328
- e.currentTarget.style.backgroundColor =
11329
- "var(--bw-surface-muted, rgba(59, 130, 246, 0.1))";
11330
- }
11331
- }, onMouseLeave: (e) => {
11332
- if (!isFullyBooked && !isPastEvent && event.bookingOpen) {
11333
- e.currentTarget.style.transform = "scale(1)";
11334
- e.currentTarget.style.backgroundColor = "var(--bw-surface-color)";
11335
- }
11336
- }, children: [selectedEventInstanceId === event.id && isLoadingEventDetails && (jsx("div", { style: {
11337
- position: "absolute",
11338
- top: 0,
11339
- left: 0,
11340
- width: "100%",
11341
- height: "100%",
11342
- backgroundColor: "var(--bw-overlay-color, rgba(15, 23, 42, 0.8))",
11691
+ return (jsxs(Fragment$1, { children: [idx > 0 && monthYearGroups[idx - 1].year !== year && (jsx("div", { style: {
11692
+ height: 1,
11693
+ backgroundColor: "var(--bw-border-color)",
11694
+ margin: "4px 0",
11695
+ } })), jsx(Accordion, { title: label, priceInfo: jsx("div", { style: {
11696
+ fontSize: "1rem",
11697
+ backgroundColor: monthPriceDisplayInfo
11698
+ ? monthPriceDisplayInfo.backgroundColor
11699
+ : "#14532d",
11700
+ color: monthPriceDisplayInfo
11701
+ ? monthPriceDisplayInfo.textColor
11702
+ : "#4ade80",
11703
+ fontWeight: 500,
11704
+ marginLeft: "auto",
11705
+ padding: "4px 8px",
11706
+ borderRadius: "var(--bw-border-radius-small)",
11707
+ border: monthPriceDisplayInfo ? "none" : undefined,
11708
+ boxShadow: monthPriceDisplayInfo
11709
+ ? "0 2px 4px rgba(0, 0, 0, 0.2)"
11710
+ : undefined,
11711
+ }, children: `ab ${formatCurrency(minPrice)}` }), isOpen: openGroups.has(key), onToggle: () => toggleGroup(key), children: jsx("div", { style: {
11712
+ display: "flex",
11713
+ flexDirection: "column",
11714
+ gap: "12px",
11715
+ paddingTop: "12px",
11716
+ }, children: events.map((event) => {
11717
+ const availableSpots = event.maxParticipants - event.participantCount;
11718
+ const isFullyBooked = availableSpots === 0;
11719
+ const startDate = new Date(event.startTime);
11720
+ const isPastEvent = today.toISOString() >= startDate.toISOString();
11721
+ return (jsxs("div", { className: "bw-event-instance-card", style: {
11722
+ position: "relative",
11723
+ cursor: !isFullyBooked && !isPastEvent && event.bookingOpen
11724
+ ? "pointer"
11725
+ : "not-allowed",
11726
+ border: "1px solid var(--bw-border-color)",
11727
+ backgroundColor: "var(--bw-surface-color)",
11343
11728
  borderRadius: "var(--bw-border-radius)",
11344
- display: "flex",
11345
- alignItems: "center",
11346
- justifyContent: "center",
11347
- }, children: jsx("div", { style: {
11348
- width: "32px",
11349
- height: "32px",
11350
- color: "var(--bw-highlight-color-muted, rgba(59, 130, 246, 0.8))",
11351
- animation: "spin 1s linear infinite",
11352
- fontSize: "32px",
11353
- }, children: spinner() }) })), jsx(SpecialPriceBadge, { price: event.price, yearPrices: yearPrices }), jsx(AllocationBadge, { availableSpots: availableSpots, maxParticipants: event.maxParticipants }), jsxs("div", { style: {
11354
- display: "flex",
11355
- justifyContent: "space-between",
11356
- width: "100%",
11357
- alignItems: "start",
11358
- gap: "12px",
11359
- marginBottom: "4px",
11360
- }, children: [jsxs("div", { style: { display: "flex", alignItems: "start", gap: "12px" }, children: [jsx("div", { className: "bw-event-instance-datebox", style: {
11361
- fontSize: "var(--bw-font-size)",
11362
- transition: "all 0.2s ease",
11363
- borderRadius: "var(--bw-border-radius-small)",
11364
- borderTop: `4px solid var(--bw-border-color)`,
11365
- border: "1px solid var(--bw-border-color)",
11366
- width: "40px",
11367
- height: "40px",
11368
- display: "flex",
11369
- alignItems: "center",
11370
- justifyContent: "center",
11371
- fontWeight: "bold",
11372
- color: "var(--bw-text-color)",
11373
- backgroundColor: "var(--bw-background-color)",
11374
- }, children: startDate.getDate() }), jsxs("div", { style: {
11375
- fontSize: "var(--bw-font-size)",
11376
- color: "var(--bw-text-color)",
11729
+ padding: "16px 20px",
11730
+ transition: "all 0.2s ease",
11731
+ opacity: isFullyBooked || isPastEvent ? 0.3 : 1,
11732
+ filter: isFullyBooked || isPastEvent ? "grayscale(40%)" : "none",
11733
+ fontFamily: "var(--bw-font-family)",
11734
+ }, onClick: () => {
11735
+ if (!isFullyBooked && !isPastEvent && event.bookingOpen) {
11736
+ handleEventInstanceSelect(event);
11737
+ }
11738
+ }, onMouseEnter: (e) => {
11739
+ if (!isFullyBooked && !isPastEvent && event.bookingOpen) {
11740
+ e.currentTarget.style.transform = "scale(1.02)";
11741
+ e.currentTarget.style.backgroundColor =
11742
+ "var(--bw-surface-muted, rgba(59, 130, 246, 0.1))";
11743
+ }
11744
+ }, onMouseLeave: (e) => {
11745
+ if (!isFullyBooked && !isPastEvent && event.bookingOpen) {
11746
+ e.currentTarget.style.transform = "scale(1)";
11747
+ e.currentTarget.style.backgroundColor = "var(--bw-surface-color)";
11748
+ }
11749
+ }, children: [selectedEventInstanceId === event.id && isLoadingEventDetails && (jsx("div", { style: {
11750
+ position: "absolute",
11751
+ top: 0,
11752
+ left: 0,
11753
+ width: "100%",
11754
+ height: "100%",
11755
+ backgroundColor: "var(--bw-overlay-color, rgba(15, 23, 42, 0.8))",
11756
+ borderRadius: "var(--bw-border-radius)",
11757
+ display: "flex",
11758
+ alignItems: "center",
11759
+ justifyContent: "center",
11760
+ }, children: jsx("div", { style: {
11761
+ width: "32px",
11762
+ height: "32px",
11763
+ color: "var(--bw-highlight-color-muted, rgba(59, 130, 246, 0.8))",
11764
+ animation: "spin 1s linear infinite",
11765
+ fontSize: "32px",
11766
+ }, children: spinner() }) })), jsx(SpecialPriceBadge, { price: event.price, yearPrices: yearPrices }), jsx(AllocationBadge, { availableSpots: availableSpots, maxParticipants: event.maxParticipants }), jsxs("div", { style: {
11767
+ display: "flex",
11768
+ justifyContent: "space-between",
11769
+ width: "100%",
11770
+ alignItems: "start",
11771
+ gap: "12px",
11772
+ marginBottom: "4px",
11773
+ }, children: [jsxs("div", { style: { display: "flex", alignItems: "start", gap: "12px" }, children: [jsx("div", { className: "bw-event-instance-datebox", style: {
11774
+ fontSize: "var(--bw-font-size)",
11775
+ transition: "all 0.2s ease",
11776
+ borderRadius: "var(--bw-border-radius-small)",
11777
+ borderTop: `4px solid var(--bw-border-color)`,
11778
+ border: "1px solid var(--bw-border-color)",
11779
+ width: "40px",
11780
+ height: "40px",
11781
+ display: "flex",
11782
+ alignItems: "center",
11783
+ justifyContent: "center",
11784
+ fontWeight: "bold",
11785
+ color: "var(--bw-text-color)",
11786
+ backgroundColor: "var(--bw-background-color)",
11787
+ }, children: startDate.getDate() }), jsxs("div", { style: {
11788
+ fontSize: "var(--bw-font-size)",
11789
+ color: "var(--bw-text-color)",
11790
+ display: "flex",
11791
+ flexDirection: "column",
11792
+ alignItems: "start",
11793
+ justifyContent: "start",
11794
+ lineHeight: "1.2",
11795
+ }, children: [jsxs("div", { children: [jsx("span", { className: "bw-event-instance-title", style: { fontWeight: "600", marginBottom: "2px" }, children: formatWeekday(event.startTime) }), formatWeekday(event.startTime) !==
11796
+ formatWeekday(event.endTime) && (jsxs(Fragment, { children: [jsx("span", { style: {
11797
+ color: "var(--bw-text-muted)",
11798
+ fontSize: "14px",
11799
+ }, children: " - " }), jsx("span", { className: "bw-event-instance-title", style: { fontWeight: "600", marginBottom: "2px" }, children: formatWeekday(event.endTime) })] }))] }), jsx("div", { children: formatWeekday(event.startTime) ===
11800
+ formatWeekday(event.endTime) ? (jsxs(Fragment, { children: [jsx("span", { style: {
11801
+ color: "var(--bw-text-muted)",
11802
+ fontSize: "14px",
11803
+ }, children: formatTime(event.startTime) }), jsx("span", { style: {
11804
+ color: "var(--bw-text-muted)",
11805
+ fontSize: "14px",
11806
+ }, children: " - " }), jsx("span", { style: {
11807
+ color: "var(--bw-text-muted)",
11808
+ fontSize: "14px",
11809
+ }, children: formatTime(event.endTime) })] })) : (jsxs("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: [formatTime(event.startTime), " Uhr"] })) })] }), jsxs("span", { style: {
11810
+ fontSize: "12px",
11811
+ fontWeight: 400,
11812
+ color: "var(--bw-text-muted)",
11813
+ marginLeft: "6px",
11814
+ background: "var(--bw-background-muted)",
11815
+ whiteSpace: "nowrap",
11816
+ }, children: [event.durationDays, " Tag", event.durationDays > 1 ? "e" : ""] })] }), jsx("div", { className: "bw-event-instance-price", style: {
11817
+ textAlign: "right",
11377
11818
  display: "flex",
11378
11819
  flexDirection: "column",
11379
- alignItems: "start",
11380
- justifyContent: "start",
11381
- lineHeight: "1.2",
11382
- }, children: [jsxs("div", { children: [jsx("span", { className: "bw-event-instance-title", style: { fontWeight: "600", marginBottom: "2px" }, children: formatWeekday(event.startTime) }), formatWeekday(event.startTime) !==
11383
- formatWeekday(event.endTime) && (jsxs(Fragment, { children: [jsx("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: " - " }), jsx("span", { className: "bw-event-instance-title", style: { fontWeight: "600", marginBottom: "2px" }, children: formatWeekday(event.endTime) })] }))] }), jsx("div", { children: formatWeekday(event.startTime) ===
11384
- formatWeekday(event.endTime) ? (jsxs(Fragment, { children: [jsx("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: formatTime(event.startTime) }), jsx("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: " - " }), jsx("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: formatTime(event.endTime) })] })) : (jsxs("span", { style: { color: "var(--bw-text-muted)", fontSize: "14px" }, children: [formatTime(event.startTime), " Uhr"] })) })] }), jsxs("span", { style: {
11385
- fontSize: "12px",
11386
- fontWeight: 400,
11387
- color: "var(--bw-text-muted)",
11388
- marginLeft: "6px",
11389
- background: "var(--bw-background-muted)",
11390
- whiteSpace: "nowrap",
11391
- }, children: [event.durationDays, " Tag", event.durationDays > 1 ? "e" : ""] })] }), jsx("div", { className: "bw-event-instance-price", style: {
11392
- textAlign: "right",
11820
+ alignItems: "end",
11821
+ }, children: jsx(PriceDisplay, { price: event.price, yearPrices: yearPrices }) })] }), event.name !== selectedEventType?.name && (jsx("h4", { className: "bw-event-instance-title", style: {
11822
+ fontSize: "var(--bw-font-size)",
11823
+ fontWeight: "600",
11824
+ color: "var(--bw-text-color)",
11825
+ lineHeight: "1.25",
11826
+ margin: "0 0 2px 0",
11393
11827
  display: "flex",
11394
- flexDirection: "column",
11395
- alignItems: "end",
11396
- }, children: jsx(PriceDisplay, { price: event.price, yearPrices: yearPrices }) })] }), event.name !== selectedEventType?.name && (jsx("h4", { className: "bw-event-instance-title", style: {
11397
- fontSize: "var(--bw-font-size)",
11398
- fontWeight: "600",
11399
- color: "var(--bw-text-color)",
11400
- lineHeight: "1.25",
11401
- margin: "0 0 2px 0",
11402
- display: "flex",
11403
- alignItems: "center",
11404
- gap: "8px",
11405
- maxWidth: "230px",
11406
- }, children: event.name }))] }, event.id));
11407
- }) }) }, month));
11828
+ alignItems: "center",
11829
+ gap: "8px",
11830
+ maxWidth: "230px",
11831
+ }, children: event.name }))] }, event.id));
11832
+ }) }) })] }, key));
11408
11833
  }) }) }) })] }));
11409
11834
  }
11410
11835