webapp/src/modules/market/components/MerchantStore.vue
padreug e6107839a0 Implement store creation dialog in MerchantStore component
- Introduce a dialog for creating new stores, allowing users to input store name, description, currency, and shipping zones.
- Add functionality to manage shipping zones, including the ability to create new zones and select existing ones.
- Enhance the stall creation process with error handling and loading states, providing better user feedback during store setup.
- Update the NostrmarketAPI to support fetching available currencies and shipping zones, improving integration with the backend services.

These changes streamline the store creation experience for merchants, ensuring a more intuitive and guided process.

Refactor getCurrencies method in NostrmarketAPI to improve currency retrieval logic

- Introduce base currencies and enhance the logic to combine them with API currencies, ensuring no duplicates.
- Update debug logging to provide clearer information on currency retrieval outcomes.
- Simplify fallback mechanism to use base currencies directly in case of API failures.

These changes enhance the reliability and clarity of currency data handling in the NostrmarketAPI.

Refactor MerchantStore component to use NATIVE checkbox selection and add debug information

- Replace Checkbox component with native input checkboxes for zone selection, simplifying the binding with v-model.
- Enhance the user interface by adding debug information displaying the store name, selected zones count, and creation status.
- These changes improve the clarity of the zone selection process and provide useful debugging insights during store creation.

Enhance zone selection functionality in MerchantStore component

- Replace v-model with native checkbox handling for zone selection, improving clarity and user interaction.
- Add debug information to display currently selected zones, aiding in user understanding of their selections.
- Implement a new method to manage zone toggling, ensuring accurate updates to the selected zones array.

These changes streamline the zone selection process and provide better feedback for users during stall creation.

Improve zone selection handling and debugging in MerchantStore component

- Update zone selection to use a custom Checkbox component, enhancing user interaction and clarity.
- Add detailed debug information for selected zones, including raw array output and type, to aid in troubleshooting.
- Refactor the zone toggle logic to handle various input types, ensuring accurate updates to the selected zones array.

These changes enhance the user experience during stall creation by providing better feedback and more robust zone selection functionality.

Refactor Checkbox handling in MerchantStore component for improved zone selection

- Update zone selection to utilize the Shadcn/UI Checkbox component with v-model for better state management.
- Remove manual zone toggle logic and debug information, streamlining the component's functionality.
- Enhance user interaction by following recommended patterns for checkbox usage, ensuring reliable selections.

These changes improve the clarity and reliability of zone selection during stall creation.

Refactor MerchantStore component to utilize Shadcn Form components and improve form handling

- Replace existing form elements with Shadcn Form components (FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage) for better structure and validation.
- Integrate vee-validate and zod for type-safe form validation, enhancing user experience and error handling.
- Update shipping zone selection to use the new form structure, improving clarity and accessibility.
- Implement form submission logic with validation checks, ensuring required fields are filled before submission.

These changes enhance the overall form handling and user interaction during the store creation process.
2025-09-08 16:58:10 +02:00

1232 lines
42 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
<!-- No Stalls Empty State (has merchant profile but no stalls) -->
<div v-else-if="!userHasStalls" 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">
<Store class="w-12 h-12 text-muted-foreground" />
</div>
<h2 class="text-2xl font-bold text-foreground mb-2">Create Your First Store</h2>
<p class="text-muted-foreground text-center mb-6 max-w-md">
Great! You have a merchant profile. Now create your first store (stall) to start listing products and receiving orders.
</p>
<Button @click="initializeStallCreation" variant="default" size="lg">
<Plus class="w-5 h-5 mr-2" />
Create Store
</Button>
</div>
<!-- Store Content (shown when user has a store) -->
<div v-else>
<!-- 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> <!-- End of Store Content wrapper -->
</div>
<!-- Create Stall Dialog -->
<Dialog v-model:open="showStallDialog">
<DialogContent class="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Create New Store</DialogTitle>
</DialogHeader>
<form @submit="onSubmit" class="space-y-6 py-4">
<!-- 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"
/>
</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"
/>
</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"
/>
</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"
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="showStallDialog = false"
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 } 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, 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)
// Stall creation state
const isCreatingStall = ref(false)
const stallCreateError = ref<string | null>(null)
const showStallDialog = ref(false)
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 } = form
// Helper function to get field values safely
const getFieldValue = (fieldName: string) => {
return fieldName.split('.').reduce((obj, key) => obj?.[key], values)
}
// Form validation computed
const isFormValid = computed(() => meta.value.valid)
// Form submit handler
const onSubmit = form.handleSubmit(async (values) => {
console.log('Form submitted with values:', 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(() => {
// Check if user has any stalls in the market store
const currentUserPubkey = auth.currentUser?.value?.pubkey
if (!currentUserPubkey) return false
// Check if any stalls belong to the current user
const userStalls = marketStore.stalls.filter(stall =>
stall.pubkey === currentUserPubkey
)
return userStalls.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
}
// Set default wallet
setFieldValue('wallet', currentUser.wallets[0].id)
// Load currencies and zones
await Promise.all([
loadAvailableCurrencies(),
loadAvailableZones()
])
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
} catch (error) {
console.error('Failed to load zones:', error)
}
}
const loadStallsList = async () => {
const currentUser = auth.currentUser?.value
if (!currentUser?.wallets?.length) return
try {
const stalls = await nostrmarketAPI.getStalls(currentUser.wallets[0].inkey)
// Update the merchant stalls list - this could be connected to a store if needed
console.log('Updated stalls list:', stalls.length)
} catch (error) {
console.error('Failed to load stalls:', error)
}
}
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
}
}
console.log('Creating stall:', {
name,
currency,
zonesCount: selectedZoneData.length
})
const newStall = await nostrmarketAPI.createStall(
currentUser.wallets[0].adminkey,
stallData
)
console.log('Stall created successfully:', {
stallId: newStall.id,
stallName: newStall.name
})
// Update stalls list
await loadStallsList()
// Reset form and close dialog
resetForm({
values: {
name: '',
description: '',
currency: 'sat',
wallet: currentUser.wallets[0].id,
selectedZones: [],
newZone: {
name: '',
cost: 0,
selectedCountries: []
}
}
})
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
}
}
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()
})
// 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>