@eventlook/sdk 1.4.43 → 1.4.45

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.
Files changed (80) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/dist/cjs/form/Payment.js +0 -2
  3. package/dist/cjs/form/Payment.js.map +1 -1
  4. package/dist/cjs/form/PaymentOverviewBox.js +55 -77
  5. package/dist/cjs/form/PaymentOverviewBox.js.map +1 -1
  6. package/dist/cjs/form/TicketForm.js +2 -1
  7. package/dist/cjs/form/TicketForm.js.map +1 -1
  8. package/dist/cjs/form/TicketSelection.js +24 -5
  9. package/dist/cjs/form/TicketSelection.js.map +1 -1
  10. package/dist/cjs/form/TicketWithMerchandiseSelection.js +43 -5
  11. package/dist/cjs/form/TicketWithMerchandiseSelection.js.map +1 -1
  12. package/dist/cjs/form/payment/FeeBox.js +1 -1
  13. package/dist/cjs/form/payment/FeeBox.js.map +1 -1
  14. package/dist/cjs/locales/cs.js +3 -0
  15. package/dist/cjs/locales/cs.js.map +1 -1
  16. package/dist/cjs/locales/en.js +3 -0
  17. package/dist/cjs/locales/en.js.map +1 -1
  18. package/dist/cjs/locales/es.js +3 -0
  19. package/dist/cjs/locales/es.js.map +1 -1
  20. package/dist/cjs/locales/pl.js +3 -0
  21. package/dist/cjs/locales/pl.js.map +1 -1
  22. package/dist/cjs/locales/sk.js +3 -0
  23. package/dist/cjs/locales/sk.js.map +1 -1
  24. package/dist/cjs/locales/uk.js +3 -0
  25. package/dist/cjs/locales/uk.js.map +1 -1
  26. package/dist/cjs/modules/shopping-cart.js +10 -9
  27. package/dist/cjs/modules/shopping-cart.js.map +1 -1
  28. package/dist/esm/form/Payment.js +0 -2
  29. package/dist/esm/form/Payment.js.map +1 -1
  30. package/dist/esm/form/PaymentOverviewBox.js +57 -79
  31. package/dist/esm/form/PaymentOverviewBox.js.map +1 -1
  32. package/dist/esm/form/TicketForm.js +2 -1
  33. package/dist/esm/form/TicketForm.js.map +1 -1
  34. package/dist/esm/form/TicketSelection.js +25 -6
  35. package/dist/esm/form/TicketSelection.js.map +1 -1
  36. package/dist/esm/form/TicketWithMerchandiseSelection.js +44 -6
  37. package/dist/esm/form/TicketWithMerchandiseSelection.js.map +1 -1
  38. package/dist/esm/form/payment/FeeBox.js +1 -1
  39. package/dist/esm/form/payment/FeeBox.js.map +1 -1
  40. package/dist/esm/locales/cs.js +3 -0
  41. package/dist/esm/locales/cs.js.map +1 -1
  42. package/dist/esm/locales/en.js +3 -0
  43. package/dist/esm/locales/en.js.map +1 -1
  44. package/dist/esm/locales/es.js +3 -0
  45. package/dist/esm/locales/es.js.map +1 -1
  46. package/dist/esm/locales/pl.js +3 -0
  47. package/dist/esm/locales/pl.js.map +1 -1
  48. package/dist/esm/locales/sk.js +3 -0
  49. package/dist/esm/locales/sk.js.map +1 -1
  50. package/dist/esm/locales/uk.js +3 -0
  51. package/dist/esm/locales/uk.js.map +1 -1
  52. package/dist/esm/modules/shopping-cart.js +9 -8
  53. package/dist/esm/modules/shopping-cart.js.map +1 -1
  54. package/dist/types/locales/cs.d.ts +3 -0
  55. package/dist/types/locales/en.d.ts +3 -0
  56. package/dist/types/locales/es.d.ts +3 -0
  57. package/dist/types/locales/pl.d.ts +3 -0
  58. package/dist/types/locales/sk.d.ts +3 -0
  59. package/dist/types/locales/uk.d.ts +3 -0
  60. package/dist/types/modules/shopping-cart.d.ts +3 -5
  61. package/dist/types/utils/data/shopping-cart.d.ts +5 -0
  62. package/dist/types/utils/types/shopping-cart.type.d.ts +42 -40
  63. package/dist/types/utils/types/ticket.type.d.ts +1 -2
  64. package/package.json +1 -1
  65. package/src/form/Payment.tsx +0 -2
  66. package/src/form/PaymentOverviewBox.tsx +71 -88
  67. package/src/form/TicketForm.tsx +2 -1
  68. package/src/form/TicketSelection.tsx +29 -7
  69. package/src/form/TicketWithMerchandiseSelection.tsx +54 -7
  70. package/src/form/payment/FeeBox.tsx +2 -3
  71. package/src/locales/cs.tsx +3 -0
  72. package/src/locales/en.tsx +3 -0
  73. package/src/locales/es.tsx +3 -0
  74. package/src/locales/pl.tsx +3 -0
  75. package/src/locales/sk.tsx +3 -0
  76. package/src/locales/uk.tsx +3 -0
  77. package/src/modules/shopping-cart.ts +14 -11
  78. package/src/utils/data/shopping-cart.ts +5 -0
  79. package/src/utils/types/shopping-cart.type.ts +40 -36
  80. package/src/utils/types/ticket.type.ts +1 -2
@@ -2,14 +2,14 @@ import { IPromoCode } from '@utils/types/promo-code.type';
2
2
  import { ITicketFormTicket } from '@utils/types/ticket.type';
3
3
  import { IEventProductForm } from '@utils/types/product.type';
4
4
  import { Dayjs } from 'dayjs';
5
- import { OrderFeeType } from '@utils/data/order';
5
+ import { ShoppingCartStatus } from '@utils/data/shopping-cart';
6
6
  export interface ICalculateShoppingCartDto {
7
7
  uuid?: string | null;
8
8
  eventId: number;
9
9
  tickets?: {
10
10
  [eventId: string]: ITicketFormTicket[];
11
11
  };
12
- promoCodeIds: number[] | null;
12
+ promoCodeIds?: number[] | null;
13
13
  products?: {
14
14
  [eventId: string]: IEventProductForm[];
15
15
  };
@@ -20,57 +20,59 @@ export interface ICalculateShoppingCartDto {
20
20
  branchId: string | null;
21
21
  price: number;
22
22
  };
23
- firstName: string | null;
24
- lastName: string | null;
25
- email: string | null;
26
- phone: string | null;
27
- birthdate: Dayjs | null;
28
- gender: string | null;
29
- paymentMethodOptionId: number | null;
30
- paymentMethodId: number | null;
23
+ firstName?: string | null;
24
+ lastName?: string | null;
25
+ email?: string | null;
26
+ phone?: string | null;
27
+ birthdate?: Dayjs | null;
28
+ gender?: string | null;
29
+ paymentMethodId?: number | null;
30
+ paymentMethodOptionId?: number | null;
31
31
  ticketInsurance?: boolean;
32
32
  smsNotification?: boolean;
33
33
  }
34
- export interface IShoppingCart {
35
- uuid: string;
34
+ export interface ITicketInsurance {
35
+ enabled: boolean;
36
+ price: number;
37
+ pricePerUnit: number;
38
+ }
39
+ export interface ISmsNotification {
40
+ enabled: boolean;
41
+ price: number;
42
+ }
43
+ export interface IShoppingCartDto {
36
44
  id: number;
37
- serviceFee: number;
38
- createdAt: string;
39
- updatedAt: string;
40
- deletedAt: string | null;
45
+ uuid: string;
46
+ eventId: number;
47
+ status: ShoppingCartStatus;
48
+ tickets: {
49
+ [eventId: string]: ITicketFormTicket[];
50
+ };
51
+ products: {
52
+ [eventId: string]: IEventProductForm[];
53
+ };
41
54
  originalPrice: number;
42
55
  totalPrice: number;
43
56
  ticketsTotalPrice: number;
44
57
  ticketProductsTotal: number;
45
58
  productsTotalPrice: number;
59
+ shippingFee: number;
60
+ serviceFee: number;
61
+ paymentMethodFee: number;
62
+ promoCodes: IPromoCode[];
63
+ email: string | null;
64
+ phone: string | null;
65
+ firstName: string | null;
66
+ lastName: string | null;
67
+ birthdate: string | null;
68
+ gender: string | null;
46
69
  shipping: {
47
70
  shippingMethodId: number | null;
48
71
  branchId: string | null;
49
72
  price: number;
50
73
  };
51
- expiryTime: string;
52
- promoCodes: IPromoCode[] | null;
53
- tickets: {
54
- [eventId: string]: ITicketFormTicket[];
55
- };
56
- products: {
57
- [eventId: string]: IEventProductForm[];
58
- };
59
- firstName: string | null;
60
- lastName: string | null;
61
- email: string | null;
62
- phone: string | null;
63
- birthdate: Dayjs | null;
64
- gender: string | null;
65
- paymentMethodOptionId: number | null;
66
74
  paymentMethodId: number | null;
67
- paymentMethodFee: number | null;
68
- orderFeeValue: number | null;
69
- orderFeeType: OrderFeeType | null;
70
- shippingFee: number;
71
- ticketInsurance?: boolean;
72
- ticketInsurancePrice?: number;
73
- ticketInsurancePricePerUnit?: number;
74
- smsNotification?: boolean;
75
- smsNotificationPrice?: number;
75
+ paymentMethodOptionId: number | null;
76
+ ticketInsurance: ITicketInsurance;
77
+ smsNotification: ISmsNotification;
76
78
  }
@@ -68,7 +68,6 @@ export interface ITicketForm {
68
68
  smsNotification: boolean;
69
69
  smsNotificationPrice: number;
70
70
  iframeCampaignId?: number;
71
- promoCode: IPromoCode | null;
72
71
  products: {
73
72
  [eventId: string]: IEventProductForm[];
74
73
  };
@@ -84,7 +83,7 @@ export interface ITicketForm {
84
83
  totalFee?: number | null;
85
84
  isPaymentVerify: boolean;
86
85
  }
87
- export interface ITicketBody extends Omit<ITicketForm, 'promoCode'> {
86
+ export interface ITicketBody extends ITicketForm {
88
87
  promoCodeIds: number[];
89
88
  }
90
89
  export interface IPaidTicket {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventlook/sdk",
3
- "version": "1.4.43",
3
+ "version": "1.4.45",
4
4
  "main": "dist/cjs/index.js",
5
5
  "module": "dist/esm/index.js",
6
6
  "types": "dist/types/index.d.ts",
@@ -87,12 +87,10 @@ const Payment: React.FC<Props> = ({ event }) => {
87
87
  setValue('promoCodes', [...promoCodes, promo]);
88
88
  showSnackbar(res.message);
89
89
  setPromoCodeError(null);
90
- setValue('promoCode', promo);
91
90
  setPromoCode('');
92
91
  } catch (err) {
93
92
  setPromoCodeError(t('form.validation.promo_code_invalid'));
94
93
  console.error(err);
95
- setValue('promoCode', null);
96
94
  }
97
95
  },
98
96
  1000,
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { Iconify, Image, TextIconLabel } from '@components';
3
3
  import { Box, Divider, Grid, IconButton, Stack, Typography } from '@mui/material';
4
4
  import PaymentOverviewCheckbox from '@form/payment/PaymentOverviewCheckbox.tsx';
@@ -16,16 +16,14 @@ import FeeBox from '@form/payment/FeeBox';
16
16
  import { IPromoCode } from '@utils/types/promo-code.type';
17
17
  import { fCurrency } from '@utils/formatNumber';
18
18
  import { IEventProductForm, ISelectedProductVariant } from '@utils/types/product.type';
19
- import { isEqual } from 'lodash';
20
19
  import calendarIcon from '@iconify/icons-carbon/calendar';
21
20
  import userIcon from '@iconify/icons-carbon/user';
22
21
  import { EventType } from '@utils/data/event';
23
22
  import dayjs from 'dayjs';
24
23
  import useGlobal from '@hooks/useGlobal.ts';
25
- import { getCreateShoppingCart, recalculateCart } from '@modules/shopping-cart.ts';
26
- import { ICalculateShoppingCartDto, IShoppingCart } from '@utils/types/shopping-cart.type.ts';
24
+ import { calculateCart, getCart } from '@modules/shopping-cart.ts';
25
+ import { ICalculateShoppingCartDto, IShoppingCartDto } from '@utils/types/shopping-cart.type.ts';
27
26
  import { PromoCodeTypes } from '@utils/data/promo-code.ts';
28
- import { useFirstRender } from '@hooks/useFirstRender.ts';
29
27
  import useActiveEventProducts from '@hooks/data/useActiveEventProducts';
30
28
  import useDebounce from '@hooks/useDebounce';
31
29
 
@@ -35,9 +33,6 @@ interface Props {
35
33
 
36
34
  const PaymentOverviewBox: React.FC<Props> = ({ event }) => {
37
35
  const { t, lang, options } = useGlobal();
38
- const firstRender = useFirstRender();
39
- const secondRender = useRef(false);
40
- const lastCartRef = useRef<IShoppingCart | null>(null);
41
36
  const xs = useResponsive('only', 'xs');
42
37
  const md = useResponsive('only', 'md');
43
38
  const isMobile = useResponsive('down', 'md');
@@ -139,44 +134,45 @@ const PaymentOverviewBox: React.FC<Props> = ({ event }) => {
139
134
 
140
135
  const shoppingCartBody = useMemo<ICalculateShoppingCartDto>(
141
136
  () => ({
142
- uuid: uuid,
137
+ uuid,
143
138
  eventId: event.id,
144
- tickets: Object.entries(values.tickets).reduce(
145
- (acc, [eventId, items]) => {
146
- acc[Number(eventId)] = items
147
- .filter((item) => !!item.quantity && !!item.releaseId)
139
+ tickets: Object.fromEntries(
140
+ Object.entries(values.tickets).map(([id, items]) => [
141
+ id,
142
+ items
143
+ .filter((item) => item.quantity && item.releaseId)
148
144
  .map((item) => ({
149
- releaseId: Number(item.releaseId),
150
- quantity: Number(item.quantity),
151
- price: Number(item.price),
152
- itemName: item.itemName,
153
- products: item.products,
154
- extraFields: item.extraFields,
155
- eventTimeslotId: Number(eventTimeslotId) || null,
156
- }));
157
- return acc;
158
- },
159
- {} as Record<number, ITicketFormTicket[]>
145
+ ...item,
146
+ eventTimeslotId: eventTimeslotId ?? null,
147
+ })),
148
+ ])
160
149
  ),
161
- products: Object.entries(values.products).reduce(
162
- (acc, [eventId, items]) => {
163
- acc[Number(eventId)] = items.map((item) => ({
164
- eventProductVariantId: item.eventProductVariantId,
165
- productVariantId: item.productVariantId,
166
- quantity: item.quantity,
167
- price: item.price,
168
- excludedShippingMethodIds: item.excludedShippingMethodIds,
169
- }));
170
- return acc;
171
- },
172
- {} as Record<number, IEventProductForm[]>
150
+ products: Object.fromEntries(
151
+ Object.entries(values.products).map(([id, items]) => [
152
+ id,
153
+ items.map(
154
+ ({
155
+ eventProductVariantId,
156
+ productVariantId,
157
+ quantity,
158
+ price,
159
+ excludedShippingMethodIds,
160
+ }) => ({
161
+ eventProductVariantId,
162
+ productVariantId,
163
+ quantity,
164
+ price,
165
+ excludedShippingMethodIds,
166
+ })
167
+ ),
168
+ ])
173
169
  ),
174
170
  shipping: {
175
- shippingMethodId: shipping?.shippingMethodId ? Number(shipping.shippingMethodId) : null,
176
- branchId: shipping?.branchId,
177
- price: shipping?.price ? Number(shipping.price) : 0,
171
+ shippingMethodId: shipping?.shippingMethodId ?? null,
172
+ branchId: shipping?.branchId ?? null,
173
+ price: shipping?.price ?? 0,
178
174
  },
179
- promoCodeIds: promoCodes?.map((p) => p.id) || null,
175
+ promoCodeIds: promoCodes?.map((p) => p.id) ?? null,
180
176
  language: lang,
181
177
  firstName,
182
178
  lastName,
@@ -184,8 +180,8 @@ const PaymentOverviewBox: React.FC<Props> = ({ event }) => {
184
180
  phone,
185
181
  birthdate,
186
182
  gender,
187
- paymentMethodId: paymentMethodId === null ? null : Number(paymentMethodId),
188
- paymentMethodOptionId: paymentMethodOptionId === null ? null : Number(paymentMethodOptionId),
183
+ paymentMethodId: paymentMethodId ? Number(paymentMethodId) : null,
184
+ paymentMethodOptionId: paymentMethodOptionId ? Number(paymentMethodOptionId) : null,
189
185
  ticketInsurance,
190
186
  smsNotification,
191
187
  }),
@@ -194,9 +190,7 @@ const PaymentOverviewBox: React.FC<Props> = ({ event }) => {
194
190
  event.id,
195
191
  tickets,
196
192
  products,
197
- shipping.shippingMethodId,
198
- shipping.branchId,
199
- shipping.price,
193
+ shipping,
200
194
  promoCodes,
201
195
  lang,
202
196
  firstName,
@@ -215,13 +209,13 @@ const PaymentOverviewBox: React.FC<Props> = ({ event }) => {
215
209
 
216
210
  const debouncedShoppingCartBody = useDebounce(shoppingCartBody, 300);
217
211
 
218
- const setShoppingCartValues = (data: IShoppingCart, recalculate = false) => {
212
+ const setShoppingCartValues = (data: IShoppingCartDto, recalculate = false) => {
219
213
  if (!recalculate) {
220
214
  const hasTickets =
221
215
  !!data.tickets && typeof data.tickets === 'object' && Object.keys(data.tickets).length > 0;
222
216
 
223
217
  if (hasTickets) {
224
- setValue('tickets', { ...tickets, ...data.tickets });
218
+ setValue('tickets', data.tickets);
225
219
  }
226
220
 
227
221
  const hasProducts =
@@ -230,7 +224,7 @@ const PaymentOverviewBox: React.FC<Props> = ({ event }) => {
230
224
  Object.keys(data.products).length > 0;
231
225
 
232
226
  if (hasProducts) {
233
- setValue('products', { ...products, ...data.products });
227
+ setValue('products', data.products);
234
228
  }
235
229
 
236
230
  setValue('promoCodes', data.promoCodes || []);
@@ -259,11 +253,11 @@ const PaymentOverviewBox: React.FC<Props> = ({ event }) => {
259
253
  setValue('totalFee', feeTotal);
260
254
 
261
255
  setShippingFee(data.shippingFee);
262
- setValue('ticketInsurance', data.ticketInsurance || false);
263
- setValue('ticketInsurancePrice', data.ticketInsurancePrice || 0);
264
- setValue('ticketInsurancePricePerUnit', data.ticketInsurancePricePerUnit || 0);
265
- setValue('smsNotification', data.smsNotification || false);
266
- setValue('smsNotificationPrice', data.smsNotificationPrice || 0);
256
+ setValue('ticketInsurance', data.ticketInsurance.enabled);
257
+ setValue('ticketInsurancePrice', data.ticketInsurance.price);
258
+ setValue('ticketInsurancePricePerUnit', data.ticketInsurance.pricePerUnit);
259
+ setValue('smsNotification', data.smsNotification.enabled);
260
+ setValue('smsNotificationPrice', data.smsNotification.price);
267
261
  };
268
262
 
269
263
  const totalItemCount = useMemo(() => {
@@ -335,30 +329,16 @@ const PaymentOverviewBox: React.FC<Props> = ({ event }) => {
335
329
  return discount;
336
330
  }, [promoCodes, flatTickets, flatProducts]);
337
331
 
338
- useEffect(() => {
339
- if (!secondRender.current) {
340
- secondRender.current = true;
341
- } else {
342
- return;
343
- }
344
-
345
- (async () => {
346
- const existing = localStorage.getItem('cartToken');
347
- const res = await getCreateShoppingCart(existing, event.id);
348
- const data: IShoppingCart = res.data;
349
-
350
- localStorage.setItem('cartToken', data.uuid);
351
- setShoppingCartValues(data);
352
- })();
332
+ const hasOrderId = useMemo(() => {
333
+ const urlParams = new URLSearchParams(window.location.search);
334
+ return urlParams.has('id');
353
335
  }, []);
354
336
 
355
337
  useEffect(() => {
356
- if (firstRender) {
357
- return;
338
+ if (!hasOrderId) {
339
+ recalculateItems(debouncedShoppingCartBody);
358
340
  }
359
-
360
- recalculateItems(debouncedShoppingCartBody);
361
- }, [debouncedShoppingCartBody]);
341
+ }, [debouncedShoppingCartBody, hasOrderId]);
362
342
 
363
343
  /** Iframe code **/
364
344
  useEffect(() => {
@@ -378,27 +358,30 @@ const PaymentOverviewBox: React.FC<Props> = ({ event }) => {
378
358
  };
379
359
  /** End Iframe code **/
380
360
 
381
- const updateCartIfChanged = (newCart: IShoppingCart, recalculate = false) => {
382
- if (isEqual(lastCartRef.current, newCart)) {
383
- return;
384
- }
385
-
386
- lastCartRef.current = newCart;
387
- setShoppingCartValues(newCart, recalculate);
388
- };
389
-
390
361
  const recalculateItems = useCallback(async (body: ICalculateShoppingCartDto) => {
391
- const { uuid } = body;
392
- if (!uuid) return;
393
362
  try {
394
- const response = await recalculateCart(body);
395
- const cart = response?.data?.data;
363
+ const isInitial = !body.uuid;
364
+
365
+ let response: any;
366
+ if (isInitial) {
367
+ const existing = localStorage.getItem('cartToken');
368
+ const storedUuid =
369
+ existing && existing !== 'undefined' && existing !== 'null' ? existing : null;
370
+ response = await getCart(body.eventId, storedUuid);
371
+ } else {
372
+ response = await calculateCart(body);
373
+ }
374
+
375
+ const cart = response?.data;
396
376
 
397
377
  if (cart) {
398
- updateCartIfChanged(cart, true);
378
+ if (cart.uuid) {
379
+ localStorage.setItem('cartToken', cart.uuid);
380
+ }
381
+ setShoppingCartValues(cart, !isInitial);
399
382
  }
400
383
  } catch (err) {
401
- console.error('Error recalculating cart:', err);
384
+ console.error('Error calculating cart:', err);
402
385
  }
403
386
  }, []);
404
387
 
@@ -220,7 +220,6 @@ const TicketForm: React.FC<Props> = ({
220
220
  smsNotification: false,
221
221
  smsNotificationPrice: 0,
222
222
  iframeCampaignId: undefined,
223
- promoCode: null,
224
223
  promoCodes: [],
225
224
  products: {},
226
225
  shipping: {
@@ -327,6 +326,7 @@ const TicketForm: React.FC<Props> = ({
327
326
  } else {
328
327
  try {
329
328
  const data: ITicketBody = cloneObject(values);
329
+ data.paymentMethodId = Number(values.paymentMethodId);
330
330
  data.promoCodeIds = values.promoCodes?.map((item) => item.id);
331
331
  const urlParams = new URLSearchParams(window.location.search);
332
332
  const iframeCallback = urlParams.get('callback');
@@ -350,6 +350,7 @@ const TicketForm: React.FC<Props> = ({
350
350
  {} as Record<number, ITicketFormTicket[]>
351
351
  );
352
352
  const { data: orderData } = await postOrder(data);
353
+ localStorage.removeItem('cartToken');
353
354
  const items = [
354
355
  ...orderData.tickets.map((ticket) => ({
355
356
  item_id: ticket.number,
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
2
  import { useFormContext, useWatch } from 'react-hook-form';
3
3
  import {
4
4
  Box,
@@ -39,6 +39,7 @@ const TicketSelection: React.FC<Props> = ({ event }) => {
39
39
  }) as ITicketFormTicket[];
40
40
  const eventTimeslotId = watch('eventTimeslotId');
41
41
  const [soldOutReleaseCategoryNames, setSoldOutReleaseCategoryNames] = useState<string[]>([]);
42
+ const isProcessingRef = useRef(false);
42
43
  const { data: activeReleases, mutate } = useEventActiveReleases(
43
44
  event.id,
44
45
  false,
@@ -48,8 +49,8 @@ const TicketSelection: React.FC<Props> = ({ event }) => {
48
49
  const showLoading = !activeReleases && event.type !== EventType.RECURRING;
49
50
 
50
51
  useEffect(() => {
51
- selectedTickets();
52
- }, [tickets]);
52
+ if (!isProcessingRef.current) selectedTickets();
53
+ }, [tickets, activeReleases]);
53
54
 
54
55
  const isReleaseSelected = (id: number) => !!tickets.find((ticket) => ticket.releaseId === id);
55
56
 
@@ -92,7 +93,7 @@ const TicketSelection: React.FC<Props> = ({ event }) => {
92
93
  const lockedSelectedReleases: boolean[] | undefined = releases?.map((item, index) => {
93
94
  const nextRelease = releases?.find(
94
95
  (item2) =>
95
- item2.releaseCategoryName === item.releaseCategoryName && item2.order === item.order++
96
+ item2.releaseCategoryName === item.releaseCategoryName && item2.order === item.order + 1
96
97
  );
97
98
  const selected = tickets.find((ticket) => ticket.releaseId === item.id);
98
99
  return !!nextRelease && item.locked && !!selected && index + 1 == tickets.length;
@@ -139,7 +140,11 @@ const TicketSelection: React.FC<Props> = ({ event }) => {
139
140
  soldOutReleaseCategories?.map((item) => item.releaseCategoryName) || []
140
141
  );
141
142
 
142
- if (
143
+ const hasSelectableRelease = activeReleases?.some(
144
+ (release) => !isReleaseSelected(release.id) && !release.locked
145
+ );
146
+
147
+ const shouldAddRow =
143
148
  (soldOutReleaseCategories &&
144
149
  selectedReleaseIsSoldOut(releases) &&
145
150
  tickets.length < soldOutReleaseCategories.length + countUnlockedReleases() &&
@@ -149,8 +154,16 @@ const TicketSelection: React.FC<Props> = ({ event }) => {
149
154
  activeReleases?.length > tickets.length &&
150
155
  tickets.length < soldOutReleaseCategories.length + countUnlockedReleases() &&
151
156
  !allFilled.length) ||
152
- (tickets.length < countReleaseCategories() && !allFilled.length)
153
- ) {
157
+ (tickets.length < countReleaseCategories() && !allFilled.length);
158
+
159
+ const shouldRemoveEmptyRows =
160
+ allFilled.length > 0 &&
161
+ tickets.length > 1 &&
162
+ !hasSelectableRelease &&
163
+ (!soldOutReleaseCategories?.length || !selectedReleaseIsSoldOut(releases));
164
+
165
+ if (shouldAddRow) {
166
+ isProcessingRef.current = true;
154
167
  setValue(`tickets.${event.id}`, [
155
168
  ...tickets,
156
169
  {
@@ -162,6 +175,15 @@ const TicketSelection: React.FC<Props> = ({ event }) => {
162
175
  extraFields: [],
163
176
  },
164
177
  ]);
178
+ setTimeout(() => (isProcessingRef.current = false), 0);
179
+ } else if (shouldRemoveEmptyRows) {
180
+ // Only remove completely empty rows (no releaseId), keep rows where user started selecting
181
+ const nonEmptyTickets = tickets.filter((item) => item.releaseId);
182
+ if (nonEmptyTickets.length < tickets.length) {
183
+ isProcessingRef.current = true;
184
+ setValue(`tickets.${event.id}`, nonEmptyTickets);
185
+ setTimeout(() => (isProcessingRef.current = false), 0);
186
+ }
165
187
  }
166
188
  };
167
189
 
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useMemo, useRef } from 'react';
1
+ import React, { useEffect, useRef } from 'react';
2
2
  import { useFormContext, useWatch } from 'react-hook-form';
3
3
  import { Box, Divider, Skeleton, Stack, Typography } from '@mui/material';
4
4
  import { ITicketForm, ITicketFormTicket } from '@utils/types/ticket.type';
@@ -22,6 +22,7 @@ const TicketWithMerchandiseSelection: React.FC<Props> = ({ event }) => {
22
22
  const { setValue, watch } = useFormContext<ITicketForm>();
23
23
  const tickets: ITicketFormTicket[] = useWatch({ name: `tickets.${event.id}`, defaultValue: [] });
24
24
  const eventTimeslotId = watch('eventTimeslotId');
25
+ const isProcessingRef = useRef(false);
25
26
  const { data: activeReleases, mutate } = useEventActiveReleases(
26
27
  event.id,
27
28
  false,
@@ -29,8 +30,8 @@ const TicketWithMerchandiseSelection: React.FC<Props> = ({ event }) => {
29
30
  );
30
31
 
31
32
  useEffect(() => {
32
- selectedTickets();
33
- }, [tickets]);
33
+ if (!isProcessingRef.current) selectedTickets();
34
+ }, [tickets, activeReleases]);
34
35
 
35
36
  const countReleaseCategories = (): number => {
36
37
  const grouped = groupBy(activeReleases || [], 'releaseCategoryName');
@@ -41,7 +42,7 @@ const TicketWithMerchandiseSelection: React.FC<Props> = ({ event }) => {
41
42
  const lockedSelectedReleases: boolean[] | undefined = releases?.map((item, index) => {
42
43
  const nextRelease = releases?.find(
43
44
  (item2) =>
44
- item2.releaseCategoryName === item.releaseCategoryName && item2.order === item.order++
45
+ item2.releaseCategoryName === item.releaseCategoryName && item2.order === item.order + 1
45
46
  );
46
47
  const selected = tickets.find((ticket) => ticket.releaseId === item.id);
47
48
  return !!nextRelease && item.locked && !!selected && index + 1 == tickets.length;
@@ -51,6 +52,8 @@ const TicketWithMerchandiseSelection: React.FC<Props> = ({ event }) => {
51
52
 
52
53
  const countUnlockedReleases = () => activeReleases?.filter((item) => !item.locked).length || 0;
53
54
 
55
+ const isReleaseSelected = (id: number) => !!tickets.find((ticket) => ticket.releaseId === id);
56
+
54
57
  const selectedTickets = async () => {
55
58
  const releases = await mutate();
56
59
  const allFilled = tickets.filter((item) => !item.releaseId || !item.quantity);
@@ -64,7 +67,34 @@ const TicketWithMerchandiseSelection: React.FC<Props> = ({ event }) => {
64
67
  )
65
68
  );
66
69
 
67
- if (
70
+ // Unlock next releases when current release is sold out
71
+ if (soldOutReleaseCategories?.length && activeReleases) {
72
+ let hasChanges = false;
73
+ const updatedReleases = activeReleases.map((release) => {
74
+ // For each sold-out release, find and unlock the next one
75
+ const soldOutRelease = soldOutReleaseCategories.find(
76
+ (soldOut) =>
77
+ soldOut.releaseCategoryName === release.releaseCategoryName &&
78
+ soldOut.order === release.order - 1 &&
79
+ release.locked
80
+ );
81
+ if (soldOutRelease) {
82
+ hasChanges = true;
83
+ return { ...release, locked: false };
84
+ }
85
+ return release;
86
+ });
87
+
88
+ if (hasChanges) {
89
+ await mutate(updatedReleases, false);
90
+ }
91
+ }
92
+
93
+ const hasSelectableRelease = activeReleases?.some(
94
+ (release) => !isReleaseSelected(release.id) && !release.locked
95
+ );
96
+
97
+ const shouldAddRow =
68
98
  (soldOutReleaseCategories &&
69
99
  selectedReleaseIsSoldOut(releases) &&
70
100
  tickets.length < soldOutReleaseCategories.length + countUnlockedReleases() &&
@@ -74,8 +104,16 @@ const TicketWithMerchandiseSelection: React.FC<Props> = ({ event }) => {
74
104
  activeReleases?.length > tickets.length &&
75
105
  tickets.length < soldOutReleaseCategories.length + countUnlockedReleases() &&
76
106
  !allFilled.length) ||
77
- (tickets.length < countReleaseCategories() && !allFilled.length)
78
- ) {
107
+ (tickets.length < countReleaseCategories() && !allFilled.length);
108
+
109
+ const shouldRemoveEmptyRows =
110
+ allFilled.length > 0 &&
111
+ tickets.length > 1 &&
112
+ !hasSelectableRelease &&
113
+ (!soldOutReleaseCategories?.length || !selectedReleaseIsSoldOut(releases));
114
+
115
+ if (shouldAddRow) {
116
+ isProcessingRef.current = true;
79
117
  setValue(`tickets.${event.id}`, [
80
118
  ...tickets,
81
119
  {
@@ -87,6 +125,15 @@ const TicketWithMerchandiseSelection: React.FC<Props> = ({ event }) => {
87
125
  extraFields: [],
88
126
  },
89
127
  ]);
128
+ setTimeout(() => (isProcessingRef.current = false), 0);
129
+ } else if (shouldRemoveEmptyRows) {
130
+ // Only remove completely empty rows (no releaseId), keep rows where user started selecting
131
+ const nonEmptyTickets = tickets.filter((item) => item.releaseId);
132
+ if (nonEmptyTickets.length < tickets.length) {
133
+ isProcessingRef.current = true;
134
+ setValue(`tickets.${event.id}`, nonEmptyTickets);
135
+ setTimeout(() => (isProcessingRef.current = false), 0);
136
+ }
90
137
  }
91
138
  };
92
139
 
@@ -1,7 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Box, Stack, Tooltip, Typography } from '@mui/material';
3
- import { OrderFeeType } from '@utils/data/order';
4
- import { fCurrency, fPercent } from '@utils/formatNumber';
3
+ import { fCurrency } from '@utils/formatNumber';
5
4
  import { Iconify } from '@components/iconify';
6
5
  import { IEvent } from '@utils/types/event.type';
7
6
  import useResponsive from '@hooks/useResponsive';
@@ -23,7 +22,7 @@ const FeeBox: React.FC<Props> = ({ event, align = 'left' }) => {
23
22
 
24
23
  return (
25
24
  <Box mb={1} textAlign={isRight ? 'right' : undefined}>
26
- {values.promoCode && (
25
+ {values.promoCodes.length > 0 && (
27
26
  <Stack
28
27
  direction="row"
29
28
  justifyContent={isRight ? 'flex-end' : 'space-between'}
@@ -61,6 +61,9 @@ const cs = {
61
61
  invalid_date: 'Datum není platné.',
62
62
  terms_and_conditions: 'Musíte souhlasit s obchodními podmínkami.',
63
63
  promo_code_invalid: 'Slevový kód není platný.',
64
+ promo_code_applied: 'Tento slevový kód již byl použit.',
65
+ promo_code_cant_combine: 'Tento slevový kód nelze kombinovat s jinými kódy.',
66
+ min_purchase_not_met: 'Minimální částka nákupu nebyla dosažena: ',
64
67
  count_tickets_or_products: 'Musíte vybrat alespoň 1 vstupenku nebo produkt.',
65
68
  },
66
69
  },
@@ -61,6 +61,9 @@ const en = {
61
61
  invalid_date: 'Date is not valid.',
62
62
  terms_and_conditions: 'You must agree to the terms and conditions.',
63
63
  promo_code_invalid: 'Promo code is not valid.',
64
+ promo_code_applied: 'This promo code has already been applied.',
65
+ promo_code_cant_combine: 'This promo code cannot be combined with other codes.',
66
+ min_purchase_not_met: 'Minimum purchase amount not met: ',
64
67
  count_tickets_or_products: 'You must select at least 1 ticket or product.',
65
68
  },
66
69
  },