@eventlook/sdk 1.5.0-beta.6 → 1.5.0-beta.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.
Files changed (91) hide show
  1. package/.claude/settings.local.json +6 -10
  2. package/.env.example +1 -0
  3. package/README.md +18 -16
  4. package/dist/cjs/{index-DvUR1fp8.js → index-CUIxdwQn.js} +3340 -577
  5. package/dist/cjs/index-CUIxdwQn.js.map +1 -0
  6. package/dist/cjs/index-D5rQiSGP.js +38574 -0
  7. package/dist/cjs/index-D5rQiSGP.js.map +1 -0
  8. package/dist/cjs/index.js +2 -2
  9. package/dist/cjs/{index.umd-6SU6nkkJ.js → index.umd-BoFEW91M.js} +9 -19
  10. package/dist/cjs/index.umd-BoFEW91M.js.map +1 -0
  11. package/dist/cjs/index.umd-BzSM62qM.js +13397 -0
  12. package/dist/cjs/index.umd-BzSM62qM.js.map +1 -0
  13. package/dist/esm/index-Cm7V8Zl3.js +38571 -0
  14. package/dist/esm/index-Cm7V8Zl3.js.map +1 -0
  15. package/dist/esm/{index-BlTqx0jm.js → index-fvLIN6eP.js} +3327 -563
  16. package/dist/esm/index-fvLIN6eP.js.map +1 -0
  17. package/dist/esm/index.js +2 -2
  18. package/dist/esm/{index.umd-Dn0hjh7E.js → index.umd-BKBHcCnm.js} +9 -19
  19. package/dist/esm/index.umd-BKBHcCnm.js.map +1 -0
  20. package/dist/esm/index.umd-bIV_YpEF.js +13395 -0
  21. package/dist/esm/index.umd-bIV_YpEF.js.map +1 -0
  22. package/dist/types/components/hook-form/FormProvider.d.ts +2 -1
  23. package/dist/types/form/PaymentOverviewBox.d.ts +2 -0
  24. package/dist/types/form/PaymentOverviewDrawer.d.ts +10 -0
  25. package/dist/types/form/TicketForm.d.ts +1 -0
  26. package/dist/types/form/index.d.ts +2 -1
  27. package/dist/types/form/merchandise/MerchandiseSelection.d.ts +9 -0
  28. package/dist/types/form/merchandise/MerchandiseSlider.d.ts +10 -0
  29. package/dist/types/form/payment/PaymentOverviewCheckbox.d.ts +0 -4
  30. package/dist/types/form/product/ProductVariantsDialog.d.ts +3 -1
  31. package/dist/types/form/services/index.d.ts +7 -0
  32. package/dist/types/form/style.d.ts +1 -0
  33. package/dist/types/form/tickets/ReleaseDescription.d.ts +10 -0
  34. package/dist/types/form/tickets/ReleaseWithMerchandise.d.ts +12 -0
  35. package/dist/types/form/tickets/TicketQuantityControl.d.ts +13 -0
  36. package/dist/types/form/tickets/TicketSelectionMobile.d.ts +16 -0
  37. package/dist/types/hooks/useScrollToFirstError.d.ts +4 -0
  38. package/dist/types/locales/cs.d.ts +22 -0
  39. package/dist/types/locales/en.d.ts +22 -0
  40. package/dist/types/locales/es.d.ts +22 -0
  41. package/dist/types/locales/pl.d.ts +22 -0
  42. package/dist/types/locales/sk.d.ts +22 -0
  43. package/dist/types/locales/uk.d.ts +22 -0
  44. package/dist/types/utils/data/global.d.ts +1 -0
  45. package/dist/types/utils/data/ticket.d.ts +1 -0
  46. package/package.json +10 -4
  47. package/rollup.config.mjs +7 -12
  48. package/src/components/hook-form/FormProvider.tsx +5 -2
  49. package/src/form/ChildEventDialog.tsx +3 -3
  50. package/src/form/ContactPerson.tsx +1 -1
  51. package/src/form/PaymentOverviewBox.tsx +96 -123
  52. package/src/form/PaymentOverviewDrawer.tsx +445 -0
  53. package/src/form/PaymentPending.tsx +19 -4
  54. package/src/form/ReleaseWithMerchandise.tsx +4 -4
  55. package/src/form/Shipping.tsx +48 -33
  56. package/src/form/TicketForm.tsx +146 -41
  57. package/src/form/index.tsx +3 -1
  58. package/src/form/merchandise/MerchandiseSelection.tsx +24 -0
  59. package/src/form/merchandise/MerchandiseSlider.tsx +62 -0
  60. package/src/form/payment/FeeBox.tsx +4 -31
  61. package/src/form/payment/PaymentOverviewCheckbox.tsx +68 -69
  62. package/src/form/product/ProductCard.tsx +258 -59
  63. package/src/form/product/ProductVariantsDialog.tsx +292 -139
  64. package/src/form/services/index.tsx +262 -0
  65. package/src/form/style.ts +16 -4
  66. package/src/form/tickets/ReleaseDescription.tsx +46 -0
  67. package/src/form/tickets/ReleaseWithMerchandise.tsx +267 -0
  68. package/src/form/tickets/TicketQuantityControl.tsx +100 -0
  69. package/src/form/tickets/TicketSelection.tsx +236 -0
  70. package/src/form/{TicketSelectionMap.tsx → tickets/TicketSelectionMap.tsx} +18 -2
  71. package/src/form/tickets/TicketSelectionMobile.tsx +188 -0
  72. package/src/form/{TicketWithMerchandiseSelection.tsx → tickets/TicketWithMerchandiseSelection.tsx} +52 -38
  73. package/src/hooks/useScrollToFirstError.ts +99 -0
  74. package/src/locales/cs.tsx +25 -3
  75. package/src/locales/en.tsx +23 -1
  76. package/src/locales/es.tsx +23 -1
  77. package/src/locales/pl.tsx +23 -1
  78. package/src/locales/sk.tsx +24 -2
  79. package/src/locales/uk.tsx +23 -1
  80. package/src/utils/data/global.ts +1 -0
  81. package/src/utils/data/ticket.ts +1 -0
  82. package/tsconfig.json +1 -1
  83. package/README +0 -1
  84. package/dist/cjs/index-DvUR1fp8.js.map +0 -1
  85. package/dist/cjs/index.umd-6SU6nkkJ.js.map +0 -1
  86. package/dist/esm/index-BlTqx0jm.js.map +0 -1
  87. package/dist/esm/index.umd-Dn0hjh7E.js.map +0 -1
  88. package/src/form/TicketSelection.tsx +0 -307
  89. /package/dist/types/form/{TicketSelection.d.ts → tickets/TicketSelection.d.ts} +0 -0
  90. /package/dist/types/form/{TicketSelectionMap.d.ts → tickets/TicketSelectionMap.d.ts} +0 -0
  91. /package/dist/types/form/{TicketWithMerchandiseSelection.d.ts → tickets/TicketWithMerchandiseSelection.d.ts} +0 -0
@@ -0,0 +1,262 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import {
3
+ Box,
4
+ Button,
5
+ Checkbox,
6
+ Dialog,
7
+ DialogContent,
8
+ DialogTitle,
9
+ Divider,
10
+ FormControlLabel,
11
+ IconButton,
12
+ Link,
13
+ Stack,
14
+ Typography,
15
+ } from '@mui/material';
16
+ import { useWatch, Controller, useFormContext } from 'react-hook-form';
17
+ import { fCurrency } from '@utils/formatNumber';
18
+ import useGlobal from '@hooks/useGlobal';
19
+ import { Iconify } from '@components';
20
+ import { ITicketForm, ITicketFormTicket } from '@utils/types/ticket.type';
21
+ import { IEvent } from '@utils/types/event.type';
22
+
23
+ interface Props {
24
+ event: IEvent;
25
+ }
26
+
27
+ interface BorderedCheckboxProps {
28
+ name: string;
29
+ disabled: boolean;
30
+ title: React.ReactNode;
31
+ price: React.ReactNode;
32
+ onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
33
+ }
34
+
35
+ const BorderedCheckbox: React.FC<BorderedCheckboxProps> = ({
36
+ name,
37
+ disabled,
38
+ title,
39
+ price,
40
+ onChange,
41
+ }) => {
42
+ const { control } = useFormContext<ITicketForm>();
43
+
44
+ return (
45
+ <Controller
46
+ name={name as any}
47
+ control={control}
48
+ render={({ field }) => (
49
+ <FormControlLabel
50
+ control={
51
+ <Checkbox
52
+ {...field}
53
+ checked={field.value || false}
54
+ onChange={(e) => {
55
+ onChange?.(e);
56
+ if (!e.defaultPrevented) {
57
+ field.onChange(e);
58
+ }
59
+ }}
60
+ disabled={disabled}
61
+ />
62
+ }
63
+ label={
64
+ <Stack
65
+ direction="row"
66
+ justifyContent="space-between"
67
+ alignItems="center"
68
+ sx={{ width: '100%', pr: 1 }}
69
+ >
70
+ <Typography variant="body2" fontWeight={400}>
71
+ {title}
72
+ </Typography>
73
+ <Typography variant="body2" fontWeight={700} sx={{ whiteSpace: 'nowrap' }}>
74
+ {price}
75
+ </Typography>
76
+ </Stack>
77
+ }
78
+ sx={{
79
+ m: 0,
80
+ px: 1,
81
+ pr: 0.5,
82
+ pl: 0,
83
+ borderRadius: 1,
84
+ width: '100%',
85
+ border: '1px solid',
86
+ borderColor: 'primary.main',
87
+ '& .MuiFormControlLabel-label': {
88
+ width: '100%',
89
+ },
90
+ }}
91
+ />
92
+ )}
93
+ />
94
+ );
95
+ };
96
+
97
+ const Services: React.FC<Props> = ({ event }) => {
98
+ const { t, lang, showSnackbar } = useGlobal();
99
+ const [open, setOpen] = useState(false);
100
+ const ticketInsurancePricePerUnit = Number(
101
+ useWatch<ITicketForm>({
102
+ name: 'ticketInsurancePricePerUnit',
103
+ defaultValue: 0,
104
+ }) || 0
105
+ );
106
+ const smsNotificationPrice = Number(
107
+ useWatch<ITicketForm>({
108
+ name: 'smsNotificationPrice',
109
+ defaultValue: 0,
110
+ }) || 0
111
+ );
112
+ const tickets = useWatch<ITicketForm, 'tickets'>({
113
+ name: 'tickets',
114
+ defaultValue: {},
115
+ });
116
+
117
+ const totalTickets = useMemo(() => {
118
+ const items: ITicketFormTicket[] = Object.values(tickets || {}).flat();
119
+ return items.reduce((sum, ticket) => sum + (Number(ticket.quantity) || 0), 0);
120
+ }, [tickets]);
121
+
122
+ const handleOpen = (e: React.MouseEvent<HTMLAnchorElement>) => {
123
+ e.preventDefault();
124
+ setOpen(true);
125
+ };
126
+
127
+ const handleClose = () => setOpen(false);
128
+
129
+ const handleServiceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
130
+ if (totalTickets <= 0) {
131
+ e.preventDefault();
132
+ showSnackbar(t('event.tickets.services.add_tickets_first'), {
133
+ variant: 'error',
134
+ });
135
+ }
136
+ };
137
+
138
+ return (
139
+ <Stack spacing={1}>
140
+ {/*<BorderedCheckbox*/}
141
+ {/* name="ticketInsurance"*/}
142
+ {/* disabled={false}*/}
143
+ {/* title={t('event.tickets.insurance.label')}*/}
144
+ {/* price={*/}
145
+ {/* <>*/}
146
+ {/* + {fCurrency(ticketInsurancePricePerUnit, lang, event.currency)} /{' '}*/}
147
+ {/* {t('event.tickets.insurance.per_ticket')}*/}
148
+ {/* </>*/}
149
+ {/* }*/}
150
+ {/* onChange={handleServiceChange}*/}
151
+ {/*/>*/}
152
+
153
+ <BorderedCheckbox
154
+ name="smsNotification"
155
+ disabled={false}
156
+ title={t('event.tickets.sms_notification.label')}
157
+ price={<>+ {fCurrency(smsNotificationPrice, lang, event.currency)}</>}
158
+ onChange={handleServiceChange}
159
+ />
160
+
161
+ <Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
162
+ <Link variant="body2" underline="always" onClick={handleOpen} color={'text.primary'}>
163
+ {t('event.tickets.stepper.8.additional_info')}
164
+ </Link>
165
+ </Box>
166
+
167
+ <Dialog
168
+ open={open}
169
+ onClose={handleClose}
170
+ maxWidth="sm"
171
+ fullWidth
172
+ PaperProps={{
173
+ sx: {
174
+ borderRadius: 4,
175
+ overflow: 'hidden',
176
+ },
177
+ }}
178
+ >
179
+ <DialogTitle
180
+ sx={{
181
+ position: 'relative',
182
+ textAlign: 'center',
183
+ fontWeight: 700,
184
+ fontSize: { xs: '1.5rem', sm: '1.75rem' },
185
+ pt: { xs: 3.5, sm: 4 },
186
+ pb: { xs: 2, sm: 2.5 },
187
+ px: { xs: 3, sm: 4 },
188
+ }}
189
+ >
190
+ {t('event.tickets.stepper.8.title')}:
191
+ <IconButton
192
+ onClick={handleClose}
193
+ size="small"
194
+ sx={{ position: 'absolute', right: 16, top: 16 }}
195
+ >
196
+ <Iconify icon="carbon:close" />
197
+ </IconButton>
198
+ </DialogTitle>
199
+ <DialogContent sx={{ px: { xs: 3, sm: 4 }, pb: { xs: 4, sm: 5 } }}>
200
+ <Stack spacing={3}>
201
+ {/*<Box>*/}
202
+ {/* <Typography variant="subtitle2" fontWeight={700} gutterBottom>*/}
203
+ {/* {t('event.tickets.insurance.label')}*/}
204
+ {/* </Typography>*/}
205
+ {/* <Typography variant="body2" paragraph>*/}
206
+ {/* {t('event.tickets.insurance.modal.description')}*/}
207
+ {/* </Typography>*/}
208
+ {/* <Typography variant="body2" paragraph>*/}
209
+ {/* {t('event.tickets.insurance.modal.coverage')}*/}
210
+ {/* </Typography>*/}
211
+ {/* <BorderedCheckbox*/}
212
+ {/* name="ticketInsurance"*/}
213
+ {/* disabled={false}*/}
214
+ {/* title={t('event.tickets.insurance.label')}*/}
215
+ {/* price={*/}
216
+ {/* <>*/}
217
+ {/* + {fCurrency(ticketInsurancePricePerUnit, lang, event.currency)} /{' '}*/}
218
+ {/* {t('event.tickets.insurance.per_ticket')}*/}
219
+ {/* </>*/}
220
+ {/* }*/}
221
+ {/* onChange={handleServiceChange}*/}
222
+ {/* />*/}
223
+ {/*</Box>*/}
224
+ {/*<Divider />*/}
225
+ <Box>
226
+ <Typography variant="subtitle2" fontWeight={700} gutterBottom>
227
+ {t('event.tickets.sms_notification.label')}
228
+ </Typography>
229
+ <Typography variant="body2" paragraph>
230
+ {t('event.tickets.sms_notification.modal.description')}
231
+ </Typography>
232
+ <BorderedCheckbox
233
+ name="smsNotification"
234
+ disabled={false}
235
+ title={t('event.tickets.sms_notification.label')}
236
+ price={<>+ {fCurrency(smsNotificationPrice, lang, event.currency)}</>}
237
+ onChange={handleServiceChange}
238
+ />
239
+ </Box>
240
+ <Button
241
+ variant="outlined"
242
+ onClick={handleClose}
243
+ sx={{
244
+ mt: 1,
245
+ py: 1.25,
246
+ borderRadius: 1,
247
+ height: 36,
248
+ fontWeight: 700,
249
+ borderColor: 'grey.300',
250
+ color: 'text.primary',
251
+ }}
252
+ >
253
+ {t('close')}
254
+ </Button>
255
+ </Stack>
256
+ </DialogContent>
257
+ </Dialog>
258
+ </Stack>
259
+ );
260
+ };
261
+
262
+ export default Services;
package/src/form/style.ts CHANGED
@@ -5,17 +5,29 @@ export const OverviewCard = styled(Card)<{ stickyHeaderTop: number }>(
5
5
  ({ theme, stickyHeaderTop }) => ({
6
6
  position: 'sticky',
7
7
  top: stickyHeaderTop,
8
+ borderRadius: theme.spacing(2),
9
+
10
+ [theme.breakpoints.down('sm')]: {
11
+ borderRadius: theme.spacing(1),
12
+ boxShadow: 'none',
13
+ },
8
14
  })
9
15
  );
10
16
 
11
17
  export const ShippingMethodItem = styled(Box, {
12
- shouldForwardProp: (prop) => prop !== 'active',
13
- })<{ active: boolean }>(({ theme, active }) => ({
18
+ shouldForwardProp: (prop) => prop !== 'active' && prop !== 'hasError',
19
+ })<{ active: boolean; hasError?: boolean }>(({ theme, active, hasError }) => ({
14
20
  borderRadius: theme.spacing(1),
15
- border: `1px solid ${active ? theme.palette.primary.main : theme.palette.grey.A200}`,
21
+ border: `1px solid ${
22
+ hasError
23
+ ? theme.palette.error.main
24
+ : active
25
+ ? theme.palette.primary.main
26
+ : theme.palette.grey.A200
27
+ }`,
16
28
  marginTop: theme.spacing(1),
17
29
  width: '100%',
18
- padding: `0 ${theme.spacing(2)}`,
30
+ padding: `0 0 0 ${theme.spacing(2)}`,
19
31
  transitionDuration: '.3s',
20
32
 
21
33
  '& > .MuiFormControlLabel-root': {
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { Box, Collapse, Link, Stack, Typography } from '@mui/material';
3
+ import { Iconify } from '@components/iconify';
4
+
5
+ interface ReleaseDescriptionProps {
6
+ description?: string | null;
7
+ isExpanded: boolean;
8
+ onToggle: () => void;
9
+ moreInfoLabel: string;
10
+ showCollapse?: boolean;
11
+ }
12
+
13
+ const ReleaseDescription: React.FC<ReleaseDescriptionProps> = ({
14
+ description,
15
+ isExpanded,
16
+ onToggle,
17
+ moreInfoLabel,
18
+ showCollapse = false,
19
+ }) => {
20
+ if (!description) return null;
21
+
22
+ if (showCollapse) {
23
+ return (
24
+ <Collapse in={isExpanded}>
25
+ <Typography variant="body2" color="text.secondary">
26
+ {description}
27
+ </Typography>
28
+ </Collapse>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <Link onClick={onToggle} color="inherit" underline="always" fontSize={12}>
34
+ <Stack direction="row" alignItems="center" spacing={0}>
35
+ <Box>{moreInfoLabel}</Box>
36
+
37
+ <Iconify
38
+ sx={{ width: 24, height: 24 }}
39
+ icon={isExpanded ? 'eva:chevron-up-fill' : 'eva:chevron-down-fill'}
40
+ />
41
+ </Stack>
42
+ </Link>
43
+ );
44
+ };
45
+
46
+ export default ReleaseDescription;
@@ -0,0 +1,267 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import { Box, Stack, Typography } from '@mui/material';
3
+ import ProductVariantsDialog from '@form/product/ProductVariantsDialog';
4
+ import TicketQuantityControl from '@form/tickets/TicketQuantityControl';
5
+ import { IReleaseShort } from '@utils/types/release.type';
6
+ import { ITicketForm, ITicketFormTicket } from '@utils/types/ticket.type';
7
+ import { useFormContext, useWatch } from 'react-hook-form';
8
+ import { IEventProductForm } from '@utils/types/product.type';
9
+ import { fCurrency } from '@utils/formatNumber';
10
+ import { Currencies } from '@utils/data/currency';
11
+ import { MAX_TICKETS_PER_ORDER } from '@utils/data/ticket';
12
+ import { getSelectedQuantityByVariant } from '@utils/product';
13
+ import ReleaseExtraFields from '@form/extra-field/ReleaseExtraFields';
14
+ import ReleaseDescription from '@form/tickets/ReleaseDescription';
15
+ import useGlobal from '@hooks/useGlobal';
16
+
17
+ interface Props {
18
+ eventId: number;
19
+ release: IReleaseShort;
20
+ activeReleases: IReleaseShort[];
21
+ currency: Currencies;
22
+ index: number;
23
+ }
24
+
25
+ const ReleaseWithMerchandise: React.FC<Props> = ({
26
+ eventId,
27
+ release,
28
+ activeReleases,
29
+ currency,
30
+ index,
31
+ }) => {
32
+ const { t, lang } = useGlobal();
33
+ const [openVariantDialog, setOpenVariantDialog] = useState<'add' | 'increase' | null>(null);
34
+ const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
35
+ const { setValue } = useFormContext<ITicketForm>();
36
+ const tickets: ITicketFormTicket[] = useWatch({ name: `tickets.${eventId}`, defaultValue: [] });
37
+ const products: IEventProductForm[] = useWatch({ name: `products.${eventId}`, defaultValue: [] });
38
+ const addedRelease = tickets.find((ticket) => ticket.releaseId === release.id);
39
+ const countTickets = addedRelease?.quantity || 0;
40
+
41
+ const getReleaseTitle = (release: IReleaseShort) =>
42
+ release.releaseCategoryName || release.name || '';
43
+
44
+ const getSelectedQuantity = (id: number) =>
45
+ tickets.find((ticket) => ticket.releaseId === id)?.quantity || 0;
46
+
47
+ const countSelectedTickets = () => {
48
+ let count = 0;
49
+ for (const ticket of tickets) {
50
+ count += Number(ticket.quantity || 0);
51
+ }
52
+
53
+ return count;
54
+ };
55
+
56
+ const getAvailableTicketsForRelease = (release: ITicketFormTicket): number => {
57
+ const selectedRelease = activeReleases?.find((item) => item.id === release.releaseId);
58
+ const availableQuantity = selectedRelease ? selectedRelease.availableTickets : 0;
59
+ return availableQuantity > MAX_TICKETS_PER_ORDER ? MAX_TICKETS_PER_ORDER : availableQuantity;
60
+ };
61
+
62
+ const isMaxQuantity = (releaseId: number) => {
63
+ const release = tickets.find((ticket) => ticket.releaseId === releaseId);
64
+ if (!release) return false;
65
+ return getSelectedQuantity(releaseId) >= getAvailableTicketsForRelease(release);
66
+ };
67
+
68
+ const addRelease = (productsToAdd?: IEventProductForm[] | IEventProductForm) => {
69
+ const normalizedProducts = Array.isArray(productsToAdd)
70
+ ? productsToAdd
71
+ : productsToAdd
72
+ ? [productsToAdd]
73
+ : [];
74
+ const requestedQuantity = normalizedProducts.length ? normalizedProducts.length : 1;
75
+ const releaseMaxQuantity = Math.min(release.availableTickets || 0, MAX_TICKETS_PER_ORDER);
76
+ const remainingOrderCapacity = Math.max(0, MAX_TICKETS_PER_ORDER - countSelectedTickets());
77
+ const quantity = Math.min(requestedQuantity, releaseMaxQuantity, remainingOrderCapacity);
78
+
79
+ if (quantity <= 0) {
80
+ setOpenVariantDialog(null);
81
+ return;
82
+ }
83
+
84
+ const selectedProducts = normalizedProducts.slice(0, quantity);
85
+ const extraFields = release.extraFields?.length
86
+ ? Array.from({ length: quantity }, () =>
87
+ release.extraFields!.map((field) => ({
88
+ eventExtraFieldId: field.id,
89
+ value: '',
90
+ }))
91
+ )
92
+ : [];
93
+
94
+ setValue(`tickets.${eventId}`, [
95
+ ...tickets,
96
+ {
97
+ releaseId: release.id,
98
+ quantity,
99
+ itemName: '',
100
+ price: 0,
101
+ products: selectedProducts,
102
+ extraFields,
103
+ },
104
+ ]);
105
+ setOpenVariantDialog(null);
106
+ };
107
+
108
+ const increaseQuantity = (productsToAdd?: IEventProductForm[]) => {
109
+ const normalizedProducts = productsToAdd ?? [];
110
+ const addedRelease = tickets.find((ticket) => ticket.releaseId === release.id);
111
+ if (addedRelease) {
112
+ const increment = normalizedProducts.length ? normalizedProducts.length : 1;
113
+ const maxQuantity = getAvailableTicketsForRelease(addedRelease);
114
+ const remainingOrderCapacity = Math.max(0, MAX_TICKETS_PER_ORDER - countSelectedTickets());
115
+ const availableIncrement = Math.max(
116
+ 0,
117
+ Math.min(increment, maxQuantity - Number(addedRelease.quantity), remainingOrderCapacity)
118
+ );
119
+ if (availableIncrement === 0) return;
120
+
121
+ const newQuantity = Number(addedRelease.quantity) + availableIncrement;
122
+ const productsSlice = normalizedProducts.slice(0, availableIncrement);
123
+ const extraFieldsToAdd = release.extraFields?.length
124
+ ? Array.from({ length: availableIncrement }, () =>
125
+ release.extraFields!.map((field) => ({
126
+ eventExtraFieldId: field.id,
127
+ value: '',
128
+ }))
129
+ )
130
+ : [];
131
+
132
+ setValue(
133
+ `tickets.${eventId}`,
134
+ tickets.map((ticket) =>
135
+ ticket.releaseId === release.id
136
+ ? {
137
+ ...ticket,
138
+ quantity: newQuantity > maxQuantity ? maxQuantity : newQuantity,
139
+ products: [...ticket.products, ...productsSlice],
140
+ extraFields: release.extraFields?.length
141
+ ? [...ticket.extraFields, ...extraFieldsToAdd]
142
+ : [],
143
+ }
144
+ : ticket
145
+ )
146
+ );
147
+ setOpenVariantDialog(null);
148
+ }
149
+ };
150
+
151
+ const decreaseQuantity = useCallback(() => {
152
+ const addedRelease = tickets.find((ticket) => ticket.releaseId === release.id);
153
+ if (addedRelease) {
154
+ const newQuantity = Number(addedRelease.quantity) - 1;
155
+ if (newQuantity < 1) {
156
+ setValue(
157
+ `tickets.${eventId}`,
158
+ tickets.filter((ticket) => ticket.releaseId !== release.id)
159
+ );
160
+ } else {
161
+ setValue(
162
+ `tickets.${eventId}`,
163
+ tickets.map((ticket) => {
164
+ if (ticket.releaseId !== release.id) return ticket;
165
+
166
+ return {
167
+ ...ticket,
168
+ quantity: newQuantity,
169
+ products: ticket?.products?.slice(0, -1), // non-mutating "pop"
170
+ extraFields: ticket?.extraFields?.slice(0, -1),
171
+ };
172
+ })
173
+ );
174
+ }
175
+ }
176
+ }, [tickets, release.id, setValue]);
177
+
178
+ const remainingOrderCapacity = Math.max(0, MAX_TICKETS_PER_ORDER - countSelectedTickets());
179
+ const nextRelease = activeReleases?.find(
180
+ (item) =>
181
+ item.releaseCategoryName === release.releaseCategoryName && item.order === release.order + 1
182
+ );
183
+ const hasSelectedNextRelease = !!nextRelease && getSelectedQuantity(nextRelease.id) > 0;
184
+
185
+ return (
186
+ <Box
187
+ sx={{
188
+ pt: 1,
189
+ pr: 0.5,
190
+ pb: 0.5,
191
+ pl: 2,
192
+ borderRadius: 1,
193
+ bgcolor: (theme) =>
194
+ theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[800],
195
+ }}
196
+ >
197
+ <Stack spacing={0}>
198
+ <Box>
199
+ <Typography variant="subtitle2" fontWeight={700}>
200
+ {getReleaseTitle(release)}
201
+ </Typography>
202
+ </Box>
203
+
204
+ <Stack direction="row" alignItems="center" justifyContent="space-between">
205
+ <Stack>
206
+ <Typography variant="body2">
207
+ {release.price === 0 ? t('free') : fCurrency(release.price, lang, currency)} -{' '}
208
+ {release.name}
209
+ </Typography>
210
+
211
+ <ReleaseDescription
212
+ description={release.description}
213
+ isExpanded={isDescriptionExpanded}
214
+ onToggle={() => setIsDescriptionExpanded((prev) => !prev)}
215
+ moreInfoLabel={t('more_info')}
216
+ />
217
+ </Stack>
218
+
219
+ <TicketQuantityControl
220
+ quantity={getSelectedQuantity(release.id)}
221
+ isDisabled={release.locked || hasSelectedNextRelease}
222
+ canAddFirst={!release.locked && !hasSelectedNextRelease && remainingOrderCapacity > 0}
223
+ canAddMore={
224
+ !isMaxQuantity(release.id) && !hasSelectedNextRelease && remainingOrderCapacity > 0
225
+ }
226
+ addLabel={t('add')}
227
+ onDecrement={() => decreaseQuantity()}
228
+ onIncrement={() =>
229
+ release.product ? setOpenVariantDialog('increase') : increaseQuantity()
230
+ }
231
+ onAddFirst={() => (release.product ? setOpenVariantDialog('add') : addRelease())}
232
+ />
233
+ </Stack>
234
+
235
+ <ReleaseDescription
236
+ description={release.description}
237
+ isExpanded={isDescriptionExpanded}
238
+ onToggle={() => setIsDescriptionExpanded((prev) => !prev)}
239
+ moreInfoLabel={t('more_info')}
240
+ showCollapse
241
+ />
242
+ {release.extraFields && release.extraFields.length > 0 && (
243
+ <ReleaseExtraFields
244
+ release={release}
245
+ eventId={eventId}
246
+ releaseIndex={index}
247
+ quantity={countTickets}
248
+ />
249
+ )}
250
+ {release.product && (
251
+ <ProductVariantsDialog
252
+ eventProduct={release.product}
253
+ openDialog={!!openVariantDialog}
254
+ callback={openVariantDialog === 'increase' ? increaseQuantity : addRelease}
255
+ onClose={() => setOpenVariantDialog(null)}
256
+ selectedQuantityByVariant={getSelectedQuantityByVariant(products, tickets)}
257
+ eventId={eventId}
258
+ canAddOnlyOneAtATime
259
+ maxSelectableQuantity={remainingOrderCapacity}
260
+ />
261
+ )}
262
+ </Stack>
263
+ </Box>
264
+ );
265
+ };
266
+
267
+ export default ReleaseWithMerchandise;