feat(activities): one row per attendee + render N QRs on multi-buy
Companion to aiolabs/events PR #15's d087bf3 (N rows sharing one payment_hash). Now that the backend persists each attendee as a distinct scannable row, the webapp surfaces them properly: - TicketPaymentStatus carries `ticketIds: string[]` (every row), with `ticketId` kept for back-compat. checkPaymentStatus reads both fields off the polling response. - useTicketPurchase tracks `purchasedTicketIds` + `ticketQRCodes` (parallel map id → data url). After payment lands the composable generates one QR per row so each attendee has their own. - PurchaseTicketDialog success screen renders every QR + ticket id in a stack with "Ticket N of M" labels. Each can be shared with a different attendee for an independent door scan. Reverts the "seats via extra.quantity" workarounds that landed in the previous two commits — now that rows == tickets the counters go back to row-count semantics across MyTickets, ActivityCard badges, ActivityDetailPage owned-tickets, useUserTickets group tallies, and the dialog's success header. Door-scan compatibility: the existing LNbits register-page scanner (events ext static/js/register.js) already reads `ticket://<id>` QRs and PUTs /tickets/register/<id>. With N rows each having a unique uuid id, each attendee's QR maps to a distinct PUT — independent registration, all 3 friends can enter separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c6d3e5cb26
commit
493a12a86b
8 changed files with 98 additions and 102 deletions
|
|
@ -53,8 +53,8 @@ const {
|
||||||
handleOpenLightningWallet,
|
handleOpenLightningWallet,
|
||||||
resetPaymentState,
|
resetPaymentState,
|
||||||
cleanup,
|
cleanup,
|
||||||
ticketQRCode,
|
ticketQRCodes,
|
||||||
purchasedTicketId,
|
purchasedTicketIds,
|
||||||
showTicketQR
|
showTicketQR
|
||||||
} = useTicketPurchase()
|
} = useTicketPurchase()
|
||||||
|
|
||||||
|
|
@ -543,35 +543,48 @@ onUnmounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ticket QR Code (After Successful Purchase) -->
|
<!-- Ticket QR Codes (After Successful Purchase) — one QR per
|
||||||
<div v-else-if="showTicketQR && ticketQRCode" class="py-4 flex flex-col items-center gap-4">
|
row so each attendee can be scanned independently at the
|
||||||
|
door. Single-ticket purchases just render one. -->
|
||||||
|
<div v-else-if="showTicketQR && purchasedTicketIds.length > 0" class="py-4 flex flex-col items-center gap-4">
|
||||||
<div class="text-center space-y-2">
|
<div class="text-center space-y-2">
|
||||||
<h3 class="text-lg font-semibold text-green-600">
|
<h3 class="text-lg font-semibold text-green-600">
|
||||||
{{ quantity > 1 ? `${quantity} tickets purchased!` : 'Ticket purchased!' }}
|
{{ purchasedTicketIds.length > 1
|
||||||
|
? `${purchasedTicketIds.length} tickets purchased!`
|
||||||
|
: 'Ticket purchased!' }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
<span v-if="quantity > 1">
|
<span v-if="purchasedTicketIds.length > 1">
|
||||||
{{ quantity }} tickets are now in your tickets area on a
|
Each attendee scans their own QR at the door —
|
||||||
single purchase row.
|
independent of the others.
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
Your ticket has been purchased and is now available in
|
Your ticket is now in your tickets area.
|
||||||
your tickets area.
|
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-muted/50 rounded-lg p-4 w-full">
|
<div class="w-full space-y-3">
|
||||||
<div class="text-center space-y-3">
|
<div
|
||||||
<div class="flex justify-center">
|
v-for="(ticketId, index) in purchasedTicketIds"
|
||||||
<Ticket class="w-12 h-12 text-green-600" />
|
:key="ticketId"
|
||||||
|
class="bg-muted/50 rounded-lg p-4"
|
||||||
|
>
|
||||||
|
<div class="text-center space-y-2">
|
||||||
|
<div v-if="purchasedTicketIds.length > 1" class="text-xs font-medium text-muted-foreground">
|
||||||
|
Ticket {{ index + 1 }} of {{ purchasedTicketIds.length }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex justify-center">
|
||||||
<p class="text-sm font-medium">
|
<img
|
||||||
{{ quantity > 1 ? 'Purchase ID (covers all tickets)' : 'Ticket ID' }}
|
v-if="ticketQRCodes[ticketId]"
|
||||||
</p>
|
:src="ticketQRCodes[ticketId]"
|
||||||
<div class="bg-background border rounded px-2 py-1 max-w-full mt-1">
|
:alt="`Ticket QR ${index + 1}`"
|
||||||
<p class="text-xs font-mono break-all">{{ purchasedTicketId }}</p>
|
class="w-32 h-32 border rounded-lg bg-white p-1"
|
||||||
|
/>
|
||||||
|
<Ticket v-else class="w-12 h-12 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div class="bg-background border rounded px-2 py-1 max-w-full">
|
||||||
|
<p class="text-[10px] font-mono break-all">{{ ticketId }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -74,17 +74,11 @@ function getTickets(activityId: string): ActivityTicket[] {
|
||||||
return ticketsByActivity.value.get(activityId) ?? []
|
return ticketsByActivity.value.get(activityId) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Seats represented by a single ticket row. One row carries N
|
/** Number of paid ticket rows for an activity. With the
|
||||||
* seats via extra.quantity; default 1 for legacy single-purchase rows. */
|
* multi-ticket-as-N-rows backend (events ext >= v1.6.1-aio.2),
|
||||||
function seatsOnRow(ticket: ActivityTicket): number {
|
* this matches the number of attendees / scannable QRs. */
|
||||||
return Math.max(1, ticket.extra?.quantity ?? 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Total paid seats across all rows for one activity (sums extra.quantity). */
|
|
||||||
function paidCount(activityId: string): number {
|
function paidCount(activityId: string): number {
|
||||||
return getTickets(activityId)
|
return getTickets(activityId).filter(t => t.paid).length
|
||||||
.filter(t => t.paid)
|
|
||||||
.reduce((sum, t) => sum + seatsOnRow(t), 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useOwnedTickets() {
|
export function useOwnedTickets() {
|
||||||
|
|
@ -125,7 +119,6 @@ export function useOwnedTickets() {
|
||||||
ownedActivityIds,
|
ownedActivityIds,
|
||||||
getTickets,
|
getTickets,
|
||||||
paidCount,
|
paidCount,
|
||||||
seatsOnRow,
|
|
||||||
refresh: fetchTickets,
|
refresh: fetchTickets,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,16 @@ export function useTicketPurchase() {
|
||||||
const qrCode = ref<string | null>(null)
|
const qrCode = ref<string | null>(null)
|
||||||
const isPaymentPending = ref(false)
|
const isPaymentPending = ref(false)
|
||||||
|
|
||||||
// Ticket QR code state
|
// Ticket QR code state. After payment lands, `purchasedTicketIds`
|
||||||
|
// is populated with every row id created on the invoice (one for
|
||||||
|
// a single-ticket purchase, N for multi). `ticketQRCodes` is a
|
||||||
|
// parallel map id → QR data URL so the UI can render one QR per
|
||||||
|
// attendee. `purchasedTicketId` stays for back-compat with the
|
||||||
|
// single-id success path.
|
||||||
const ticketQRCode = ref<string | null>(null)
|
const ticketQRCode = ref<string | null>(null)
|
||||||
|
const ticketQRCodes = ref<Record<string, string>>({})
|
||||||
const purchasedTicketId = ref<string | null>(null)
|
const purchasedTicketId = ref<string | null>(null)
|
||||||
|
const purchasedTicketIds = ref<string[]>([])
|
||||||
const showTicketQR = ref(false)
|
const showTicketQR = ref(false)
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
|
|
@ -94,7 +101,9 @@ export function useTicketPurchase() {
|
||||||
paymentRequest.value = null
|
paymentRequest.value = null
|
||||||
qrCode.value = null
|
qrCode.value = null
|
||||||
ticketQRCode.value = null
|
ticketQRCode.value = null
|
||||||
|
ticketQRCodes.value = {}
|
||||||
purchasedTicketId.value = null
|
purchasedTicketId.value = null
|
||||||
|
purchasedTicketIds.value = []
|
||||||
showTicketQR.value = false
|
showTicketQR.value = false
|
||||||
currentEventId.value = eventId
|
currentEventId.value = eventId
|
||||||
|
|
||||||
|
|
@ -169,13 +178,34 @@ export function useTicketPurchase() {
|
||||||
clearInterval(checkInterval)
|
clearInterval(checkInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.ticketId) {
|
// Multi-ticket purchases come back with `ticketIds` (N rows
|
||||||
purchasedTicketId.value = result.ticketId
|
// sharing one invoice). Single-ticket purchases include
|
||||||
await generateTicketQRCode(result.ticketId)
|
// `ticketId` only. Render one QR per row so each attendee
|
||||||
|
// has their own scannable code at the door.
|
||||||
|
const ids = result.ticketIds && result.ticketIds.length > 0
|
||||||
|
? result.ticketIds
|
||||||
|
: result.ticketId
|
||||||
|
? [result.ticketId]
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (ids.length > 0) {
|
||||||
|
purchasedTicketIds.value = ids
|
||||||
|
purchasedTicketId.value = ids[0]
|
||||||
|
const qrMap: Record<string, string> = {}
|
||||||
|
for (const id of ids) {
|
||||||
|
const dataUrl = await generateTicketQRCode(id)
|
||||||
|
if (dataUrl) qrMap[id] = dataUrl
|
||||||
|
}
|
||||||
|
ticketQRCodes.value = qrMap
|
||||||
|
ticketQRCode.value = qrMap[ids[0]] ?? null
|
||||||
showTicketQR.value = true
|
showTicketQR.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success('Ticket purchased successfully!')
|
toast.success(
|
||||||
|
ids.length > 1
|
||||||
|
? `${ids.length} tickets purchased!`
|
||||||
|
: 'Ticket purchased successfully!',
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error checking payment status:', err)
|
console.error('Error checking payment status:', err)
|
||||||
|
|
@ -197,7 +227,9 @@ export function useTicketPurchase() {
|
||||||
qrCode.value = null
|
qrCode.value = null
|
||||||
isPaymentPending.value = false
|
isPaymentPending.value = false
|
||||||
ticketQRCode.value = null
|
ticketQRCode.value = null
|
||||||
|
ticketQRCodes.value = {}
|
||||||
purchasedTicketId.value = null
|
purchasedTicketId.value = null
|
||||||
|
purchasedTicketIds.value = []
|
||||||
showTicketQR.value = false
|
showTicketQR.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,7 +257,9 @@ export function useTicketPurchase() {
|
||||||
isPaymentPending,
|
isPaymentPending,
|
||||||
isPayingWithWallet,
|
isPayingWithWallet,
|
||||||
ticketQRCode,
|
ticketQRCode,
|
||||||
|
ticketQRCodes,
|
||||||
purchasedTicketId,
|
purchasedTicketId,
|
||||||
|
purchasedTicketIds,
|
||||||
showTicketQR,
|
showTicketQR,
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
|
|
|
||||||
|
|
@ -66,20 +66,6 @@ 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>()
|
||||||
|
|
@ -99,18 +85,14 @@ export function useUserTickets() {
|
||||||
const group = groups.get(eventKey)!
|
const group = groups.get(eventKey)!
|
||||||
group.tickets.push(ticket)
|
group.tickets.push(ticket)
|
||||||
|
|
||||||
// A ticket row represents N seats via extra.quantity (default 1
|
|
||||||
// for legacy single-purchase rows). Counters sum seats, not
|
|
||||||
// rows, so the user sees the actual ticket count they bought.
|
|
||||||
const seats = Math.max(1, ticket.extra?.quantity ?? 1)
|
|
||||||
if (ticket.paid) {
|
if (ticket.paid) {
|
||||||
group.paidCount += seats
|
group.paidCount++
|
||||||
} else {
|
} else {
|
||||||
group.pendingCount += seats
|
group.pendingCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ticket.registered) {
|
if (ticket.registered) {
|
||||||
group.registeredCount += seats
|
group.registeredCount++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -134,11 +116,6 @@ export function useUserTickets() {
|
||||||
registeredTickets,
|
registeredTickets,
|
||||||
unregisteredTickets,
|
unregisteredTickets,
|
||||||
groupedTickets,
|
groupedTickets,
|
||||||
totalSeats,
|
|
||||||
paidSeats,
|
|
||||||
pendingSeats,
|
|
||||||
registeredSeats,
|
|
||||||
seatsOnRow,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
refresh: loadTickets,
|
refresh: loadTickets,
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ export class TicketApiService {
|
||||||
return {
|
return {
|
||||||
paid: data.paid === true,
|
paid: data.paid === true,
|
||||||
ticketId: data.ticket_id,
|
ticketId: data.ticket_id,
|
||||||
|
ticketIds: Array.isArray(data.ticket_ids) ? data.ticket_ids : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,6 @@ export interface ActivityTicketExtra {
|
||||||
email_notification_sent: boolean
|
email_notification_sent: boolean
|
||||||
nostr_notification_sent: boolean
|
nostr_notification_sent: boolean
|
||||||
refunded: boolean
|
refunded: boolean
|
||||||
/** Number of seats represented by this ticket row (1..10). One
|
|
||||||
* row per purchase keeps the schema unchanged; callers must
|
|
||||||
* multiply by this when surfacing seat counts. */
|
|
||||||
quantity?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActivityTicket {
|
export interface ActivityTicket {
|
||||||
|
|
@ -100,7 +96,13 @@ export interface TicketPurchaseInvoice {
|
||||||
|
|
||||||
export interface TicketPaymentStatus {
|
export interface TicketPaymentStatus {
|
||||||
paid: boolean
|
paid: boolean
|
||||||
|
/** First ticket id created on this invoice. Back-compat with
|
||||||
|
* single-ticket purchases — equals the payment_hash. */
|
||||||
ticketId?: string
|
ticketId?: string
|
||||||
|
/** Every row created on this invoice — one for single-ticket
|
||||||
|
* purchases, N for multi-ticket. Each row is independently
|
||||||
|
* scannable at the door. */
|
||||||
|
ticketIds?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ function goBack() {
|
||||||
|
|
||||||
// --- Ticket purchase + owned-tickets surface ----------------------
|
// --- Ticket purchase + owned-tickets surface ----------------------
|
||||||
|
|
||||||
const { getTickets, paidCount, seatsOnRow, refresh: refreshOwnedTickets } = useOwnedTickets()
|
const { getTickets, paidCount, refresh: refreshOwnedTickets } = useOwnedTickets()
|
||||||
|
|
||||||
const ownedTicketsForActivity = computed(() => getTickets(activityId))
|
const ownedTicketsForActivity = computed(() => getTickets(activityId))
|
||||||
const ownedPaidCount = computed(() => paidCount(activityId))
|
const ownedPaidCount = computed(() => paidCount(activityId))
|
||||||
|
|
@ -290,16 +290,9 @@ function goToMyTickets() {
|
||||||
<div
|
<div
|
||||||
v-for="ticket in ownedTicketsForActivity.filter(t => t.paid)"
|
v-for="ticket in ownedTicketsForActivity.filter(t => t.paid)"
|
||||||
:key="ticket.id"
|
:key="ticket.id"
|
||||||
class="flex items-center gap-2 text-xs text-muted-foreground"
|
class="text-xs font-mono text-muted-foreground break-all"
|
||||||
>
|
>
|
||||||
<Badge
|
{{ ticket.id }}
|
||||||
v-if="seatsOnRow(ticket) > 1"
|
|
||||||
variant="secondary"
|
|
||||||
class="shrink-0 text-[10px] font-mono px-1.5"
|
|
||||||
>
|
|
||||||
×{{ seatsOnRow(ticket) }}
|
|
||||||
</Badge>
|
|
||||||
<span class="font-mono break-all">{{ ticket.id }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" class="gap-1.5" @click="goToMyTickets">
|
<Button variant="outline" size="sm" class="gap-1.5" @click="goToMyTickets">
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,6 @@ const {
|
||||||
pendingTickets,
|
pendingTickets,
|
||||||
registeredTickets,
|
registeredTickets,
|
||||||
groupedTickets,
|
groupedTickets,
|
||||||
totalSeats,
|
|
||||||
paidSeats,
|
|
||||||
pendingSeats,
|
|
||||||
registeredSeats,
|
|
||||||
seatsOnRow,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
refresh
|
refresh
|
||||||
|
|
@ -163,10 +158,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 ({{ totalSeats }})</TabsTrigger>
|
<TabsTrigger value="all">All ({{ tickets.length }})</TabsTrigger>
|
||||||
<TabsTrigger value="paid">Paid ({{ paidSeats }})</TabsTrigger>
|
<TabsTrigger value="paid">Paid ({{ paidTickets.length }})</TabsTrigger>
|
||||||
<TabsTrigger value="pending">Pending ({{ pendingSeats }})</TabsTrigger>
|
<TabsTrigger value="pending">Pending ({{ pendingTickets.length }})</TabsTrigger>
|
||||||
<TabsTrigger value="registered">Registered ({{ registeredSeats }})</TabsTrigger>
|
<TabsTrigger value="registered">Registered ({{ registeredTickets.length }})</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<!-- All Tickets Tab -->
|
<!-- All Tickets Tab -->
|
||||||
|
|
@ -178,14 +173,11 @@ 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.paidCount + group.pendingCount }} ticket{{ (group.paidCount + group.pendingCount) !== 1 ? 's' : '' }}
|
{{ group.tickets.length }} ticket{{ group.tickets.length !== 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">
|
||||||
|
|
@ -225,18 +217,9 @@ 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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue