- Introduce keys for reactivity in the stall creation dialog and form elements to ensure proper rendering and state management. - Implement autocomplete and spellcheck attributes for input fields to enhance user experience during store creation. - Refactor form initialization and reset logic to ensure a clean state upon dialog opening and closing, improving usability. - Add validation triggers after loading data to ensure form integrity and user feedback during the stall creation process. These changes streamline the store creation experience, providing a more responsive and user-friendly interface for merchants.
1425 lines
48 KiB
Vue
1425 lines
48 KiB
Vue
<template>
|
||
<div class="space-y-6">
|
||
<!-- Loading State -->
|
||
<div v-if="isLoadingMerchant" class="flex flex-col items-center justify-center py-12">
|
||
<div class="w-16 h-16 mx-auto mb-4 bg-muted/50 rounded-full flex items-center justify-center">
|
||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||
</div>
|
||
<h3 class="text-lg font-medium text-foreground mb-2">Checking Merchant Status</h3>
|
||
<p class="text-muted-foreground">Loading your merchant profile...</p>
|
||
</div>
|
||
|
||
<!-- Error State -->
|
||
<div v-else-if="merchantCheckError" class="flex flex-col items-center justify-center py-12">
|
||
<div class="w-16 h-16 mx-auto mb-4 bg-red-500/10 rounded-full flex items-center justify-center">
|
||
<AlertCircle class="w-8 h-8 text-red-500" />
|
||
</div>
|
||
<h3 class="text-lg font-medium text-foreground mb-2">Error Loading Merchant Status</h3>
|
||
<p class="text-muted-foreground mb-4">{{ merchantCheckError }}</p>
|
||
<Button @click="checkMerchantProfile" variant="outline">
|
||
Try Again
|
||
</Button>
|
||
</div>
|
||
|
||
<!-- No Merchant Profile Empty State -->
|
||
<div v-else-if="!userHasMerchantProfile" class="flex flex-col items-center justify-center py-12">
|
||
<div class="w-24 h-24 bg-muted rounded-full flex items-center justify-center mb-6">
|
||
<User class="w-12 h-12 text-muted-foreground" />
|
||
</div>
|
||
<h2 class="text-2xl font-bold text-foreground mb-2">Create Your Merchant Profile</h2>
|
||
<p class="text-muted-foreground text-center mb-6 max-w-md">
|
||
Before you can create a store, you need to set up your merchant profile. This will create your merchant identity on the Nostr marketplace.
|
||
</p>
|
||
<Button
|
||
@click="createMerchantProfile"
|
||
variant="default"
|
||
size="lg"
|
||
:disabled="isCreatingMerchant"
|
||
>
|
||
<div v-if="isCreatingMerchant" class="flex items-center">
|
||
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||
<span>Creating...</span>
|
||
</div>
|
||
<div v-else class="flex items-center">
|
||
<Plus class="w-5 h-5 mr-2" />
|
||
<span>Create Merchant Profile</span>
|
||
</div>
|
||
</Button>
|
||
</div>
|
||
|
||
<!-- Stores Grid (shown when merchant profile exists) -->
|
||
<div v-else>
|
||
<!-- Header Section -->
|
||
<div class="mb-8">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<div>
|
||
<h2 class="text-2xl font-bold text-foreground">My Stores</h2>
|
||
<p class="text-muted-foreground mt-1">
|
||
Manage your stores and products
|
||
</p>
|
||
</div>
|
||
<Button @click="navigateToMarket" variant="outline">
|
||
<Store class="w-4 h-4 mr-2" />
|
||
Browse Market
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Loading State for Stalls -->
|
||
<div v-if="isLoadingStalls" class="flex justify-center py-12">
|
||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||
</div>
|
||
|
||
<!-- Stores Cards Grid -->
|
||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||
<!-- Existing Store Cards -->
|
||
<div v-for="stall in userStalls" :key="stall.id"
|
||
class="bg-card rounded-lg border shadow-sm hover:shadow-md transition-shadow">
|
||
<div class="p-6">
|
||
<div class="flex items-start justify-between mb-4">
|
||
<div class="flex-1">
|
||
<h3 class="text-lg font-semibold text-foreground">{{ stall.name }}</h3>
|
||
<p class="text-sm text-muted-foreground mt-1">
|
||
{{ stall.config?.description || 'No description' }}
|
||
</p>
|
||
</div>
|
||
<Badge variant="secondary">{{ stall.currency }}</Badge>
|
||
</div>
|
||
|
||
<div class="space-y-3">
|
||
<!-- Store Metrics -->
|
||
<div class="flex items-center justify-between text-sm">
|
||
<span class="text-muted-foreground">Products</span>
|
||
<span class="font-medium">{{ stall.products?.length || 0 }}</span>
|
||
</div>
|
||
<div class="flex items-center justify-between text-sm">
|
||
<span class="text-muted-foreground">Shipping Zones</span>
|
||
<span class="font-medium">{{ stall.shipping_zones?.length || 0 }}</span>
|
||
</div>
|
||
|
||
<!-- Action Buttons -->
|
||
<div class="flex gap-2 pt-3">
|
||
<Button
|
||
@click="manageStall(stall.id)"
|
||
variant="default"
|
||
size="sm"
|
||
class="flex-1"
|
||
>
|
||
<Settings class="w-4 h-4 mr-1" />
|
||
Manage
|
||
</Button>
|
||
<Button
|
||
@click="viewStallProducts(stall.id)"
|
||
variant="outline"
|
||
size="sm"
|
||
class="flex-1"
|
||
>
|
||
<Package class="w-4 h-4 mr-1" />
|
||
Products
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create New Store Card -->
|
||
<div class="bg-card rounded-lg border-2 border-dashed border-muted-foreground/25 hover:border-primary/50 transition-colors">
|
||
<button
|
||
@click="initializeStallCreation"
|
||
class="w-full h-full p-6 flex flex-col items-center justify-center min-h-[200px] hover:bg-muted/30 transition-colors"
|
||
>
|
||
<div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-3">
|
||
<Plus class="w-6 h-6 text-primary" />
|
||
</div>
|
||
<h3 class="text-lg font-semibold text-foreground mb-1">Create New Store</h3>
|
||
<p class="text-sm text-muted-foreground text-center">
|
||
Add another store to expand your marketplace presence
|
||
</p>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Active Store Dashboard (shown when a store is selected) -->
|
||
<div v-if="activeStall">
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between mb-6">
|
||
<div>
|
||
<div class="flex items-center gap-3 mb-2">
|
||
<Button @click="activeStallId = null" variant="ghost" size="sm">
|
||
← Back to Stores
|
||
</Button>
|
||
<div class="h-4 w-px bg-border"></div>
|
||
<Badge variant="secondary">{{ activeStall.currency }}</Badge>
|
||
</div>
|
||
<h2 class="text-2xl font-bold text-foreground">{{ activeStall.name }}</h2>
|
||
<p class="text-muted-foreground mt-1">{{ activeStall.config?.description || '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> <!-- End of Active Store Dashboard -->
|
||
</div> <!-- End of Stores Grid section -->
|
||
|
||
</div> <!-- End of main container -->
|
||
|
||
<!-- Create Stall Dialog -->
|
||
<Dialog v-model:open="showStallDialog" :key="dialogKey">
|
||
<DialogContent class="sm:max-w-2xl">
|
||
<DialogHeader>
|
||
<DialogTitle>Create New Store</DialogTitle>
|
||
</DialogHeader>
|
||
|
||
<form @submit="onSubmit" class="space-y-6 py-4" :key="formKey" autocomplete="off">
|
||
<!-- Basic Store Info -->
|
||
<div class="space-y-4">
|
||
<FormField v-slot="{ componentField }" name="name">
|
||
<FormItem>
|
||
<FormLabel>Store Name *</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
placeholder="Enter your store name"
|
||
:disabled="isCreatingStall"
|
||
v-bind="componentField"
|
||
:key="`store-name-${formKey}`"
|
||
autocomplete="off"
|
||
spellcheck="false"
|
||
/>
|
||
</FormControl>
|
||
<FormDescription>
|
||
Choose a unique name for your store
|
||
</FormDescription>
|
||
<FormMessage />
|
||
</FormItem>
|
||
</FormField>
|
||
|
||
<FormField v-slot="{ componentField }" name="description">
|
||
<FormItem>
|
||
<FormLabel>Description</FormLabel>
|
||
<FormControl>
|
||
<Textarea
|
||
placeholder="Describe your store and products"
|
||
:disabled="isCreatingStall"
|
||
v-bind="componentField"
|
||
:key="`store-description-${formKey}`"
|
||
autocomplete="off"
|
||
spellcheck="false"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
</FormField>
|
||
|
||
<FormField v-slot="{ componentField }" name="currency">
|
||
<FormItem>
|
||
<FormLabel>Currency *</FormLabel>
|
||
<Select :disabled="isCreatingStall" v-bind="componentField">
|
||
<FormControl>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="Select currency" />
|
||
</SelectTrigger>
|
||
</FormControl>
|
||
<SelectContent>
|
||
<SelectItem v-for="currency in availableCurrencies" :key="currency" :value="currency">
|
||
{{ currency }}
|
||
</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
<FormMessage />
|
||
</FormItem>
|
||
</FormField>
|
||
</div>
|
||
|
||
<!-- Shipping Zones Section -->
|
||
<FormField name="selectedZones">
|
||
<FormItem>
|
||
<div class="mb-4">
|
||
<FormLabel class="text-base">Shipping Zones *</FormLabel>
|
||
<FormDescription>
|
||
Select existing zones or create new ones for your store
|
||
</FormDescription>
|
||
</div>
|
||
|
||
<!-- Existing Zones -->
|
||
<div v-if="availableZones.length > 0" class="space-y-3">
|
||
<FormLabel class="text-sm font-medium">Available Zones:</FormLabel>
|
||
<div class="space-y-2 max-h-32 overflow-y-auto">
|
||
<FormField
|
||
v-for="zone in availableZones"
|
||
:key="zone.id"
|
||
v-slot="{ value, handleChange }"
|
||
type="checkbox"
|
||
:value="zone.id"
|
||
:unchecked-value="false"
|
||
name="selectedZones"
|
||
>
|
||
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
|
||
<FormControl>
|
||
<Checkbox
|
||
:model-value="value.includes(zone.id)"
|
||
@update:model-value="handleChange"
|
||
:disabled="isCreatingStall"
|
||
/>
|
||
</FormControl>
|
||
<FormLabel class="text-sm cursor-pointer font-normal">
|
||
{{ zone.name }} - {{ zone.cost }} {{ zone.currency }}
|
||
<span class="text-muted-foreground ml-1">({{ zone.countries.slice(0, 2).join(', ') }}{{ zone.countries.length > 2 ? '...' : '' }})</span>
|
||
</FormLabel>
|
||
</FormItem>
|
||
</FormField>
|
||
</div>
|
||
</div>
|
||
<FormMessage />
|
||
</FormItem>
|
||
</FormField>
|
||
|
||
<!-- Create New Zone -->
|
||
<div class="border-t pt-4">
|
||
<Label class="text-sm font-medium">Create New Zone:</Label>
|
||
<div class="space-y-3 mt-2">
|
||
<FormField v-slot="{ componentField }" name="newZone.name">
|
||
<FormItem>
|
||
<FormLabel>Zone Name</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
placeholder="e.g., Europe, Worldwide"
|
||
:disabled="isCreatingStall"
|
||
v-bind="componentField"
|
||
:key="`zone-name-${formKey}`"
|
||
autocomplete="off"
|
||
spellcheck="false"
|
||
/>
|
||
</FormControl>
|
||
</FormItem>
|
||
</FormField>
|
||
|
||
<FormField v-slot="{ componentField }" name="newZone.cost">
|
||
<FormItem>
|
||
<FormLabel>Shipping Cost</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
type="number"
|
||
min="0"
|
||
:placeholder="`Cost in ${getFieldValue('currency') || 'sat'}`"
|
||
:disabled="isCreatingStall"
|
||
:key="`zone-cost-${formKey}`"
|
||
v-bind="componentField"
|
||
/>
|
||
</FormControl>
|
||
</FormItem>
|
||
</FormField>
|
||
|
||
<FormField v-slot="{ componentField }" name="newZone.selectedCountries">
|
||
<FormItem>
|
||
<FormLabel>Countries/Regions</FormLabel>
|
||
<FormControl>
|
||
<Select multiple :disabled="isCreatingStall" v-bind="componentField">
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="Select countries/regions" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem v-for="country in countries" :key="country" :value="country">
|
||
{{ country }}
|
||
</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</FormControl>
|
||
<div v-if="getFieldValue('newZone')?.selectedCountries?.length > 0" class="mt-2">
|
||
<div class="flex flex-wrap gap-1">
|
||
<Badge
|
||
v-for="country in getFieldValue('newZone').selectedCountries"
|
||
:key="country"
|
||
variant="secondary"
|
||
class="text-xs"
|
||
>
|
||
{{ country }}
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
</FormItem>
|
||
</FormField>
|
||
|
||
<Button
|
||
@click="addNewZone"
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
:disabled="isCreatingStall || !getFieldValue('newZone')?.name || !getFieldValue('newZone')?.selectedCountries?.length"
|
||
>
|
||
<Plus class="w-4 h-4 mr-2" />
|
||
Add Zone
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Error Display -->
|
||
<div v-if="stallCreateError" class="text-sm text-destructive">
|
||
{{ stallCreateError }}
|
||
</div>
|
||
|
||
<div class="flex justify-end space-x-2 pt-4">
|
||
<Button
|
||
@click="closeStallDialog"
|
||
variant="outline"
|
||
:disabled="isCreatingStall"
|
||
>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
type="submit"
|
||
:disabled="isCreatingStall || !isFormValid"
|
||
>
|
||
<span v-if="isCreatingStall">Creating...</span>
|
||
<span v-else>Create Store</span>
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useForm } from 'vee-validate'
|
||
import { toTypedSchema } from '@vee-validate/zod'
|
||
import * as z from 'zod'
|
||
import { useMarketStore } from '@/modules/market/stores/market'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Badge } from '@/components/ui/badge'
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Label } from '@/components/ui/label'
|
||
import { Textarea } from '@/components/ui/textarea'
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||
import { Checkbox } from '@/components/ui/checkbox'
|
||
import {
|
||
FormControl,
|
||
FormDescription,
|
||
FormField,
|
||
FormItem,
|
||
FormLabel,
|
||
FormMessage,
|
||
} from '@/components/ui/form'
|
||
import {
|
||
Package,
|
||
Store,
|
||
DollarSign,
|
||
Star,
|
||
Plus,
|
||
Zap,
|
||
Eye,
|
||
Check,
|
||
AlertCircle,
|
||
CheckCircle,
|
||
Download,
|
||
Settings,
|
||
BarChart3,
|
||
User
|
||
} from 'lucide-vue-next'
|
||
import type { OrderStatus } from '@/modules/market/stores/market'
|
||
import type { NostrmarketService } from '../services/nostrmarketService'
|
||
import type { NostrmarketAPI, Merchant, Zone, Stall, CreateStallRequest } from '../services/nostrmarketAPI'
|
||
import { auth } from '@/composables/useAuthService'
|
||
import { useToast } from '@/core/composables/useToast'
|
||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||
|
||
const router = useRouter()
|
||
const marketStore = useMarketStore()
|
||
const nostrmarketService = injectService(SERVICE_TOKENS.NOSTRMARKET_SERVICE) as NostrmarketService
|
||
const nostrmarketAPI = injectService(SERVICE_TOKENS.NOSTRMARKET_API) as NostrmarketAPI
|
||
const toast = useToast()
|
||
|
||
// Local state
|
||
const isGeneratingInvoice = ref<string | null>(null)
|
||
const merchantProfile = ref<Merchant | null>(null)
|
||
const isLoadingMerchant = ref(false)
|
||
const merchantCheckError = ref<string | null>(null)
|
||
const isCreatingMerchant = ref(false)
|
||
const merchantCreateError = ref<string | null>(null)
|
||
|
||
// Multiple stalls state management
|
||
const userStalls = ref<Stall[]>([])
|
||
const activeStallId = ref<string | null>(null)
|
||
const isLoadingStalls = ref(false)
|
||
const activeStall = computed(() =>
|
||
userStalls.value.find(stall => stall.id === activeStallId.value)
|
||
)
|
||
|
||
// Stall creation state
|
||
const isCreatingStall = ref(false)
|
||
const stallCreateError = ref<string | null>(null)
|
||
const formKey = ref(0) // Force re-render of form
|
||
const showStallDialog = ref(false)
|
||
const dialogKey = ref(0) // Force complete dialog re-render
|
||
const availableCurrencies = ref<string[]>(['sat'])
|
||
const availableZones = ref<Zone[]>([])
|
||
const countries = ref([
|
||
'Free (digital)', 'Worldwide', 'Europe', 'Australia', 'Austria', 'Belgium', 'Brazil', 'Canada',
|
||
'Denmark', 'Finland', 'France', 'Germany', 'Greece', 'Hong Kong', 'Hungary',
|
||
'Ireland', 'Indonesia', 'Israel', 'Italy', 'Japan', 'Kazakhstan', 'Korea',
|
||
'Luxembourg', 'Malaysia', 'Mexico', 'Netherlands', 'New Zealand', 'Norway',
|
||
'Poland', 'Portugal', 'Romania', 'Russia', 'Saudi Arabia', 'Singapore',
|
||
'Spain', 'Sweden', 'Switzerland', 'Thailand', 'Turkey', 'Ukraine',
|
||
'United Kingdom', 'United States', 'Vietnam', 'China'
|
||
])
|
||
|
||
// Stall form schema
|
||
const stallFormSchema = toTypedSchema(z.object({
|
||
name: z.string().min(1, "Store name is required").max(100, "Store name must be less than 100 characters"),
|
||
description: z.string().max(500, "Description must be less than 500 characters").optional(),
|
||
currency: z.string().min(1, "Currency is required"),
|
||
wallet: z.string().optional(),
|
||
selectedZones: z.array(z.string()).min(1, "Select at least one shipping zone"),
|
||
newZone: z.object({
|
||
name: z.string().optional(),
|
||
cost: z.number().min(0, "Cost must be 0 or greater").optional(),
|
||
selectedCountries: z.array(z.string()).optional()
|
||
}).optional()
|
||
}))
|
||
|
||
// Form setup with vee-validate
|
||
const form = useForm({
|
||
validationSchema: stallFormSchema,
|
||
initialValues: {
|
||
name: '',
|
||
description: '',
|
||
currency: 'sat',
|
||
wallet: '',
|
||
selectedZones: [] as string[],
|
||
newZone: {
|
||
name: '',
|
||
cost: 0,
|
||
selectedCountries: [] as string[]
|
||
}
|
||
}
|
||
})
|
||
|
||
// Destructure form methods for easier access
|
||
const { setFieldValue, resetForm, values, meta, validate } = form
|
||
|
||
// Helper function to get field values safely
|
||
const getFieldValue = (fieldName: string) => {
|
||
return fieldName.split('.').reduce((obj, key) => obj?.[key], values)
|
||
}
|
||
|
||
// Form validation computed with detailed debugging
|
||
const isFormValid = computed(() => {
|
||
return meta.value.valid
|
||
})
|
||
|
||
// Form submit handler
|
||
const onSubmit = form.handleSubmit(async (values) => {
|
||
await createStall(values)
|
||
})
|
||
|
||
// Computed properties
|
||
const userHasMerchantProfile = computed(() => {
|
||
// Use the actual API response to determine if user has merchant profile
|
||
return merchantProfile.value !== null
|
||
})
|
||
|
||
const userHasStalls = computed(() => {
|
||
return userStalls.value.length > 0
|
||
})
|
||
|
||
|
||
const incomingOrders = computed(() => {
|
||
// Filter orders to only show those where the current user is the seller
|
||
const currentUserPubkey = auth.currentUser?.value?.pubkey
|
||
if (!currentUserPubkey) return []
|
||
|
||
return Object.values(marketStore.orders)
|
||
.filter(order => order.sellerPubkey === currentUserPubkey && order.status === 'pending')
|
||
.sort((a, b) => b.createdAt - a.createdAt)
|
||
})
|
||
|
||
const storeStats = computed(() => {
|
||
const currentUserPubkey = auth.currentUser?.value?.pubkey
|
||
if (!currentUserPubkey) {
|
||
return {
|
||
incomingOrders: 0,
|
||
pendingOrders: 0,
|
||
paidOrders: 0,
|
||
totalSales: 0,
|
||
totalProducts: 0,
|
||
activeProducts: 0,
|
||
satisfaction: 0,
|
||
totalReviews: 0
|
||
}
|
||
}
|
||
|
||
// Filter orders to only count those where current user is the seller
|
||
const myOrders = Object.values(marketStore.orders).filter(o => o.sellerPubkey === currentUserPubkey)
|
||
const now = Date.now() / 1000
|
||
const thirtyDaysAgo = now - (30 * 24 * 60 * 60)
|
||
|
||
return {
|
||
incomingOrders: myOrders.filter(o => o.status === 'pending').length,
|
||
pendingOrders: myOrders.filter(o => o.status === 'pending').length,
|
||
paidOrders: myOrders.filter(o => o.status === 'paid').length,
|
||
totalSales: myOrders
|
||
.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: userHasStalls.value ? 95 : 0, // 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 createMerchantProfile = async () => {
|
||
const currentUser = auth.currentUser?.value
|
||
if (!currentUser) {
|
||
console.error('No authenticated user for merchant creation')
|
||
return
|
||
}
|
||
|
||
const userWallets = currentUser.wallets || []
|
||
if (userWallets.length === 0) {
|
||
console.error('No wallets available for merchant creation')
|
||
toast.error('No wallet available. Please ensure you have at least one wallet configured.')
|
||
return
|
||
}
|
||
|
||
const wallet = userWallets[0] // Use first wallet
|
||
if (!wallet.adminkey) {
|
||
console.error('Wallet missing admin key for merchant creation')
|
||
toast.error('Wallet missing admin key. Admin key is required to create merchant profiles.')
|
||
return
|
||
}
|
||
|
||
isCreatingMerchant.value = true
|
||
merchantCreateError.value = null
|
||
|
||
try {
|
||
console.log('Creating merchant profile...', {
|
||
walletId: wallet.id,
|
||
adminKeyLength: wallet.adminkey.length,
|
||
adminKeyPreview: wallet.adminkey.substring(0, 8) + '...'
|
||
})
|
||
|
||
// Create merchant with empty config, exactly like the nostrmarket extension
|
||
const merchantData = {
|
||
config: {}
|
||
}
|
||
|
||
const newMerchant = await nostrmarketAPI.createMerchant(wallet.adminkey, merchantData)
|
||
|
||
console.log('Merchant profile created successfully:', {
|
||
merchantId: newMerchant.id,
|
||
publicKey: newMerchant.public_key
|
||
})
|
||
|
||
// Update local state
|
||
merchantProfile.value = newMerchant
|
||
|
||
// Show success message
|
||
toast.success('Merchant profile created successfully! You can now create your first store.')
|
||
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : 'Failed to create merchant profile'
|
||
console.error('Error creating merchant profile:', error)
|
||
merchantCreateError.value = errorMessage
|
||
|
||
// Show error to user
|
||
toast.error(`Failed to create merchant profile: ${errorMessage}`)
|
||
} finally {
|
||
isCreatingMerchant.value = false
|
||
}
|
||
}
|
||
|
||
const initializeStallCreation = async () => {
|
||
const currentUser = auth.currentUser?.value
|
||
if (!currentUser?.wallets?.length) {
|
||
toast.error('No wallets available')
|
||
return
|
||
}
|
||
|
||
// Reset form to initial state first
|
||
resetForm({
|
||
values: {
|
||
name: '',
|
||
description: '',
|
||
currency: 'sat',
|
||
wallet: currentUser.wallets[0].id,
|
||
selectedZones: [],
|
||
newZone: {
|
||
name: '',
|
||
cost: 0,
|
||
selectedCountries: []
|
||
}
|
||
},
|
||
errors: {},
|
||
touched: {},
|
||
initialValues: {
|
||
name: '',
|
||
description: '',
|
||
currency: 'sat',
|
||
wallet: currentUser.wallets[0].id,
|
||
selectedZones: [],
|
||
newZone: {
|
||
name: '',
|
||
cost: 0,
|
||
selectedCountries: []
|
||
}
|
||
}
|
||
})
|
||
|
||
// Wait for Vue's reactivity to update
|
||
await nextTick()
|
||
|
||
// Force complete dialog re-render
|
||
dialogKey.value++
|
||
formKey.value++
|
||
|
||
// Clear any previous errors
|
||
stallCreateError.value = null
|
||
|
||
// Load currencies and zones
|
||
await Promise.all([
|
||
loadAvailableCurrencies(),
|
||
loadAvailableZones()
|
||
])
|
||
|
||
// Trigger validation after data is loaded to ensure form state is correct
|
||
await validate()
|
||
|
||
showStallDialog.value = true
|
||
}
|
||
|
||
const loadAvailableCurrencies = async () => {
|
||
try {
|
||
const currencies = await nostrmarketAPI.getCurrencies()
|
||
availableCurrencies.value = currencies
|
||
} catch (error) {
|
||
console.error('Failed to load currencies:', error)
|
||
// Keep default 'sat'
|
||
}
|
||
}
|
||
|
||
const loadAvailableZones = async () => {
|
||
const currentUser = auth.currentUser?.value
|
||
if (!currentUser?.wallets?.length) return
|
||
|
||
try {
|
||
const zones = await nostrmarketAPI.getZones(currentUser.wallets[0].inkey)
|
||
availableZones.value = zones
|
||
|
||
// Auto-select the first available zone to make form valid
|
||
if (zones.length > 0) {
|
||
const currentSelectedZones = getFieldValue('selectedZones') || []
|
||
if (currentSelectedZones.length === 0) {
|
||
setFieldValue('selectedZones', [zones[0].id])
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load zones:', error)
|
||
}
|
||
}
|
||
|
||
const loadStallsList = async () => {
|
||
const currentUser = auth.currentUser?.value
|
||
|
||
if (!currentUser?.wallets?.length) {
|
||
return
|
||
}
|
||
|
||
isLoadingStalls.value = true
|
||
try {
|
||
const stalls = await nostrmarketAPI.getStalls(currentUser.wallets[0].inkey)
|
||
userStalls.value = stalls || []
|
||
|
||
// If there are stalls but no active one selected, select the first
|
||
if (stalls?.length > 0 && !activeStallId.value) {
|
||
activeStallId.value = stalls[0].id
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load stalls:', error)
|
||
userStalls.value = []
|
||
} finally {
|
||
isLoadingStalls.value = false
|
||
}
|
||
}
|
||
|
||
const addNewZone = async () => {
|
||
const currentUser = auth.currentUser?.value
|
||
if (!currentUser?.wallets?.length) {
|
||
toast.error('No wallets available')
|
||
return
|
||
}
|
||
|
||
const newZone = getFieldValue('newZone')
|
||
|
||
if (!newZone?.name || !newZone.selectedCountries?.length || (newZone.cost ?? -1) < 0) {
|
||
toast.error('Please fill in all zone details')
|
||
return
|
||
}
|
||
|
||
try {
|
||
const createdZone = await nostrmarketAPI.createZone(
|
||
currentUser.wallets[0].adminkey,
|
||
{
|
||
name: newZone.name,
|
||
currency: getFieldValue('currency'),
|
||
cost: newZone.cost,
|
||
countries: newZone.selectedCountries
|
||
}
|
||
)
|
||
|
||
// Add to available zones and select it
|
||
availableZones.value.push(createdZone)
|
||
const currentSelectedZones = getFieldValue('selectedZones') || []
|
||
setFieldValue('selectedZones', [...currentSelectedZones, createdZone.id])
|
||
|
||
// Reset the new zone form
|
||
setFieldValue('newZone', {
|
||
name: '',
|
||
cost: 0,
|
||
selectedCountries: []
|
||
})
|
||
|
||
toast.success(`Zone "${newZone.name}" created successfully`)
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : 'Failed to create zone'
|
||
toast.error(`Failed to create zone: ${errorMessage}`)
|
||
}
|
||
}
|
||
|
||
|
||
const createStall = async (formData: any) => {
|
||
const currentUser = auth.currentUser?.value
|
||
if (!currentUser?.wallets?.length) {
|
||
toast.error('No wallets available')
|
||
return
|
||
}
|
||
|
||
const { name, description, currency, selectedZones } = formData
|
||
|
||
isCreatingStall.value = true
|
||
stallCreateError.value = null
|
||
|
||
try {
|
||
// Get the selected zones data
|
||
const selectedZoneData = availableZones.value.filter(zone =>
|
||
selectedZones.includes(zone.id)
|
||
)
|
||
|
||
const stallData: CreateStallRequest = {
|
||
name,
|
||
wallet: currentUser.wallets[0].id,
|
||
currency,
|
||
shipping_zones: selectedZoneData,
|
||
config: {
|
||
description: description || undefined
|
||
}
|
||
}
|
||
|
||
const newStall = await nostrmarketAPI.createStall(
|
||
currentUser.wallets[0].adminkey,
|
||
stallData
|
||
)
|
||
|
||
// Update stalls list
|
||
await loadStallsList()
|
||
|
||
// Reset form and close dialog using standard Shadcn pattern
|
||
resetForm({
|
||
values: {
|
||
name: '',
|
||
description: '',
|
||
currency: 'sat',
|
||
wallet: currentUser.wallets[0].id,
|
||
selectedZones: [],
|
||
newZone: {
|
||
name: '',
|
||
cost: 0,
|
||
selectedCountries: []
|
||
}
|
||
},
|
||
errors: {},
|
||
touched: {},
|
||
initialValues: {
|
||
name: '',
|
||
description: '',
|
||
currency: 'sat',
|
||
wallet: currentUser.wallets[0].id,
|
||
selectedZones: [],
|
||
newZone: {
|
||
name: '',
|
||
cost: 0,
|
||
selectedCountries: []
|
||
}
|
||
}
|
||
})
|
||
|
||
// Force complete dialog re-render
|
||
dialogKey.value++
|
||
formKey.value++
|
||
|
||
showStallDialog.value = false
|
||
|
||
toast.success('Store created successfully! You can now add products.')
|
||
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : 'Failed to create store'
|
||
console.error('Error creating stall:', error)
|
||
stallCreateError.value = errorMessage
|
||
toast.error(`Failed to create store: ${errorMessage}`)
|
||
} finally {
|
||
isCreatingStall.value = false
|
||
}
|
||
}
|
||
|
||
// Missing functions for stall management
|
||
const manageStall = (stallId: string) => {
|
||
console.log('Managing stall:', stallId)
|
||
// Set as active stall for the dashboard view
|
||
activeStallId.value = stallId
|
||
}
|
||
|
||
const viewStallProducts = (stallId: string) => {
|
||
console.log('Viewing products for stall:', stallId)
|
||
// TODO: Navigate to products view for specific stall
|
||
// For now, set as active stall and scroll to products section
|
||
activeStallId.value = stallId
|
||
}
|
||
|
||
const closeStallDialog = () => {
|
||
// Reset form and clear errors when closing the dialog
|
||
stallCreateError.value = null
|
||
showStallDialog.value = false
|
||
}
|
||
|
||
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'
|
||
}
|
||
|
||
// Methods
|
||
const checkMerchantProfile = async () => {
|
||
const currentUser = auth.currentUser?.value
|
||
if (!currentUser) return
|
||
|
||
const userWallets = currentUser.wallets || []
|
||
if (userWallets.length === 0) {
|
||
console.warn('No wallets available for merchant check')
|
||
return
|
||
}
|
||
|
||
const wallet = userWallets[0] // Use first wallet
|
||
if (!wallet.inkey) {
|
||
console.warn('Wallet missing invoice key for merchant check')
|
||
return
|
||
}
|
||
|
||
isLoadingMerchant.value = true
|
||
merchantCheckError.value = null
|
||
|
||
try {
|
||
console.log('Checking for merchant profile...')
|
||
const merchant = await nostrmarketAPI.getMerchant(wallet.inkey)
|
||
merchantProfile.value = merchant
|
||
|
||
console.log('Merchant profile check result:', {
|
||
hasMerchant: !!merchant,
|
||
merchantId: merchant?.id,
|
||
active: merchant?.config?.active
|
||
})
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : 'Failed to check merchant profile'
|
||
console.error('Error checking merchant profile:', error)
|
||
merchantCheckError.value = errorMessage
|
||
merchantProfile.value = null
|
||
} finally {
|
||
isLoadingMerchant.value = false
|
||
}
|
||
}
|
||
|
||
// Lifecycle
|
||
onMounted(async () => {
|
||
console.log('Merchant Store component loaded')
|
||
await checkMerchantProfile()
|
||
|
||
// Load stalls if merchant profile exists
|
||
if (merchantProfile.value) {
|
||
console.log('Merchant profile exists, loading stalls...')
|
||
await loadStallsList()
|
||
} else {
|
||
console.log('No merchant profile found, skipping stalls loading')
|
||
}
|
||
})
|
||
|
||
// Watch for auth changes and re-check merchant profile
|
||
watch(() => auth.currentUser?.value?.pubkey, async (newPubkey, oldPubkey) => {
|
||
if (newPubkey !== oldPubkey) {
|
||
console.log('User changed, re-checking merchant profile')
|
||
await checkMerchantProfile()
|
||
}
|
||
})
|
||
</script>
|
||
|