Skip to content

Add toggle button to disable/enable ticketing sales #116

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 74 additions & 2 deletions src/api/routes/tickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@ import { genericConfig } from "../../common/config.js";
import {
BaseError,
DatabaseFetchError,
DatabaseInsertError,
NotFoundError,
NotSupportedError,
TicketNotFoundError,
TicketNotValidError,
UnauthenticatedError,
ValidationError,
} from "../../common/errors/index.js";
import { unmarshall } from "@aws-sdk/util-dynamodb";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
import { validateEmail } from "../functions/validation.js";
import { AppRoles } from "../../common/roles.js";
import { zodToJsonSchema } from "zod-to-json-schema";
import { ItemPostData } from "common/types/tickets.js";

const postMerchSchema = z.object({
type: z.literal("merch"),
Expand Down Expand Up @@ -105,6 +107,12 @@ type TicketsListRequest = {
Body: undefined;
};

type TicketsPostRequest = {
Params: { eventId: string };
Querystring: undefined;
Body: ItemPostData;
};

const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
fastify.get<TicketsListRequest>(
"/",
Expand Down Expand Up @@ -200,7 +208,6 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
});
}
}

reply.send({ merch: merchItems, tickets: ticketItems });
},
);
Expand Down Expand Up @@ -271,6 +278,71 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
return reply.send(response);
},
);
fastify.patch<TicketsPostRequest>(
"/:eventId",
{
onRequest: async (request, reply) => {
await fastify.authorize(request, reply, [AppRoles.TICKETS_MANAGER]);
},
},
async (request, reply) => {
const eventId = request.params.eventId;
const eventType = request.body.type;
const eventActiveSet = request.body.itemSalesActive;
let newActiveTime: number = 0;
if (typeof eventActiveSet === "boolean") {
if (!eventActiveSet) {
newActiveTime = -1;
}
} else {
newActiveTime = parseInt(
(eventActiveSet.valueOf() / 1000).toFixed(0),
10,
);
}
let command: UpdateItemCommand;
switch (eventType) {
case "merch":
command = new UpdateItemCommand({
TableName: genericConfig.MerchStoreMetadataTableName,
Key: marshall({ item_id: eventId }),
UpdateExpression: "SET item_sales_active_utc = :new_val",
ConditionExpression: "item_id = :item_id",
ExpressionAttributeValues: {
":new_val": { N: newActiveTime.toString() },
":item_id": { S: eventId },
},
});
break;
case "ticket":
command = new UpdateItemCommand({
TableName: genericConfig.TicketMetadataTableName,
Key: marshall({ event_id: eventId }),
UpdateExpression: "SET event_sales_active_utc = :new_val",
ConditionExpression: "event_id = :item_id",
ExpressionAttributeValues: {
":new_val": { N: newActiveTime.toString() },
":item_id": { S: eventId },
},
});
break;
}
try {
await fastify.dynamoClient.send(command);
} catch (e) {
if (e instanceof ConditionalCheckFailedException) {
throw new NotFoundError({
endpointName: request.url,
});
}
fastify.log.error(e);
throw new DatabaseInsertError({
message: "Could not update active time for item.",
});
}
return reply.status(201).send();
},
);
fastify.post<{ Body: VerifyPostRequest }>(
"/checkIn",
{
Expand Down
7 changes: 7 additions & 0 deletions src/common/types/tickets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod';
export const postMetadataSchema = z.object({
type: z.union([z.literal("merch"), z.literal("ticket")]),
itemSalesActive: z.union([z.date(), z.boolean()]),
})

export type ItemPostData = z.infer<typeof postMetadataSchema>;
109 changes: 77 additions & 32 deletions src/ui/pages/tickets/SelectEventId.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen';
import { AuthGuard } from '@ui/components/AuthGuard';
import { useApi } from '@ui/util/api';
import { AppRoles } from '@common/roles';
import App from '@ui/App';
import { ItemPostData } from '@common/types/tickets';

const baseItemMetadata = z.object({
itemId: z.string().min(1),
Expand Down Expand Up @@ -106,31 +106,29 @@ const SelectTicketsPage: React.FC = () => {
const [reversedSort, setReversedSort] = useState(false);
const api = useApi('core');
const navigate = useNavigate();

const fetchItems = async () => {
try {
setLoading(true);
const response = await api.get('/api/v1/tickets/');
const parsed = listItemsResponseSchema.parse(response.data);
setItems({
tickets: parsed.tickets,
merch: parsed.merch,
});
handleSort('status');
} catch (error) {
console.error('Error fetching items:', error);
notifications.show({
title: 'Error fetching items',
message: 'Failed to load available items. Please try again later.',
color: 'red',
});
} finally {
setLoading(false);
}
};
useEffect(() => {
const fetchItems = async () => {
try {
setLoading(true);
const response = await api.get('/api/v1/tickets/');
const parsed = listItemsResponseSchema.parse(response.data);
setItems({
tickets: parsed.tickets,
merch: parsed.merch,
});
} catch (error) {
console.error('Error fetching items:', error);
notifications.show({
title: 'Error fetching items',
message: 'Failed to load available items. Please try again later.',
color: 'red',
});
} finally {
setLoading(false);
}
};

fetchItems();
handleSort('status');
}, []);

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

const handleToggleSales = async (item: ItemMetadata | TicketItemMetadata) => {
let newIsActive = false;
if (isTicketItem(item)) {
newIsActive = !(getTicketStatus(item).color === 'green');
} else {
newIsActive = !(getMerchStatus(item).color === 'green');
}
try {
setLoading(true);
const data: ItemPostData = {
itemSalesActive: newIsActive,
type: isTicketItem(item) ? 'ticket' : 'merch',
};
await api.patch(`/api/v1/tickets/${item.itemId}`, data);
await fetchItems();
notifications.show({
title: 'Changes saved',
message: `Sales for ${item.itemName} are ${newIsActive ? 'enabled' : 'disabled'}!`,
});
} catch (error) {
console.error('Error setting new status:', error);
notifications.show({
title: 'Error setting status',
message: 'Failed to set status. Please try again later.',
color: 'red',
});
} finally {
setLoading(false);
}
};

const handleManageClick = (itemId: string) => {
navigate(`/tickets/manage/${itemId}`);
};
Expand Down Expand Up @@ -253,12 +282,19 @@ const SelectTicketsPage: React.FC = () => {
resourceDef={{ service: 'core', validRoles: [AppRoles.TICKETS_MANAGER] }}
>
<Button
variant="outline"
variant="primary"
onClick={() => handleManageClick(item.itemId)}
id={`merch-${item.itemId}-manage`}
>
View Sales
</Button>
<Button
color={getMerchStatus(item).color === 'green' ? 'red' : 'green'}
onClick={() => handleToggleSales(item)}
id={`tickets-${item.itemId}-toggle-status`}
>
{getMerchStatus(item).color === 'green' ? 'Disable' : 'Enable'} Sales
</Button>
</AuthGuard>
</Group>
</Table.Td>
Expand Down Expand Up @@ -330,13 +366,22 @@ const SelectTicketsPage: React.FC = () => {
isAppShell={false}
resourceDef={{ service: 'core', validRoles: [AppRoles.TICKETS_MANAGER] }}
>
<Button
variant="outline"
onClick={() => handleManageClick(ticket.itemId)}
id={`tickets-${ticket.itemId}-manage`}
>
View Sales
</Button>
<Group>
<Button
variant="primary"
onClick={() => handleManageClick(ticket.itemId)}
id={`tickets-${ticket.itemId}-manage`}
>
View Sales
</Button>
<Button
color={getTicketStatus(ticket).color === 'green' ? 'red' : 'green'}
onClick={() => handleToggleSales(ticket)}
id={`tickets-${ticket.itemId}-toggle-status`}
>
{getTicketStatus(ticket).color === 'green' ? 'Disable' : 'Enable'} Sales
</Button>
</Group>
</AuthGuard>
</Group>
</Table.Td>
Expand Down
Loading