fix(activities): MyTickets tab pills + group header count seats not rows

Last commit fixed the dialog + ActivityDetailPage to read extra.quantity,
but missed three more row-count → seat-count surfaces in
MyTicketsPage:

- Tab pills (All / Paid / Pending / Registered) used
  `paidTickets.length` etc. on the filtered row arrays — so a user
  who bought 1+5+5+6+3+1+1+1 = 23 seats across 8 rows saw "All
  (8)". Now reads from useUserTickets.{total,paid,pending,
  registered}Seats which sum extra.quantity.
- Group header badge "{{ group.tickets.length }} tickets" → uses
  group.paidCount + pendingCount (already seat-summed by the
  previous fix to groupedTickets).
- Group description gains a "({N} purchases)" sub-line when seats
  ≠ rows so the buyer can see at a glance "you have 23 tickets
  across 8 purchases".
- Per-row carousel card grows a `×N` chip next to the truncated
  Ticket #ID when that row represents multi-seat — same chip
  language as the ActivityDetailPage owned-tickets section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-23 22:25:33 +02:00
commit c6d3e5cb26
2 changed files with 45 additions and 8 deletions

View file

@ -66,6 +66,21 @@ export function useUserTickets() {
return sortedTickets.value.filter(ticket => ticket.paid && !ticket.registered) return sortedTickets.value.filter(ticket => ticket.paid && !ticket.registered)
}) })
// Seat counts (sum extra.quantity across rows) so the tab pills
// surface "what the buyer bought" rather than "how many purchase
// rows are in the database". A multi-ticket purchase is one row
// with extra.quantity = N seats.
function seatsOnRow(t: ActivityTicket): number {
return Math.max(1, t.extra?.quantity ?? 1)
}
function sumSeats(arr: ActivityTicket[]): number {
return arr.reduce((total, t) => total + seatsOnRow(t), 0)
}
const totalSeats = computed(() => sumSeats(sortedTickets.value))
const paidSeats = computed(() => sumSeats(paidTickets.value))
const pendingSeats = computed(() => sumSeats(pendingTickets.value))
const registeredSeats = computed(() => sumSeats(registeredTickets.value))
const groupedTickets = computed(() => { const groupedTickets = computed(() => {
const groups = new Map<string, GroupedTickets>() const groups = new Map<string, GroupedTickets>()
@ -119,6 +134,11 @@ export function useUserTickets() {
registeredTickets, registeredTickets,
unregisteredTickets, unregisteredTickets,
groupedTickets, groupedTickets,
totalSeats,
paidSeats,
pendingSeats,
registeredSeats,
seatsOnRow,
isLoading, isLoading,
error, error,
refresh: loadTickets, refresh: loadTickets,

View file

@ -17,6 +17,11 @@ const {
pendingTickets, pendingTickets,
registeredTickets, registeredTickets,
groupedTickets, groupedTickets,
totalSeats,
paidSeats,
pendingSeats,
registeredSeats,
seatsOnRow,
isLoading, isLoading,
error, error,
refresh refresh
@ -158,10 +163,10 @@ onMounted(async () => {
<div v-else-if="tickets.length > 0"> <div v-else-if="tickets.length > 0">
<Tabs default-value="all" class="w-full"> <Tabs default-value="all" class="w-full">
<TabsList class="grid w-full grid-cols-4"> <TabsList class="grid w-full grid-cols-4">
<TabsTrigger value="all">All ({{ tickets.length }})</TabsTrigger> <TabsTrigger value="all">All ({{ totalSeats }})</TabsTrigger>
<TabsTrigger value="paid">Paid ({{ paidTickets.length }})</TabsTrigger> <TabsTrigger value="paid">Paid ({{ paidSeats }})</TabsTrigger>
<TabsTrigger value="pending">Pending ({{ pendingTickets.length }})</TabsTrigger> <TabsTrigger value="pending">Pending ({{ pendingSeats }})</TabsTrigger>
<TabsTrigger value="registered">Registered ({{ registeredTickets.length }})</TabsTrigger> <TabsTrigger value="registered">Registered ({{ registeredSeats }})</TabsTrigger>
</TabsList> </TabsList>
<!-- All Tickets Tab --> <!-- All Tickets Tab -->
@ -173,11 +178,14 @@ onMounted(async () => {
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<CardTitle class="text-foreground">Event: {{ group.eventId.slice(0, 8) }}...</CardTitle> <CardTitle class="text-foreground">Event: {{ group.eventId.slice(0, 8) }}...</CardTitle>
<Badge variant="outline"> <Badge variant="outline">
{{ group.tickets.length }} ticket{{ group.tickets.length !== 1 ? 's' : '' }} {{ group.paidCount + group.pendingCount }} ticket{{ (group.paidCount + group.pendingCount) !== 1 ? 's' : '' }}
</Badge> </Badge>
</div> </div>
<CardDescription> <CardDescription>
{{ group.paidCount }} paid · {{ group.pendingCount }} pending · {{ group.registeredCount }} registered {{ group.paidCount }} paid · {{ group.pendingCount }} pending · {{ group.registeredCount }} registered
<span v-if="group.tickets.length !== (group.paidCount + group.pendingCount)" class="block text-xs">
({{ group.tickets.length }} purchase{{ group.tickets.length !== 1 ? 's' : '' }})
</span>
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent class="flex-grow"> <CardContent class="flex-grow">
@ -217,9 +225,18 @@ onMounted(async () => {
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-1.5">
<span class="text-sm font-medium"> <span class="text-sm font-medium">
Ticket #{{ getCurrentTicket(group.tickets, group.eventId).id.slice(0, 8) }} Ticket #{{ getCurrentTicket(group.tickets, group.eventId).id.slice(0, 8) }}
</span> </span>
<Badge
v-if="seatsOnRow(getCurrentTicket(group.tickets, group.eventId)) > 1"
variant="secondary"
class="text-[10px] font-mono px-1.5"
>
×{{ seatsOnRow(getCurrentTicket(group.tickets, group.eventId)) }}
</Badge>
</div>
<Badge :variant="getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).status === 'pending' ? 'secondary' : 'default'"> <Badge :variant="getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).status === 'pending' ? 'secondary' : 'default'">
{{ getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).label }} {{ getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).label }}
</Badge> </Badge>