- Introduce an empty state for users without a store, prompting them to create one. - Implement computed property to check if the user has a store based on their order history. - Update store statistics to reflect only the user's orders. - Add a placeholder function for future store creation functionality. These changes improve user experience by guiding new merchants to set up their stores effectively.
543 lines
20 KiB
Vue
543 lines
20 KiB
Vue
<template>
|
||
<div class="space-y-6">
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<h2 class="text-2xl font-bold text-foreground">My Orders</h2>
|
||
<p class="text-muted-foreground mt-1">Track all your market orders and payments</p>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<!-- Order Events Status -->
|
||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||
<div class="w-2 h-2 rounded-full" :class="orderEvents.isSubscribed ? 'bg-green-500' : 'bg-yellow-500'"></div>
|
||
<span>{{ orderEvents.isSubscribed ? 'Live updates' : 'Connecting...' }}</span>
|
||
</div>
|
||
<Button @click="navigateToMarket" variant="outline">
|
||
<Store class="w-4 h-4 mr-2" />
|
||
Browse Market
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filters and Stats -->
|
||
<div class="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:</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-amber-600">{{ pendingOrders }}</Badge>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-muted-foreground">Paid:</span>
|
||
<Badge variant="outline" class="text-green-600">{{ paidOrders }}</Badge>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-muted-foreground">Payment Due:</span>
|
||
<Badge variant="outline" class="text-red-600">{{ pendingPayments }}</Badge>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filter Controls -->
|
||
<div class="flex gap-2">
|
||
<select v-model="statusFilter"
|
||
class="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="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>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-3">
|
||
<Badge :variant="getStatusVariant(getEffectiveStatus(order))" :class="getStatusColor(getEffectiveStatus(order))">
|
||
{{ formatStatus(getEffectiveStatus(order)) }}
|
||
</Badge>
|
||
<div class="text-right">
|
||
<p class="text-lg font-semibold text-foreground">
|
||
{{ formatPrice(order.total, order.currency) }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Order Items -->
|
||
<div class="mb-4">
|
||
<h4 class="font-medium text-foreground mb-2">Items</h4>
|
||
<div class="space-y-1">
|
||
<div v-for="item in order.items.slice(0, 3)" :key="item.productId" class="text-sm text-muted-foreground">
|
||
{{ item.productName }} × {{ item.quantity }}
|
||
</div>
|
||
<div v-if="order.items.length > 3" class="text-sm text-muted-foreground">
|
||
+{{ order.items.length - 3 }} more items
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Paid Section -->
|
||
<div v-if="isOrderPaid(order)" class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg dark:bg-green-950 dark:border-green-800">
|
||
<div class="flex items-center gap-2">
|
||
<CheckCircle class="w-5 h-5 text-green-600" />
|
||
<span class="font-medium text-green-800 dark:text-green-200">Payment Confirmed</span>
|
||
<Badge variant="default" class="text-xs bg-green-600 text-white">
|
||
Paid
|
||
</Badge>
|
||
</div>
|
||
<p v-if="order.paidAt" class="text-sm text-green-700 dark:text-green-300 mt-1">
|
||
Paid on {{ formatDate(order.paidAt) }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Payment Section -->
|
||
<div v-if="order.paymentRequest && !isOrderPaid(order)" class="mb-4 p-4 bg-muted/50 border border-border rounded-lg">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<div class="flex items-center gap-2">
|
||
<Zap class="w-4 h-4 text-yellow-500" />
|
||
<span class="font-medium text-foreground">Payment Required</span>
|
||
</div>
|
||
<Badge :variant="isOrderPaid(order) ? 'default' : 'secondary'" class="text-xs"
|
||
:class="isOrderPaid(order) ? 'text-green-600' : 'text-amber-600'">
|
||
{{ isOrderPaid(order) ? 'Paid' : 'Pending' }}
|
||
</Badge>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<!-- Payment Details -->
|
||
<div class="space-y-3">
|
||
<!-- Payment Request -->
|
||
<div>
|
||
<label class="block text-xs font-medium text-muted-foreground mb-1">
|
||
Payment Request
|
||
</label>
|
||
<div class="flex items-center gap-2">
|
||
<input :value="order.paymentRequest || ''" readonly disabled
|
||
class="flex-1 font-mono text-xs bg-muted border border-input rounded-md px-3 py-1 text-foreground" />
|
||
<Button @click="copyPaymentRequest(order.paymentRequest || '')" variant="outline" size="sm">
|
||
<Copy class="w-3 h-3" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Payment Actions -->
|
||
<div class="flex gap-2">
|
||
<Button @click="payWithLightning(order)" variant="default" size="sm"
|
||
class="flex-1" :disabled="!order.paymentRequest || isPayingWithWallet">
|
||
<Zap class="w-3 h-3 mr-1" />
|
||
{{ isPayingWithWallet ? 'Paying...' : hasWalletWithBalance ? 'Pay with Wallet' : 'Pay with Lightning' }}
|
||
</Button>
|
||
<Button @click="toggleQRCode(order.id)" variant="outline" size="sm">
|
||
<QrCode class="w-3 h-3" />
|
||
{{ order.showQRCode ? 'Hide QR' : 'Show QR' }}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- QR Code -->
|
||
<div v-if="order.showQRCode" class="flex justify-center">
|
||
<div class="w-48 h-48">
|
||
<div v-if="order.qrCodeDataUrl && !order.qrCodeError" class="w-full h-full">
|
||
<img :src="order.qrCodeDataUrl"
|
||
:alt="`QR Code for ${formatPrice(order.total, order.currency)} payment`"
|
||
class="w-full h-full border border-border rounded-lg" />
|
||
</div>
|
||
<div v-else-if="order.qrCodeLoading"
|
||
class="w-full h-full bg-muted rounded-lg flex items-center justify-center">
|
||
<div class="text-center text-muted-foreground">
|
||
<div class="text-4xl mb-2 animate-pulse">⚡</div>
|
||
<div class="text-sm">Generating QR...</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="w-full h-full bg-muted rounded-lg flex items-center justify-center">
|
||
<div class="text-center text-muted-foreground">
|
||
<div class="text-4xl mb-2">⚡</div>
|
||
<div class="text-sm">No QR</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Waiting for Invoice -->
|
||
<div v-else-if="order.status === 'pending' && !isOrderPaid(order)" class="mb-4 p-4 bg-muted/50 border border-border rounded-lg">
|
||
<div class="flex items-center gap-2 mb-2">
|
||
<div class="w-2 h-2 bg-amber-500 rounded-full animate-pulse"></div>
|
||
<span class="font-medium text-foreground">Waiting for Payment Invoice</span>
|
||
</div>
|
||
<p class="text-sm text-muted-foreground">
|
||
The merchant will send you a Lightning invoice via Nostr once they process your order
|
||
</p>
|
||
</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 variant="outline" size="sm" @click="copyOrderId(order.id)">
|
||
Copy Order ID
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Debug Information (Development Only) -->
|
||
<div v-if="isDevelopment" class="mt-8 p-4 bg-gray-100 rounded-lg">
|
||
<h4 class="font-medium mb-2">Debug Information</h4>
|
||
<div class="text-sm space-y-1">
|
||
<div>Total Orders in Store: {{ Object.keys(marketStore.orders).length }}</div>
|
||
<div>Filtered Orders: {{ filteredOrders.length }}</div>
|
||
<div>Order Events Subscribed: {{ orderEvents.isSubscribed ? 'Yes' : 'No' }}</div>
|
||
<div>Relay Hub Connected: {{ relayHub.isConnected ? 'Yes' : 'No' }}</div>
|
||
<div>Auth Status: {{ auth.isAuthenticated ? 'Authenticated' : 'Not Authenticated' }}</div>
|
||
<div>Current User: {{ auth.currentUser?.value?.pubkey ? 'Yes' : 'No' }}</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" />
|
||
</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="navigateToMarket" variant="default">
|
||
Browse Market
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, watch } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useMarketStore } from '../stores/market'
|
||
// import { useOrderEvents } from '@/composables/useOrderEvents' // TODO: Move to market module
|
||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||
import { auth } from '@/composables/useAuthService'
|
||
import { useLightningPayment } from '../composables/useLightningPayment'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Badge } from '@/components/ui/badge'
|
||
import { Package, Store, Zap, Copy, QrCode, CheckCircle } from 'lucide-vue-next'
|
||
import { toast } from 'vue-sonner'
|
||
import type { OrderStatus } from '@/modules/market/stores/market'
|
||
// Order type no longer needed since we use any for readonly compatibility
|
||
|
||
const router = useRouter()
|
||
const marketStore = useMarketStore()
|
||
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any
|
||
const paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE) as any
|
||
const { handlePayment, isPayingWithWallet, hasWalletWithBalance } = useLightningPayment()
|
||
|
||
// const orderEvents = useOrderEvents() // TODO: Move to market module
|
||
const orderEvents = {
|
||
isSubscribed: ref(false),
|
||
subscribeToOrderEvents: () => {},
|
||
cleanup: () => {},
|
||
initialize: () => {
|
||
console.log('OrderEvents mock initialize called')
|
||
orderEvents.isSubscribed.value = true
|
||
}
|
||
} // Temporary mock
|
||
|
||
// Local state
|
||
const statusFilter = ref('')
|
||
const sortBy = ref('createdAt')
|
||
|
||
// Computed properties
|
||
const allOrders = computed(() => {
|
||
// Filter orders to only show those where the current user is the buyer
|
||
const currentUserPubkey = auth.currentUser?.value?.pubkey
|
||
if (!currentUserPubkey) return []
|
||
|
||
return Object.values(marketStore.orders).filter(order =>
|
||
order.buyerPubkey === currentUserPubkey
|
||
)
|
||
})
|
||
|
||
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 paidOrders = computed(() => allOrders.value.filter(o => o.status === 'paid').length)
|
||
const pendingPayments = computed(() => allOrders.value.filter(o => !isOrderPaid(o)).length)
|
||
|
||
const isDevelopment = computed(() => import.meta.env.DEV)
|
||
|
||
// Methods
|
||
const isOrderPaid = (order: any) => {
|
||
// Prioritize the 'paid' field from Nostr status updates (type 2)
|
||
if (order.paid !== undefined) {
|
||
return order.paid
|
||
}
|
||
// Fallback to paymentStatus field
|
||
return order.paymentStatus === 'paid'
|
||
}
|
||
|
||
const getEffectiveStatus = (order: any) => {
|
||
// If paid, return 'paid' regardless of original status
|
||
if (isOrderPaid(order)) {
|
||
return order.shipped ? 'shipped' : 'paid'
|
||
}
|
||
// Otherwise return the original status
|
||
return order.status
|
||
}
|
||
|
||
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 getStatusColor = (status: OrderStatus) => {
|
||
const colorMap: Record<OrderStatus, string> = {
|
||
pending: 'text-amber-600',
|
||
paid: 'text-green-600',
|
||
processing: 'text-blue-600',
|
||
shipped: 'text-blue-600',
|
||
delivered: 'text-green-600',
|
||
cancelled: 'text-red-600'
|
||
}
|
||
return colorMap[status] || 'text-muted-foreground'
|
||
}
|
||
|
||
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 copyOrderId = async (orderId: string) => {
|
||
try {
|
||
await navigator.clipboard.writeText(orderId)
|
||
toast.success('Order ID copied to clipboard')
|
||
console.log('Order ID copied to clipboard')
|
||
} catch (err) {
|
||
console.error('Failed to copy order ID:', err)
|
||
toast.error('Failed to copy order ID')
|
||
}
|
||
}
|
||
|
||
const copyPaymentRequest = async (paymentRequest: string) => {
|
||
console.log('Copying payment request:', {
|
||
paymentRequest: paymentRequest?.substring(0, 50) + '...',
|
||
hasValue: !!paymentRequest,
|
||
length: paymentRequest?.length
|
||
})
|
||
|
||
if (!paymentRequest) {
|
||
toast.error('No payment request available', {
|
||
description: 'Please wait for the merchant to send the payment request'
|
||
})
|
||
return
|
||
}
|
||
|
||
try {
|
||
await navigator.clipboard.writeText(paymentRequest)
|
||
toast.success('Payment request copied to clipboard', {
|
||
description: 'You can now paste it into your Lightning wallet'
|
||
})
|
||
console.log('Payment request copied to clipboard')
|
||
} catch (err) {
|
||
console.error('Failed to copy payment request:', err)
|
||
toast.error('Failed to copy payment request', {
|
||
description: 'Please try again or copy manually'
|
||
})
|
||
}
|
||
}
|
||
|
||
const payWithLightning = async (order: any) => {
|
||
try {
|
||
await handlePayment(order.paymentRequest, order.id)
|
||
} catch (error) {
|
||
console.error('Payment failed:', error)
|
||
}
|
||
}
|
||
|
||
const toggleQRCode = async (orderId: string) => {
|
||
// Toggle QR code visibility for the order
|
||
const order = marketStore.orders[orderId]
|
||
if (order) {
|
||
// If showing QR code and it doesn't exist yet, generate it
|
||
if (!order.showQRCode && order.lightningInvoice?.bolt11 && !order.qrCodeDataUrl) {
|
||
await generateQRCode(orderId, order.lightningInvoice.bolt11)
|
||
}
|
||
|
||
marketStore.updateOrder(orderId, {
|
||
showQRCode: !order.showQRCode
|
||
})
|
||
}
|
||
}
|
||
|
||
const generateQRCode = async (orderId: string, bolt11: string) => {
|
||
try {
|
||
// Set loading state
|
||
marketStore.updateOrder(orderId, {
|
||
qrCodeLoading: true,
|
||
qrCodeError: null
|
||
})
|
||
|
||
// Import QRCode library dynamically
|
||
const QRCode = await import('qrcode')
|
||
|
||
// Generate QR code
|
||
const qrCodeDataUrl = await QRCode.toDataURL(bolt11, {
|
||
width: 192,
|
||
margin: 2,
|
||
color: {
|
||
dark: '#000000',
|
||
light: '#FFFFFF'
|
||
}
|
||
})
|
||
|
||
// Update order with QR code
|
||
marketStore.updateOrder(orderId, {
|
||
qrCodeDataUrl,
|
||
qrCodeLoading: false,
|
||
qrCodeError: null
|
||
})
|
||
|
||
console.log('QR code generated for order:', orderId)
|
||
} catch (error) {
|
||
console.error('Failed to generate QR code:', error)
|
||
marketStore.updateOrder(orderId, {
|
||
qrCodeLoading: false,
|
||
qrCodeError: error instanceof Error ? error.message : 'Failed to generate QR code'
|
||
})
|
||
}
|
||
}
|
||
|
||
const navigateToMarket = () => router.push('/market')
|
||
|
||
// Load orders on mount
|
||
onMounted(() => {
|
||
// Reset payment state to prevent stuck "Paying..." buttons (safety measure)
|
||
if (paymentService?.forceResetPaymentState) {
|
||
paymentService.forceResetPaymentState()
|
||
}
|
||
|
||
// Orders are already loaded in the market store
|
||
console.log('Order History component loaded with', allOrders.value.length, 'orders')
|
||
console.log('Market store orders:', marketStore.orders)
|
||
|
||
// Debug: Log order details for orders with payment requests
|
||
allOrders.value.forEach(order => {
|
||
if (order.paymentRequest) {
|
||
console.log('Order with payment request:', {
|
||
id: order.id,
|
||
paymentRequest: order.paymentRequest.substring(0, 50) + '...',
|
||
hasPaymentRequest: !!order.paymentRequest,
|
||
status: order.status,
|
||
paymentStatus: order.paymentStatus
|
||
})
|
||
}
|
||
})
|
||
|
||
console.log('Order events status:', orderEvents.isSubscribed.value)
|
||
console.log('Relay hub connected:', relayHub.isConnected.value)
|
||
console.log('Auth status:', auth.isAuthenticated)
|
||
console.log('Current user:', auth.currentUser?.value?.pubkey)
|
||
|
||
// Start listening for order events if not already listening
|
||
if (!orderEvents.isSubscribed.value) {
|
||
console.log('Starting order events listener...')
|
||
orderEvents.initialize()
|
||
} else {
|
||
console.log('Order events already listening')
|
||
}
|
||
})
|
||
|
||
// Watch for authentication and relay hub readiness
|
||
watch(
|
||
[() => auth.isAuthenticated, () => relayHub.isConnected.value],
|
||
([isAuth, isConnected]) => {
|
||
if (isAuth && isConnected && !orderEvents.isSubscribed.value) {
|
||
console.log('Auth and relay hub ready, starting order events listener...')
|
||
orderEvents.initialize()
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
</script>
|