Refactor imports and enhance type handling across components
- Update import paths for useTicketPurchase in PurchaseTicketDialog.vue to reflect new module structure. - Adjust type handling in Navbar.vue and various market components to use 'any' for improved compatibility with existing data structures. - Enhance useLightningPayment composable to include shipping zone details, ensuring better order management. - Remove unused pages (events.vue, MyTickets.vue, OrderHistory.vue) to streamline the codebase and improve maintainability.
This commit is contained in:
parent
18f48581cd
commit
861c032300
12 changed files with 37 additions and 1217 deletions
|
|
@ -4,7 +4,7 @@ import { onUnmounted } from 'vue'
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
||||||
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 { useTicketPurchase } from '@/composables/useTicketPurchase'
|
import { useTicketPurchase } from '@/modules/events/composables/useTicketPurchase'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { User, Wallet, CreditCard, Zap, Ticket } from 'lucide-vue-next'
|
import { User, Wallet, CreditCard, Zap, Ticket } from 'lucide-vue-next'
|
||||||
import { formatEventPrice, formatWalletBalance } from '@/lib/utils/formatting'
|
import { formatEventPrice, formatWalletBalance } from '@/lib/utils/formatting'
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ const totalBalance = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Try to get chat service from DI (may not be available if chat module not loaded)
|
// Try to get chat service from DI (may not be available if chat module not loaded)
|
||||||
const chatService = tryInjectService(SERVICE_TOKENS.CHAT_SERVICE)
|
const chatService = tryInjectService(SERVICE_TOKENS.CHAT_SERVICE) as any
|
||||||
|
|
||||||
// Compute total unread messages (reactive)
|
// Compute total unread messages (reactive)
|
||||||
const totalUnreadMessages = computed(() => {
|
const totalUnreadMessages = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -254,10 +254,11 @@ import { Badge } from '@/components/ui/badge'
|
||||||
import { Package, Store, Zap, Copy, QrCode, CheckCircle } from 'lucide-vue-next'
|
import { Package, Store, Zap, Copy, QrCode, CheckCircle } from 'lucide-vue-next'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import type { OrderStatus } from '@/stores/market'
|
import type { OrderStatus } from '@/stores/market'
|
||||||
|
// Order type no longer needed since we use any for readonly compatibility
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const marketStore = useMarketStore()
|
const marketStore = useMarketStore()
|
||||||
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any
|
||||||
const { handlePayment, isPayingWithWallet, hasWalletWithBalance } = useLightningPayment()
|
const { handlePayment, isPayingWithWallet, hasWalletWithBalance } = useLightningPayment()
|
||||||
|
|
||||||
// const orderEvents = useOrderEvents() // TODO: Move to market module
|
// const orderEvents = useOrderEvents() // TODO: Move to market module
|
||||||
|
|
@ -305,7 +306,7 @@ const pendingPayments = computed(() => allOrders.value.filter(o => !isOrderPaid(
|
||||||
const isDevelopment = computed(() => import.meta.env.DEV)
|
const isDevelopment = computed(() => import.meta.env.DEV)
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const isOrderPaid = (order: Order) => {
|
const isOrderPaid = (order: any) => {
|
||||||
// Prioritize the 'paid' field from Nostr status updates (type 2)
|
// Prioritize the 'paid' field from Nostr status updates (type 2)
|
||||||
if (order.paid !== undefined) {
|
if (order.paid !== undefined) {
|
||||||
return order.paid
|
return order.paid
|
||||||
|
|
@ -314,7 +315,7 @@ const isOrderPaid = (order: Order) => {
|
||||||
return order.paymentStatus === 'paid'
|
return order.paymentStatus === 'paid'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEffectiveStatus = (order: Order) => {
|
const getEffectiveStatus = (order: any) => {
|
||||||
// If paid, return 'paid' regardless of original status
|
// If paid, return 'paid' regardless of original status
|
||||||
if (isOrderPaid(order)) {
|
if (isOrderPaid(order)) {
|
||||||
return order.shipped ? 'shipped' : 'paid'
|
return order.shipped ? 'shipped' : 'paid'
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,11 @@ export function useLightningPayment() {
|
||||||
paidAt: Math.floor(Date.now() / 1000),
|
paidAt: Math.floor(Date.now() / 1000),
|
||||||
paymentHash: paymentResult.payment_hash,
|
paymentHash: paymentResult.payment_hash,
|
||||||
feeMsat: paymentResult.fee_msat,
|
feeMsat: paymentResult.fee_msat,
|
||||||
items: [...order.items] // Convert readonly to mutable
|
items: [...order.items], // Convert readonly to mutable
|
||||||
|
shippingZone: order.shippingZone ? {
|
||||||
|
...order.shippingZone,
|
||||||
|
countries: order.shippingZone.countries ? [...order.shippingZone.countries] : undefined
|
||||||
|
} : order.shippingZone
|
||||||
}
|
}
|
||||||
marketStore.updateOrder(orderId, updatedOrder)
|
marketStore.updateOrder(orderId, updatedOrder)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -395,7 +395,6 @@ export function useMarket() {
|
||||||
console.log('🔔 Received order-related DM:', event.id, 'from:', event.pubkey.slice(0, 8))
|
console.log('🔔 Received order-related DM:', event.id, 'from:', event.pubkey.slice(0, 8))
|
||||||
|
|
||||||
// TODO: Confirm if this should use nostrStore.account?.pubkey or authService.user.value?.pubkey
|
// TODO: Confirm if this should use nostrStore.account?.pubkey or authService.user.value?.pubkey
|
||||||
const userPubkey = nostrStore.account?.pubkey || authService.user.value?.pubkey
|
|
||||||
const userPrivkey = nostrStore.account?.privkey || authService.user.value?.prvkey
|
const userPrivkey = nostrStore.account?.privkey || authService.user.value?.prvkey
|
||||||
|
|
||||||
if (!userPrivkey) {
|
if (!userPrivkey) {
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,11 @@ import { useAuth } from '@/composables/useAuth'
|
||||||
import { useMarketStore } from '../stores/market'
|
import { useMarketStore } from '../stores/market'
|
||||||
|
|
||||||
// Simplified bolt11 parser to extract payment hash
|
// Simplified bolt11 parser to extract payment hash
|
||||||
function parseBolt11(bolt11: string): { paymentHash?: string } {
|
function parseBolt11(_bolt11: string): { paymentHash?: string } {
|
||||||
try {
|
try {
|
||||||
// Remove lightning: prefix if present
|
|
||||||
const cleanBolt11 = bolt11.replace(/^lightning:/, '')
|
|
||||||
|
|
||||||
// Very basic bolt11 parsing - in a real app you'd use a proper library
|
// Very basic bolt11 parsing - in a real app you'd use a proper library
|
||||||
// For now, we'll return empty since this requires complex bech32 decoding
|
// For now, we'll return empty since this requires complex bech32 decoding
|
||||||
|
// Note: Remove lightning: prefix if present: bolt11.replace(/^lightning:/, '')
|
||||||
return {}
|
return {}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse bolt11:', error)
|
console.error('Failed to parse bolt11:', error)
|
||||||
|
|
@ -94,7 +92,11 @@ export function usePaymentStatusChecker() {
|
||||||
status: 'paid' as const,
|
status: 'paid' as const,
|
||||||
paymentStatus: 'paid' as const,
|
paymentStatus: 'paid' as const,
|
||||||
paidAt: Math.floor(Date.now() / 1000),
|
paidAt: Math.floor(Date.now() / 1000),
|
||||||
items: [...order.items] // Convert readonly to mutable
|
items: [...order.items], // Convert readonly to mutable
|
||||||
|
shippingZone: order.shippingZone ? {
|
||||||
|
...order.shippingZone,
|
||||||
|
countries: order.shippingZone.countries ? [...order.shippingZone.countries] : undefined
|
||||||
|
} : order.shippingZone
|
||||||
}
|
}
|
||||||
|
|
||||||
marketStore.updateOrder(orderId, updatedOrder)
|
marketStore.updateOrder(orderId, updatedOrder)
|
||||||
|
|
|
||||||
|
|
@ -68,12 +68,12 @@ export interface NostrmarketOrderStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NostrmarketService {
|
export class NostrmarketService {
|
||||||
private get relayHub() {
|
private get relayHub(): any {
|
||||||
const hub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
const hub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
||||||
if (!hub) {
|
if (!hub) {
|
||||||
throw new Error('RelayHub not available. Make sure base module is installed.')
|
throw new Error('RelayHub not available. Make sure base module is installed.')
|
||||||
}
|
}
|
||||||
return hub
|
return hub as any
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -299,7 +299,7 @@ export class NostrmarketService {
|
||||||
})
|
})
|
||||||
|
|
||||||
const event = finalizeEvent(eventTemplate, prvkeyBytes)
|
const event = finalizeEvent(eventTemplate, prvkeyBytes)
|
||||||
const result = await relayHub.publishEvent(event)
|
const result = await this.relayHub.publishEvent(event)
|
||||||
|
|
||||||
console.log('Order published to nostrmarket:', {
|
console.log('Order published to nostrmarket:', {
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
|
|
@ -345,7 +345,11 @@ export class NostrmarketService {
|
||||||
paymentStatus: 'pending' as const,
|
paymentStatus: 'pending' as const,
|
||||||
status: 'pending' as const, // Ensure status is pending for payment
|
status: 'pending' as const, // Ensure status is pending for payment
|
||||||
updatedAt: Math.floor(Date.now() / 1000),
|
updatedAt: Math.floor(Date.now() / 1000),
|
||||||
items: [...order.items] // Convert readonly to mutable
|
items: [...order.items], // Convert readonly to mutable
|
||||||
|
shippingZone: order.shippingZone ? {
|
||||||
|
...order.shippingZone,
|
||||||
|
countries: order.shippingZone.countries ? [...order.shippingZone.countries] : undefined
|
||||||
|
} : order.shippingZone
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate QR code for the payment request
|
// Generate QR code for the payment request
|
||||||
|
|
@ -415,7 +419,11 @@ export class NostrmarketService {
|
||||||
const updatedOrder = {
|
const updatedOrder = {
|
||||||
...order,
|
...order,
|
||||||
updatedAt: Math.floor(Date.now() / 1000),
|
updatedAt: Math.floor(Date.now() / 1000),
|
||||||
items: [...order.items] // Convert readonly to mutable
|
items: [...order.items], // Convert readonly to mutable
|
||||||
|
shippingZone: order.shippingZone ? {
|
||||||
|
...order.shippingZone,
|
||||||
|
countries: order.shippingZone.countries ? [...order.shippingZone.countries] : undefined
|
||||||
|
} : order.shippingZone
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update payment status
|
// Update payment status
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useMarketStore } from '@/stores/market'
|
import { useMarketStore } from '@/stores/market'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import {
|
import {
|
||||||
|
|
@ -290,7 +290,6 @@ import {
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
const marketStore = useMarketStore()
|
const marketStore = useMarketStore()
|
||||||
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any
|
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const props = defineProps<{
|
||||||
feedType?: 'all' | 'announcements' | 'events' | 'general'
|
feedType?: 'all' | 'announcements' | 'events' | 'general'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
const notes = ref<any[]>([])
|
const notes = ref<any[]>([])
|
||||||
|
|
@ -97,7 +97,7 @@ async function loadNotes() {
|
||||||
const events = await relayHub.queryEvents(filters)
|
const events = await relayHub.queryEvents(filters)
|
||||||
|
|
||||||
// Process and filter events
|
// Process and filter events
|
||||||
let processedNotes = events.map(event => ({
|
let processedNotes = events.map((event: any) => ({
|
||||||
id: event.id,
|
id: event.id,
|
||||||
pubkey: event.pubkey,
|
pubkey: event.pubkey,
|
||||||
content: event.content,
|
content: event.content,
|
||||||
|
|
@ -111,11 +111,11 @@ async function loadNotes() {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Sort by creation time (newest first)
|
// Sort by creation time (newest first)
|
||||||
processedNotes.sort((a, b) => b.created_at - a.created_at)
|
processedNotes.sort((a: any, b: any) => b.created_at - a.created_at)
|
||||||
|
|
||||||
// For general feed, exclude admin posts
|
// For general feed, exclude admin posts
|
||||||
if (props.feedType === 'general' && hasAdminPubkeys.value) {
|
if (props.feedType === 'general' && hasAdminPubkeys.value) {
|
||||||
processedNotes = processedNotes.filter(note => !isAdminPost(note.pubkey))
|
processedNotes = processedNotes.filter((note: any) => !isAdminPost(note.pubkey))
|
||||||
}
|
}
|
||||||
|
|
||||||
notes.value = processedNotes
|
notes.value = processedNotes
|
||||||
|
|
@ -153,7 +153,7 @@ async function startRealtimeSubscription() {
|
||||||
unsubscribe = relayHub.subscribe({
|
unsubscribe = relayHub.subscribe({
|
||||||
id: `feed-${props.feedType || 'all'}`,
|
id: `feed-${props.feedType || 'all'}`,
|
||||||
filters,
|
filters,
|
||||||
onEvent: (event) => {
|
onEvent: (event: any) => {
|
||||||
// Add new note to the beginning of the list
|
// Add new note to the beginning of the list
|
||||||
const newNote = {
|
const newNote = {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
|
|
|
||||||
|
|
@ -1,599 +0,0 @@
|
||||||
<!-- 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>
|
|
||||||
|
|
@ -1,426 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="container mx-auto px-4 py-8">
|
|
||||||
<!-- Page Header -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<h1 class="text-3xl font-bold text-foreground">Order History</h1>
|
|
||||||
<p class="text-muted-foreground mt-2">
|
|
||||||
View and track all your market orders
|
|
||||||
</p>
|
|
||||||
<!-- Order Events Status -->
|
|
||||||
<div class="mt-3 flex items-center gap-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
class="w-2 h-2 rounded-full"
|
|
||||||
:class="orderEvents.isSubscribed ? 'bg-green-500' : 'bg-yellow-500'"
|
|
||||||
></div>
|
|
||||||
<span class="text-sm text-muted-foreground">
|
|
||||||
{{ orderEvents.isSubscribed ? 'Listening for order updates' : 'Connecting to order events...' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="orderEvents.lastEventTimestamp.value > 0" class="text-xs text-muted-foreground">
|
|
||||||
Last update: {{ formatDate(orderEvents.lastEventTimestamp.value) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filters and Stats -->
|
|
||||||
<div class="mb-6 flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
|
||||||
<!-- Order Stats -->
|
|
||||||
<div class="flex gap-4 text-sm">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-muted-foreground">Total Orders:</span>
|
|
||||||
<Badge variant="secondary">{{ totalOrders }}</Badge>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-muted-foreground">Pending:</span>
|
|
||||||
<Badge variant="outline" class="text-yellow-600">{{ pendingOrders }}</Badge>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-muted-foreground">Completed:</span>
|
|
||||||
<Badge variant="outline" class="text-green-600">{{ completedOrders }}</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filter Controls -->
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<select v-model="statusFilter" class="w-40 px-3 py-2 border border-input rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground">
|
|
||||||
<option value="">All Statuses</option>
|
|
||||||
<option value="pending">Pending</option>
|
|
||||||
<option value="paid">Paid</option>
|
|
||||||
<option value="processing">Processing</option>
|
|
||||||
<option value="shipped">Shipped</option>
|
|
||||||
<option value="delivered">Delivered</option>
|
|
||||||
<option value="cancelled">Cancelled</option>
|
|
||||||
</select>
|
|
||||||
<select v-model="sortBy" class="w-40 px-3 py-2 border border-input rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground">
|
|
||||||
<option value="createdAt">Date Created</option>
|
|
||||||
<option value="total">Order Total</option>
|
|
||||||
<option value="status">Status</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Orders List -->
|
|
||||||
<div v-if="filteredOrders.length > 0" class="space-y-4">
|
|
||||||
<div
|
|
||||||
v-for="order in sortedOrders"
|
|
||||||
:key="order.id"
|
|
||||||
class="bg-card border rounded-lg p-6 hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<!-- Order Header -->
|
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center">
|
|
||||||
<Package class="w-5 h-5 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-semibold text-foreground">Order #{{ order.id.slice(-8) }}</h3>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
{{ formatDate(order.createdAt) }}
|
|
||||||
</p>
|
|
||||||
<!-- Nostr Status -->
|
|
||||||
<div v-if="order.sentViaNostr !== undefined" class="flex items-center gap-2 mt-1">
|
|
||||||
<div v-if="order.sentViaNostr" class="flex items-center gap-1 text-xs text-green-600">
|
|
||||||
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
||||||
Sent via Nostr
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex items-center gap-1 text-xs text-red-600">
|
|
||||||
<div class="w-2 h-2 bg-red-500 rounded-full"></div>
|
|
||||||
Nostr failed
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<Badge :variant="getStatusVariant(order.status)">
|
|
||||||
{{ formatStatus(order.status) }}
|
|
||||||
</Badge>
|
|
||||||
<!-- Payment Status Indicator -->
|
|
||||||
<div v-if="order.lightningInvoice" class="flex items-center gap-2">
|
|
||||||
<Badge
|
|
||||||
:variant="order.paymentStatus === 'paid' ? 'default' : 'secondary'"
|
|
||||||
class="text-xs"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<div v-if="order.paymentStatus === 'paid'" class="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
||||||
<div v-else class="w-2 h-2 bg-yellow-500 rounded-full animate-pulse"></div>
|
|
||||||
{{ order.paymentStatus === 'paid' ? 'Paid' : 'Payment Pending' }}
|
|
||||||
</div>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<p class="text-lg font-semibold text-foreground">
|
|
||||||
{{ formatPrice(order.total, order.currency) }}
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-muted-foreground">{{ order.currency.toUpperCase() }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Order Details -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
|
|
||||||
<!-- Items -->
|
|
||||||
<div>
|
|
||||||
<h4 class="font-medium text-foreground mb-2">Items</h4>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div
|
|
||||||
v-for="item in order.items"
|
|
||||||
:key="item.productId"
|
|
||||||
class="flex justify-between text-sm"
|
|
||||||
>
|
|
||||||
<span class="text-muted-foreground">
|
|
||||||
{{ item.productName }} × {{ item.quantity }}
|
|
||||||
</span>
|
|
||||||
<span class="font-medium">
|
|
||||||
{{ formatPrice(item.price * item.quantity, item.currency) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Order Info -->
|
|
||||||
<div>
|
|
||||||
<h4 class="font-medium text-foreground mb-2">Order Details</h4>
|
|
||||||
<div class="space-y-2 text-sm">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">Subtotal:</span>
|
|
||||||
<span>{{ formatPrice(order.subtotal, order.currency) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">Shipping:</span>
|
|
||||||
<span>{{ formatPrice(order.shippingCost, order.currency) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">Payment Method:</span>
|
|
||||||
<span class="capitalize">{{ order.paymentMethod.replace('_', ' ') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contact & Shipping -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
|
|
||||||
<!-- Contact Info -->
|
|
||||||
<div v-if="order.contactInfo.email || order.contactInfo.message">
|
|
||||||
<h4 class="font-medium text-foreground mb-2">Contact Information</h4>
|
|
||||||
<div class="space-y-1 text-sm text-muted-foreground">
|
|
||||||
<p v-if="order.contactInfo.email">
|
|
||||||
<span class="font-medium">Email:</span> {{ order.contactInfo.email }}
|
|
||||||
</p>
|
|
||||||
<p v-if="order.contactInfo.message">
|
|
||||||
<span class="font-medium">Message:</span> {{ order.contactInfo.message }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Shipping Info -->
|
|
||||||
<div v-if="order.shippingZone">
|
|
||||||
<h4 class="font-medium text-foreground mb-2">Shipping</h4>
|
|
||||||
<div class="space-y-1 text-sm text-muted-foreground">
|
|
||||||
<p>
|
|
||||||
<span class="font-medium">Zone:</span> {{ order.shippingZone.name }}
|
|
||||||
</p>
|
|
||||||
<p v-if="order.shippingZone.estimatedDays">
|
|
||||||
<span class="font-medium">Est. Delivery:</span> {{ order.shippingZone.estimatedDays }}
|
|
||||||
</p>
|
|
||||||
<p v-if="order.contactInfo.address">
|
|
||||||
<span class="font-medium">Address:</span> {{ order.contactInfo.address }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nostr Event Details -->
|
|
||||||
<div v-if="order.sentViaNostr !== undefined" class="mb-4 p-4 bg-muted rounded-lg">
|
|
||||||
<h4 class="font-medium text-foreground mb-2">Nostr Network Status</h4>
|
|
||||||
<div class="space-y-2 text-sm">
|
|
||||||
<div v-if="order.sentViaNostr" class="space-y-1">
|
|
||||||
<p v-if="order.nostrEventId" class="text-muted-foreground">
|
|
||||||
<span class="font-medium">Event ID:</span>
|
|
||||||
<code class="bg-background px-2 py-1 rounded text-xs">{{ order.nostrEventId.slice(0, 16) }}...</code>
|
|
||||||
</p>
|
|
||||||
<p v-if="order.nostrEventSig" class="text-muted-foreground">
|
|
||||||
<span class="font-medium">Signature:</span>
|
|
||||||
<code class="bg-background px-2 py-1 rounded text-xs">{{ order.nostrEventSig.slice(0, 16) }}...</code>
|
|
||||||
</p>
|
|
||||||
<p class="text-green-600">
|
|
||||||
<span class="font-medium">✓</span> Order successfully transmitted to merchant via Nostr network
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="space-y-1">
|
|
||||||
<p v-if="order.nostrError" class="text-red-600">
|
|
||||||
<span class="font-medium">✗</span> Failed to send via Nostr: {{ order.nostrError }}
|
|
||||||
</p>
|
|
||||||
<p class="text-yellow-600">
|
|
||||||
<span class="font-medium">⚠</span> Order stored locally only - merchant may not receive it
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Payment Status & Actions -->
|
|
||||||
<div v-if="order.status === 'pending'" class="mb-4 p-4 bg-muted/50 border border-border rounded-lg">
|
|
||||||
<div class="flex-1">
|
|
||||||
<h4 class="font-medium text-foreground mb-2">Payment Required</h4>
|
|
||||||
<div v-if="order.lightningInvoice" class="space-y-2">
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
<span class="font-medium text-green-600">✓</span> Lightning invoice received from merchant
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Amount: <span class="font-medium text-foreground">{{ formatPrice(order.total, order.currency) }}</span>
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Status: <span class="font-medium text-foreground">{{ order.paymentStatus === 'paid' ? 'Paid' : 'Pending Payment' }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="space-y-2">
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
<span class="font-medium text-amber-600">⏳</span> Waiting for merchant to generate payment invoice
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
The merchant will send you a Lightning invoice via Nostr once they process your order
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div class="flex justify-end gap-2 pt-4 border-t border-border">
|
|
||||||
<Button
|
|
||||||
v-if="order.status === 'pending'"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="cancelOrder(order.id)"
|
|
||||||
>
|
|
||||||
Cancel Order
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="order.lightningInvoice"
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
@click="togglePaymentDisplay(order.id)"
|
|
||||||
>
|
|
||||||
<Wallet class="w-4 h-4 mr-2" />
|
|
||||||
{{ expandedPayments.has(order.id) ? 'Hide' : 'Show' }} Payment
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="copyOrderId(order.id)"
|
|
||||||
>
|
|
||||||
Copy Order ID
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Payment Display (Expandable) -->
|
|
||||||
<div v-if="expandedPayments.has(order.id) && order.lightningInvoice" class="mt-4 pt-4 border-t border-border">
|
|
||||||
<PaymentDisplay
|
|
||||||
:order-id="order.id"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty State -->
|
|
||||||
<div v-else class="text-center py-12">
|
|
||||||
<div class="w-16 h-16 bg-muted rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Package class="w-8 h-8 text-muted-foreground/50" />
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-medium text-foreground mb-2">No orders yet</h3>
|
|
||||||
<p class="text-muted-foreground mb-6">
|
|
||||||
Start shopping in the market to see your order history here
|
|
||||||
</p>
|
|
||||||
<Button @click="router.push('/market')" variant="default">
|
|
||||||
Browse Market
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, onMounted } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useMarketStore } from '@/stores/market'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Package, Wallet } from 'lucide-vue-next'
|
|
||||||
import type { OrderStatus } from '@/stores/market'
|
|
||||||
import PaymentDisplay from '@/components/market/PaymentDisplay.vue'
|
|
||||||
import { useOrderEvents } from '@/composables/useOrderEvents'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const marketStore = useMarketStore()
|
|
||||||
const orderEvents = useOrderEvents()
|
|
||||||
|
|
||||||
// Local state
|
|
||||||
const statusFilter = ref('')
|
|
||||||
const sortBy = ref('createdAt')
|
|
||||||
const expandedPayments = ref(new Set<string>())
|
|
||||||
|
|
||||||
// Computed properties
|
|
||||||
const allOrders = computed(() => Object.values(marketStore.orders))
|
|
||||||
|
|
||||||
const filteredOrders = computed(() => {
|
|
||||||
if (!statusFilter.value) return allOrders.value
|
|
||||||
return allOrders.value.filter(order => order.status === statusFilter.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const sortedOrders = computed(() => {
|
|
||||||
const orders = [...filteredOrders.value]
|
|
||||||
|
|
||||||
switch (sortBy.value) {
|
|
||||||
case 'total':
|
|
||||||
return orders.sort((a, b) => b.total - a.total)
|
|
||||||
case 'status':
|
|
||||||
return orders.sort((a, b) => a.status.localeCompare(b.status))
|
|
||||||
case 'createdAt':
|
|
||||||
default:
|
|
||||||
return orders.sort((a, b) => b.createdAt - a.createdAt)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const totalOrders = computed(() => allOrders.value.length)
|
|
||||||
const pendingOrders = computed(() => allOrders.value.filter(o => o.status === 'pending').length)
|
|
||||||
const completedOrders = computed(() =>
|
|
||||||
allOrders.value.filter(o => ['delivered', 'shipped'].includes(o.status)).length
|
|
||||||
)
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
const formatDate = (timestamp: number) => {
|
|
||||||
return new Date(timestamp * 1000).toLocaleDateString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatStatus = (status: OrderStatus) => {
|
|
||||||
const statusMap: Record<OrderStatus, string> = {
|
|
||||||
pending: 'Pending',
|
|
||||||
paid: 'Paid',
|
|
||||||
processing: 'Processing',
|
|
||||||
shipped: 'Shipped',
|
|
||||||
delivered: 'Delivered',
|
|
||||||
cancelled: 'Cancelled'
|
|
||||||
}
|
|
||||||
return statusMap[status] || status
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusVariant = (status: OrderStatus) => {
|
|
||||||
const variantMap: Record<OrderStatus, 'default' | 'secondary' | 'outline' | 'destructive'> = {
|
|
||||||
pending: 'outline',
|
|
||||||
paid: 'secondary',
|
|
||||||
processing: 'secondary',
|
|
||||||
shipped: 'default',
|
|
||||||
delivered: 'default',
|
|
||||||
cancelled: 'destructive'
|
|
||||||
}
|
|
||||||
return variantMap[status] || 'outline'
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatPrice = (price: number, currency: string) => {
|
|
||||||
return marketStore.formatPrice(price, currency)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelOrder = (orderId: string) => {
|
|
||||||
// TODO: Implement order cancellation
|
|
||||||
console.log('Cancelling order:', orderId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// const viewPayment = (_order: any) => {
|
|
||||||
// // TODO: Implement payment viewing
|
|
||||||
// console.log('Viewing payment for order:', _order.id)
|
|
||||||
// }
|
|
||||||
|
|
||||||
const copyOrderId = async (orderId: string) => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(orderId)
|
|
||||||
// TODO: Show toast notification
|
|
||||||
console.log('Order ID copied to clipboard')
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to copy order ID:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const togglePaymentDisplay = (orderId: string) => {
|
|
||||||
if (expandedPayments.value.has(orderId)) {
|
|
||||||
expandedPayments.value.delete(orderId)
|
|
||||||
} else {
|
|
||||||
expandedPayments.value.add(orderId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load orders on mount
|
|
||||||
onMounted(() => {
|
|
||||||
// Orders are already loaded in the market store
|
|
||||||
console.log('Order History page loaded with', allOrders.value.length, 'orders')
|
|
||||||
|
|
||||||
// Start listening for order events if not already listening
|
|
||||||
if (!orderEvents.isSubscribed.value) {
|
|
||||||
orderEvents.initialize()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
<!-- eslint-disable vue/multi-word-component-names -->
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useEvents } from '@/composables/useEvents'
|
|
||||||
import { useAuth } from '@/composables/useAuth'
|
|
||||||
import { Card, CardContent, CardDescription, CardFooter, 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 PurchaseTicketDialog from '@/components/events/PurchaseTicketDialog.vue'
|
|
||||||
import { RefreshCw, User, LogIn } from 'lucide-vue-next'
|
|
||||||
import { formatEventPrice } from '@/lib/utils/formatting'
|
|
||||||
|
|
||||||
const { upcomingEvents, pastEvents, isLoading, error, refresh } = useEvents()
|
|
||||||
const { isAuthenticated, userDisplay } = useAuth()
|
|
||||||
const showPurchaseDialog = ref(false)
|
|
||||||
const selectedEvent = ref<{
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
price_per_ticket: number
|
|
||||||
currency: string
|
|
||||||
} | null>(null)
|
|
||||||
|
|
||||||
function formatDate(dateStr: string) {
|
|
||||||
return format(new Date(dateStr), 'PPP')
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePurchaseClick(event: {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
price_per_ticket: number
|
|
||||||
currency: string
|
|
||||||
}) {
|
|
||||||
if (!isAuthenticated.value) {
|
|
||||||
// Show login prompt or redirect to login
|
|
||||||
// You could emit an event to show login dialog here
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedEvent.value = event
|
|
||||||
showPurchaseDialog.value = true
|
|
||||||
}
|
|
||||||
</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">Events</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">
|
|
||||||
<LogIn class="w-4 h-4" />
|
|
||||||
<span>Please log in to purchase tickets</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button variant="secondary" size="sm" @click="refresh" :disabled="isLoading">
|
|
||||||
<RefreshCw class="w-4 h-4 mr-2" :class="{ 'animate-spin': isLoading }" />
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs default-value="upcoming" class="w-full">
|
|
||||||
<TabsList class="grid w-full grid-cols-2">
|
|
||||||
<TabsTrigger value="upcoming">Upcoming Events</TabsTrigger>
|
|
||||||
<TabsTrigger value="past">Past Events</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div v-if="error" class="mt-4 p-4 bg-destructive/15 text-destructive rounded-lg">
|
|
||||||
{{ error.message }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TabsContent value="upcoming">
|
|
||||||
<!-- {{ upcomingEvents }} -->
|
|
||||||
<ScrollArea class="h-[600px] w-full pr-4" v-if="upcomingEvents.length">
|
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
<Card v-for="event in upcomingEvents" :key="event.id" class="flex flex-col">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="text-foreground">{{ event.name }}</CardTitle>
|
|
||||||
<CardDescription>{{ event.info }}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="flex-grow">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">Start Date:</span>
|
|
||||||
<span class="text-foreground">{{ formatDate(event.event_start_date) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">End Date:</span>
|
|
||||||
<span class="text-foreground">{{ formatDate(event.event_end_date) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">Tickets Available:</span>
|
|
||||||
<span class="text-foreground">{{ event.amount_tickets - event.sold }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">Price:</span>
|
|
||||||
<span class="text-foreground">{{ formatEventPrice(event.price_per_ticket, event.currency) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter>
|
|
||||||
<Button
|
|
||||||
class="w-full"
|
|
||||||
variant="default"
|
|
||||||
:disabled="event.amount_tickets <= event.sold || !isAuthenticated"
|
|
||||||
@click="handlePurchaseClick(event)"
|
|
||||||
>
|
|
||||||
<span v-if="!isAuthenticated" class="flex items-center gap-2">
|
|
||||||
<LogIn class="w-4 h-4" />
|
|
||||||
Login to Purchase
|
|
||||||
</span>
|
|
||||||
<span v-else>Buy Ticket</span>
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
<div v-else-if="!isLoading" class="text-center py-8 text-muted-foreground">
|
|
||||||
No upcoming events found
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="past">
|
|
||||||
<ScrollArea class="h-[600px] w-full pr-4" v-if="pastEvents.length">
|
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
<Card v-for="event in pastEvents" :key="event.id" class="flex flex-col opacity-75">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="text-foreground">{{ event.name }}</CardTitle>
|
|
||||||
<CardDescription>{{ event.info }}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="flex-grow">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">Start Date:</span>
|
|
||||||
<span class="text-foreground">{{ formatDate(event.event_start_date) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">End Date:</span>
|
|
||||||
<span class="text-foreground">{{ formatDate(event.event_end_date) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">Total Tickets:</span>
|
|
||||||
<span class="text-foreground">{{ event.amount_tickets }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">Tickets Sold:</span>
|
|
||||||
<span class="text-foreground">{{ event.sold }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
<div v-else-if="!isLoading" class="text-center py-8 text-muted-foreground">
|
|
||||||
No past events found
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<PurchaseTicketDialog v-if="selectedEvent" :event="selectedEvent" v-model:is-open="showPurchaseDialog" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue