Skip to content

Commit 3c448aa

Browse files
authored
Merge pull request #99 from mbpictures/feature/limit-ticket-amount
feature/limit-ticket-amount
2 parents 0b8c8d5 + 621e2bd commit 3c448aa

File tree

10 files changed

+74
-18
lines changed

10 files changed

+74
-18
lines changed

cypress/fixtures/admin/events.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@
3232
"price": 30.99,
3333
"color": "#00AA00",
3434
"occupiedColor": "#FF0000",
35-
"activeColor": "#00FF00"
35+
"activeColor": "#00FF00",
36+
"maxTickets": null
3637
},
3738
{
3839
"label": "Premium",
3940
"price": 60.99,
4041
"color": "#0000AA",
4142
"occupiedColor": "#FF0000",
42-
"activeColor": "#0000FF"
43+
"activeColor": "#0000FF",
44+
"maxTickets": null
4345
}
4446
]
4547
}

locale/de/seatselection.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"event-booked-out": "Tut uns leid, aber das Event ist bereits voll ausgebucht",
55
"seat-map-preview": "Sitzplan Vorschau",
66
"seat-booked": "Dieser Platz wurde bereits gebucht",
7-
"tickets-left": "Tickets verfügbar: {{ticketsLeft}}"
7+
"tickets-left": "Tickets verfügbar: {{ticketsLeft}}",
8+
"max-tickets": "Maximale Tickets pro Bestellung: {{maxTickets}}"
89
}

locale/en/seatselection.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"event-booked-out": "We are sorry, but the event is already fully booked",
55
"seat-map-preview": "Seat Map Preview",
66
"seat-booked": "This seat has already been booked",
7-
"tickets-left": "Tickets left: {{ticketsLeft}}"
7+
"tickets-left": "Tickets left: {{ticketsLeft}}",
8+
"max-tickets": "Maximum tickets per order: {{maxTickets}}"
89
}

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ model Category {
6464
occupiedColor String?
6565
events CategoriesOnEvents[]
6666
tickets Ticket[]
67+
maxTickets Int?
6768
}
6869

6970
model CategoriesOnEvents {

src/components/admin/dialogs/ManageCategoryDialog.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ export const ManageCategoryDialog = ({ open, onClose, onChange, category, curren
3939
price: Yup.number().required("Price is required"),
4040
color: Yup.string(),
4141
activeColor: Yup.string(),
42-
occupiedColor: Yup.string()
42+
occupiedColor: Yup.string(),
43+
maxTickets: Yup.number().nullable()
4344
});
4445

4546

@@ -49,7 +50,8 @@ export const ManageCategoryDialog = ({ open, onClose, onChange, category, curren
4950
price: category?.price ?? 0,
5051
color: category?.color ?? SEAT_COLORS.normal,
5152
activeColor: category?.activeColor ?? SEAT_COLORS.active,
52-
occupiedColor: category?.occupiedColor ?? SEAT_COLORS.occupied
53+
occupiedColor: category?.occupiedColor ?? SEAT_COLORS.occupied,
54+
maxTickets: category?.maxTickets ?? null
5355
},
5456
validationSchema: schema,
5557
onSubmit: async (values) => {
@@ -83,6 +85,7 @@ export const ManageCategoryDialog = ({ open, onClose, onChange, category, curren
8385
formik.setFieldValue("color", category.color);
8486
formik.setFieldValue("activeColor", category.activeColor);
8587
formik.setFieldValue("occupiedColor", category.occupiedColor);
88+
formik.setFieldValue("maxTickets", category.maxTickets);
8689
}, [category]);
8790

8891
const handleDeleteCategory = async () => {
@@ -222,6 +225,19 @@ export const ManageCategoryDialog = ({ open, onClose, onChange, category, curren
222225
hideTextfield
223226
/>
224227
</Stack>
228+
<Stack
229+
direction={"column"}
230+
spacing={2}
231+
>
232+
<TextField
233+
label={"Maximum Tickets"}
234+
sx={{ flexGrow: 1 }}
235+
{...getFieldProps("maxTickets")}
236+
/>
237+
<Typography variant="caption">
238+
Leave empty for unlimited tickets per order
239+
</Typography>
240+
</Stack>
225241
<Stack direction={"row"}>
226242
<LoadingButton
227243
fullWidth

src/components/seatselection/free/SeatSelectionFreeEntry.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const SeatSelectionFreeEntry = ({
3737
label: string;
3838
price: number;
3939
ticketsLeft: number;
40+
maxTickets: number | null;
4041
}>;
4142
index: number;
4243
currentlySelectedCategories: Array<number>;
@@ -57,21 +58,25 @@ export const SeatSelectionFreeEntry = ({
5758

5859
const getTicketsLeft = (categoryId?: number) => {
5960
return categories.find((value) => value.id === (categoryId ?? category))?.ticketsLeft ?? Infinity;
61+
};
62+
63+
const getMaxTickets = (categoryId?: number) => {
64+
return categories.find((value) => value.id === (categoryId ?? category))?.maxTickets ?? Infinity;
6065
}
6166

6267
const handleChange = (event) => {
6368
if (event.target.value === "") {
6469
setTicketAmount(-1);
6570
return;
6671
}
67-
const newValue = Math.min(isNaN(parseInt(event.target.value)) ? 0 : parseInt(event.target.value), getTicketsLeft());
72+
const newValue = Math.min(isNaN(parseInt(event.target.value)) ? 0 : parseInt(event.target.value), getTicketsLeft(), getMaxTickets());
6873
setTicketAmount(newValue);
6974
onChange(index, newValue, category, category);
7075
};
7176

7277
const onAdd = () => {
7378
if (category === -1) return;
74-
const val = Math.min(ticketAmount + 1, getTicketsLeft());
79+
const val = Math.min(ticketAmount + 1, getTicketsLeft(), getMaxTickets());
7580
setTicketAmount(val);
7681
onChange(index, val, category, category);
7782
};
@@ -83,7 +88,7 @@ export const SeatSelectionFreeEntry = ({
8388
};
8489

8590
const handleCategoryChange = (event) => {
86-
onChange(index, Math.min(ticketAmount, getTicketsLeft(parseInt(event.target.value))), parseInt(event.target.value), category);
91+
onChange(index, Math.min(ticketAmount, getTicketsLeft(parseInt(event.target.value)), getMaxTickets(parseInt(event.target.value))), parseInt(event.target.value), category);
8792
};
8893

8994
const price =
@@ -123,6 +128,13 @@ export const SeatSelectionFreeEntry = ({
123128
</Typography>
124129
)
125130
}
131+
{
132+
getMaxTickets() < Infinity && (
133+
<Typography variant={"caption"}>
134+
{t("seatselection:max-tickets", {maxTickets: getMaxTickets()})}
135+
</Typography>
136+
)
137+
}
126138
</Stack>
127139
<Box width={20} />
128140
<Stack>

src/constants/serverUtil.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { eventDateIsBookable } from "./util";
1111
import { SeatMap } from "../components/seatselection/seatmap/SeatSelectionMap";
1212
import { randomBytes } from "crypto";
1313
import { Prisma } from ".prisma/client";
14-
import OrderFindManyArgs = Prisma.OrderFindManyArgs;
15-
import SelectSubset = Prisma.SelectSubset;
14+
type OrderFindManyArgs = Prisma.OrderFindManyArgs;
15+
type SelectSubset<X, U> = Prisma.SelectSubset<X, U>;
1616

1717
export function getStaticAssetFile(file, options = null) {
1818
let basePath = process.cwd();
@@ -183,7 +183,8 @@ export const validateOrder = async (tickets: Tickets, eventDateId, reservationId
183183
categories: {
184184
select: {
185185
categoryId: true,
186-
maxAmount: true
186+
maxAmount: true,
187+
category: true,
187188
}
188189
}
189190
}
@@ -212,6 +213,20 @@ export const validateOrder = async (tickets: Tickets, eventDateId, reservationId
212213

213214
let currentAmounts = await getCategoryTicketAmount(eventDateId, tickets, reservationId);
214215
let invalidTickets = [];
216+
const orderTicketSum = tickets.reduce((group, ticket) => {
217+
if (!group[ticket.categoryId]) group[ticket.categoryId] = 0;
218+
group[ticket.categoryId] += ticket.amount;
219+
return group;
220+
}, {} as Record<number, number>);
221+
const invalidTicketCategories = Object.entries(orderTicketSum).filter(entry => {
222+
const category = eventDate.event.categories.find(c => c.categoryId === parseInt(entry[0]))?.category;
223+
const maxPerOrder = category ? (category.maxTickets ?? 0) : 0;
224+
return entry[1] > maxPerOrder && maxPerOrder > 0;
225+
}).map(entry => parseInt(entry[0], 10));
226+
if (invalidTicketCategories.length > 0) {
227+
return [false, tickets.filter(ticket => invalidTicketCategories.includes(ticket.categoryId))];
228+
}
229+
215230
for (let ticket of tickets) {
216231
if (typeof currentAmounts[ticket.categoryId] === "undefined")
217232
currentAmounts[ticket.categoryId] = 0;

src/pages/api/admin/category/[id].ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,15 @@ export default async function handler(
5353
}
5454

5555
if (req.method === "PUT") {
56-
const { label, price, color, activeColor, occupiedColor } =
56+
const { label, price, color, activeColor, occupiedColor, maxTickets } =
5757
req.body;
5858
const data = {
5959
...(label && { label }),
6060
...(price && { price }),
6161
...(color && { color }),
6262
...(activeColor && { activeColor }),
63-
...(occupiedColor && { occupiedColor })
63+
...(occupiedColor && { occupiedColor }),
64+
...(maxTickets && maxTickets !== "" && { maxTickets: parseInt(maxTickets) }),
6465
};
6566
await prisma.category.update({
6667
where: {

src/pages/api/admin/category/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ export default async function handler(
2323
}
2424

2525
if (req.method === "POST") {
26-
const { label, price, color, activeColor, occupiedColor } = req.body;
26+
const { label, price, color, activeColor, occupiedColor, maxTickets } = req.body;
2727
const category = await prisma.category.create({
2828
data: {
2929
label: label,
3030
price: parseFloat(price),
3131
color: color,
3232
activeColor: activeColor,
33-
occupiedColor: occupiedColor
33+
occupiedColor: occupiedColor,
34+
maxTickets: maxTickets === "" || maxTickets === null ? null : parseInt(maxTickets)
3435
}
3536
});
3637
res.status(200).end(category.id.toFixed(0));

src/pages/api/admin/ticket/[id].ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,15 @@ export default async function handler(
2626
include: {
2727
order: {
2828
include: {
29-
user: true
29+
user: true,
30+
eventDate: {
31+
include: {
32+
event: true
33+
}
34+
}
3035
}
31-
}
36+
},
37+
category: true
3238
}
3339
});
3440
if (!ticket)

0 commit comments

Comments
 (0)