Skip to content

Commit 20e9bf6

Browse files
committed
Display purchased at data when available
1 parent f076739 commit 20e9bf6

File tree

3 files changed

+61
-4
lines changed

3 files changed

+61
-4
lines changed

src/api/functions/tickets.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type RawMerchEntry = {
3232
scannerEmail?: string;
3333
size: string;
3434
total_paid?: number;
35+
purchased_at?: number;
3536
};
3637

3738
export async function getUserTicketingPurchases({
@@ -135,6 +136,7 @@ export async function getUserMerchPurchases({
135136
refunded: item.refunded,
136137
fulfilled: item.fulfilled,
137138
totalPaid: item.total_paid,
139+
purchasedAt: item.purchased_at,
138140
});
139141
}
140142
return issuedTickets;

src/api/routes/tickets.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ const ticketEntryZod = z
7070
description:
7171
"The total amount paid by the customer, in cents, net of refunds.",
7272
}),
73+
purchasedAt: z.optional(z.number()).meta({
74+
description:
75+
"The time at which the user's checkout session completed, in seconds since Epoch.",
76+
}),
7377
})
7478
.meta({
7579
description: "An entry describing one merch or tickets transaction.",
@@ -297,7 +301,25 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
297301
message: `Retrieving tickets currently only supported on type "merch"!`,
298302
});
299303
}
300-
const response = { tickets: issuedTickets };
304+
const response = {
305+
tickets: issuedTickets.sort((a, b) => {
306+
// Valid tickets first
307+
if (a.valid !== b.valid) {
308+
return a.valid ? -1 : 1;
309+
}
310+
311+
if (a.purchasedAt === undefined && b.purchasedAt === undefined) {
312+
return 0;
313+
}
314+
if (a.purchasedAt === undefined) {
315+
return 1;
316+
}
317+
if (b.purchasedAt === undefined) {
318+
return -1;
319+
}
320+
return b.purchasedAt - a.purchasedAt;
321+
}),
322+
};
301323
return reply.send(response);
302324
},
303325
);

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

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Stack,
1212
TextInput,
1313
Alert,
14+
Tooltip,
1415
} from "@mantine/core";
1516
import { IconAlertCircle } from "@tabler/icons-react";
1617
import { notifications } from "@mantine/notifications";
@@ -30,6 +31,7 @@ const purchaseSchema = z.object({
3031
productId: z.string(),
3132
quantity: z.number().int().positive(),
3233
size: z.string().optional(),
34+
purchasedAt: z.number().optional(),
3335
});
3436

3537
const ticketEntrySchema = z.object({
@@ -59,6 +61,13 @@ const getTicketStatus = (
5961
return { status: "unfulfilled", color: "orange" };
6062
};
6163

64+
const formatPurchaseTime = (timestamp?: number): string => {
65+
if (!timestamp) {
66+
return "N/A";
67+
}
68+
return new Date(timestamp * 1000).toLocaleString();
69+
};
70+
6271
enum TicketsCopyMode {
6372
ALL,
6473
FULFILLED,
@@ -257,6 +266,14 @@ const ViewTicketsPage: React.FC = () => {
257266
async function checkInUser(ticket: TicketEntry) {
258267
handleOpenConfirmModal(ticket);
259268
}
269+
270+
const copyTicketId = (ticketId: string) => {
271+
navigator.clipboard.writeText(ticketId);
272+
notifications.show({
273+
message: "Ticket ID copied to clipboard!",
274+
});
275+
};
276+
260277
const getTickets = async () => {
261278
try {
262279
setLoading(true);
@@ -335,7 +352,7 @@ const ViewTicketsPage: React.FC = () => {
335352
<Table.Th>Status</Table.Th>
336353
<Table.Th>Quantity</Table.Th>
337354
<Table.Th>Size</Table.Th>
338-
<Table.Th>Ticket ID</Table.Th>
355+
<Table.Th>Purchased At</Table.Th>
339356
<Table.Th>Actions</Table.Th>
340357
</Table.Tr>
341358
</Table.Thead>
@@ -344,13 +361,29 @@ const ViewTicketsPage: React.FC = () => {
344361
const { status, color } = getTicketStatus(ticket);
345362
return (
346363
<Table.Tr key={ticket.ticketId}>
347-
<Table.Td>{ticket.purchaserData.email}</Table.Td>
364+
<Table.Td>
365+
<Tooltip
366+
label="Click to copy ticket ID"
367+
position="top"
368+
withArrow
369+
>
370+
<Text
371+
style={{ cursor: "pointer" }}
372+
onClick={() => copyTicketId(ticket.ticketId)}
373+
size="sm"
374+
>
375+
{ticket.purchaserData.email}
376+
</Text>
377+
</Tooltip>
378+
</Table.Td>
348379
<Table.Td>
349380
<Badge color={color}>{status}</Badge>
350381
</Table.Td>
351382
<Table.Td>{ticket.purchaserData.quantity}</Table.Td>
352383
<Table.Td>{ticket.purchaserData.size || "N/A"}</Table.Td>
353-
<Table.Td>{ticket.ticketId}</Table.Td>
384+
<Table.Td>
385+
{formatPurchaseTime(ticket.purchaserData.purchasedAt)}
386+
</Table.Td>
354387
<Table.Td>
355388
{!(ticket.fulfilled || ticket.refunded) && (
356389
<AuthGuard

0 commit comments

Comments
 (0)