- Delete the useNostrOrders composable as it is no longer needed. - Update MerchantStore.vue to utilize nostrmarketService for publishing orders instead of the removed composable. - Refactor market store to check the readiness of nostrmarketService instead of useNostrOrders. - Remove the Checkout.vue page, streamlining the checkout process and improving code maintainability.
549 lines
19 KiB
Vue
549 lines
19 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 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 { nostrmarketService } from '../services/nostrmarketService'
|
||
import { auth } from '@/composables/useAuth'
|
||
|
||
const router = useRouter()
|
||
const marketStore = useMarketStore()
|
||
|
||
// 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 nostrmarketService.publishOrder(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>
|
||
|