feat: Add My Tickets feature with ticket management
- Introduce MyTickets.vue page to display user tickets with filtering options for paid, pending, and registered tickets. - Implement useUserTickets composable for fetching and managing user ticket data. - Update Navbar.vue to include a link to the My Tickets page. - Enhance events API to support fetching user tickets. - Define Ticket type in event.ts for better type safety.
This commit is contained in:
parent
f7450627bc
commit
63d636a8a0
6 changed files with 403 additions and 2 deletions
269
src/pages/MyTickets.vue
Normal file
269
src/pages/MyTickets.vue
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script setup lang="ts">
|
||||
import { onMounted } 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, Calendar, CreditCard, CheckCircle, Clock, AlertCircle } from 'lucide-vue-next'
|
||||
|
||||
const { isAuthenticated, userDisplay } = useAuth()
|
||||
const {
|
||||
tickets,
|
||||
paidTickets,
|
||||
pendingTickets,
|
||||
registeredTickets,
|
||||
unregisteredTickets,
|
||||
isLoading,
|
||||
error,
|
||||
refresh
|
||||
} = useUserTickets()
|
||||
|
||||
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' }
|
||||
}
|
||||
|
||||
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-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="ticket in tickets" :key="ticket.id" class="flex flex-col">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
|
||||
<Badge :variant="getTicketStatus(ticket).status === 'pending' ? 'secondary' : 'default'">
|
||||
{{ getTicketStatus(ticket).label }}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Event ID: {{ ticket.event }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="flex-grow">
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Status:</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<component :is="getTicketStatus(ticket).icon" class="w-4 h-4" :class="getTicketStatus(ticket).color" />
|
||||
<span class="text-sm">{{ getTicketStatus(ticket).label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Purchased:</span>
|
||||
<span class="text-sm">{{ formatDate(ticket.time) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Time:</span>
|
||||
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
||||
</div>
|
||||
<div v-if="ticket.reg_timestamp" class="flex justify-between">
|
||||
<span class="text-muted-foreground">Registered:</span>
|
||||
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="paid">
|
||||
<ScrollArea class="h-[600px] w-full pr-4">
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="ticket in paidTickets" :key="ticket.id" class="flex flex-col">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
|
||||
<Badge variant="default">
|
||||
{{ getTicketStatus(ticket).label }}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Event ID: {{ ticket.event }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="flex-grow">
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Status:</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<component :is="getTicketStatus(ticket).icon" class="w-4 h-4" :class="getTicketStatus(ticket).color" />
|
||||
<span class="text-sm">{{ getTicketStatus(ticket).label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Purchased:</span>
|
||||
<span class="text-sm">{{ formatDate(ticket.time) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Time:</span>
|
||||
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
||||
</div>
|
||||
<div v-if="ticket.reg_timestamp" class="flex justify-between">
|
||||
<span class="text-muted-foreground">Registered:</span>
|
||||
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pending">
|
||||
<ScrollArea class="h-[600px] w-full pr-4">
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="ticket in pendingTickets" :key="ticket.id" class="flex flex-col opacity-75">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
|
||||
<Badge variant="secondary">
|
||||
{{ getTicketStatus(ticket).label }}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Event ID: {{ ticket.event }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="flex-grow">
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Status:</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<component :is="getTicketStatus(ticket).icon" class="w-4 h-4" :class="getTicketStatus(ticket).color" />
|
||||
<span class="text-sm">{{ getTicketStatus(ticket).label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Created:</span>
|
||||
<span class="text-sm">{{ formatDate(ticket.time) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Time:</span>
|
||||
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="registered">
|
||||
<ScrollArea class="h-[600px] w-full pr-4">
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="ticket in registeredTickets" :key="ticket.id" class="flex flex-col">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
|
||||
<Badge variant="default">
|
||||
{{ getTicketStatus(ticket).label }}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Event ID: {{ ticket.event }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="flex-grow">
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Status:</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<component :is="getTicketStatus(ticket).icon" class="w-4 h-4" :class="getTicketStatus(ticket).color" />
|
||||
<span class="text-sm">{{ getTicketStatus(ticket).label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Purchased:</span>
|
||||
<span class="text-sm">{{ formatDate(ticket.time) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Time:</span>
|
||||
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
||||
</div>
|
||||
<div v-if="ticket.reg_timestamp" class="flex justify-between">
|
||||
<span class="text-muted-foreground">Registered:</span>
|
||||
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Loading…
Add table
Add a link
Reference in a new issue