Skip to content

Commit 1741ba6

Browse files
authored
Add toggle button to disable/enable ticketing sales (#116)
* add button to the UI * allow enabling and disabling sales from the UI * code cleanup
1 parent 6ed8537 commit 1741ba6

File tree

3 files changed

+158
-34
lines changed

3 files changed

+158
-34
lines changed

src/api/routes/tickets.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@ import { genericConfig } from "../../common/config.js";
1010
import {
1111
BaseError,
1212
DatabaseFetchError,
13+
DatabaseInsertError,
1314
NotFoundError,
1415
NotSupportedError,
1516
TicketNotFoundError,
1617
TicketNotValidError,
1718
UnauthenticatedError,
1819
ValidationError,
1920
} from "../../common/errors/index.js";
20-
import { unmarshall } from "@aws-sdk/util-dynamodb";
21+
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
2122
import { validateEmail } from "../functions/validation.js";
2223
import { AppRoles } from "../../common/roles.js";
2324
import { zodToJsonSchema } from "zod-to-json-schema";
25+
import { ItemPostData } from "common/types/tickets.js";
2426

2527
const postMerchSchema = z.object({
2628
type: z.literal("merch"),
@@ -105,6 +107,12 @@ type TicketsListRequest = {
105107
Body: undefined;
106108
};
107109

110+
type TicketsPostRequest = {
111+
Params: { eventId: string };
112+
Querystring: undefined;
113+
Body: ItemPostData;
114+
};
115+
108116
const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
109117
fastify.get<TicketsListRequest>(
110118
"/",
@@ -200,7 +208,6 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
200208
});
201209
}
202210
}
203-
204211
reply.send({ merch: merchItems, tickets: ticketItems });
205212
},
206213
);
@@ -271,6 +278,71 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
271278
return reply.send(response);
272279
},
273280
);
281+
fastify.patch<TicketsPostRequest>(
282+
"/:eventId",
283+
{
284+
onRequest: async (request, reply) => {
285+
await fastify.authorize(request, reply, [AppRoles.TICKETS_MANAGER]);
286+
},
287+
},
288+
async (request, reply) => {
289+
const eventId = request.params.eventId;
290+
const eventType = request.body.type;
291+
const eventActiveSet = request.body.itemSalesActive;
292+
let newActiveTime: number = 0;
293+
if (typeof eventActiveSet === "boolean") {
294+
if (!eventActiveSet) {
295+
newActiveTime = -1;
296+
}
297+
} else {
298+
newActiveTime = parseInt(
299+
(eventActiveSet.valueOf() / 1000).toFixed(0),
300+
10,
301+
);
302+
}
303+
let command: UpdateItemCommand;
304+
switch (eventType) {
305+
case "merch":
306+
command = new UpdateItemCommand({
307+
TableName: genericConfig.MerchStoreMetadataTableName,
308+
Key: marshall({ item_id: eventId }),
309+
UpdateExpression: "SET item_sales_active_utc = :new_val",
310+
ConditionExpression: "item_id = :item_id",
311+
ExpressionAttributeValues: {
312+
":new_val": { N: newActiveTime.toString() },
313+
":item_id": { S: eventId },
314+
},
315+
});
316+
break;
317+
case "ticket":
318+
command = new UpdateItemCommand({
319+
TableName: genericConfig.TicketMetadataTableName,
320+
Key: marshall({ event_id: eventId }),
321+
UpdateExpression: "SET event_sales_active_utc = :new_val",
322+
ConditionExpression: "event_id = :item_id",
323+
ExpressionAttributeValues: {
324+
":new_val": { N: newActiveTime.toString() },
325+
":item_id": { S: eventId },
326+
},
327+
});
328+
break;
329+
}
330+
try {
331+
await fastify.dynamoClient.send(command);
332+
} catch (e) {
333+
if (e instanceof ConditionalCheckFailedException) {
334+
throw new NotFoundError({
335+
endpointName: request.url,
336+
});
337+
}
338+
fastify.log.error(e);
339+
throw new DatabaseInsertError({
340+
message: "Could not update active time for item.",
341+
});
342+
}
343+
return reply.status(201).send();
344+
},
345+
);
274346
fastify.post<{ Body: VerifyPostRequest }>(
275347
"/checkIn",
276348
{

src/common/types/tickets.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { z } from 'zod';
2+
export const postMetadataSchema = z.object({
3+
type: z.union([z.literal("merch"), z.literal("ticket")]),
4+
itemSalesActive: z.union([z.date(), z.boolean()]),
5+
})
6+
7+
export type ItemPostData = z.infer<typeof postMetadataSchema>;

src/ui/pages/tickets/SelectEventId.page.tsx

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen';
1919
import { AuthGuard } from '@ui/components/AuthGuard';
2020
import { useApi } from '@ui/util/api';
2121
import { AppRoles } from '@common/roles';
22-
import App from '@ui/App';
22+
import { ItemPostData } from '@common/types/tickets';
2323

2424
const baseItemMetadata = z.object({
2525
itemId: z.string().min(1),
@@ -106,31 +106,29 @@ const SelectTicketsPage: React.FC = () => {
106106
const [reversedSort, setReversedSort] = useState(false);
107107
const api = useApi('core');
108108
const navigate = useNavigate();
109-
109+
const fetchItems = async () => {
110+
try {
111+
setLoading(true);
112+
const response = await api.get('/api/v1/tickets/');
113+
const parsed = listItemsResponseSchema.parse(response.data);
114+
setItems({
115+
tickets: parsed.tickets,
116+
merch: parsed.merch,
117+
});
118+
handleSort('status');
119+
} catch (error) {
120+
console.error('Error fetching items:', error);
121+
notifications.show({
122+
title: 'Error fetching items',
123+
message: 'Failed to load available items. Please try again later.',
124+
color: 'red',
125+
});
126+
} finally {
127+
setLoading(false);
128+
}
129+
};
110130
useEffect(() => {
111-
const fetchItems = async () => {
112-
try {
113-
setLoading(true);
114-
const response = await api.get('/api/v1/tickets/');
115-
const parsed = listItemsResponseSchema.parse(response.data);
116-
setItems({
117-
tickets: parsed.tickets,
118-
merch: parsed.merch,
119-
});
120-
} catch (error) {
121-
console.error('Error fetching items:', error);
122-
notifications.show({
123-
title: 'Error fetching items',
124-
message: 'Failed to load available items. Please try again later.',
125-
color: 'red',
126-
});
127-
} finally {
128-
setLoading(false);
129-
}
130-
};
131-
132131
fetchItems();
133-
handleSort('status');
134132
}, []);
135133

136134
const handleSort = (field: SortBy) => {
@@ -170,6 +168,37 @@ const SelectTicketsPage: React.FC = () => {
170168
return <FullScreenLoader />;
171169
}
172170

171+
const handleToggleSales = async (item: ItemMetadata | TicketItemMetadata) => {
172+
let newIsActive = false;
173+
if (isTicketItem(item)) {
174+
newIsActive = !(getTicketStatus(item).color === 'green');
175+
} else {
176+
newIsActive = !(getMerchStatus(item).color === 'green');
177+
}
178+
try {
179+
setLoading(true);
180+
const data: ItemPostData = {
181+
itemSalesActive: newIsActive,
182+
type: isTicketItem(item) ? 'ticket' : 'merch',
183+
};
184+
await api.patch(`/api/v1/tickets/${item.itemId}`, data);
185+
await fetchItems();
186+
notifications.show({
187+
title: 'Changes saved',
188+
message: `Sales for ${item.itemName} are ${newIsActive ? 'enabled' : 'disabled'}!`,
189+
});
190+
} catch (error) {
191+
console.error('Error setting new status:', error);
192+
notifications.show({
193+
title: 'Error setting status',
194+
message: 'Failed to set status. Please try again later.',
195+
color: 'red',
196+
});
197+
} finally {
198+
setLoading(false);
199+
}
200+
};
201+
173202
const handleManageClick = (itemId: string) => {
174203
navigate(`/tickets/manage/${itemId}`);
175204
};
@@ -253,12 +282,19 @@ const SelectTicketsPage: React.FC = () => {
253282
resourceDef={{ service: 'core', validRoles: [AppRoles.TICKETS_MANAGER] }}
254283
>
255284
<Button
256-
variant="outline"
285+
variant="primary"
257286
onClick={() => handleManageClick(item.itemId)}
258287
id={`merch-${item.itemId}-manage`}
259288
>
260289
View Sales
261290
</Button>
291+
<Button
292+
color={getMerchStatus(item).color === 'green' ? 'red' : 'green'}
293+
onClick={() => handleToggleSales(item)}
294+
id={`tickets-${item.itemId}-toggle-status`}
295+
>
296+
{getMerchStatus(item).color === 'green' ? 'Disable' : 'Enable'} Sales
297+
</Button>
262298
</AuthGuard>
263299
</Group>
264300
</Table.Td>
@@ -330,13 +366,22 @@ const SelectTicketsPage: React.FC = () => {
330366
isAppShell={false}
331367
resourceDef={{ service: 'core', validRoles: [AppRoles.TICKETS_MANAGER] }}
332368
>
333-
<Button
334-
variant="outline"
335-
onClick={() => handleManageClick(ticket.itemId)}
336-
id={`tickets-${ticket.itemId}-manage`}
337-
>
338-
View Sales
339-
</Button>
369+
<Group>
370+
<Button
371+
variant="primary"
372+
onClick={() => handleManageClick(ticket.itemId)}
373+
id={`tickets-${ticket.itemId}-manage`}
374+
>
375+
View Sales
376+
</Button>
377+
<Button
378+
color={getTicketStatus(ticket).color === 'green' ? 'red' : 'green'}
379+
onClick={() => handleToggleSales(ticket)}
380+
id={`tickets-${ticket.itemId}-toggle-status`}
381+
>
382+
{getTicketStatus(ticket).color === 'green' ? 'Disable' : 'Enable'} Sales
383+
</Button>
384+
</Group>
340385
</AuthGuard>
341386
</Group>
342387
</Table.Td>

0 commit comments

Comments
 (0)