Skip to main content
The seller dashboard provides event organizers with comprehensive insights into their events’ performance, including ticket sales, revenue, refunds, and access to Stripe Connect for payout management.

Dashboard Metrics

Event organizers can track key performance indicators for each event:

Tickets Sold

Number of valid and used tickets

Revenue

Total earnings from ticket sales

Refunds

Number and amount of refunded tickets

Cancelled Tickets

Tickets from cancelled events

Metrics Type Definition

From convex/events.ts:10-15:
export type Metrics = {
  soldTickets: number;
  refundedTickets: number;
  cancelledTickets: number;
  revenue: number;
};

Seller Events Query

The main query that powers the seller dashboard retrieves all events created by a user along with their metrics.

Implementation

From convex/events.ts:389-429:
export const getSellerEvents = query({
  args: { userId: v.string() },
  handler: async (ctx, { userId }) => {
    const events = await ctx.db
      .query("events")
      .filter((q) => q.eq(q.field("userId"), userId))
      .collect();

    // For each event, get ticket sales data
    const eventsWithMetrics = await Promise.all(
      events.map(async (event) => {
        const tickets = await ctx.db
          .query("tickets")
          .withIndex("by_event", (q) => q.eq("eventId", event._id))
          .collect();

        const validTickets = tickets.filter(
          (t) => t.status === "valid" || t.status === "used"
        );
        const refundedTickets = tickets.filter((t) => t.status === "refunded");
        const cancelledTickets = tickets.filter(
          (t) => t.status === "cancelled"
        );

        const metrics: Metrics = {
          soldTickets: validTickets.length,
          refundedTickets: refundedTickets.length,
          cancelledTickets: cancelledTickets.length,
          revenue: validTickets.length * event.price,
        };

        return {
          ...event,
          metrics,
        };
      })
    );

    return eventsWithMetrics;
  },
});
Revenue is calculated as soldTickets × event.price and only includes valid and used tickets.

Seller Event Display

The seller dashboard displays events in two categories:

Upcoming Events

Events with eventDate > Date.now()

Past Events

Events with eventDate <= Date.now()From src/components/seller-events-list.tsx:37-69:
const upcomingEvents = events.filter((e) => e.eventDate > Date.now());
const pastEvents = events.filter((e) => e.eventDate <= Date.now());

return (
  <div className="mx-auto space-y-8">
    {/* Upcoming Events */}
    <div>
      <h2 className="text-2xl font-bold text-gray-900 mb-4">
        Upcoming Events
      </h2>
      <div className="grid grid-cols-1 gap-6">
        {upcomingEvents.map((event) => (
          <SellerEventCard key={event._id} event={event} />
        ))}
        {upcomingEvents.length === 0 && (
          <p className="text-gray-500">No upcoming events</p>
        )}
      </div>
    </div>

    {/* Past Events */}
    {pastEvents.length > 0 && (
      <div>
        <h2 className="text-2xl font-bold text-gray-900 mb-4">Past Events</h2>
        <div className="grid grid-cols-1 gap-6">
          {pastEvents.map((event) => (
            <SellerEventCard key={event._id} event={event} />
          ))}
        </div>
      </div>
    )}
  </div>
);

Event Management Actions

For upcoming, non-cancelled events, organizers have access to:
Modify event details while respecting ticket sale constraints.
<Link
  href={`/seller/events/${event._id}/edit`}
  className="shrink-0 flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
>
  <Edit className="w-4 h-4" />
  Edit
</Link>
See Event Management - Updating Events for details on update constraints.
Cancel the event and trigger automatic refunds for all ticket holders.
<CancelEventButton eventId={event._id} />
See Event Management - Cancelling Events for the cancellation process.
Edit and cancel actions are only available for upcoming events that haven’t been cancelled.

Stripe Connect Integration

Event organizers must connect a Stripe account to receive payouts from ticket sales.

Create Stripe Connect Account

Ticket Hub uses Stripe Connect with the Express account type:
const account = await stripe.accounts.create({
  type: "express",
  email: user.emailAddresses[0].emailAddress,
  capabilities: {
    card_payments: { requested: true },
    transfers: { requested: true },
  },
});

Access Stripe Dashboard

Organizers can access their Stripe Express dashboard to:
  • View detailed transaction history
  • Manage payout schedules
  • Update banking information
  • View platform fees
From src/actions/create-stripe-connect-login-link.ts:5-17:
export async function createStripeConnectLoginLink(stripeAccountId: string) {
  if (!stripeAccountId) {
    throw new Error("No Stripe account ID provided");
  }

  try {
    const loginLink = await stripe.accounts.createLoginLink(stripeAccountId);
    return loginLink.url;
  } catch (error) {
    console.error("Error creating Stripe Connect login link:", error);
    throw new Error("Failed to create Stripe Connect login link");
  }
}
The login link is temporary and expires after a short period for security.

Revenue Calculation

Platform Fee

Ticket Hub charges a 1% platform fee on each ticket sale:
payment_intent_data: {
  application_fee_amount: Math.round(event.price * 100 * 0.01),
}

Net Revenue

Organizers receive:
  • 99% of the ticket price
  • Minus Stripe processing fees (typically 2.9% + £0.30)

Example Calculation

For a £50 ticket:
  • Gross price: £50.00
  • Platform fee (1%): £0.50
  • To organizer: £49.50
  • Stripe fees: ~£1.48
  • Net to organizer: ~£48.02
Stripe processing fees are deducted automatically before payout.

Event Performance Tracking

Metrics Breakdown

1

Sold Tickets

Count of tickets with status valid or used:
const validTickets = tickets.filter(
  (t) => t.status === "valid" || t.status === "used"
);
const soldTickets = validTickets.length;
2

Revenue

Total earnings from sold tickets:
const revenue = validTickets.length * event.price;
3

Refunded Tickets

Tickets that were refunded:
const refundedTickets = tickets.filter((t) => t.status === "refunded");
4

Cancelled Tickets

Tickets from cancelled events:
const cancelledTickets = tickets.filter(
  (t) => t.status === "cancelled"
);

Event Status Indicators

Events display different status indicators based on their state:
Upcoming events that are currently on sale and accepting ticket purchases.
  • Event date is in the future
  • is_cancelled is not set
  • Displayed with standard styling

Dashboard Queries

Get Event Availability

Check real-time ticket availability for an event: From convex/events.ts:324-368:
export const getEventAvailability = query({
  args: { eventId: v.id("events") },
  handler: async (ctx, { eventId }) => {
    const event = await ctx.db.get(eventId);
    if (!event) throw new Error("Event not found");

    // Count total purchased tickets
    const purchasedCount = await ctx.db
      .query("tickets")
      .withIndex("by_event", (q) => q.eq("eventId", eventId))
      .collect()
      .then(
        (tickets) =>
          tickets.filter(
            (t) =>
              t.status === TICKET_STATUS.VALID ||
              t.status === TICKET_STATUS.USED
          ).length
      );

    // Count current valid offers
    const now = Date.now();
    const activeOffers = await ctx.db
      .query("waitingList")
      .withIndex("by_event_status", (q) =>
        q
          .eq("eventId", eventId)
          .eq("status", WAITING_LIST_STATUS.OFFERED ?? "offered")
      )
      .collect()
      .then(
        (entries) => entries.filter((e) => (e.offerExpiresAt ?? 0) > now).length
      );

    const totalReserved = purchasedCount + activeOffers;

    return {
      isSoldOut: totalReserved >= event.totalTickets,
      totalTickets: event.totalTickets,
      purchasedCount,
      activeOffers,
      remainingTickets: Math.max(0, event.totalTickets - totalReserved),
    };
  },
});

Get Valid Tickets for Event

Retrieve all valid tickets for an event (used for scanning/verification): From convex/tickets.ts:37-48:
export const getValidTicketsForEvent = query({
  args: { eventId: v.id("events") },
  handler: async (ctx, { eventId }) => {
    return await ctx.db
      .query("tickets")
      .withIndex("by_event", (q) => q.eq("eventId", eventId))
      .filter((q) =>
        q.or(q.eq(q.field("status"), "valid"), q.eq(q.field("status"), "used"))
      )
      .collect();
  },
});

Next Steps

Event Management

Create and manage events

Ticket Purchasing

Understand the buyer experience

Build docs developers (and LLMs) love