From fb36caa0b25e48d2242d8f50387d9364af5d50cf Mon Sep 17 00:00:00 2001 From: padreug Date: Tue, 6 Jan 2026 19:04:24 +0100 Subject: [PATCH] Add shipping zones management to Store Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added full CRUD for shipping zones in MarketSettings.vue - Added updateZone and deleteZone API methods to nostrmarketAPI.ts - Zone form supports name, cost, and countries/regions - Edit and delete zones with confirmation dialog - Loading states and proper error handling with toast notifications 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../market/components/MarketSettings.vue | 311 +++++++++++++++++- src/modules/market/services/nostrmarketAPI.ts | 35 ++ 2 files changed, 331 insertions(+), 15 deletions(-) diff --git a/src/modules/market/components/MarketSettings.vue b/src/modules/market/components/MarketSettings.vue index 6c90733..f636dcc 100644 --- a/src/modules/market/components/MarketSettings.vue +++ b/src/modules/market/components/MarketSettings.vue @@ -77,19 +77,11 @@ - -
-
- -
{{ currentStall.currency }}
-

Currency is set when the store is created and cannot be changed

-
- -
- -
{{ currentStall.shipping_zones?.length || 0 }} zone(s) configured
-

Manage shipping zones when creating a new store

-
+ +
+ +
{{ currentStall.currency }}
+

Currency is set when the store is created and cannot be changed

@@ -100,7 +92,148 @@
+ + +
+
+
+

Shipping Zones

+

Configure where you ship and the associated costs

+
+ +
+ + +
+

{{ editingZone ? 'Edit Zone' : 'Add New Zone' }}

+
+
+
+ + +
+
+ + +
+
+ +
+ + +

Comma-separated list of countries or regions this zone covers

+
+ +
+ + +
+
+
+ + +
+
+
+ +
+ +

No shipping zones configured

+

Add a shipping zone to enable shipping for your products

+
+ +
+
+
+
{{ zone.name }}
+
+ {{ formatCost(zone.cost) }} {{ zone.currency }} + + · {{ zone.countries.join(', ') }} + +
+
+
+ + +
+
+
+
+ + + + + + Delete Shipping Zone + + Are you sure you want to delete "{{ zoneToDelete?.name }}"? + This action cannot be undone. + + + + + + + + @@ -120,9 +253,17 @@ import { FormLabel, FormMessage, } from '@/components/ui/form' -import { Store } from 'lucide-vue-next' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Store, Plus, Pencil, Trash2, Truck } from 'lucide-vue-next' import { injectService, SERVICE_TOKENS } from '@/core/di-container' -import type { NostrmarketAPI, Stall } from '../services/nostrmarketAPI' +import type { NostrmarketAPI, Stall, Zone } from '../services/nostrmarketAPI' import { auth } from '@/composables/useAuthService' import { useToast } from '@/core/composables/useToast' @@ -136,6 +277,26 @@ const isLoading = ref(true) const isSaving = ref(false) const currentStall = ref(null) +// Zone state +const zones = ref([]) +const isZoneLoading = ref(false) +const isZoneSaving = ref(false) +const showAddZoneForm = ref(false) +const editingZone = ref(null) +const showDeleteConfirm = ref(false) +const zoneToDelete = ref(null) + +// Zone form +const zoneForm = ref({ + name: '', + cost: 0, + countriesInput: '' +}) + +const isZoneFormValid = computed(() => { + return zoneForm.value.name.trim().length > 0 && zoneForm.value.cost >= 0 +}) + // Form schema - only fields that exist in the Stall model const formSchema = toTypedSchema(z.object({ name: z.string().min(1, "Store name is required").max(100, "Store name must be less than 100 characters"), @@ -156,6 +317,11 @@ const form = useForm({ const { resetForm, meta } = form const isFormValid = computed(() => meta.value.valid) +// Format cost for display +const formatCost = (cost: number) => { + return cost === 0 ? 'Free' : cost.toString() +} + // Load store data const loadStoreData = async () => { const currentUser = auth.currentUser?.value @@ -183,6 +349,9 @@ const loadStoreData = async () => { imageUrl: stalls[0].config?.image_url || '' } }) + + // Load zones + await loadZones() } } catch (error) { console.error('Failed to load store data:', error) @@ -192,6 +361,22 @@ const loadStoreData = async () => { } } +// Load shipping zones +const loadZones = async () => { + const inkey = paymentService.getPreferredWalletInvoiceKey() + if (!inkey) return + + isZoneLoading.value = true + try { + zones.value = await nostrmarketAPI.getZones(inkey) + } catch (error) { + console.error('Failed to load zones:', error) + toast.error('Failed to load shipping zones') + } finally { + isZoneLoading.value = false + } +} + // Save store settings const onSubmit = form.handleSubmit(async (values) => { if (!currentStall.value?.id) return @@ -223,6 +408,102 @@ const onSubmit = form.handleSubmit(async (values) => { } }) +// Zone form functions +const resetZoneForm = () => { + zoneForm.value = { + name: '', + cost: 0, + countriesInput: '' + } + editingZone.value = null + showAddZoneForm.value = false +} + +const cancelZoneForm = () => { + resetZoneForm() +} + +const editZone = (zone: Zone) => { + editingZone.value = zone + zoneForm.value = { + name: zone.name, + cost: zone.cost, + countriesInput: zone.countries?.join(', ') || '' + } + showAddZoneForm.value = true +} + +const saveZone = async () => { + const adminKey = paymentService.getPreferredWalletAdminKey() + if (!adminKey || !currentStall.value) return + + isZoneSaving.value = true + + try { + const countries = zoneForm.value.countriesInput + .split(',') + .map(c => c.trim()) + .filter(c => c.length > 0) + + const zoneData: Zone = { + id: editingZone.value?.id || '', + name: zoneForm.value.name.trim(), + currency: currentStall.value.currency, + cost: zoneForm.value.cost, + countries + } + + if (editingZone.value) { + // Update existing zone + await nostrmarketAPI.updateZone(adminKey, editingZone.value.id, zoneData) + toast.success('Shipping zone updated!') + } else { + // Create new zone + await nostrmarketAPI.createZone(adminKey, { + name: zoneData.name, + currency: zoneData.currency, + cost: zoneData.cost, + countries: zoneData.countries + }) + toast.success('Shipping zone added!') + } + + // Reload zones and reset form + await loadZones() + resetZoneForm() + } catch (error) { + console.error('Failed to save zone:', error) + toast.error(editingZone.value ? 'Failed to update zone' : 'Failed to add zone') + } finally { + isZoneSaving.value = false + } +} + +const confirmDeleteZone = (zone: Zone) => { + zoneToDelete.value = zone + showDeleteConfirm.value = true +} + +const deleteZone = async () => { + const adminKey = paymentService.getPreferredWalletAdminKey() + if (!adminKey || !zoneToDelete.value) return + + isZoneSaving.value = true + + try { + await nostrmarketAPI.deleteZone(adminKey, zoneToDelete.value.id) + toast.success('Shipping zone deleted!') + await loadZones() + } catch (error) { + console.error('Failed to delete zone:', error) + toast.error('Failed to delete zone') + } finally { + isZoneSaving.value = false + showDeleteConfirm.value = false + zoneToDelete.value = null + } +} + // Watch for auth changes watch(() => auth.currentUser?.value?.pubkey, async (newPubkey, oldPubkey) => { if (newPubkey !== oldPubkey) { diff --git a/src/modules/market/services/nostrmarketAPI.ts b/src/modules/market/services/nostrmarketAPI.ts index 589986b..528336f 100644 --- a/src/modules/market/services/nostrmarketAPI.ts +++ b/src/modules/market/services/nostrmarketAPI.ts @@ -398,6 +398,41 @@ export class NostrmarketAPI extends BaseService { return zone } + /** + * Update an existing shipping zone + */ + async updateZone( + walletAdminkey: string, + zoneId: string, + zoneData: Zone + ): Promise { + const zone = await this.request( + `/api/v1/zone/${zoneId}`, + walletAdminkey, + { + method: 'PATCH', + body: JSON.stringify(zoneData), + } + ) + + this.debug('Updated zone:', { zoneId: zone.id, zoneName: zone.name }) + + return zone + } + + /** + * Delete a shipping zone + */ + async deleteZone(walletAdminkey: string, zoneId: string): Promise { + await this.request( + `/api/v1/zone/${zoneId}`, + walletAdminkey, + { method: 'DELETE' } + ) + + this.debug('Deleted zone:', { zoneId }) + } + /** * Get available currencies from the LNbits core API * This endpoint returns currencies allowed by the server configuration