diff --git a/src/modules/events/components/CreateEventDialog.vue b/src/modules/events/components/CreateEventDialog.vue index ee8f487..ea10109 100644 --- a/src/modules/events/components/CreateEventDialog.vue +++ b/src/modules/events/components/CreateEventDialog.vue @@ -26,7 +26,7 @@ import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Switch } from '@/components/ui/switch' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' -import { Bell, ChevronDown } from 'lucide-vue-next' +import { Bell, ChevronDown, Tag } from 'lucide-vue-next' import { ScrollArea } from '@/components/ui/scroll-area' import { Select, @@ -55,6 +55,14 @@ import type { ImageUploadService, UploadedImage } from '@/modules/base/services/ import type { TicketApiService } from '../services/TicketApiService' import type { CreateEventRequest, TicketedEvent } from '../types/ticket' import { ALL_CATEGORIES } from '../types/category' +import PromoCodesEditor from './PromoCodesEditor.vue' +import { + fromWirePromoCodes, + hasPromoErrors, + toWirePromoCodes, + validatePromoRows, + type PromoCodeRow, +} from '../lib/promoCodes' interface Props { open: boolean @@ -252,6 +260,7 @@ function splitDateTime(value: string | null | undefined): { date: string; time: // edit-mode population with start-date side effects mid-setValues. const isPopulating = ref(false) const notificationsOpen = ref(false) +const promoOpen = ref(false) // Auto-mirror end date to start: when the user picks a start date, // surface that same date in the end-date picker so a one-day event @@ -277,6 +286,10 @@ const imageService = injectService(SERVICE_TOKENS.IMAGE_UPLO const availableCurrencies = ref(['sat']) const loadingCurrencies = ref(false) const selectedCategories = ref([]) +// Promo codes live outside vee-validate like categories: a plain list +// with its own pure validator (`validatePromoRows`), gating submit below. +const promoRows = ref([]) +const promoErrors = computed(() => validatePromoRows(promoRows.value)) async function populateFromEvent(event: TicketedEvent) { isPopulating.value = true @@ -301,6 +314,10 @@ async function populateFromEvent(event: TicketedEvent) { notification_body: event.extra?.notification_body ?? '', }) selectedCategories.value = [...(event.categories ?? [])] + // Codes come only on the organizer-authenticated record (public + // responses omit them) — useMyEvents / EventDetailPage hand us that one. + promoRows.value = fromWirePromoCodes(event.extra?.promo_codes) + promoOpen.value = promoRows.value.length > 0 if (event.banner) { // Re-render the stored banner via its pict-rs file ID. delete_token // is intentionally empty: we don't own the original upload's token @@ -339,11 +356,13 @@ watch(() => props.open, async (isOpen) => { } } else { selectedCategories.value = [] + promoRows.value = [] + promoOpen.value = false } }) const { resetForm, meta } = form -const isFormValid = computed(() => meta.value.valid) +const isFormValid = computed(() => meta.value.valid && !hasPromoErrors(promoErrors.value)) const isLoading = ref(false) const today = computed(() => format(new Date(), 'yyyy-MM-dd')) @@ -414,10 +433,11 @@ const onSubmit = form.handleSubmit(async (formValues) => { if (formValues.price_per_ticket) eventData.price_per_ticket = formValues.price_per_ticket if (selectedCategories.value.length > 0) eventData.categories = selectedCategories.value - // Notification config goes inside the `extra` envelope. On edit - // overlay onto the existing event.extra so unrelated fields the - // LNbits admin UI sets (promo_codes, conditional, min_tickets) - // survive the round-trip. + // Notification config + promo codes go inside the `extra` envelope. + // On edit overlay onto the existing event.extra so fields only the + // LNbits admin UI sets (conditional, min_tickets) survive the + // round-trip. promo_codes is always sent explicitly: what the + // organizer sees in the editor is the truth, an empty list clears. eventData.extra = { ...(props.event?.extra ?? {}), payment_methods: formValues.payment_methods, @@ -425,6 +445,7 @@ const onSubmit = form.handleSubmit(async (formValues) => { nostr_notifications: formValues.nostr_notifications, notification_subject: formValues.notification_subject, notification_body: formValues.notification_body, + promo_codes: toWirePromoCodes(promoRows.value), } if (isEditMode.value) { @@ -455,6 +476,7 @@ const onSubmit = form.handleSubmit(async (formValues) => { resetForm() selectedCategories.value = [] + promoRows.value = [] bannerImages.value = [] emit('update:open', false) } catch (error) { @@ -474,6 +496,7 @@ const handleOpenChange = (open: boolean) => { if (!open && !isLoading.value) { resetForm() selectedCategories.value = [] + promoRows.value = [] bannerImages.value = [] } emit('update:open', open) @@ -854,6 +877,27 @@ const handleOpenChange = (open: boolean) => { + + + + + + + + + +