feat: Implement ticket cycling functionality in MyTickets
- Add navigation buttons for cycling through tickets within each event group, enhancing user experience. - Introduce state management for current ticket index, allowing users to view tickets sequentially. - Update ticket display logic to reflect the currently selected ticket, ensuring accurate information presentation. - Refactor layout to accommodate new ticket navigation features while maintaining a cohesive design.
This commit is contained in:
parent
67e4c0db87
commit
774df2757d
1 changed files with 248 additions and 144 deletions
|
|
@ -9,7 +9,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { Ticket, User, Calendar, CreditCard, CheckCircle, Clock, AlertCircle, QrCode } from 'lucide-vue-next'
|
import { Ticket, User, Calendar, CreditCard, CheckCircle, Clock, AlertCircle, QrCode, ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
||||||
|
|
||||||
const { isAuthenticated, userDisplay } = useAuth()
|
const { isAuthenticated, userDisplay } = useAuth()
|
||||||
const {
|
const {
|
||||||
|
|
@ -28,6 +28,9 @@ const {
|
||||||
const qrCodes = ref<Record<string, string>>({})
|
const qrCodes = ref<Record<string, string>>({})
|
||||||
const showQRCode = ref<Record<string, boolean>>({})
|
const showQRCode = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
// Ticket cycling state
|
||||||
|
const currentTicketIndex = ref<Record<string, number>>({})
|
||||||
|
|
||||||
function formatDate(dateStr: string) {
|
function formatDate(dateStr: string) {
|
||||||
return format(new Date(dateStr), 'PPP')
|
return format(new Date(dateStr), 'PPP')
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +74,30 @@ function toggleQRCode(ticketId: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ticket cycling functions
|
||||||
|
function getCurrentTicketIndex(eventId: string) {
|
||||||
|
return currentTicketIndex.value[eventId] || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrentTicketIndex(eventId: string, index: number) {
|
||||||
|
currentTicketIndex.value[eventId] = index
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextTicket(eventId: string, totalTickets: number) {
|
||||||
|
const current = getCurrentTicketIndex(eventId)
|
||||||
|
setCurrentTicketIndex(eventId, (current + 1) % totalTickets)
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevTicket(eventId: string, totalTickets: number) {
|
||||||
|
const current = getCurrentTicketIndex(eventId)
|
||||||
|
setCurrentTicketIndex(eventId, current === 0 ? totalTickets - 1 : current - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentTicket(tickets: any[], eventId: string) {
|
||||||
|
const index = getCurrentTicketIndex(eventId)
|
||||||
|
return tickets[index] || tickets[0]
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (isAuthenticated.value) {
|
if (isAuthenticated.value) {
|
||||||
await refresh()
|
await refresh()
|
||||||
|
|
@ -132,75 +159,92 @@ onMounted(async () => {
|
||||||
|
|
||||||
<TabsContent value="all">
|
<TabsContent value="all">
|
||||||
<ScrollArea class="h-[600px] w-full pr-4">
|
<ScrollArea class="h-[600px] w-full pr-4">
|
||||||
<div class="space-y-4">
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<div v-for="group in groupedTickets" :key="group.eventId" class="space-y-3">
|
<Card v-for="group in groupedTickets" :key="group.eventId" class="flex flex-col">
|
||||||
<!-- Event Header -->
|
<CardHeader>
|
||||||
<div class="bg-muted/50 rounded-lg p-4">
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<CardTitle class="text-foreground">Event: {{ group.eventId }}</CardTitle>
|
||||||
<h3 class="font-semibold text-foreground">Event: {{ group.eventId }}</h3>
|
<Badge variant="outline">
|
||||||
<p class="text-sm text-muted-foreground">
|
{{ group.tickets.length }} ticket{{ group.tickets.length !== 1 ? 's' : '' }}
|
||||||
{{ group.tickets.length }} ticket{{ group.tickets.length !== 1 ? 's' : '' }} •
|
|
||||||
{{ group.paidCount }} paid •
|
|
||||||
{{ group.pendingCount }} pending •
|
|
||||||
{{ group.registeredCount }} registered
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" class="text-xs">
|
|
||||||
{{ group.tickets.length }} total
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<!-- Tickets Grid -->
|
<!-- Current Ticket Display -->
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div v-if="getCurrentTicket(group.tickets, group.eventId)" class="space-y-3">
|
||||||
<Card v-for="ticket in group.tickets" :key="ticket.id" class="flex flex-col">
|
|
||||||
<CardHeader>
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
|
<span class="text-sm font-medium">
|
||||||
|
Ticket #{{ getCurrentTicket(group.tickets, group.eventId).id.slice(0, 8) }}
|
||||||
|
</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge :variant="getTicketStatus(ticket).status === 'pending' ? 'secondary' : 'default'">
|
<Badge :variant="getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).status === 'pending' ? 'secondary' : 'default'">
|
||||||
{{ getTicketStatus(ticket).label }}
|
{{ getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).label }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="toggleQRCode(ticket.id)"
|
@click="toggleQRCode(getCurrentTicket(group.tickets, group.eventId).id)"
|
||||||
class="h-6 w-6 p-0"
|
class="h-6 w-6 p-0"
|
||||||
>
|
>
|
||||||
<QrCode class="h-4 w-4" />
|
<QrCode class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="flex-grow">
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Status:</span>
|
<span class="text-muted-foreground">Status:</span>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<component :is="getTicketStatus(ticket).icon" class="w-4 h-4" :class="getTicketStatus(ticket).color" />
|
<component :is="getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).icon" class="w-4 h-4" :class="getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).color" />
|
||||||
<span class="text-sm">{{ getTicketStatus(ticket).label }}</span>
|
<span class="text-sm">{{ getTicketStatus(getCurrentTicket(group.tickets, group.eventId)).label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Purchased:</span>
|
<span class="text-muted-foreground">Purchased:</span>
|
||||||
<span class="text-sm">{{ formatDate(ticket.time) }}</span>
|
<span class="text-sm">{{ formatDate(getCurrentTicket(group.tickets, group.eventId).time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Time:</span>
|
<span class="text-muted-foreground">Time:</span>
|
||||||
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
<span class="text-sm">{{ formatTime(getCurrentTicket(group.tickets, group.eventId).time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="ticket.reg_timestamp" class="flex justify-between">
|
<div v-if="getCurrentTicket(group.tickets, group.eventId).reg_timestamp" class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Registered:</span>
|
<span class="text-muted-foreground">Registered:</span>
|
||||||
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
|
<span class="text-sm">{{ formatDate(getCurrentTicket(group.tickets, group.eventId).reg_timestamp) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QR Code Section -->
|
<!-- QR Code Section -->
|
||||||
<div v-if="showQRCode[ticket.id]" class="mt-4 pt-4 border-t">
|
<div v-if="showQRCode[getCurrentTicket(group.tickets, group.eventId).id]" class="mt-4 pt-4 border-t">
|
||||||
<div class="flex flex-col items-center space-y-2">
|
<div class="flex flex-col items-center space-y-2">
|
||||||
<img
|
<img
|
||||||
v-if="qrCodes[ticket.id]"
|
v-if="qrCodes[getCurrentTicket(group.tickets, group.eventId).id]"
|
||||||
:src="qrCodes[ticket.id]"
|
:src="qrCodes[getCurrentTicket(group.tickets, group.eventId).id]"
|
||||||
alt="Ticket QR Code"
|
alt="Ticket QR Code"
|
||||||
class="w-32 h-32 border rounded-lg"
|
class="w-32 h-32 border rounded-lg"
|
||||||
/>
|
/>
|
||||||
|
|
@ -210,88 +254,108 @@ onMounted(async () => {
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-xs text-muted-foreground">Ticket ID</p>
|
<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">
|
<div class="bg-background border rounded px-2 py-1 max-w-full mt-1">
|
||||||
<p class="text-xs font-mono break-all">{{ ticket.id }}</p>
|
<p class="text-xs font-mono break-all">{{ getCurrentTicket(group.tickets, group.eventId).id }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="paid">
|
<TabsContent value="paid">
|
||||||
<ScrollArea class="h-[600px] w-full pr-4">
|
<ScrollArea class="h-[600px] w-full pr-4">
|
||||||
<div class="space-y-4">
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<div v-for="group in groupedTickets.filter(g => g.paidCount > 0)" :key="group.eventId" class="space-y-3">
|
<Card v-for="group in groupedTickets.filter(g => g.paidCount > 0)" :key="group.eventId" class="flex flex-col">
|
||||||
<!-- Event Header -->
|
<CardHeader>
|
||||||
<div class="bg-muted/50 rounded-lg p-4">
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<CardTitle class="text-foreground">Event: {{ group.eventId }}</CardTitle>
|
||||||
<h3 class="font-semibold text-foreground">Event: {{ group.eventId }}</h3>
|
<Badge variant="default">
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
{{ group.paidCount }} paid ticket{{ group.paidCount !== 1 ? 's' : '' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Badge variant="default" class="text-xs">
|
|
||||||
{{ group.paidCount }} paid
|
{{ group.paidCount }} paid
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<!-- Paid Tickets Grid -->
|
<!-- Current Ticket Display -->
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div v-if="getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId)" class="space-y-3">
|
||||||
<Card v-for="ticket in group.tickets.filter(t => t.paid)" :key="ticket.id" class="flex flex-col">
|
|
||||||
<CardHeader>
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
|
<span class="text-sm font-medium">
|
||||||
|
Ticket #{{ getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).id.slice(0, 8) }}
|
||||||
|
</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="default">
|
<Badge variant="default">
|
||||||
{{ getTicketStatus(ticket).label }}
|
{{ getTicketStatus(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId)).label }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="toggleQRCode(ticket.id)"
|
@click="toggleQRCode(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).id)"
|
||||||
class="h-6 w-6 p-0"
|
class="h-6 w-6 p-0"
|
||||||
>
|
>
|
||||||
<QrCode class="h-4 w-4" />
|
<QrCode class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="flex-grow">
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Status:</span>
|
<span class="text-muted-foreground">Status:</span>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<component :is="getTicketStatus(ticket).icon" class="w-4 h-4" :class="getTicketStatus(ticket).color" />
|
<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 class="text-sm">{{ getTicketStatus(ticket).label }}</span>
|
<span class="text-sm">{{ getTicketStatus(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId)).label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Purchased:</span>
|
<span class="text-muted-foreground">Purchased:</span>
|
||||||
<span class="text-sm">{{ formatDate(ticket.time) }}</span>
|
<span class="text-sm">{{ formatDate(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Time:</span>
|
<span class="text-muted-foreground">Time:</span>
|
||||||
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
<span class="text-sm">{{ formatTime(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="ticket.reg_timestamp" class="flex justify-between">
|
<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 class="text-muted-foreground">Registered:</span>
|
||||||
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
|
<span class="text-sm">{{ formatDate(getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).reg_timestamp) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QR Code Section -->
|
<!-- QR Code Section -->
|
||||||
<div v-if="showQRCode[ticket.id]" class="mt-4 pt-4 border-t">
|
<div v-if="showQRCode[getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).id]" class="mt-4 pt-4 border-t">
|
||||||
<div class="flex flex-col items-center space-y-2">
|
<div class="flex flex-col items-center space-y-2">
|
||||||
<img
|
<img
|
||||||
v-if="qrCodes[ticket.id]"
|
v-if="qrCodes[getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).id]"
|
||||||
:src="qrCodes[ticket.id]"
|
:src="qrCodes[getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).id]"
|
||||||
alt="Ticket QR Code"
|
alt="Ticket QR Code"
|
||||||
class="w-32 h-32 border rounded-lg"
|
class="w-32 h-32 border rounded-lg"
|
||||||
/>
|
/>
|
||||||
|
|
@ -301,84 +365,104 @@ onMounted(async () => {
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-xs text-muted-foreground">Ticket ID</p>
|
<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">
|
<div class="bg-background border rounded px-2 py-1 max-w-full mt-1">
|
||||||
<p class="text-xs font-mono break-all">{{ ticket.id }}</p>
|
<p class="text-xs font-mono break-all">{{ getCurrentTicket(group.tickets.filter(t => t.paid), group.eventId).id }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="pending">
|
<TabsContent value="pending">
|
||||||
<ScrollArea class="h-[600px] w-full pr-4">
|
<ScrollArea class="h-[600px] w-full pr-4">
|
||||||
<div class="space-y-4">
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<div v-for="group in groupedTickets.filter(g => g.pendingCount > 0)" :key="group.eventId" class="space-y-3">
|
<Card v-for="group in groupedTickets.filter(g => g.pendingCount > 0)" :key="group.eventId" class="flex flex-col">
|
||||||
<!-- Event Header -->
|
<CardHeader>
|
||||||
<div class="bg-muted/50 rounded-lg p-4">
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<CardTitle class="text-foreground">Event: {{ group.eventId }}</CardTitle>
|
||||||
<h3 class="font-semibold text-foreground">Event: {{ group.eventId }}</h3>
|
<Badge variant="secondary">
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
{{ group.pendingCount }} pending ticket{{ group.pendingCount !== 1 ? 's' : '' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Badge variant="secondary" class="text-xs">
|
|
||||||
{{ group.pendingCount }} pending
|
{{ group.pendingCount }} pending
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<!-- Pending Tickets Grid -->
|
<!-- Current Ticket Display -->
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div v-if="getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId)" class="space-y-3">
|
||||||
<Card v-for="ticket in group.tickets.filter(t => !t.paid)" :key="ticket.id" class="flex flex-col opacity-75">
|
|
||||||
<CardHeader>
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
|
<span class="text-sm font-medium">
|
||||||
|
Ticket #{{ getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).id.slice(0, 8) }}
|
||||||
|
</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="secondary">
|
<Badge variant="secondary">
|
||||||
{{ getTicketStatus(ticket).label }}
|
{{ getTicketStatus(getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId)).label }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="toggleQRCode(ticket.id)"
|
@click="toggleQRCode(getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).id)"
|
||||||
class="h-6 w-6 p-0"
|
class="h-6 w-6 p-0"
|
||||||
>
|
>
|
||||||
<QrCode class="h-4 w-4" />
|
<QrCode class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="flex-grow">
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Status:</span>
|
<span class="text-muted-foreground">Status:</span>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<component :is="getTicketStatus(ticket).icon" class="w-4 h-4" :class="getTicketStatus(ticket).color" />
|
<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 class="text-sm">{{ getTicketStatus(ticket).label }}</span>
|
<span class="text-sm">{{ getTicketStatus(getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId)).label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Created:</span>
|
<span class="text-muted-foreground">Created:</span>
|
||||||
<span class="text-sm">{{ formatDate(ticket.time) }}</span>
|
<span class="text-sm">{{ formatDate(getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Time:</span>
|
<span class="text-muted-foreground">Time:</span>
|
||||||
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
<span class="text-sm">{{ formatTime(getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QR Code Section -->
|
<!-- QR Code Section -->
|
||||||
<div v-if="showQRCode[ticket.id]" class="mt-4 pt-4 border-t">
|
<div v-if="showQRCode[getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).id]" class="mt-4 pt-4 border-t">
|
||||||
<div class="flex flex-col items-center space-y-2">
|
<div class="flex flex-col items-center space-y-2">
|
||||||
<img
|
<img
|
||||||
v-if="qrCodes[ticket.id]"
|
v-if="qrCodes[getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).id]"
|
||||||
:src="qrCodes[ticket.id]"
|
:src="qrCodes[getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).id]"
|
||||||
alt="Ticket QR Code"
|
alt="Ticket QR Code"
|
||||||
class="w-32 h-32 border rounded-lg"
|
class="w-32 h-32 border rounded-lg"
|
||||||
/>
|
/>
|
||||||
|
|
@ -388,88 +472,108 @@ onMounted(async () => {
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-xs text-muted-foreground">Ticket ID</p>
|
<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">
|
<div class="bg-background border rounded px-2 py-1 max-w-full mt-1">
|
||||||
<p class="text-xs font-mono break-all">{{ ticket.id }}</p>
|
<p class="text-xs font-mono break-all">{{ getCurrentTicket(group.tickets.filter(t => !t.paid), group.eventId).id }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="registered">
|
<TabsContent value="registered">
|
||||||
<ScrollArea class="h-[600px] w-full pr-4">
|
<ScrollArea class="h-[600px] w-full pr-4">
|
||||||
<div class="space-y-4">
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<div v-for="group in groupedTickets.filter(g => g.registeredCount > 0)" :key="group.eventId" class="space-y-3">
|
<Card v-for="group in groupedTickets.filter(g => g.registeredCount > 0)" :key="group.eventId" class="flex flex-col">
|
||||||
<!-- Event Header -->
|
<CardHeader>
|
||||||
<div class="bg-muted/50 rounded-lg p-4">
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<CardTitle class="text-foreground">Event: {{ group.eventId }}</CardTitle>
|
||||||
<h3 class="font-semibold text-foreground">Event: {{ group.eventId }}</h3>
|
<Badge variant="default">
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
{{ group.registeredCount }} registered ticket{{ group.registeredCount !== 1 ? 's' : '' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Badge variant="default" class="text-xs">
|
|
||||||
{{ group.registeredCount }} registered
|
{{ group.registeredCount }} registered
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<!-- Registered Tickets Grid -->
|
<!-- Current Ticket Display -->
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div v-if="getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId)" class="space-y-3">
|
||||||
<Card v-for="ticket in group.tickets.filter(t => t.registered)" :key="ticket.id" class="flex flex-col">
|
|
||||||
<CardHeader>
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
|
<span class="text-sm font-medium">
|
||||||
|
Ticket #{{ getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).id.slice(0, 8) }}
|
||||||
|
</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="default">
|
<Badge variant="default">
|
||||||
{{ getTicketStatus(ticket).label }}
|
{{ getTicketStatus(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId)).label }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="toggleQRCode(ticket.id)"
|
@click="toggleQRCode(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).id)"
|
||||||
class="h-6 w-6 p-0"
|
class="h-6 w-6 p-0"
|
||||||
>
|
>
|
||||||
<QrCode class="h-4 w-4" />
|
<QrCode class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="flex-grow">
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Status:</span>
|
<span class="text-muted-foreground">Status:</span>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<component :is="getTicketStatus(ticket).icon" class="w-4 h-4" :class="getTicketStatus(ticket).color" />
|
<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 class="text-sm">{{ getTicketStatus(ticket).label }}</span>
|
<span class="text-sm">{{ getTicketStatus(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId)).label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Purchased:</span>
|
<span class="text-muted-foreground">Purchased:</span>
|
||||||
<span class="text-sm">{{ formatDate(ticket.time) }}</span>
|
<span class="text-sm">{{ formatDate(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">Time:</span>
|
<span class="text-muted-foreground">Time:</span>
|
||||||
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
<span class="text-sm">{{ formatTime(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="ticket.reg_timestamp" class="flex justify-between">
|
<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 class="text-muted-foreground">Registered:</span>
|
||||||
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
|
<span class="text-sm">{{ formatDate(getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).reg_timestamp) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QR Code Section -->
|
<!-- QR Code Section -->
|
||||||
<div v-if="showQRCode[ticket.id]" class="mt-4 pt-4 border-t">
|
<div v-if="showQRCode[getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).id]" class="mt-4 pt-4 border-t">
|
||||||
<div class="flex flex-col items-center space-y-2">
|
<div class="flex flex-col items-center space-y-2">
|
||||||
<img
|
<img
|
||||||
v-if="qrCodes[ticket.id]"
|
v-if="qrCodes[getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).id]"
|
||||||
:src="qrCodes[ticket.id]"
|
:src="qrCodes[getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).id]"
|
||||||
alt="Ticket QR Code"
|
alt="Ticket QR Code"
|
||||||
class="w-32 h-32 border rounded-lg"
|
class="w-32 h-32 border rounded-lg"
|
||||||
/>
|
/>
|
||||||
|
|
@ -479,16 +583,16 @@ onMounted(async () => {
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-xs text-muted-foreground">Ticket ID</p>
|
<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">
|
<div class="bg-background border rounded px-2 py-1 max-w-full mt-1">
|
||||||
<p class="text-xs font-mono break-all">{{ ticket.id }}</p>
|
<p class="text-xs font-mono break-all">{{ getCurrentTicket(group.tickets.filter(t => t.registered), group.eventId).id }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue