Skip to content
Closed
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
2 changes: 2 additions & 0 deletions src/api/functions/tickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type RawMerchEntry = {
scannerEmail?: string;
size: string;
total_paid?: number;
purchased_at?: number;
};

export async function getUserTicketingPurchases({
Expand Down Expand Up @@ -135,6 +136,7 @@ export async function getUserMerchPurchases({
refunded: item.refunded,
fulfilled: item.fulfilled,
totalPaid: item.total_paid,
purchasedAt: item.purchased_at,
});
}
return issuedTickets;
Expand Down
24 changes: 23 additions & 1 deletion src/api/routes/tickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ const ticketEntryZod = z
description:
"The total amount paid by the customer, in cents, net of refunds.",
}),
purchasedAt: z.optional(z.number()).meta({
description:
"The time at which the user's checkout session completed, in seconds since Epoch.",
}),
})
.meta({
description: "An entry describing one merch or tickets transaction.",
Expand Down Expand Up @@ -297,7 +301,25 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
message: `Retrieving tickets currently only supported on type "merch"!`,
});
}
const response = { tickets: issuedTickets };
const response = {
tickets: issuedTickets.sort((a, b) => {
// Valid tickets first
if (a.valid !== b.valid) {
return a.valid ? -1 : 1;
}

if (a.purchasedAt === undefined && b.purchasedAt === undefined) {
return 0;
}
if (a.purchasedAt === undefined) {
return 1;
}
if (b.purchasedAt === undefined) {
return -1;
}
return b.purchasedAt - a.purchasedAt;
}),
};
return reply.send(response);
},
);
Expand Down
39 changes: 36 additions & 3 deletions src/ui/pages/tickets/ViewTickets.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Stack,
TextInput,
Alert,
Tooltip,
} from "@mantine/core";
import { IconAlertCircle } from "@tabler/icons-react";
import { notifications } from "@mantine/notifications";
Expand All @@ -30,6 +31,7 @@ const purchaseSchema = z.object({
productId: z.string(),
quantity: z.number().int().positive(),
size: z.string().optional(),
purchasedAt: z.number().optional(),
});

const ticketEntrySchema = z.object({
Expand Down Expand Up @@ -59,6 +61,13 @@ const getTicketStatus = (
return { status: "unfulfilled", color: "orange" };
};

const formatPurchaseTime = (timestamp?: number): string => {
if (!timestamp) {
return "N/A";
}
return new Date(timestamp * 1000).toLocaleString();
};

enum TicketsCopyMode {
ALL,
FULFILLED,
Expand Down Expand Up @@ -257,6 +266,14 @@ const ViewTicketsPage: React.FC = () => {
async function checkInUser(ticket: TicketEntry) {
handleOpenConfirmModal(ticket);
}

const copyTicketId = (ticketId: string) => {
navigator.clipboard.writeText(ticketId);
notifications.show({
message: "Ticket ID copied to clipboard!",
});
};

const getTickets = async () => {
try {
setLoading(true);
Expand Down Expand Up @@ -335,7 +352,7 @@ const ViewTicketsPage: React.FC = () => {
<Table.Th>Status</Table.Th>
<Table.Th>Quantity</Table.Th>
<Table.Th>Size</Table.Th>
<Table.Th>Ticket ID</Table.Th>
<Table.Th>Purchased At</Table.Th>
<Table.Th>Actions</Table.Th>
</Table.Tr>
</Table.Thead>
Expand All @@ -344,13 +361,29 @@ const ViewTicketsPage: React.FC = () => {
const { status, color } = getTicketStatus(ticket);
return (
<Table.Tr key={ticket.ticketId}>
<Table.Td>{ticket.purchaserData.email}</Table.Td>
<Table.Td>
<Tooltip
label="Click to copy ticket ID"
position="top"
withArrow
>
<Text
style={{ cursor: "pointer" }}
onClick={() => copyTicketId(ticket.ticketId)}
size="sm"
>
{ticket.purchaserData.email}
</Text>
</Tooltip>
</Table.Td>
<Table.Td>
<Badge color={color}>{status}</Badge>
</Table.Td>
<Table.Td>{ticket.purchaserData.quantity}</Table.Td>
<Table.Td>{ticket.purchaserData.size || "N/A"}</Table.Td>
<Table.Td>{ticket.ticketId}</Table.Td>
<Table.Td>
{formatPurchaseTime(ticket.purchaserData.purchasedAt)}
</Table.Td>
<Table.Td>
{!(ticket.fulfilled || ticket.refunded) && (
<AuthGuard
Expand Down
Loading