Enhance market module with new chat and events features
- Introduce chat module with components, services, and composables for real-time messaging. - Implement events module with API service, components, and ticket purchasing functionality. - Update app configuration to include new modules and their respective settings. - Refactor existing components to integrate with the new chat and events features. - Enhance market store and services to support new functionalities and improve order management. - Update routing to accommodate new views for chat and events, ensuring seamless navigation.
This commit is contained in:
parent
519a9003d4
commit
e40ac91417
46 changed files with 6305 additions and 3264 deletions
|
|
@ -1,550 +0,0 @@
|
|||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-foreground">My Store</h2>
|
||||
<p class="text-muted-foreground mt-1">Manage incoming orders and your products</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button @click="navigateToMarket" variant="outline">
|
||||
<Store class="w-4 h-4 mr-2" />
|
||||
Browse Market
|
||||
</Button>
|
||||
<Button @click="addProduct" variant="default">
|
||||
<Plus class="w-4 h-4 mr-2" />
|
||||
Add Product
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Store Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<!-- Incoming Orders -->
|
||||
<div class="bg-card p-6 rounded-lg border shadow-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Incoming Orders</p>
|
||||
<p class="text-2xl font-bold text-foreground">{{ storeStats.incomingOrders }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||
<Package class="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center text-sm text-muted-foreground">
|
||||
<span>{{ storeStats.pendingOrders }} pending</span>
|
||||
<span class="mx-2">•</span>
|
||||
<span>{{ storeStats.paidOrders }} paid</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Sales -->
|
||||
<div class="bg-card p-6 rounded-lg border shadow-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Total Sales</p>
|
||||
<p class="text-2xl font-bold text-foreground">{{ formatPrice(storeStats.totalSales, 'sat') }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-green-500/10 rounded-lg flex items-center justify-center">
|
||||
<DollarSign class="w-6 h-6 text-green-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center text-sm text-muted-foreground">
|
||||
<span>Last 30 days</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Products -->
|
||||
<div class="bg-card p-6 rounded-lg border shadow-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Products</p>
|
||||
<p class="text-2xl font-bold text-foreground">{{ storeStats.totalProducts }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-purple-500/10 rounded-lg flex items-center justify-center">
|
||||
<Store class="w-6 h-6 text-purple-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center text-sm text-muted-foreground">
|
||||
<span>{{ storeStats.activeProducts }} active</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer Satisfaction -->
|
||||
<div class="bg-card p-6 rounded-lg border shadow-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Satisfaction</p>
|
||||
<p class="text-2xl font-bold text-foreground">{{ storeStats.satisfaction }}%</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-yellow-500/10 rounded-lg flex items-center justify-center">
|
||||
<Star class="w-6 h-6 text-yellow-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center text-sm text-muted-foreground">
|
||||
<span>{{ storeStats.totalReviews }} reviews</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Incoming Orders Section -->
|
||||
<div class="bg-card rounded-lg border shadow-sm">
|
||||
<div class="p-6 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground">Incoming Orders</h3>
|
||||
<p class="text-sm text-muted-foreground mt-1">Orders waiting for your attention</p>
|
||||
</div>
|
||||
|
||||
<div v-if="incomingOrders.length > 0" class="divide-y divide-border">
|
||||
<div
|
||||
v-for="order in incomingOrders"
|
||||
:key="order.id"
|
||||
class="p-6 hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<!-- Order Header -->
|
||||
<div class="flex items-center justify-between 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>
|
||||
<h4 class="font-semibold text-foreground">Order #{{ order.id.slice(-8) }}</h4>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ formatDate(order.createdAt) }} • {{ formatPrice(order.total, order.currency) }}
|
||||
</p>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<Badge :variant="getStatusVariant(order.status)">
|
||||
{{ formatStatus(order.status) }}
|
||||
</Badge>
|
||||
<Badge v-if="order.paymentStatus === 'pending'" variant="secondary">
|
||||
Payment Pending
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Wallet Indicator -->
|
||||
<div v-if="order.status === 'pending' && !order.lightningInvoice" class="text-xs text-muted-foreground mr-2">
|
||||
<span>Wallet: {{ getFirstWalletName() }}</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="order.status === 'pending' && !order.lightningInvoice"
|
||||
@click="generateInvoice(order.id)"
|
||||
:disabled="isGeneratingInvoice === order.id"
|
||||
size="sm"
|
||||
variant="default"
|
||||
>
|
||||
<div v-if="isGeneratingInvoice === order.id" class="flex items-center space-x-2">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<span>Generating...</span>
|
||||
</div>
|
||||
<div v-else class="flex items-center space-x-2">
|
||||
<Zap class="w-4 h-4" />
|
||||
<span>Generate Invoice</span>
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="order.lightningInvoice"
|
||||
@click="viewOrderDetails(order.id)"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Eye class="w-4 h-4 mr-2" />
|
||||
View Details
|
||||
</Button>
|
||||
<Button
|
||||
@click="processOrder(order.id)"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Check class="w-4 h-4 mr-2" />
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Items -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<h5 class="font-medium text-foreground mb-2">Items</h5>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="item in order.items"
|
||||
:key="item.productId"
|
||||
class="text-sm text-muted-foreground"
|
||||
>
|
||||
{{ item.productName }} × {{ item.quantity }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="font-medium text-foreground mb-2">Customer Info</h5>
|
||||
<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>
|
||||
<p v-if="order.contactInfo.address">
|
||||
<span class="font-medium">Address:</span> {{ order.contactInfo.address }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Status -->
|
||||
<div v-if="order.lightningInvoice" class="p-4 bg-green-500/10 border border-green-200 rounded-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<CheckCircle class="w-5 h-5 text-green-600" />
|
||||
<span class="text-sm font-medium text-green-900">Lightning Invoice Generated</span>
|
||||
</div>
|
||||
<div class="text-sm text-green-700">
|
||||
Amount: {{ formatPrice(order.total, order.currency) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="p-4 bg-yellow-500/10 border border-yellow-200 rounded-lg">
|
||||
<div class="flex items-center gap-2">
|
||||
<AlertCircle class="w-5 h-5 text-yellow-600" />
|
||||
<span class="text-sm font-medium text-yellow-900">Invoice Required</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="p-6 text-center text-muted-foreground">
|
||||
<Package class="w-12 h-12 mx-auto mb-3 text-muted-foreground/50" />
|
||||
<p>No incoming orders</p>
|
||||
<p class="text-sm">Orders from customers will appear here</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Order Management -->
|
||||
<div class="bg-card p-6 rounded-lg border shadow-sm">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4">Order Management</h3>
|
||||
<div class="space-y-3">
|
||||
<Button
|
||||
@click="viewAllOrders"
|
||||
variant="default"
|
||||
class="w-full justify-start"
|
||||
>
|
||||
<Package class="w-4 h-4 mr-2" />
|
||||
View All Orders
|
||||
</Button>
|
||||
<Button
|
||||
@click="generateBulkInvoices"
|
||||
variant="outline"
|
||||
class="w-full justify-start"
|
||||
>
|
||||
<Zap class="w-4 h-4 mr-2" />
|
||||
Generate Bulk Invoices
|
||||
</Button>
|
||||
<Button
|
||||
@click="exportOrders"
|
||||
variant="outline"
|
||||
class="w-full justify-start"
|
||||
>
|
||||
<Download class="w-4 h-4 mr-2" />
|
||||
Export Orders
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Store Management -->
|
||||
<div class="bg-card p-6 rounded-lg border shadow-sm">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4">Store Management</h3>
|
||||
<div class="space-y-3">
|
||||
<Button
|
||||
@click="manageProducts"
|
||||
variant="default"
|
||||
class="w-full justify-start"
|
||||
>
|
||||
<Store class="w-4 h-4 mr-2" />
|
||||
Manage Products
|
||||
</Button>
|
||||
<Button
|
||||
@click="storeSettings"
|
||||
variant="outline"
|
||||
class="w-full justify-start"
|
||||
>
|
||||
<Settings class="w-4 h-4 mr-2" />
|
||||
Store Settings
|
||||
</Button>
|
||||
<Button
|
||||
@click="analytics"
|
||||
variant="outline"
|
||||
class="w-full justify-start"
|
||||
>
|
||||
<BarChart3 class="w-4 h-4 mr-2" />
|
||||
View Analytics
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</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,
|
||||
Store,
|
||||
DollarSign,
|
||||
Star,
|
||||
Plus,
|
||||
Zap,
|
||||
Eye,
|
||||
Check,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
Download,
|
||||
Settings,
|
||||
BarChart3
|
||||
} from 'lucide-vue-next'
|
||||
import type { OrderStatus } from '@/stores/market'
|
||||
import { nostrOrders } from '@/composables/useNostrOrders'
|
||||
import { auth } from '@/composables/useAuth'
|
||||
|
||||
const router = useRouter()
|
||||
const marketStore = useMarketStore()
|
||||
const nostrOrdersComposable = nostrOrders
|
||||
|
||||
// Local state
|
||||
const isGeneratingInvoice = ref<string | null>(null)
|
||||
|
||||
// Computed properties
|
||||
const incomingOrders = computed(() => {
|
||||
// For now, show all orders as "incoming" since we don't have merchant filtering yet
|
||||
// In a real implementation, this would filter orders where the current user is the seller
|
||||
return Object.values(marketStore.orders)
|
||||
.filter(order => order.status === 'pending')
|
||||
.sort((a, b) => b.createdAt - a.createdAt)
|
||||
})
|
||||
|
||||
const storeStats = computed(() => {
|
||||
const orders = Object.values(marketStore.orders)
|
||||
const now = Date.now() / 1000
|
||||
const thirtyDaysAgo = now - (30 * 24 * 60 * 60)
|
||||
|
||||
return {
|
||||
incomingOrders: orders.filter(o => o.status === 'pending').length,
|
||||
pendingOrders: orders.filter(o => o.status === 'pending').length,
|
||||
paidOrders: orders.filter(o => o.status === 'paid').length,
|
||||
totalSales: orders
|
||||
.filter(o => o.status === 'paid' && o.createdAt > thirtyDaysAgo)
|
||||
.reduce((sum, o) => sum + o.total, 0),
|
||||
totalProducts: 0, // TODO: Implement product management
|
||||
activeProducts: 0, // TODO: Implement product management
|
||||
satisfaction: 95, // TODO: Implement review system
|
||||
totalReviews: 0 // TODO: Implement review system
|
||||
}
|
||||
})
|
||||
|
||||
// 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 generateInvoice = async (orderId: string) => {
|
||||
console.log('Generating invoice for order:', orderId)
|
||||
isGeneratingInvoice.value = orderId
|
||||
|
||||
try {
|
||||
// Get the order from the store
|
||||
const order = marketStore.orders[orderId]
|
||||
if (!order) {
|
||||
throw new Error('Order not found')
|
||||
}
|
||||
|
||||
// Temporary fix: If buyerPubkey is missing, try to get it from auth
|
||||
if (!order.buyerPubkey && auth.currentUser?.value?.pubkey) {
|
||||
console.log('Fixing missing buyerPubkey for existing order')
|
||||
marketStore.updateOrder(order.id, { buyerPubkey: auth.currentUser.value.pubkey })
|
||||
}
|
||||
|
||||
// Temporary fix: If sellerPubkey is missing, use current user's pubkey
|
||||
if (!order.sellerPubkey && auth.currentUser?.value?.pubkey) {
|
||||
console.log('Fixing missing sellerPubkey for existing order')
|
||||
marketStore.updateOrder(order.id, { sellerPubkey: auth.currentUser.value.pubkey })
|
||||
}
|
||||
|
||||
// Get the updated order
|
||||
const updatedOrder = marketStore.orders[orderId]
|
||||
|
||||
console.log('Order details for invoice generation:', {
|
||||
orderId: updatedOrder.id,
|
||||
orderFields: Object.keys(updatedOrder),
|
||||
buyerPubkey: updatedOrder.buyerPubkey,
|
||||
sellerPubkey: updatedOrder.sellerPubkey,
|
||||
status: updatedOrder.status,
|
||||
total: updatedOrder.total
|
||||
})
|
||||
|
||||
// Get the user's wallet list
|
||||
const userWallets = auth.currentUser?.value?.wallets || []
|
||||
console.log('Available wallets:', userWallets)
|
||||
|
||||
if (userWallets.length === 0) {
|
||||
throw new Error('No wallet available to generate invoice. Please ensure you have at least one wallet configured.')
|
||||
}
|
||||
|
||||
// Use the first available wallet for invoice generation
|
||||
const walletId = userWallets[0].id
|
||||
const walletName = userWallets[0].name
|
||||
const adminKey = userWallets[0].adminkey
|
||||
console.log('Using wallet for invoice generation:', { walletId, walletName, balance: userWallets[0].balance_msat })
|
||||
|
||||
const invoice = await marketStore.createLightningInvoice(orderId, adminKey)
|
||||
|
||||
if (invoice) {
|
||||
console.log('Lightning invoice created:', invoice)
|
||||
|
||||
// Send the invoice to the customer via Nostr
|
||||
await sendInvoiceToCustomer(updatedOrder, invoice)
|
||||
|
||||
console.log('Invoice sent to customer successfully')
|
||||
|
||||
// Show success message (you could add a toast notification here)
|
||||
alert(`Invoice generated successfully using wallet: ${walletName}`)
|
||||
} else {
|
||||
throw new Error('Failed to create Lightning invoice')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate invoice:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
// Show error message to user
|
||||
alert(`Failed to generate invoice: ${errorMessage}`)
|
||||
} finally {
|
||||
isGeneratingInvoice.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const sendInvoiceToCustomer = async (order: any, invoice: any) => {
|
||||
try {
|
||||
console.log('Sending invoice to customer for order:', {
|
||||
orderId: order.id,
|
||||
buyerPubkey: order.buyerPubkey,
|
||||
sellerPubkey: order.sellerPubkey,
|
||||
invoiceFields: Object.keys(invoice)
|
||||
})
|
||||
|
||||
// Check if we have the buyer's public key
|
||||
if (!order.buyerPubkey) {
|
||||
console.error('Missing buyerPubkey in order:', order)
|
||||
throw new Error('Cannot send invoice: buyer public key not found')
|
||||
}
|
||||
|
||||
// Update the order with the invoice details
|
||||
const updatedOrder = {
|
||||
...order,
|
||||
lightningInvoice: invoice,
|
||||
paymentHash: invoice.payment_hash,
|
||||
paymentStatus: 'pending',
|
||||
paymentRequest: invoice.bolt11, // Use bolt11 field from LNBits response
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
// Update the order in the store
|
||||
marketStore.updateOrder(order.id, updatedOrder)
|
||||
|
||||
// Send the updated order to the customer via Nostr
|
||||
// This will include the invoice information
|
||||
await nostrOrdersComposable.publishOrderEvent(updatedOrder, order.buyerPubkey)
|
||||
|
||||
console.log('Updated order with invoice sent via Nostr to customer:', order.buyerPubkey)
|
||||
} catch (error) {
|
||||
console.error('Failed to send invoice to customer:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const viewOrderDetails = (orderId: string) => {
|
||||
// TODO: Navigate to detailed order view
|
||||
console.log('Viewing order details:', orderId)
|
||||
}
|
||||
|
||||
const processOrder = (orderId: string) => {
|
||||
// TODO: Implement order processing
|
||||
console.log('Processing order:', orderId)
|
||||
}
|
||||
|
||||
const addProduct = () => {
|
||||
// TODO: Navigate to add product form
|
||||
console.log('Adding new product')
|
||||
}
|
||||
|
||||
const navigateToMarket = () => router.push('/market')
|
||||
const viewAllOrders = () => router.push('/market-dashboard?tab=orders')
|
||||
const generateBulkInvoices = () => console.log('Generate bulk invoices')
|
||||
const exportOrders = () => console.log('Export orders')
|
||||
const manageProducts = () => console.log('Manage products')
|
||||
const storeSettings = () => router.push('/market-dashboard?tab=settings')
|
||||
const analytics = () => console.log('View analytics')
|
||||
|
||||
const getFirstWalletName = () => {
|
||||
const userWallets = auth.currentUser?.value?.wallets || []
|
||||
if (userWallets.length > 0) {
|
||||
return userWallets[0].name
|
||||
}
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
console.log('Merchant Store component loaded')
|
||||
})
|
||||
</script>
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue