@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.
- package/.claude/settings.local.json +6 -10
- package/.env.example +1 -0
- package/README.md +18 -16
- package/dist/cjs/{index-DvUR1fp8.js → index-CUIxdwQn.js} +3340 -577
- package/dist/cjs/index-CUIxdwQn.js.map +1 -0
- package/dist/cjs/index-D5rQiSGP.js +38574 -0
- package/dist/cjs/index-D5rQiSGP.js.map +1 -0
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/{index.umd-6SU6nkkJ.js → index.umd-BoFEW91M.js} +9 -19
- package/dist/cjs/index.umd-BoFEW91M.js.map +1 -0
- package/dist/cjs/index.umd-BzSM62qM.js +13397 -0
- package/dist/cjs/index.umd-BzSM62qM.js.map +1 -0
- package/dist/esm/index-Cm7V8Zl3.js +38571 -0
- package/dist/esm/index-Cm7V8Zl3.js.map +1 -0
- package/dist/esm/{index-BlTqx0jm.js → index-fvLIN6eP.js} +3327 -563
- package/dist/esm/index-fvLIN6eP.js.map +1 -0
- package/dist/esm/index.js +2 -2
- package/dist/esm/{index.umd-Dn0hjh7E.js → index.umd-BKBHcCnm.js} +9 -19
- package/dist/esm/index.umd-BKBHcCnm.js.map +1 -0
- package/dist/esm/index.umd-bIV_YpEF.js +13395 -0
- package/dist/esm/index.umd-bIV_YpEF.js.map +1 -0
- package/dist/types/components/hook-form/FormProvider.d.ts +2 -1
- package/dist/types/form/PaymentOverviewBox.d.ts +2 -0
- package/dist/types/form/PaymentOverviewDrawer.d.ts +10 -0
- package/dist/types/form/TicketForm.d.ts +1 -0
- package/dist/types/form/index.d.ts +2 -1
- package/dist/types/form/merchandise/MerchandiseSelection.d.ts +9 -0
- package/dist/types/form/merchandise/MerchandiseSlider.d.ts +10 -0
- package/dist/types/form/payment/PaymentOverviewCheckbox.d.ts +0 -4
- package/dist/types/form/product/ProductVariantsDialog.d.ts +3 -1
- package/dist/types/form/services/index.d.ts +7 -0
- package/dist/types/form/style.d.ts +1 -0
- package/dist/types/form/tickets/ReleaseDescription.d.ts +10 -0
- package/dist/types/form/tickets/ReleaseWithMerchandise.d.ts +12 -0
- package/dist/types/form/tickets/TicketQuantityControl.d.ts +13 -0
- package/dist/types/form/tickets/TicketSelectionMobile.d.ts +16 -0
- package/dist/types/hooks/useScrollToFirstError.d.ts +4 -0
- package/dist/types/locales/cs.d.ts +22 -0
- package/dist/types/locales/en.d.ts +22 -0
- package/dist/types/locales/es.d.ts +22 -0
- package/dist/types/locales/pl.d.ts +22 -0
- package/dist/types/locales/sk.d.ts +22 -0
- package/dist/types/locales/uk.d.ts +22 -0
- package/dist/types/utils/data/global.d.ts +1 -0
- package/dist/types/utils/data/ticket.d.ts +1 -0
- package/package.json +10 -4
- package/rollup.config.mjs +7 -12
- package/src/components/hook-form/FormProvider.tsx +5 -2
- package/src/form/ChildEventDialog.tsx +3 -3
- package/src/form/ContactPerson.tsx +1 -1
- package/src/form/PaymentOverviewBox.tsx +96 -123
- package/src/form/PaymentOverviewDrawer.tsx +445 -0
- package/src/form/PaymentPending.tsx +19 -4
- package/src/form/ReleaseWithMerchandise.tsx +4 -4
- package/src/form/Shipping.tsx +48 -33
- package/src/form/TicketForm.tsx +146 -41
- package/src/form/index.tsx +3 -1
- package/src/form/merchandise/MerchandiseSelection.tsx +24 -0
- package/src/form/merchandise/MerchandiseSlider.tsx +62 -0
- package/src/form/payment/FeeBox.tsx +4 -31
- package/src/form/payment/PaymentOverviewCheckbox.tsx +68 -69
- package/src/form/product/ProductCard.tsx +258 -59
- package/src/form/product/ProductVariantsDialog.tsx +292 -139
- package/src/form/services/index.tsx +262 -0
- package/src/form/style.ts +16 -4
- package/src/form/tickets/ReleaseDescription.tsx +46 -0
- package/src/form/tickets/ReleaseWithMerchandise.tsx +267 -0
- package/src/form/tickets/TicketQuantityControl.tsx +100 -0
- package/src/form/tickets/TicketSelection.tsx +236 -0
- package/src/form/{TicketSelectionMap.tsx → tickets/TicketSelectionMap.tsx} +18 -2
- package/src/form/tickets/TicketSelectionMobile.tsx +188 -0
- package/src/form/{TicketWithMerchandiseSelection.tsx → tickets/TicketWithMerchandiseSelection.tsx} +52 -38
- package/src/hooks/useScrollToFirstError.ts +99 -0
- package/src/locales/cs.tsx +25 -3
- package/src/locales/en.tsx +23 -1
- package/src/locales/es.tsx +23 -1
- package/src/locales/pl.tsx +23 -1
- package/src/locales/sk.tsx +24 -2
- package/src/locales/uk.tsx +23 -1
- package/src/utils/data/global.ts +1 -0
- package/src/utils/data/ticket.ts +1 -0
- package/tsconfig.json +1 -1
- package/README +0 -1
- package/dist/cjs/index-DvUR1fp8.js.map +0 -1
- package/dist/cjs/index.umd-6SU6nkkJ.js.map +0 -1
- package/dist/esm/index-BlTqx0jm.js.map +0 -1
- package/dist/esm/index.umd-Dn0hjh7E.js.map +0 -1
- package/src/form/TicketSelection.tsx +0 -307
- /package/dist/types/form/{TicketSelection.d.ts → tickets/TicketSelection.d.ts} +0 -0
- /package/dist/types/form/{TicketSelectionMap.d.ts → tickets/TicketSelectionMap.d.ts} +0 -0
- /package/dist/types/form/{TicketWithMerchandiseSelection.d.ts → tickets/TicketWithMerchandiseSelection.d.ts} +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Button, IconButton, Stack } from '@mui/material';
|
|
3
|
+
import { Iconify } from '@components/iconify';
|
|
4
|
+
import useGlobal from '@hooks/useGlobal';
|
|
5
|
+
|
|
6
|
+
interface TicketQuantityControlProps {
|
|
7
|
+
quantity: number;
|
|
8
|
+
isDisabled: boolean;
|
|
9
|
+
canAddFirst: boolean;
|
|
10
|
+
canAddMore: boolean;
|
|
11
|
+
addLabel: string;
|
|
12
|
+
onDecrement: () => void;
|
|
13
|
+
onIncrement: () => void;
|
|
14
|
+
onAddFirst: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TicketQuantityControl: React.FC<TicketQuantityControlProps> = ({
|
|
18
|
+
quantity,
|
|
19
|
+
isDisabled,
|
|
20
|
+
canAddFirst,
|
|
21
|
+
canAddMore,
|
|
22
|
+
addLabel,
|
|
23
|
+
onDecrement,
|
|
24
|
+
onIncrement,
|
|
25
|
+
onAddFirst,
|
|
26
|
+
}) => {
|
|
27
|
+
const { t } = useGlobal();
|
|
28
|
+
|
|
29
|
+
if (quantity > 0) {
|
|
30
|
+
return (
|
|
31
|
+
<Stack direction="row" spacing={{ xs: 0.5, md: 1 }} alignItems="center">
|
|
32
|
+
<IconButton
|
|
33
|
+
onClick={onDecrement}
|
|
34
|
+
aria-label={t('form.labels.ticket_quantity_decrease')}
|
|
35
|
+
disabled={isDisabled || quantity <= 0}
|
|
36
|
+
sx={{
|
|
37
|
+
width: { xs: 36, md: 40 },
|
|
38
|
+
height: { xs: 36, md: 40 },
|
|
39
|
+
borderRadius: 1,
|
|
40
|
+
border: '1px solid',
|
|
41
|
+
borderColor: 'grey.300',
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<Iconify icon="eva:minus-fill" />
|
|
45
|
+
</IconButton>
|
|
46
|
+
<Box
|
|
47
|
+
sx={{
|
|
48
|
+
width: { xs: 36, md: 40 },
|
|
49
|
+
height: { xs: 36, md: 40 },
|
|
50
|
+
borderRadius: 1,
|
|
51
|
+
border: '1px solid',
|
|
52
|
+
borderColor: 'grey.300',
|
|
53
|
+
display: 'flex',
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
justifyContent: 'center',
|
|
56
|
+
fontWeight: 600,
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{quantity}
|
|
60
|
+
</Box>
|
|
61
|
+
<IconButton
|
|
62
|
+
onClick={onIncrement}
|
|
63
|
+
aria-label={t('form.labels.ticket_quantity_increase')}
|
|
64
|
+
disabled={isDisabled || !canAddMore}
|
|
65
|
+
sx={{
|
|
66
|
+
width: { xs: 36, md: 40 },
|
|
67
|
+
height: { xs: 36, md: 40 },
|
|
68
|
+
borderRadius: 1,
|
|
69
|
+
bgcolor: 'primary.main',
|
|
70
|
+
color: 'primary.contrastText',
|
|
71
|
+
'&:hover': { bgcolor: 'primary.dark' },
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<Iconify icon="eva:plus-fill" />
|
|
75
|
+
</IconButton>
|
|
76
|
+
</Stack>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Button
|
|
82
|
+
variant="contained"
|
|
83
|
+
onClick={onAddFirst}
|
|
84
|
+
aria-label={addLabel}
|
|
85
|
+
disabled={isDisabled || !canAddFirst}
|
|
86
|
+
sx={{
|
|
87
|
+
height: { xs: 36, md: 40 },
|
|
88
|
+
width: { xs: 116, md: 136 },
|
|
89
|
+
borderRadius: 1,
|
|
90
|
+
px: 3,
|
|
91
|
+
textTransform: 'none',
|
|
92
|
+
fontWeight: 600,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
{addLabel}
|
|
96
|
+
</Button>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default TicketQuantityControl;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { useFormContext, useWatch } from 'react-hook-form';
|
|
3
|
+
import { Box, Divider, Stack, Typography } from '@mui/material';
|
|
4
|
+
import { ITicketForm, ITicketFormTicket } from '@utils/types/ticket.type';
|
|
5
|
+
import useEventActiveReleases from '@hooks/data/useEventActiveReleases';
|
|
6
|
+
import { groupBy } from '@utils/global';
|
|
7
|
+
import { IReleaseShort } from '@utils/types/release.type';
|
|
8
|
+
import { IEvent } from '@utils/types/event.type';
|
|
9
|
+
import useResponsive from '@hooks/useResponsive';
|
|
10
|
+
import ReleaseExtraFields from '@form/extra-field/ReleaseExtraFields';
|
|
11
|
+
import { EventType } from '@utils/data/event';
|
|
12
|
+
import { MAX_TICKETS_PER_ORDER } from '@utils/data/ticket';
|
|
13
|
+
import useGlobal from '@hooks/useGlobal';
|
|
14
|
+
import TicketSelectionMobile from './TicketSelectionMobile';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
event: IEvent;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const TicketSelection: React.FC<Props> = ({ event }) => {
|
|
21
|
+
const { t } = useGlobal();
|
|
22
|
+
const isMobile = useResponsive('down', 'md');
|
|
23
|
+
const { setValue, watch } = useFormContext<ITicketForm>();
|
|
24
|
+
const tickets = useWatch({
|
|
25
|
+
name: `tickets.${event.id}`,
|
|
26
|
+
defaultValue: [],
|
|
27
|
+
}) as ITicketFormTicket[];
|
|
28
|
+
const eventTimeslotId = watch('eventTimeslotId');
|
|
29
|
+
const isProcessingRef = useRef(false);
|
|
30
|
+
const { data: activeReleases, mutate } = useEventActiveReleases(
|
|
31
|
+
event.id,
|
|
32
|
+
false,
|
|
33
|
+
event.type === EventType.RECURRING ? eventTimeslotId : undefined
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const showLoading = !activeReleases && event.type !== EventType.RECURRING;
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!isProcessingRef.current) selectedTickets();
|
|
40
|
+
}, [tickets, activeReleases]);
|
|
41
|
+
|
|
42
|
+
const isReleaseSelected = (id: number) => !!tickets.find((ticket) => ticket.releaseId === id);
|
|
43
|
+
|
|
44
|
+
const getRelease = (releaseId: number) => {
|
|
45
|
+
const release = activeReleases?.find((activeRelease) => activeRelease.id === releaseId);
|
|
46
|
+
return release ? release : null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getExtraFields = (releaseId: number | '', index: number) => {
|
|
50
|
+
if (!releaseId) return null;
|
|
51
|
+
|
|
52
|
+
const release = getRelease(releaseId);
|
|
53
|
+
|
|
54
|
+
if (!release || !release?.extraFields || release?.extraFields.length <= 0) return null;
|
|
55
|
+
|
|
56
|
+
const addedRelease = tickets.find((ticket) => ticket.releaseId === release.id);
|
|
57
|
+
const countTickets = addedRelease?.quantity || 0;
|
|
58
|
+
return (
|
|
59
|
+
<ReleaseExtraFields
|
|
60
|
+
release={release}
|
|
61
|
+
eventId={event.id}
|
|
62
|
+
releaseIndex={index}
|
|
63
|
+
quantity={countTickets}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// const getAvailableTicketsForRelease = (release: ITicketFormTicket): number => {
|
|
69
|
+
// const selectedRelease = activeReleases?.find((item) => item.id === release.releaseId);
|
|
70
|
+
// const availableQuantity = selectedRelease ? selectedRelease.availableTickets : 0;
|
|
71
|
+
// return availableQuantity > MAX_TICKETS_PER_ORDER ? MAX_TICKETS_PER_ORDER : availableQuantity;
|
|
72
|
+
// };
|
|
73
|
+
|
|
74
|
+
const countReleaseCategories = (): number => {
|
|
75
|
+
const grouped = groupBy(activeReleases || [], 'releaseCategoryName');
|
|
76
|
+
return Object.keys(grouped).length;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const selectedReleaseIsSoldOut = (releases: IReleaseShort[] | undefined) => {
|
|
80
|
+
const lockedSelectedReleases: boolean[] | undefined = releases?.map((item, index) => {
|
|
81
|
+
const nextRelease = releases?.find(
|
|
82
|
+
(item2) =>
|
|
83
|
+
item2.releaseCategoryName === item.releaseCategoryName && item2.order === item.order + 1
|
|
84
|
+
);
|
|
85
|
+
const selected = tickets.find((ticket) => ticket.releaseId === item.id);
|
|
86
|
+
const maxSelectable = Math.min(item.availableTickets || 0, MAX_TICKETS_PER_ORDER);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
!!nextRelease &&
|
|
90
|
+
item.locked &&
|
|
91
|
+
!!selected &&
|
|
92
|
+
Number(selected.quantity) >= maxSelectable &&
|
|
93
|
+
index + 1 == tickets.length
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
return lockedSelectedReleases && lockedSelectedReleases.includes(true);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const countUnlockedReleases = () => activeReleases?.filter((item) => !item.locked).length || 0;
|
|
100
|
+
|
|
101
|
+
const countSelectedTickets = () => {
|
|
102
|
+
let count = 0;
|
|
103
|
+
for (const ticket of tickets) {
|
|
104
|
+
count += +ticket.quantity;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return count;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const isQuantityDisabled = (value: number, releaseId: number | '') => {
|
|
111
|
+
const releaseSelected = tickets.find((item) => item.releaseId === releaseId);
|
|
112
|
+
return releaseSelected && releaseSelected.quantity
|
|
113
|
+
? countSelectedTickets() + value - releaseSelected.quantity > MAX_TICKETS_PER_ORDER
|
|
114
|
+
: countSelectedTickets() + value > MAX_TICKETS_PER_ORDER;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const removeTicket = (indexToRemove: number) => {
|
|
118
|
+
const activeReleases = tickets.filter((_ticket, index) => index !== indexToRemove);
|
|
119
|
+
setValue(`tickets.${event.id}`, activeReleases);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const selectedTickets = async () => {
|
|
123
|
+
const releases = await mutate();
|
|
124
|
+
const currentReleases = releases || activeReleases || [];
|
|
125
|
+
const allFilled = tickets.filter((item) => !item.releaseId || !item.quantity);
|
|
126
|
+
|
|
127
|
+
const soldOutReleaseCategories = currentReleases.filter((release) =>
|
|
128
|
+
tickets.find(
|
|
129
|
+
(ticket) =>
|
|
130
|
+
release.id === ticket.releaseId &&
|
|
131
|
+
Number(ticket.quantity) >= Math.min(release.availableTickets || 0, MAX_TICKETS_PER_ORDER)
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (currentReleases.length) {
|
|
136
|
+
let hasChanges = false;
|
|
137
|
+
const updatedReleases = currentReleases.map((release) => {
|
|
138
|
+
const previousRelease = currentReleases.find(
|
|
139
|
+
(item) =>
|
|
140
|
+
item.releaseCategoryName === release.releaseCategoryName &&
|
|
141
|
+
item.order === release.order - 1
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (!release.locked || !previousRelease) return release;
|
|
145
|
+
|
|
146
|
+
const previousTicket = tickets.find((ticket) => ticket.releaseId === previousRelease.id);
|
|
147
|
+
const previousMaxSelectable = Math.min(
|
|
148
|
+
previousRelease.availableTickets || 0,
|
|
149
|
+
MAX_TICKETS_PER_ORDER
|
|
150
|
+
);
|
|
151
|
+
const shouldUnlock = Number(previousTicket?.quantity || 0) >= previousMaxSelectable;
|
|
152
|
+
|
|
153
|
+
if (!shouldUnlock) return release;
|
|
154
|
+
|
|
155
|
+
hasChanges = true;
|
|
156
|
+
return { ...release, locked: false };
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (hasChanges) {
|
|
160
|
+
await mutate(updatedReleases, false);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const hasSelectableRelease = currentReleases.some(
|
|
165
|
+
(release) => !isReleaseSelected(release.id) && !release.locked
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const shouldAddRow =
|
|
169
|
+
(soldOutReleaseCategories &&
|
|
170
|
+
selectedReleaseIsSoldOut(releases) &&
|
|
171
|
+
tickets.length < soldOutReleaseCategories.length + countUnlockedReleases() &&
|
|
172
|
+
!allFilled.length) ||
|
|
173
|
+
(currentReleases.length &&
|
|
174
|
+
soldOutReleaseCategories.length &&
|
|
175
|
+
currentReleases.length > tickets.length &&
|
|
176
|
+
tickets.length < soldOutReleaseCategories.length + countUnlockedReleases() &&
|
|
177
|
+
!allFilled.length) ||
|
|
178
|
+
(tickets.length < countReleaseCategories() && !allFilled.length);
|
|
179
|
+
|
|
180
|
+
const shouldRemoveEmptyRows =
|
|
181
|
+
allFilled.length > 0 &&
|
|
182
|
+
tickets.length > 1 &&
|
|
183
|
+
!hasSelectableRelease &&
|
|
184
|
+
(!soldOutReleaseCategories?.length || !selectedReleaseIsSoldOut(releases));
|
|
185
|
+
|
|
186
|
+
if (shouldAddRow) {
|
|
187
|
+
isProcessingRef.current = true;
|
|
188
|
+
setValue(`tickets.${event.id}`, [
|
|
189
|
+
...tickets,
|
|
190
|
+
{
|
|
191
|
+
releaseId: '',
|
|
192
|
+
quantity: '',
|
|
193
|
+
itemName: '',
|
|
194
|
+
price: 0,
|
|
195
|
+
products: [],
|
|
196
|
+
extraFields: [],
|
|
197
|
+
},
|
|
198
|
+
]);
|
|
199
|
+
setTimeout(() => (isProcessingRef.current = false), 0);
|
|
200
|
+
} else if (shouldRemoveEmptyRows) {
|
|
201
|
+
// Only remove completely empty rows (no releaseId), keep rows where user started selecting
|
|
202
|
+
const nonEmptyTickets = tickets.filter((item) => item.releaseId);
|
|
203
|
+
if (nonEmptyTickets.length < tickets.length) {
|
|
204
|
+
isProcessingRef.current = true;
|
|
205
|
+
setValue(`tickets.${event.id}`, nonEmptyTickets);
|
|
206
|
+
setTimeout(() => (isProcessingRef.current = false), 0);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<Stack
|
|
213
|
+
spacing={3}
|
|
214
|
+
direction="column"
|
|
215
|
+
divider={!isMobile ? <Divider sx={{ borderStyle: 'dashed' }} /> : undefined}
|
|
216
|
+
>
|
|
217
|
+
<TicketSelectionMobile
|
|
218
|
+
event={event}
|
|
219
|
+
activeReleases={activeReleases}
|
|
220
|
+
showLoading={showLoading}
|
|
221
|
+
tickets={tickets}
|
|
222
|
+
isQuantityDisabled={isQuantityDisabled}
|
|
223
|
+
setValue={setValue as (name: string, value: any) => void}
|
|
224
|
+
removeTicket={removeTicket}
|
|
225
|
+
getExtraFields={getExtraFields}
|
|
226
|
+
/>
|
|
227
|
+
<Box>
|
|
228
|
+
<Typography variant="caption" component="div" fontStyle="italic" mb={2}>
|
|
229
|
+
*{t('event.tickets.stepper.1.max_ticket_quantity')}
|
|
230
|
+
</Typography>
|
|
231
|
+
</Box>
|
|
232
|
+
</Stack>
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export default TicketSelection;
|
|
@@ -5,6 +5,8 @@ import { Button } from '@mui/material';
|
|
|
5
5
|
import { iframe, TicketSelection } from '@seat-picker/seat-picker-sdk';
|
|
6
6
|
import { useFormContext } from 'react-hook-form';
|
|
7
7
|
import { ITicketForm, ITicketFormTicket, ITicketLocation } from '@utils/types/ticket.type';
|
|
8
|
+
import { MAX_TICKETS_PER_ORDER } from '@utils/data/ticket';
|
|
9
|
+
import Iconify from '@components/iconify/Iconify';
|
|
8
10
|
|
|
9
11
|
interface Props {
|
|
10
12
|
event: IEvent;
|
|
@@ -42,11 +44,18 @@ const TicketSelectionMap: React.FC<Props> = ({ event }) => {
|
|
|
42
44
|
[] as { quantity: number; seat: ITicketLocation; ticket: any }[]
|
|
43
45
|
);
|
|
44
46
|
|
|
47
|
+
let remainingTicketCapacity = MAX_TICKETS_PER_ORDER;
|
|
48
|
+
|
|
45
49
|
for (const groupedSeat of groupedSeatsByZone) {
|
|
50
|
+
if (remainingTicketCapacity <= 0) break;
|
|
51
|
+
|
|
52
|
+
const quantity = Math.min(groupedSeat.quantity, remainingTicketCapacity);
|
|
53
|
+
remainingTicketCapacity -= quantity;
|
|
54
|
+
|
|
46
55
|
tickets.push({
|
|
47
56
|
releaseId: groupedSeat.ticket.id,
|
|
48
57
|
price: groupedSeat.ticket.price,
|
|
49
|
-
quantity
|
|
58
|
+
quantity,
|
|
50
59
|
itemName: `${groupedSeat.ticket.releaseCategoryName} - ${groupedSeat.ticket.name}`,
|
|
51
60
|
products: [],
|
|
52
61
|
extraFields: [],
|
|
@@ -65,7 +74,7 @@ const TicketSelectionMap: React.FC<Props> = ({ event }) => {
|
|
|
65
74
|
|
|
66
75
|
return (
|
|
67
76
|
<Button
|
|
68
|
-
variant="
|
|
77
|
+
variant="outlined"
|
|
69
78
|
onClick={() =>
|
|
70
79
|
iframe.openPicker({
|
|
71
80
|
eventId: String(event.id),
|
|
@@ -74,6 +83,13 @@ const TicketSelectionMap: React.FC<Props> = ({ event }) => {
|
|
|
74
83
|
clientId: uuid,
|
|
75
84
|
})
|
|
76
85
|
}
|
|
86
|
+
sx={{
|
|
87
|
+
width: { xs: '100%' },
|
|
88
|
+
color: 'text.primary',
|
|
89
|
+
borderColor: (theme) => theme.palette.grey['300'],
|
|
90
|
+
'& .MuiButton-endIcon': { ml: 0, fontSize: '1.5em' },
|
|
91
|
+
}}
|
|
92
|
+
endIcon={<Iconify icon="eva:chevron-right-outline" />}
|
|
77
93
|
>
|
|
78
94
|
{t('form.labels.open_map')}
|
|
79
95
|
</Button>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Skeleton, Stack, Typography, useTheme } from '@mui/material';
|
|
3
|
+
import { fCurrency } from '@utils/formatNumber';
|
|
4
|
+
import { IEvent } from '@utils/types/event.type';
|
|
5
|
+
import { IReleaseShort } from '@utils/types/release.type';
|
|
6
|
+
import { ITicketFormTicket } from '@utils/types/ticket.type';
|
|
7
|
+
import { MAX_TICKETS_PER_ORDER } from '@utils/data/ticket';
|
|
8
|
+
import useGlobal from '@hooks/useGlobal';
|
|
9
|
+
import ReleaseDescription from './ReleaseDescription';
|
|
10
|
+
import TicketQuantityControl from './TicketQuantityControl';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
event: IEvent;
|
|
14
|
+
activeReleases?: IReleaseShort[];
|
|
15
|
+
showLoading: boolean;
|
|
16
|
+
tickets: ITicketFormTicket[];
|
|
17
|
+
isQuantityDisabled: (value: number, releaseId: number | '') => boolean;
|
|
18
|
+
setValue: (name: string, value: any) => void;
|
|
19
|
+
removeTicket: (indexToRemove: number) => void;
|
|
20
|
+
getExtraFields: (releaseId: number | '', index: number) => React.ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TicketSelectionMobile: React.FC<Props> = ({
|
|
24
|
+
event,
|
|
25
|
+
activeReleases,
|
|
26
|
+
showLoading,
|
|
27
|
+
tickets,
|
|
28
|
+
isQuantityDisabled,
|
|
29
|
+
setValue,
|
|
30
|
+
removeTicket,
|
|
31
|
+
getExtraFields,
|
|
32
|
+
}) => {
|
|
33
|
+
const { t, lang } = useGlobal();
|
|
34
|
+
const [expandedReleaseIds, setExpandedReleaseIds] = useState<Record<number, boolean>>({});
|
|
35
|
+
const theme = useTheme();
|
|
36
|
+
const isLight = theme.palette.mode === 'light';
|
|
37
|
+
|
|
38
|
+
const getReleaseTitle = (release: IReleaseShort) =>
|
|
39
|
+
release.releaseCategoryName || release.name || '';
|
|
40
|
+
|
|
41
|
+
const getTicketIndexByRelease = (releaseId: number) =>
|
|
42
|
+
tickets.findIndex((ticket) => ticket.releaseId === releaseId);
|
|
43
|
+
|
|
44
|
+
const getReleaseQuantity = (releaseId: number) => {
|
|
45
|
+
const ticket = tickets.find((t) => t.releaseId === releaseId);
|
|
46
|
+
return Number(ticket?.quantity || 0);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const isReleaseVisible = (release: IReleaseShort) =>
|
|
50
|
+
!release.locked || getReleaseQuantity(release.id) > 0;
|
|
51
|
+
|
|
52
|
+
const updateReleaseQuantity = (release: IReleaseShort, nextQuantity: number) => {
|
|
53
|
+
const maxAvailable = Math.min(release.availableTickets || 0, MAX_TICKETS_PER_ORDER);
|
|
54
|
+
const clampedQuantity = Math.max(0, Math.min(nextQuantity, maxAvailable));
|
|
55
|
+
const ticketIndex = getTicketIndexByRelease(release.id);
|
|
56
|
+
|
|
57
|
+
if (clampedQuantity <= 0) {
|
|
58
|
+
if (ticketIndex >= 0) removeTicket(ticketIndex);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (ticketIndex >= 0) {
|
|
63
|
+
setValue(`tickets.${event.id}.${ticketIndex}.quantity`, clampedQuantity);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setValue(`tickets.${event.id}`, [
|
|
68
|
+
...tickets,
|
|
69
|
+
{
|
|
70
|
+
releaseId: release.id,
|
|
71
|
+
quantity: clampedQuantity,
|
|
72
|
+
itemName: getReleaseTitle(release),
|
|
73
|
+
price: release.price || 0,
|
|
74
|
+
products: [],
|
|
75
|
+
extraFields: [],
|
|
76
|
+
},
|
|
77
|
+
]);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const toggleReleaseDescription = (releaseId: number) =>
|
|
81
|
+
setExpandedReleaseIds((prev) => ({
|
|
82
|
+
...prev,
|
|
83
|
+
[releaseId]: !prev[releaseId],
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
if (showLoading) {
|
|
87
|
+
return (
|
|
88
|
+
<Stack spacing={2}>
|
|
89
|
+
{[...Array(2)].map((_, index) => (
|
|
90
|
+
<Skeleton
|
|
91
|
+
key={index}
|
|
92
|
+
variant="rounded"
|
|
93
|
+
sx={{
|
|
94
|
+
width: '100%',
|
|
95
|
+
height: (theme) => theme.spacing(12),
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
))}
|
|
99
|
+
</Stack>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Stack spacing={2}>
|
|
105
|
+
{activeReleases
|
|
106
|
+
?.filter((release) => isReleaseVisible(release))
|
|
107
|
+
.map((release) => {
|
|
108
|
+
const quantity = getReleaseQuantity(release.id);
|
|
109
|
+
const ticketIndex = getTicketIndexByRelease(release.id);
|
|
110
|
+
const maxAvailable = Math.min(release.availableTickets || 0, MAX_TICKETS_PER_ORDER);
|
|
111
|
+
const isLocked = release.locked;
|
|
112
|
+
const nextRelease = activeReleases?.find(
|
|
113
|
+
(item) =>
|
|
114
|
+
item.releaseCategoryName === release.releaseCategoryName &&
|
|
115
|
+
item.order === release.order + 1
|
|
116
|
+
);
|
|
117
|
+
const hasSelectedNextRelease = !!nextRelease && getReleaseQuantity(nextRelease.id) > 0;
|
|
118
|
+
const isDisabled = hasSelectedNextRelease || (isLocked && quantity === 0);
|
|
119
|
+
const canAddFirst = maxAvailable > 0 && !isQuantityDisabled(1, release.id);
|
|
120
|
+
const canAddMore =
|
|
121
|
+
quantity < maxAvailable && !isQuantityDisabled(quantity + 1, release.id);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Box
|
|
125
|
+
key={release.id}
|
|
126
|
+
sx={{
|
|
127
|
+
pt: 1,
|
|
128
|
+
pr: 0.5,
|
|
129
|
+
pb: 0.5,
|
|
130
|
+
pl: 2,
|
|
131
|
+
borderRadius: 1,
|
|
132
|
+
bgcolor: (theme) => (isLight ? theme.palette.grey[100] : theme.palette.grey[800]),
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<Stack spacing={0}>
|
|
136
|
+
<Box>
|
|
137
|
+
<Typography variant="subtitle2" fontWeight={700}>
|
|
138
|
+
{getReleaseTitle(release)}
|
|
139
|
+
</Typography>
|
|
140
|
+
</Box>
|
|
141
|
+
|
|
142
|
+
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
|
143
|
+
<Stack>
|
|
144
|
+
<Typography variant="body2">
|
|
145
|
+
{release.price === 0
|
|
146
|
+
? t('free')
|
|
147
|
+
: fCurrency(release.price, lang, event.currency)}{' '}
|
|
148
|
+
- {release.name}
|
|
149
|
+
</Typography>
|
|
150
|
+
|
|
151
|
+
<ReleaseDescription
|
|
152
|
+
description={release.description}
|
|
153
|
+
isExpanded={Boolean(expandedReleaseIds[release.id])}
|
|
154
|
+
onToggle={() => toggleReleaseDescription(release.id)}
|
|
155
|
+
moreInfoLabel={t('more_info')}
|
|
156
|
+
/>
|
|
157
|
+
</Stack>
|
|
158
|
+
|
|
159
|
+
<TicketQuantityControl
|
|
160
|
+
quantity={quantity}
|
|
161
|
+
isDisabled={isDisabled}
|
|
162
|
+
canAddFirst={canAddFirst}
|
|
163
|
+
canAddMore={canAddMore}
|
|
164
|
+
addLabel={t('add')}
|
|
165
|
+
onDecrement={() => updateReleaseQuantity(release, quantity - 1)}
|
|
166
|
+
onIncrement={() => updateReleaseQuantity(release, quantity + 1)}
|
|
167
|
+
onAddFirst={() => updateReleaseQuantity(release, 1)}
|
|
168
|
+
/>
|
|
169
|
+
</Stack>
|
|
170
|
+
|
|
171
|
+
<ReleaseDescription
|
|
172
|
+
description={release.description}
|
|
173
|
+
isExpanded={Boolean(expandedReleaseIds[release.id])}
|
|
174
|
+
onToggle={() => toggleReleaseDescription(release.id)}
|
|
175
|
+
moreInfoLabel={t('more_info')}
|
|
176
|
+
showCollapse
|
|
177
|
+
/>
|
|
178
|
+
|
|
179
|
+
{ticketIndex >= 0 && getExtraFields(release.id, ticketIndex)}
|
|
180
|
+
</Stack>
|
|
181
|
+
</Box>
|
|
182
|
+
);
|
|
183
|
+
})}
|
|
184
|
+
</Stack>
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export default TicketSelectionMobile;
|