diff --git a/dashboard/src/pages/manage/events/EventGuests.vue b/dashboard/src/pages/manage/events/EventGuests.vue
new file mode 100644
index 00000000..def31c74
--- /dev/null
+++ b/dashboard/src/pages/manage/events/EventGuests.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+ How it's going
+
+
+
+ {{ guests.data?.total ?? "—" }}
+
+ registered
+
+
+
+ Registrations have closed
+
+ Registrations are open
+
+
+
+ Guest list
+
+
+
+
+
+
+
+
+
+
+
+
+ Nobody here matches “{{ query }}”.
+
+ No guests yet.
+
+
+
diff --git a/dashboard/src/router.ts b/dashboard/src/router.ts
index 25340e89..4a798c9d 100644
--- a/dashboard/src/router.ts
+++ b/dashboard/src/router.ts
@@ -44,6 +44,11 @@ const routes: RouteRecordRaw[] = [
name: "event-details",
component: () => import("@/pages/manage/events/EventDetails.vue"),
},
+ {
+ path: "events/:eventId/guests",
+ name: "event-guests",
+ component: () => import("@/pages/manage/events/EventGuests.vue"),
+ },
{
path: "events/:eventId/:section",
component: () => import("@/pages/manage/WorkInProgress.vue"),
diff --git a/dashboard/src/types.ts b/dashboard/src/types.ts
index 2c3023a1..58f97184 100644
--- a/dashboard/src/types.ts
+++ b/dashboard/src/types.ts
@@ -125,6 +125,26 @@ export interface MyEvents {
past: MyEvent[]
}
+export interface GuestAddOn {
+ title: string
+ value: string | null
+}
+
+// buzz.api.events.get_event_guests: one row per submitted ticket, not per person.
+export interface EventGuest {
+ name: string
+ attendee_name: string | null
+ attendee_email: string | null
+ ticket_type: string | null
+ add_ons: GuestAddOn[]
+}
+
+export interface EventGuests {
+ total: number
+ registrations_closed: boolean
+ guests: EventGuest[]
+}
+
export interface EventVenueDetail {
name: string
address: string | null
diff --git a/e2e/tests/manage-event.spec.ts b/e2e/tests/manage-event.spec.ts
index 1fd14c1b..4313f56d 100644
--- a/e2e/tests/manage-event.spec.ts
+++ b/e2e/tests/manage-event.spec.ts
@@ -66,12 +66,51 @@ test.describe("Event workspace", () => {
});
test("swaps the sidebar for the event's own destinations", async ({ page }) => {
- for (const label of ["Back", "Details", "Attendees", "Talks"]) {
+ for (const label of ["Back", "Details", "Guests", "Talks"]) {
await expect(page.getByRole("link", { name: label })).toBeVisible({ timeout: 15000 });
}
await expect(page.getByRole("link", { name: "My Tickets" })).toHaveCount(0);
});
+ test("lists the event's guests", async ({ page }) => {
+ await page.getByRole("link", { name: "Guests" }).click();
+
+ await expect(page).toHaveURL(/\/b\/manage\/events\/\d+\/guests$/);
+ await expect(page.getByRole("heading", { name: "Guest list" })).toBeVisible();
+ // The shared event is the one tickets.setup.ts books against, so it has guests.
+ const rows = page.getByRole("list").getByRole("listitem");
+ await expect(rows.first()).toBeVisible({ timeout: 15000 });
+
+ // The count is the number of rows under it, not a separate claim.
+ const registrations = Number(await page.getByText(/^\d+$/).first().textContent());
+ expect(registrations).toBe(await rows.count());
+ await expect(page.getByText(/Registrations (are open|have closed)/)).toBeVisible();
+
+ // A row carries the person and the ticket they hold, named rather than numbered.
+ await expect(rows.first()).toContainText("@");
+ const badge = rows.first().locator("div").last();
+ await expect(badge).not.toHaveText(/^\d+$/);
+ });
+
+ test("narrows the guest list by search", async ({ page }) => {
+ await page.getByRole("link", { name: "Guests" }).click();
+
+ const rows = page.getByRole("list").getByRole("listitem");
+ await expect(rows.first()).toBeVisible({ timeout: 15000 });
+ const everyone = await rows.count();
+
+ // The email of whoever happens to be first, so this holds on any site. Read from
+ // its own element: the row's text runs the name and email together.
+ const email = (await rows.first().getByText(/@/).last().textContent())!.trim();
+ await page.getByRole("textbox", { name: "Search guests" }).fill(email);
+
+ await expect(rows).toHaveCount(1);
+ await expect(rows.first()).toContainText(email!);
+
+ await page.getByRole("textbox", { name: "Search guests" }).fill("");
+ await expect(rows).toHaveCount(everyone);
+ });
+
test("moves between sections", async ({ page }) => {
await page.getByRole("link", { name: "Talks" }).click();