feat: show real attendee count with guests in admin, expandable guest details, fix CSV export

This commit is contained in:
2026-03-11 11:37:51 +01:00
parent 8a6d3035cb
commit d25f4aa813
2 changed files with 423 additions and 308 deletions

View File

@@ -474,27 +474,49 @@ export const appRouter = {
const today = new Date();
today.setHours(0, 0, 0, 0);
const [totalResult, todayResult, artFormResult, typeResult, giftResult] =
await Promise.all([
db.select({ count: count() }).from(registration),
db
.select({ count: count() })
.from(registration)
.where(gte(registration.createdAt, today)),
db
.select({ artForm: registration.artForm, count: count() })
.from(registration)
.where(eq(registration.registrationType, "performer"))
.groupBy(registration.artForm),
db
.select({
registrationType: registration.registrationType,
count: count(),
})
.from(registration)
.groupBy(registration.registrationType),
db.select({ total: sum(registration.giftAmount) }).from(registration),
]);
const [
totalResult,
todayResult,
artFormResult,
typeResult,
giftResult,
watcherGuestRows,
] = await Promise.all([
db.select({ count: count() }).from(registration),
db
.select({ count: count() })
.from(registration)
.where(gte(registration.createdAt, today)),
db
.select({ artForm: registration.artForm, count: count() })
.from(registration)
.where(eq(registration.registrationType, "performer"))
.groupBy(registration.artForm),
db
.select({
registrationType: registration.registrationType,
count: count(),
})
.from(registration)
.groupBy(registration.registrationType),
db.select({ total: sum(registration.giftAmount) }).from(registration),
// Fetch guests column for all watcher registrations to count total guests
db
.select({ guests: registration.guests })
.from(registration)
.where(
and(
eq(registration.registrationType, "watcher"),
isNull(registration.cancelledAt),
),
),
]);
// Sum up all guest counts across all watcher registrations
const totalGuestCount = watcherGuestRows.reduce((sum, r) => {
const guests = parseGuestsJson(r.guests);
return sum + guests.length;
}, 0);
return {
total: totalResult[0]?.count ?? 0,
@@ -508,6 +530,7 @@ export const appRouter = {
count: r.count,
})),
totalGiftRevenue: giftResult[0]?.total ?? 0,
totalGuestCount,
};
}),
@@ -517,6 +540,9 @@ export const appRouter = {
.from(registration)
.orderBy(desc(registration.createdAt));
const escapeCell = (val: string) => `"${val.replace(/"/g, '""')}"`;
// Main registration headers
const headers = [
"ID",
"First Name",
@@ -527,49 +553,97 @@ export const appRouter = {
"Art Form",
"Experience",
"Is Over 16",
"Drink Card Value",
"Gift Amount",
"Drink Card Value (EUR)",
"Gift Amount (cents)",
"Guest Count",
"Guests",
"Guest 1 First Name",
"Guest 1 Last Name",
"Guest 1 Email",
"Guest 1 Phone",
"Guest 2 First Name",
"Guest 2 Last Name",
"Guest 2 Email",
"Guest 2 Phone",
"Guest 3 First Name",
"Guest 3 Last Name",
"Guest 3 Email",
"Guest 3 Phone",
"Guest 4 First Name",
"Guest 4 Last Name",
"Guest 4 Email",
"Guest 4 Phone",
"Guest 5 First Name",
"Guest 5 Last Name",
"Guest 5 Email",
"Guest 5 Phone",
"Guest 6 First Name",
"Guest 6 Last Name",
"Guest 6 Email",
"Guest 6 Phone",
"Guest 7 First Name",
"Guest 7 Last Name",
"Guest 7 Email",
"Guest 7 Phone",
"Guest 8 First Name",
"Guest 8 Last Name",
"Guest 8 Email",
"Guest 8 Phone",
"Guest 9 First Name",
"Guest 9 Last Name",
"Guest 9 Email",
"Guest 9 Phone",
"Payment Status",
"Paid At",
"Extra Questions",
"Cancelled At",
"Created At",
];
const MAX_GUESTS = 9;
const rows = data.map((r) => {
const guests = parseGuestsJson(r.guests);
const guestSummary = guests
.map(
(g) =>
`${g.firstName} ${g.lastName}${g.email ? ` <${g.email}>` : ""}${g.phone ? ` (${g.phone})` : ""}`,
)
.join(" | ");
// Build guest columns (up to 9 guests, 4 fields each)
const guestCols: string[] = [];
for (let i = 0; i < MAX_GUESTS; i++) {
const g = guests[i];
guestCols.push(g?.firstName ?? "");
guestCols.push(g?.lastName ?? "");
guestCols.push(g?.email ?? "");
guestCols.push(g?.phone ?? "");
}
return [
r.id,
r.firstName,
r.lastName,
r.email,
r.phone || "",
r.registrationType,
r.registrationType ?? "",
r.artForm || "",
r.experience || "",
r.isOver16 ? "Yes" : "No",
String(r.drinkCardValue ?? 0),
String(r.giftAmount ?? 0),
String(guests.length),
guestSummary,
r.paymentStatus === "paid" ? "Paid" : "Pending",
...guestCols,
r.paymentStatus === "paid"
? "Paid"
: r.paymentStatus === "extra_payment_pending"
? "Extra Payment Pending"
: "Pending",
r.paidAt ? r.paidAt.toISOString() : "",
r.extraQuestions || "",
r.cancelledAt ? r.cancelledAt.toISOString() : "",
r.createdAt.toISOString(),
];
});
const csvContent = [
headers.join(","),
headers.map(escapeCell).join(","),
...rows.map((row) =>
row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(","),
row.map((cell) => escapeCell(String(cell))).join(","),
),
].join("\n");