- Remove unused imports from PurchaseTicketDialog.vue and useUserTickets.ts for improved code clarity. - Update events.ts to eliminate the unused paymentRequest parameter in payInvoiceWithWallet function. - Simplify MyTickets.vue by removing the unused unregisteredTickets variable and related QR code generation logic, enhancing maintainability.
599 lines
No EOL
30 KiB
Vue
599 lines
No EOL
30 KiB
Vue
<!-- eslint-disable vue/multi-word-component-names -->
|
|
<script setup lang="ts">
|
|
import { onMounted, ref, watch } from 'vue'
|
|
import { useUserTickets } from '@/composables/useUserTickets'
|
|
import { useAuth } from '@/composables/useAuth'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { format } from 'date-fns'
|
|
import { Ticket, User, CreditCard, CheckCircle, Clock, AlertCircle, ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
|
|
|
const { isAuthenticated, userDisplay } = useAuth()
|
|
const {
|
|
tickets,
|
|
paidTickets,
|
|
pendingTickets,
|
|
registeredTickets,
|
|
|
|
groupedTickets,
|
|
isLoading,
|
|
error,
|
|
refresh
|
|
} = useUserTickets()
|
|
|
|
// QR code state - now always generate QR codes for all tickets
|
|
const qrCodes = ref<Record<string, string>>({})
|
|
|
|
// Ticket cycling state
|
|
const currentTicketIndex = ref<Record<string, number>>({})
|
|
|
|
function formatDate(dateStr: string) {
|
|
return format(new Date(dateStr), 'PPP')
|
|
}
|
|
|
|
function formatTime(dateStr: string) {
|
|
return format(new Date(dateStr), 'HH:mm')
|
|
}
|
|
|
|
function getTicketStatus(ticket: any) {
|
|
if (!ticket.paid) return { status: 'pending', label: 'Payment Pending', icon: Clock, color: 'text-yellow-600' }
|
|
if (ticket.registered) return { status: 'registered', label: 'Registered', icon: CheckCircle, color: 'text-green-600' }
|
|
return { status: 'paid', label: 'Paid', icon: CreditCard, color: 'text-blue-600' }
|
|
}
|
|
|
|
async function generateQRCode(ticketId: string) {
|
|
if (qrCodes.value[ticketId]) return qrCodes.value[ticketId]
|
|
|
|
try {
|
|
const qrcode = await import('qrcode')
|
|
const ticketUrl = `ticket://${ticketId}`
|
|
const dataUrl = await qrcode.toDataURL(ticketUrl, {
|
|
width: 200, // Larger QR code for easier scanning
|
|
margin: 2,
|
|
color: {
|
|
dark: '#000000',
|
|
light: '#FFFFFF'
|
|
}
|
|
})
|
|
qrCodes.value[ticketId] = dataUrl
|
|
return dataUrl
|
|
} catch (error) {
|
|
console.error('Error generating QR code:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Ticket cycling functions
|
|
function getCurrentTicketIndex(eventId: string) {
|
|
return currentTicketIndex.value[eventId] || 0
|
|
}
|
|
|
|
function setCurrentTicketIndex(eventId: string, index: number) {
|
|
currentTicketIndex.value[eventId] = index
|
|
}
|
|
|
|
async function nextTicket(eventId: string, totalTickets: number) {
|
|
const current = getCurrentTicketIndex(eventId)
|
|
const nextIndex = (current + 1) % totalTickets
|
|
setCurrentTicketIndex(eventId, nextIndex)
|
|
|
|
// Generate QR code for the new ticket if needed
|
|
const group = groupedTickets.value.find(g => g.eventId === eventId)
|
|
if (group) {
|
|
const newTicket = group.tickets[nextIndex]
|
|
if (newTicket && !qrCodes.value[newTicket.id]) {
|
|
await generateQRCode(newTicket.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function prevTicket(eventId: string, totalTickets: number) {
|
|
const current = getCurrentTicketIndex(eventId)
|
|
const prevIndex = current === 0 ? totalTickets - 1 : current - 1
|
|
setCurrentTicketIndex(eventId, prevIndex)
|
|
|
|
// Generate QR code for the new ticket if needed
|
|
const group = groupedTickets.value.find(g => g.eventId === eventId)
|
|
if (group) {
|
|
const newTicket = group.tickets[prevIndex]
|
|
if (newTicket && !qrCodes.value[newTicket.id]) {
|
|
await generateQRCode(newTicket.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
function getCurrentTicket(tickets: any[], eventId: string) {
|
|
const index = getCurrentTicketIndex(eventId)
|
|
return tickets[index] || tickets[0]
|
|
}
|
|
|
|
// Watch for changes in grouped tickets and generate QR codes
|
|
watch(groupedTickets, async (newGroups) => {
|
|
for (const group of newGroups) {
|
|
for (const ticket of group.tickets) {
|
|
if (!qrCodes.value[ticket.id]) {
|
|
await generateQRCode(ticket.id)
|
|
}
|
|
}
|
|
}
|
|
}, { immediate: true })
|
|
|
|
onMounted(async () => {
|
|
if (isAuthenticated.value) {
|
|
await refresh()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="container mx-auto py-8 px-4">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div class="space-y-1">
|
|
<h1 class="text-3xl font-bold text-foreground">My Tickets</h1>
|
|
<div v-if="isAuthenticated && userDisplay" class="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<User class="w-4 h-4" />
|
|
<span>Logged in as {{ userDisplay.name }}</span>
|
|
<Badge variant="outline" class="text-xs">{{ userDisplay.shortId }}</Badge>
|
|
</div>
|
|
<div v-else class="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<AlertCircle class="w-4 h-4" />
|
|
<span>Please log in to view your tickets</span>
|
|
</div>
|
|
</div>
|
|
<Button variant="secondary" size="sm" @click="refresh" :disabled="isLoading">
|
|
<span v-if="isLoading" class="animate-spin mr-2">⚡</span>
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
|
|
<div v-if="!isAuthenticated" class="text-center py-12">
|
|
<div class="flex justify-center mb-4">
|
|
<Ticket class="w-16 h-16 text-muted-foreground" />
|
|
</div>
|
|
<h2 class="text-xl font-semibold mb-2">No Tickets Found</h2>
|
|
<p class="text-muted-foreground mb-4">Please log in to view your tickets</p>
|
|
<Button @click="$router.push('/login')">Login</Button>
|
|
</div>
|
|
|
|
<div v-else-if="error" class="mt-4 p-4 bg-destructive/15 text-destructive rounded-lg">
|
|
{{ error.message }}
|
|
</div>
|
|
|
|
<div v-else-if="tickets.length === 0 && !isLoading" class="text-center py-12">
|
|
<div class="flex justify-center mb-4">
|
|
<Ticket class="w-16 h-16 text-muted-foreground" />
|
|
</div>
|
|
<h2 class="text-xl font-semibold mb-2">No Tickets Found</h2>
|
|
<p class="text-muted-foreground mb-4">You haven't purchased any tickets yet</p>
|
|
<Button @click="$router.push('/events')">Browse Events</Button>
|
|
</div>
|
|
|
|
<div v-else-if="tickets.length > 0">
|
|
<Tabs default-value="all" class="w-full">
|
|
<TabsList class="grid w-full grid-cols-4">
|
|
<TabsTrigger value="all">All ({{ tickets.length }})</TabsTrigger>
|
|
<TabsTrigger value="paid">Paid ({{ paidTickets.length }})</TabsTrigger>
|
|
<TabsTrigger value="pending">Pending ({{ pendingTickets.length }})</TabsTrigger>
|
|
<TabsTrigger value="registered">Registered ({{ registeredTickets.length }})</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="all">
|
|
<ScrollArea class="h-[600px] w-full pr-4">
|
|
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
<Card v-for="group in groupedTickets" :key="group.eventId" class="flex flex-col">
|
|
<CardHeader>
|
|
<div class="flex items-center justify-between">
|
|
<CardTitle class="text-foreground">Event: {{ group.eventId }}</CardTitle>
|
|
<Badge variant="outline">
|
|
{{ group.tickets.length }} ticket{{ group.tickets.length !== 1 ? 's' : '' }}
|
|
</Badge>
|
|
</div>
|
|
<CardDescription>
|
|
{{ group.paidCount }} paid • {{ group.pendingCount }} pending • {{ group.registeredCount }} registered
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent class="flex-grow">
|
|
<div v-if="group.tickets.length > 0" class="space-y-4">
|
|
<!-- Ticket Navigation -->
|
|
<div class="flex items-center justify-between">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="prevTicket(group.eventId, group.tickets.length)"
|
|
:disabled="group.tickets.length <= 1"
|
|
>
|
|
<ChevronLeft class="w-4 h-4" />
|
|
</Button>
|
|
<span class="text-sm text-muted-foreground">
|
|
{{ getCurrentTicketIndex(group.eventId) + 1 }} of {{ group.tickets.length }}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="nextTicket(group.eventId, group.tickets.length)"
|
|
:disabled="group.tickets.length <= 1"
|
|
>
|
|
<ChevronRight class="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Current Ticket Display -->
|
|
<div v-if="getCurrentTicket(group.tickets, group.eventId)" class="space-y-4">
|
|
<!-- QR Code - Always Visible -->
|
|
<div class="flex justify-center">
|
|
<div class="text-center space-y-2">
|
|
<img
|
|
v-if="qrCodes[getCurrentTicket(group.tickets, group.eventId).id]"
|
|
:src="qrCodes[getCurrentTicket(group.tickets, group.eventId).id]"
|
|
alt="Ticket QR Code"
|
|
class="w-48 h-48 border rounded-lg mx-auto"
|
|
/>
|
|
<div v-else class="w-48 h-48 border rounded-lg flex items-center justify-center mx-auto">
|
|
<span class="text-xs text-muted-foreground">Loading...</span>
|
|
</div>
|
|
<div class="text-center">
|
|
<p class="text-xs text-muted-foreground">Ticket ID</p>
|
|
<div class="bg-background border rounded px-2 py-1 max-w-full mt-1">
|
|
<p class="text-xs font-mono break-all">{{ getCurrentTicket(group.tickets, group.eventId).id }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ticket Details -->
|
|
<div class="space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm font-medium">
|
|
Ticket #{{ getCurrentTicket(group.tickets, group.eventId).id.slice(0, 8) }}
|
|
</span>
|
|
<Badge :variant="getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).status === 'pending' ? 'secondary' : 'default'">
|
|
{{ getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).label }}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div class="space-y-1 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-muted-foreground">Status:</span>
|
|
<div class="flex items-center gap-1">
|
|
<component :is="getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).icon" class="w-4 h-4" :class="getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).color" />
|
|
<span>{{ getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).label }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-muted-foreground">Purchased:</span>
|
|
<span>{{ formatDate(getCurrentTicket(group.tickets, group.eventId).time) }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-muted-foreground">Time:</span>
|
|
<span>{{ formatTime(getCurrentTicket(group.tickets, group.eventId).time) }}</span>
|
|
</div>
|
|
<div v-if="getCurrentTicket(group.tickets, group.eventId).reg_timestamp" class="flex justify-between">
|
|
<span class="text-muted-foreground">Registered:</span>
|
|
<span>{{ formatDate(getCurrentTicket(group.tickets, group.eventId).reg_timestamp) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</ScrollArea>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="paid">
|
|
<ScrollArea class="h-[600px] w-full pr-4">
|
|
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
<Card v-for="group in groupedTickets.filter(g => g.paidCount > 0)" :key="group.eventId" class="flex flex-col">
|
|
<CardHeader>
|
|
<div class="flex items-center justify-between">
|
|
<CardTitle class="text-foreground">Event: {{ group.eventId }}</CardTitle>
|
|
<Badge variant="default">
|
|
{{ group.paidCount }} paid
|
|
</Badge>
|
|
</div>
|
|
<CardDescription>
|
|
{{ group.paidCount }} paid ticket{{ group.paidCount !== 1 ? 's' : '' }}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent class="flex-grow">
|
|
<div v-if="group.tickets.filter(t => t.paid).length > 0" class="space-y-4">
|
|
<!-- Ticket Navigation -->
|
|
<div class="flex items-center justify-between">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="prevTicket(group.eventId, group.tickets.filter(t => t.paid).length)"
|
|
:disabled="group.tickets.filter(t => t.paid).length <= 1"
|
|
>
|
|
<ChevronLeft class="w-4 h-4" />
|
|
</Button>
|
|
<span class="text-sm text-muted-foreground">
|
|
{{ getCurrentTicketIndex(group.eventId) + 1 }} of {{ group.tickets.filter(t => t.paid).length }}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="nextTicket(group.eventId, group.tickets.filter(t => t.paid).length)"
|
|
:disabled="group.tickets.filter(t => t.paid).length <= 1"
|
|
>
|
|
<ChevronRight class="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Current Ticket Display -->
|
|
<div v-if="getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId)" class="space-y-4">
|
|
<!-- QR Code - Always Visible -->
|
|
<div class="flex justify-center">
|
|
<div class="text-center space-y-2">
|
|
<img
|
|
v-if="qrCodes[getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).id]"
|
|
:src="qrCodes[getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).id]"
|
|
alt="Ticket QR Code"
|
|
class="w-48 h-48 border rounded-lg mx-auto"
|
|
/>
|
|
<div v-else class="w-48 h-48 border rounded-lg flex items-center justify-center mx-auto">
|
|
<span class="text-xs text-muted-foreground">Loading...</span>
|
|
</div>
|
|
<div class="text-center">
|
|
<p class="text-xs text-muted-foreground">Ticket ID</p>
|
|
<div class="bg-background border rounded px-2 py-1 max-w-full mt-1">
|
|
<p class="text-xs font-mono break-all">{{ getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).id }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ticket Details -->
|
|
<div class="space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm font-medium">
|
|
Ticket #{{ getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).id.slice(0, 8) }}
|
|
</span>
|
|
<Badge variant="default">
|
|
{{ getTicketStatus(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId)).label }}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div class="space-y-1 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-muted-foreground">Status:</span>
|
|
<div class="flex items-center gap-1">
|
|
<component :is="getTicketStatus(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId)).icon" class="w-4 h-4" :class="getTicketStatus(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId)).color" />
|
|
<span>{{ getTicketStatus(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId)).label }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-muted-foreground">Purchased:</span>
|
|
<span>{{ formatDate(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).time) }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-muted-foreground">Time:</span>
|
|
<span>{{ formatTime(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).time) }}</span>
|
|
</div>
|
|
<div v-if="getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).reg_timestamp" class="flex justify-between">
|
|
<span class="text-muted-foreground">Registered:</span>
|
|
<span>{{ formatDate(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).reg_timestamp) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</ScrollArea>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="pending">
|
|
<ScrollArea class="h-[600px] w-full pr-4">
|
|
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
<Card v-for="group in groupedTickets.filter(g => g.pendingCount > 0)" :key="group.eventId" class="flex flex-col opacity-75">
|
|
<CardHeader>
|
|
<div class="flex items-center justify-between">
|
|
<CardTitle class="text-foreground">Event: {{ group.eventId }}</CardTitle>
|
|
<Badge variant="secondary">
|
|
{{ group.pendingCount }} pending
|
|
</Badge>
|
|
</div>
|
|
<CardDescription>
|
|
{{ group.pendingCount }} pending ticket{{ group.pendingCount !== 1 ? 's' : '' }}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent class="flex-grow">
|
|
<div v-if="group.tickets.filter(t => !t.paid).length > 0" class="space-y-4">
|
|
<!-- Ticket Navigation -->
|
|
<div class="flex items-center justify-between">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="prevTicket(group.eventId, group.tickets.filter(t => !t.paid).length)"
|
|
:disabled="group.tickets.filter(t => !t.paid).length <= 1"
|
|
>
|
|
<ChevronLeft class="w-4 h-4" />
|
|
</Button>
|
|
<span class="text-sm text-muted-foreground">
|
|
{{ getCurrentTicketIndex(group.eventId) + 1 }} of {{ group.tickets.filter(t => !t.paid).length }}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="nextTicket(group.eventId, group.tickets.filter(t => !t.paid).length)"
|
|
:disabled="group.tickets.filter(t => !t.paid).length <= 1"
|
|
>
|
|
<ChevronRight class="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Current Ticket Display -->
|
|
<div v-if="getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId)" class="space-y-4">
|
|
<!-- QR Code - Always Visible -->
|
|
<div class="flex justify-center">
|
|
<div class="text-center space-y-2">
|
|
<img
|
|
v-if="qrCodes[getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).id]"
|
|
:src="qrCodes[getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).id]"
|
|
alt="Ticket QR Code"
|
|
class="w-48 h-48 border rounded-lg mx-auto"
|
|
/>
|
|
<div v-else class="w-48 h-48 border rounded-lg flex items-center justify-center mx-auto">
|
|
<span class="text-xs text-muted-foreground">Loading...</span>
|
|
</div>
|
|
<div class="text-center">
|
|
<p class="text-xs text-muted-foreground">Ticket ID</p>
|
|
<div class="bg-background border rounded px-2 py-1 max-w-full mt-1">
|
|
<p class="text-xs font-mono break-all">{{ getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).id }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ticket Details -->
|
|
<div class="space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm font-medium">
|
|
Ticket #{{ getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).id.slice(0, 8) }}
|
|
</span>
|
|
<Badge variant="secondary">
|
|
{{ getTicketStatus(getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId)).label }}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div class="space-y-1 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-muted-foreground">Status:</span>
|
|
<div class="flex items-center gap-1">
|
|
<component :is="getTicketStatus(getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId)).icon" class="w-4 h-4" :class="getTicketStatus(getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId)).color" />
|
|
<span>{{ getTicketStatus(getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId)).label }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-muted-foreground">Created:</span>
|
|
<span>{{ formatDate(getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).time) }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-muted-foreground">Time:</span>
|
|
<span>{{ formatTime(getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).time) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</ScrollArea>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="registered">
|
|
<ScrollArea class="h-[600px] w-full pr-4">
|
|
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
<Card v-for="group in groupedTickets.filter(g => g.registeredCount > 0)" :key="group.eventId" class="flex flex-col">
|
|
<CardHeader>
|
|
<div class="flex items-center justify-between">
|
|
<CardTitle class="text-foreground">Event: {{ group.eventId }}</CardTitle>
|
|
<Badge variant="default">
|
|
{{ group.registeredCount }} registered
|
|
</Badge>
|
|
</div>
|
|
<CardDescription>
|
|
{{ group.registeredCount }} registered ticket{{ group.registeredCount !== 1 ? 's' : '' }}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent class="flex-grow">
|
|
<div v-if="group.tickets.filter(t => t.registered).length > 0" class="space-y-4">
|
|
<!-- Ticket Navigation -->
|
|
<div class="flex items-center justify-between">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="prevTicket(group.eventId, group.tickets.filter(t => t.registered).length)"
|
|
:disabled="group.tickets.filter(t => t.registered).length <= 1"
|
|
>
|
|
<ChevronLeft class="w-4 h-4" />
|
|
</Button>
|
|
<span class="text-sm text-muted-foreground">
|
|
{{ getCurrentTicketIndex(group.eventId) + 1 }} of {{ group.tickets.filter(t => t.registered).length }}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="nextTicket(group.eventId, group.tickets.filter(t => t.registered).length)"
|
|
:disabled="group.tickets.filter(t => t.registered).length <= 1"
|
|
>
|
|
<ChevronRight class="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Current Ticket Display -->
|
|
<div v-if="getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId)" class="space-y-4">
|
|
<!-- QR Code - Always Visible -->
|
|
<div class="flex justify-center">
|
|
<div class="text-center space-y-2">
|
|
<img
|
|
v-if="qrCodes[getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).id]"
|
|
:src="qrCodes[getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).id]"
|
|
alt="Ticket QR Code"
|
|
class="w-48 h-48 border rounded-lg mx-auto"
|
|
/>
|
|
<div v-else class="w-48 h-48 border rounded-lg flex items-center justify-center mx-auto">
|
|
<span class="text-xs text-muted-foreground">Loading...</span>
|
|
</div>
|
|
<div class="text-center">
|
|
<p class="text-xs text-muted-foreground">Ticket ID</p>
|
|
<div class="bg-background border rounded px-2 py-1 max-w-full mt-1">
|
|
<p class="text-xs font-mono break-all">{{ getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).id }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ticket Details -->
|
|
<div class="space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm font-medium">
|
|
Ticket #{{ getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).id.slice(0, 8) }}
|
|
</span>
|
|
<Badge variant="default">
|
|
{{ getTicketStatus(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId)).label }}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div class="space-y-1 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-muted-foreground">Status:</span>
|
|
<div class="flex items-center gap-1">
|
|
<component :is="getTicketStatus(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId)).icon" class="w-4 h-4" :class="getTicketStatus(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId)).color" />
|
|
<span>{{ getTicketStatus(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId)).label }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-muted-foreground">Purchased:</span>
|
|
<span>{{ formatDate(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).time) }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-muted-foreground">Time:</span>
|
|
<span>{{ formatTime(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).time) }}</span>
|
|
</div>
|
|
<div v-if="getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).reg_timestamp" class="flex justify-between">
|
|
<span class="text-muted-foreground">Registered:</span>
|
|
<span>{{ formatDate(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).reg_timestamp) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</ScrollArea>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</div>
|
|
</template> |