From 5a469b74defc6a18a6fcf23b01895db208b50190 Mon Sep 17 00:00:00 2001 From: Arc Date: Wed, 13 May 2026 13:29:09 +0100 Subject: [PATCH 1/8] email input fixes --- static/js/index.vue | 66 ++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/static/js/index.vue b/static/js/index.vue index fa39a4e..790307d 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -517,36 +517,46 @@
Send the paid ticket link automatically by email or Nostr DM.
- - +
+
+ +
+
+ +
+
+
+ + +
- - - -
Date: Wed, 13 May 2026 13:40:17 +0100 Subject: [PATCH 2/8] promo code ui fix --- static/js/index.js | 107 ++++++++++++++++++++-- static/js/index.vue | 212 +++++++++++++++++++++++--------------------- 2 files changed, 212 insertions(+), 107 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 5424a32..ec71eba 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -8,7 +8,12 @@ window.PageEvents = { currencies: [], eventsTable: { columns: [ - {name: 'id', align: 'left', label: 'ID', field: 'id'}, + { + name: 'id', + align: 'left', + label: 'ID', + field: row => this.shortenId(row.id) + }, {name: 'name', align: 'left', label: 'Name', field: 'name'}, { name: 'event_start_date', @@ -74,7 +79,12 @@ window.PageEvents = { }, ticketsTable: { columns: [ - {name: 'event', align: 'left', label: 'Event', field: 'event'}, + { + name: 'event', + align: 'left', + label: 'Event', + field: row => this.shortenId(row.event) + }, {name: 'name', align: 'left', label: 'Name', field: 'name'}, {name: 'email', align: 'left', label: 'Email', field: 'email'}, { @@ -107,13 +117,36 @@ window.PageEvents = { notification_body: '' } } + }, + promoCodesDialog: { + show: false, + data: { + id: null, + wallet: null, + name: '', + extra: { + promo_codes: [] + } + } } } }, methods: { + shortenId(value) { + if (!value) return '' + return value.length > 4 ? `${value.slice(0, 4)}...` : value + }, isFiatCurrency(currency) { return !['sat', 'sats'].includes((currency || '').toLowerCase()) }, + normalizePromoCodes(promoCodes = []) { + return promoCodes + .filter(code => code.code?.trim() !== '') + .map(code => ({ + ...code, + code: code.code.trim().toUpperCase() + })) + }, getTickets() { LNbits.api .request( @@ -196,12 +229,7 @@ window.PageEvents = { }) const data = this.formDialog.data if (data.extra?.promo_codes) { - data.extra.promo_codes = data.extra.promo_codes - .filter(code => code.code?.trim() !== '') - .map(code => ({ - ...code, - code: code.code.trim().toUpperCase() - })) + data.extra.promo_codes = this.normalizePromoCodes(data.extra.promo_codes) } if (!this.isFiatCurrency(data.currency)) { if (!data.allow_fiat) { @@ -266,6 +294,69 @@ window.PageEvents = { const link = _.findWhere(this.events, {id: formId}) this.openEventDialog(link) }, + openPromoCodesDialog(event) { + this.promoCodesDialog.data = { + ...event, + extra: { + ...event.extra, + promo_codes: [...(event.extra?.promo_codes || [])] + } + } + this.promoCodesDialog.show = true + }, + resetPromoCodesDialog() { + this.promoCodesDialog.show = false + this.promoCodesDialog.data = { + id: null, + wallet: null, + name: '', + extra: { + promo_codes: [] + } + } + }, + addPromoCodeToDialog() { + this.promoCodesDialog.data.extra.promo_codes.push({ + code: '', + discount_percent: 0, + active: true + }) + }, + savePromoCodes() { + const data = this.promoCodesDialog.data + const wallet = _.findWhere(this.g.user.wallets, { + id: data.wallet + }) + if (!wallet) return + + const payload = { + ...data, + extra: { + ...data.extra, + promo_codes: this.normalizePromoCodes(data.extra?.promo_codes || []) + } + } + + LNbits.api + .request( + 'PUT', + '/events/api/v1/events/' + data.id, + wallet.adminkey, + payload + ) + .then(response => { + this.events = this.events.map(event => + event.id === data.id ? response.data : event + ) + Quasar.Notify.create({ + type: 'positive', + message: 'Promo codes updated.', + icon: null + }) + this.resetPromoCodesDialog() + }) + .catch(LNbits.utils.notifyApiError) + }, updateEvent(wallet, data) { LNbits.api .request( diff --git a/static/js/index.vue b/static/js/index.vue index 790307d..2a1bf8c 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -100,45 +100,48 @@
-
Promo codes
+
+
Promo codes
+ +
- No promo codes for this event. + No active promo codes for this event.
-
-
+
+
- - +
-
- Discount: - % -
-
- Status: - -
@@ -441,78 +444,6 @@
-
Promo Codes
-
- Allow users to enter a promo code for discounts. -
- -
- - - - - - -
-
- Add Promo Code -
-
Ticket Delivery
Send the paid ticket link automatically by email or Nostr DM. @@ -589,5 +520,88 @@ + + + + +
Promo Codes
+
+ Allow users to enter a promo code for discounts. +
+ +
+ + + + + + +
+ +
+ Add Promo Code +
+ +
+ Save Promo Codes + Cancel +
+
+
+
From 1089e699cac0a66d457a68df084f99f45721f69b Mon Sep 17 00:00:00 2001 From: Arc Date: Wed, 13 May 2026 15:25:48 +0100 Subject: [PATCH 3/8] ticket waves --- crud.py | 10 +- models.py | 76 ++++++++++++- services.py | 15 ++- static/js/display.js | 47 +++++++- static/js/display.vue | 16 +++ static/js/index.js | 243 +++++++++++++++++++++++++++++++++++------- static/js/index.vue | 212 ++++++++++++++++++++++++++++++++++-- views_api.py | 82 ++++++++++---- 8 files changed, 625 insertions(+), 76 deletions(-) diff --git a/crud.py b/crud.py index 3046f0e..3f6d93d 100644 --- a/crud.py +++ b/crud.py @@ -3,7 +3,7 @@ from lnbits.db import Database from lnbits.helpers import urlsafe_short_hash -from .models import CreateEvent, Event, Ticket, TicketExtra +from .models import CreateEvent, Event, Ticket, TicketExtra, sync_event_ticket_waves db = Database("ext_events") @@ -75,31 +75,35 @@ async def purge_unpaid_tickets(event_id: str) -> None: async def create_event(data: CreateEvent) -> Event: event_id = urlsafe_short_hash() event = Event(id=event_id, time=datetime.now(timezone.utc), **data.dict()) + event = sync_event_ticket_waves(event) await db.insert("events.events", event) return event async def update_event(event: Event) -> Event: + event = sync_event_ticket_waves(event) await db.update("events.events", event) return event async def get_event(event_id: str) -> Event | None: - return await db.fetchone( + event = await db.fetchone( "SELECT * FROM events.events WHERE id = :id", {"id": event_id}, Event, ) + return sync_event_ticket_waves(event) if event else None async def get_events(wallet_ids: str | list[str]) -> list[Event]: if isinstance(wallet_ids, str): wallet_ids = [wallet_ids] q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids]) - return await db.fetchall( + events = await db.fetchall( f"SELECT * FROM events.events WHERE wallet IN ({q})", model=Event, ) + return [sync_event_ticket_waves(event) for event in events] async def delete_event(event_id: str) -> None: diff --git a/models.py b/models.py index fa1569f..21d174d 100644 --- a/models.py +++ b/models.py @@ -1,4 +1,5 @@ -from datetime import datetime +from datetime import date, datetime +from uuid import uuid4 from fastapi import Query from pydantic import BaseModel, EmailStr, Field, validator @@ -20,8 +21,21 @@ def validate_discount_percent(cls, v): return v +class TicketWave(BaseModel): + id: str = Field(default_factory=lambda: uuid4().hex[:8]) + title: str = "Primary wave" + opening_date: str + closing_date: str + currency: str = "sat" + allow_fiat: bool = False + fiat_currency: str = "GBP" + amount_tickets: int = Field(default=0, ge=0) + price_per_ticket: float = Field(default=0, ge=0) + + class EventExtra(BaseModel): promo_codes: list[PromoCode] = Field(default_factory=list) + ticket_waves: list[TicketWave] = Field(default_factory=list) conditional: bool = False min_tickets: int = 1 email_notifications: bool = False @@ -84,6 +98,8 @@ class PublicEvent(BaseModel): class TicketExtra(BaseModel): applied_promo_code: str | None = None + ticket_wave_id: str | None = None + ticket_wave_title: str | None = None sats_paid: int | None = None refund_address: str | None = None nostr_identifier: str | None = None @@ -96,6 +112,7 @@ class TicketExtra(BaseModel): class CreateTicket(BaseModel): name: str email: EmailStr + ticket_wave_id: str | None = None promo_code: str | None = None refund_address: str | None = None nostr_identifier: str | None = None @@ -131,3 +148,60 @@ class TicketPaymentRequest(BaseModel): fiat_payment_request: str | None = None fiat_provider: str | None = None is_fiat: bool = False + + +def _parse_date(value: str) -> date: + return datetime.strptime(value, "%Y-%m-%d").date() + + +def ensure_ticket_waves(event: Event | PublicEvent | CreateEvent) -> list[TicketWave]: + ticket_waves = list(getattr(event.extra, "ticket_waves", []) or []) + if ticket_waves: + return ticket_waves + + fallback_opening_date = None + if hasattr(event, "time") and getattr(event, "time", None): + fallback_opening_date = event.time.date().isoformat() + if not fallback_opening_date: + fallback_opening_date = getattr(event, "closing_date") + + return [ + TicketWave( + id="primary", + title="Primary wave", + opening_date=fallback_opening_date, + closing_date=getattr(event, "closing_date"), + currency=getattr(event, "currency"), + allow_fiat=getattr(event, "allow_fiat"), + fiat_currency=getattr(event, "fiat_currency"), + amount_tickets=getattr(event, "amount_tickets"), + price_per_ticket=getattr(event, "price_per_ticket"), + ) + ] + + +def sync_event_ticket_waves(event: Event | CreateEvent) -> Event | CreateEvent: + ticket_waves = ensure_ticket_waves(event) + event.extra.ticket_waves = ticket_waves + + primary_wave = ticket_waves[0] + event.closing_date = max(wave.closing_date for wave in ticket_waves) + event.currency = primary_wave.currency + event.allow_fiat = primary_wave.allow_fiat + event.fiat_currency = primary_wave.fiat_currency + event.amount_tickets = sum(wave.amount_tickets for wave in ticket_waves) + event.price_per_ticket = primary_wave.price_per_ticket + + return event + + +def get_active_ticket_waves( + event: Event | PublicEvent, today: date | None = None +) -> list[TicketWave]: + current_day = today or datetime.utcnow().date() + return [ + wave + for wave in ensure_ticket_waves(event) + if _parse_date(wave.opening_date) <= current_day <= _parse_date(wave.closing_date) + and wave.amount_tickets > 0 + ] diff --git a/services.py b/services.py index 159bbdc..f8e6fa0 100644 --- a/services.py +++ b/services.py @@ -39,7 +39,20 @@ async def set_ticket_paid(ticket: Ticket) -> Ticket: event = await get_event(ticket.event) assert event, "Couldn't get event from ticket being paid" event.sold += 1 - event.amount_tickets -= 1 + ticket_waves = event.extra.ticket_waves or [] + if ticket_waves: + selected_wave = next( + ( + wave + for wave in ticket_waves + if wave.id == ticket.extra.ticket_wave_id + ), + ticket_waves[0], + ) + if selected_wave.amount_tickets > 0: + selected_wave.amount_tickets -= 1 + elif event.amount_tickets > 0: + event.amount_tickets -= 1 await update_event(event) return ticket diff --git a/static/js/display.js b/static/js/display.js index d8be8e9..c37cdbe 100644 --- a/static/js/display.js +++ b/static/js/display.js @@ -13,6 +13,7 @@ window.PageEventsDisplay = { email: '', refund: '', nostr_identifier: '', + ticket_wave_id: null, payment_method: 'lightning' } }, @@ -40,16 +41,35 @@ window.PageEventsDisplay = { formatDescription() { return LNbits.utils.convertMarkdown(this.event?.info || '') }, + activeTicketWaves() { + const today = new Date().toISOString().slice(0, 10) + return (this.event?.extra?.ticket_waves || []).filter( + wave => + wave.amount_tickets > 0 && + wave.opening_date <= today && + wave.closing_date >= today + ) + }, + selectedTicketWave() { + return ( + this.activeTicketWaves.find( + wave => wave.id === this.formDialog.data.ticket_wave_id + ) || this.activeTicketWaves[0] || null + ) + }, + showTicketWaveSelector() { + return this.activeTicketWaves.length > 1 + }, allowFiatCheckout() { - return Boolean(this.event?.allow_fiat) + return Boolean(this.selectedTicketWave?.allow_fiat) }, fiatCheckoutLabel() { if (!this.allowFiatCheckout) return 'Fiat' const unit = ['sat', 'sats'].includes( - (this.event?.currency || '').toLowerCase() + (this.selectedTicketWave?.currency || '').toLowerCase() ) - ? this.event?.fiat_currency - : this.event?.currency + ? this.selectedTicketWave?.fiat_currency + : this.selectedTicketWave?.currency return `Fiat (${(unit || 'GBP').toUpperCase()})` }, allowEmailNotifications() { @@ -66,6 +86,16 @@ window.PageEventsDisplay = { 'GET', `/events/api/v1/events/${this.eventId}` ) + const activeWaves = (data.extra?.ticket_waves || []).filter(wave => { + const today = new Date().toISOString().slice(0, 10) + return ( + wave.amount_tickets > 0 && + wave.opening_date <= today && + wave.closing_date >= today + ) + }) + this.formDialog.data.ticket_wave_id = + activeWaves.length === 1 ? activeWaves[0].id : null return data } catch (error) { this.eventErrorLabel = 'Event unavailable.' @@ -78,6 +108,8 @@ window.PageEventsDisplay = { this.formDialog.data.email = '' this.formDialog.data.refund = '' this.formDialog.data.nostr_identifier = '' + this.formDialog.data.ticket_wave_id = + this.activeTicketWaves.length === 1 ? this.activeTicketWaves[0].id : null this.formDialog.data.payment_method = 'lightning' }, @@ -112,6 +144,8 @@ window.PageEventsDisplay = { this.formDialog.data.email = '' this.formDialog.data.refund = '' this.formDialog.data.nostr_identifier = '' + this.formDialog.data.ticket_wave_id = + this.activeTicketWaves.length === 1 ? this.activeTicketWaves[0].id : null this.formDialog.data.payment_method = 'lightning' Quasar.Notify.create({ type: 'positive', @@ -141,10 +175,13 @@ window.PageEventsDisplay = { { name: this.formDialog.data.name, email: this.formDialog.data.email, + ticket_wave_id: this.formDialog.data.ticket_wave_id || null, promo_code: this.formDialog.data.promo_code || null, refund_address: this.formDialog.data.refund || null, nostr_identifier: this.formDialog.data.nostr_identifier || null, - payment_method: this.formDialog.data.payment_method + payment_method: this.allowFiatCheckout + ? this.formDialog.data.payment_method + : 'lightning' } ) const isFiat = Boolean(data.is_fiat) diff --git a/static/js/display.vue b/static/js/display.vue index f5ac5fb..dc7c921 100644 --- a/static/js/display.vue +++ b/static/js/display.vue @@ -64,6 +64,21 @@ lazy-rules :hint="`If minimum tickets (${event.extra?.min_tickets}) are not met, refund will be sent.`" > +
{ - if (this.isFiatCurrency(row.currency)) { - return LNbits.utils.formatCurrency( - row.price_per_ticket.toFixed(2), - row.currency - ) - } - return row.price_per_ticket - } - }, - { - name: 'amount_tickets', - align: 'left', - label: 'No tickets', - field: 'amount_tickets' - }, - { - name: 'sold', - align: 'left', - label: 'Sold', - field: 'sold' - }, - {name: 'info', align: 'left', label: 'Info', field: 'info'}, - {name: 'banner', align: 'left', label: 'Banner', field: 'banner'} + } ], pagination: { rowsPerPage: 10 @@ -112,12 +78,30 @@ window.PageEvents = { allow_fiat: false, fiat_currency: 'GBP', extra: { + ticket_waves: [], promo_codes: [], notification_subject: '', notification_body: '' } } }, + ticketWaveDialog: { + show: false, + eventId: null, + wallet: null, + editingWaveId: null, + data: { + id: null, + title: '', + opening_date: '', + closing_date: '', + currency: 'sats', + allow_fiat: false, + fiat_currency: 'GBP', + amount_tickets: 0, + price_per_ticket: 0 + } + }, promoCodesDialog: { show: false, data: { @@ -136,6 +120,54 @@ window.PageEvents = { if (!value) return '' return value.length > 4 ? `${value.slice(0, 4)}...` : value }, + primaryTicketWave(data = this.formDialog.data) { + if (!data.extra) data.extra = {} + if (!data.extra.ticket_waves || data.extra.ticket_waves.length === 0) { + data.extra.ticket_waves = [ + { + id: 'primary', + title: 'Primary wave', + opening_date: data.closing_date || '', + closing_date: data.closing_date || '', + currency: data.currency || 'sats', + allow_fiat: Boolean(data.allow_fiat), + fiat_currency: data.fiat_currency || 'GBP', + amount_tickets: data.amount_tickets || 0, + price_per_ticket: data.price_per_ticket || 0 + } + ] + } + return data.extra.ticket_waves[0] + }, + syncPrimaryWaveFromForm(data = this.formDialog.data) { + const primaryWave = this.primaryTicketWave(data) + primaryWave.title = primaryWave.title || 'Primary wave' + primaryWave.opening_date = primaryWave.opening_date || '' + primaryWave.closing_date = data.closing_date || '' + primaryWave.currency = data.currency || 'sats' + primaryWave.allow_fiat = Boolean(data.allow_fiat) + primaryWave.fiat_currency = data.fiat_currency || 'GBP' + primaryWave.amount_tickets = Number(data.amount_tickets || 0) + primaryWave.price_per_ticket = Number(data.price_per_ticket || 0) + return primaryWave + }, + hydrateEventForm(data) { + const formData = { + ...data, + extra: { + ...(data.extra || {}), + ticket_waves: [...((data.extra && data.extra.ticket_waves) || [])] + } + } + const primaryWave = this.primaryTicketWave(formData) + formData.currency = primaryWave.currency || formData.currency || 'sats' + formData.allow_fiat = Boolean(primaryWave.allow_fiat) + formData.fiat_currency = primaryWave.fiat_currency || 'GBP' + formData.amount_tickets = primaryWave.amount_tickets + formData.price_per_ticket = primaryWave.price_per_ticket + formData.closing_date = primaryWave.closing_date || formData.closing_date || '' + return formData + }, isFiatCurrency(currency) { return !['sat', 'sats'].includes((currency || '').toLowerCase()) }, @@ -147,6 +179,14 @@ window.PageEvents = { code: code.code.trim().toUpperCase() })) }, + soldTicketsForWave(eventId, waveId) { + return this.tickets.filter( + ticket => + ticket.event === eventId && + ticket.paid && + ticket.extra?.ticket_wave_id === waveId + ).length + }, getTickets() { LNbits.api .request( @@ -228,6 +268,7 @@ window.PageEvents = { id: this.formDialog.data.wallet }) const data = this.formDialog.data + this.syncPrimaryWaveFromForm(data) if (data.extra?.promo_codes) { data.extra.promo_codes = this.normalizePromoCodes(data.extra.promo_codes) } @@ -236,6 +277,7 @@ window.PageEvents = { data.fiat_currency = 'GBP' } } + this.syncPrimaryWaveFromForm(data) if (data.id) { this.updateEvent(wallet, data) @@ -246,7 +288,7 @@ window.PageEvents = { openEventDialog(data = false) { if (data && data.id) { - this.formDialog.data = {...data} + this.formDialog.data = this.hydrateEventForm(data) } else { this.formDialog.data = { currency: 'sats', @@ -257,6 +299,19 @@ window.PageEvents = { min_tickets: 1, email_notifications: false, nostr_notifications: false, + ticket_waves: [ + { + id: 'primary', + title: 'Primary wave', + opening_date: '', + closing_date: '', + currency: 'sats', + allow_fiat: false, + fiat_currency: 'GBP', + amount_tickets: 0, + price_per_ticket: 0 + } + ], promo_codes: [], notification_subject: '', notification_body: '' @@ -272,8 +327,23 @@ window.PageEvents = { allow_fiat: false, fiat_currency: 'GBP', extra: { + conditional: false, + min_tickets: 1, email_notifications: false, nostr_notifications: false, + ticket_waves: [ + { + id: 'primary', + title: 'Primary wave', + opening_date: '', + closing_date: '', + currency: 'sats', + allow_fiat: false, + fiat_currency: 'GBP', + amount_tickets: 0, + price_per_ticket: 0 + } + ], promo_codes: [], notification_subject: '', notification_body: '' @@ -294,6 +364,107 @@ window.PageEvents = { const link = _.findWhere(this.events, {id: formId}) this.openEventDialog(link) }, + openTicketWaveDialog(event, wave = null) { + const primaryWave = (event.extra?.ticket_waves || [])[0] || {} + const isEditing = Boolean(wave) + this.ticketWaveDialog = { + show: true, + eventId: event.id, + wallet: event.wallet, + editingWaveId: wave?.id || null, + data: { + id: wave?.id || null, + title: wave?.title || '', + opening_date: wave?.opening_date || '', + closing_date: wave?.closing_date || '', + currency: wave?.currency || primaryWave.currency || event.currency || 'sats', + allow_fiat: isEditing + ? Boolean(wave?.allow_fiat) + : Boolean(primaryWave.allow_fiat ?? event.allow_fiat), + fiat_currency: + wave?.fiat_currency || + primaryWave.fiat_currency || + event.fiat_currency || + 'GBP', + amount_tickets: wave?.amount_tickets || 0, + price_per_ticket: + wave?.price_per_ticket || + primaryWave.price_per_ticket || + event.price_per_ticket || + 0 + } + } + }, + resetTicketWaveDialog() { + this.ticketWaveDialog = { + show: false, + eventId: null, + wallet: null, + editingWaveId: null, + data: { + id: null, + title: '', + opening_date: '', + closing_date: '', + currency: 'sats', + allow_fiat: false, + fiat_currency: 'GBP', + amount_tickets: 0, + price_per_ticket: 0 + } + } + }, + saveTicketWave() { + const event = _.findWhere(this.events, {id: this.ticketWaveDialog.eventId}) + const wallet = _.findWhere(this.g.user.wallets, { + id: this.ticketWaveDialog.wallet + }) + if (!event || !wallet) return + + const payload = { + ...event, + extra: { + ...event.extra, + ticket_waves: (event.extra?.ticket_waves || []).map(existingWave => + existingWave.id === this.ticketWaveDialog.editingWaveId + ? {...this.ticketWaveDialog.data} + : existingWave + ) + } + } + + if (!this.ticketWaveDialog.editingWaveId) { + payload.extra.ticket_waves.push({...this.ticketWaveDialog.data}) + } + + if (payload.extra?.promo_codes) { + payload.extra.promo_codes = this.normalizePromoCodes( + payload.extra.promo_codes + ) + } + + LNbits.api + .request( + 'PUT', + '/events/api/v1/events/' + payload.id, + wallet.adminkey, + payload + ) + .then(response => { + this.events = this.events.map(item => + item.id === payload.id ? response.data : item + ) + Quasar.Notify.create({ + type: 'positive', + message: this.ticketWaveDialog.editingWaveId + ? 'Ticket wave updated.' + : 'Ticket wave added.', + icon: null + }) + this.resetTicketWaveDialog() + }) + .catch(LNbits.utils.notifyApiError) + }, openPromoCodesDialog(event) { this.promoCodesDialog.data = { ...event, diff --git a/static/js/index.vue b/static/js/index.vue index 2a1bf8c..00428d4 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -101,7 +101,7 @@
-
Promo codes
+
Ticket waves
+
+
+
+
+ + + +
+
+
+ +
+
Promo codes
+
@@ -322,36 +373,57 @@ hint="Optional banner image to display on the event page" >
-
Ticket closing date
+
Event begins
-
Event begins
+
Event ends
- -
-
Event ends
-
+ +
+ Primary ticket wave (you can add other waves later) +
+
+
+
+
+ +
+
+
@@ -505,6 +577,8 @@ formDialog.data.name == null || formDialog.data.info == null || formDialog.data.closing_date == null || + primaryTicketWave().title == null || + primaryTicketWave().opening_date == null || formDialog.data.event_start_date == null || formDialog.data.event_end_date == null || formDialog.data.amount_tickets == null || @@ -603,5 +677,123 @@ + + + + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ + +
+ {{ + ticketWaveDialog.editingWaveId + ? 'Update Ticket Wave' + : 'Save Ticket Wave' + }} + Cancel +
+
+
+
diff --git a/views_api.py b/views_api.py index abdc1ef..432e3b1 100644 --- a/views_api.py +++ b/views_api.py @@ -51,6 +51,7 @@ PublicTicket, Ticket, TicketPaymentRequest, + get_active_ticket_waves, ) from .services import refund_tickets, resend_ticket_email_notification from .tasks import deregister_payment_listener, register_payment_listener @@ -86,24 +87,29 @@ async def api_get_event(event_id: str) -> Event: ) await purge_unpaid_tickets(event_id) - is_window_open = datetime.now(timezone.utc) < datetime.strptime( - event.closing_date, "%Y-%m-%d" - ).replace(tzinfo=timezone.utc) + today = datetime.now(timezone.utc).date() + active_waves = get_active_ticket_waves(event, today) + is_sales_closed = today > datetime.strptime(event.closing_date, "%Y-%m-%d").date() is_min_tickets_met = ( event.sold >= event.extra.min_tickets if event.extra.conditional else True ) if event.amount_tickets < 1: raise HTTPException(status_code=HTTPStatus.GONE, detail="Event is sold out.") - if event.extra.conditional and not is_min_tickets_met and not is_window_open: + if event.extra.conditional and not is_min_tickets_met and is_sales_closed: event.canceled = True await update_event(event) await refund_tickets(event_id) raise HTTPException(status_code=HTTPStatus.GONE, detail="Event canceled.") - if not is_window_open: + if not active_waves: raise HTTPException( - status_code=HTTPStatus.GONE, detail="Ticket closing date has passed." + status_code=HTTPStatus.GONE, + detail=( + "Ticket closing date has passed." + if is_sales_closed + else "No ticket wave is currently open." + ), ) return event @@ -127,8 +133,17 @@ async def api_event_create( raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="Not your event." ) - for k, v in data.dict().items(): - setattr(event, k, v) + event = Event( + **{ + **event.dict(), + **data.dict(), + "id": event.id, + "wallet": event.wallet, + "time": event.time, + "sold": event.sold, + "canceled": event.canceled, + } + ) event = await update_event(event) else: event = await create_event(data) @@ -215,7 +230,7 @@ async def api_ticket_create( if event.canceled: raise HTTPException(status_code=HTTPStatus.GONE, detail="Event is canceled.") - if event.amount_tickets > 0 and event.sold >= event.amount_tickets: + if event.amount_tickets < 1: raise HTTPException(status_code=HTTPStatus.GONE, detail="Event is sold out.") name = data.name @@ -237,7 +252,32 @@ async def api_ticket_create( status_code=HTTPStatus.BAD_REQUEST, detail="Invalid Nostr identifier.", ) from exc - price = event.price_per_ticket + active_waves = get_active_ticket_waves(event) + if not active_waves: + raise HTTPException( + status_code=HTTPStatus.GONE, detail="No ticket wave is currently open." + ) + + selected_wave = None + if data.ticket_wave_id: + selected_wave = next( + (wave for wave in active_waves if wave.id == data.ticket_wave_id), + None, + ) + if not selected_wave: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="Invalid ticket wave selected.", + ) + elif len(active_waves) == 1: + selected_wave = active_waves[0] + else: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="Please select a ticket wave.", + ) + + price = selected_wave.price_per_ticket extra: dict[str, Any] = {"tag": "events", "name": name, "email": email} if promo_code: @@ -249,31 +289,31 @@ async def api_ticket_create( # get the promocode promo = next(pc for pc in event.extra.promo_codes if pc.code == promo_code) extra["promo_code"] = promo.code - price = event.price_per_ticket * (1 - promo.discount_percent / 100) + price = selected_wave.price_per_ticket * (1 - promo.discount_percent / 100) - if payment_method == "fiat" and not event.allow_fiat: + if payment_method == "fiat" and not selected_wave.allow_fiat: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail="Fiat payments are not enabled for this event.", ) - if _is_fiat_currency(event.currency): + if _is_fiat_currency(selected_wave.currency): extra["fiat"] = True - extra["currency"] = event.currency + extra["currency"] = selected_wave.currency extra["fiatAmount"] = price - extra["rate"] = await get_fiat_rate_satoshis(event.currency) + extra["rate"] = await get_fiat_rate_satoshis(selected_wave.currency) if payment_method != "fiat": - price = await fiat_amount_as_satoshis(price, event.currency) + price = await fiat_amount_as_satoshis(price, selected_wave.currency) - invoice_unit = event.currency + invoice_unit = selected_wave.currency fiat_amount = price fiat_provider = None if payment_method == "fiat": - if _is_fiat_currency(event.currency): - invoice_unit = event.currency + if _is_fiat_currency(selected_wave.currency): + invoice_unit = selected_wave.currency else: - invoice_unit = event.fiat_currency + invoice_unit = selected_wave.fiat_currency fiat_amount = await satoshis_amount_as_fiat(price, invoice_unit) extra["fiat"] = True extra["currency"] = invoice_unit @@ -314,6 +354,8 @@ async def api_ticket_create( email=email, extra={ "applied_promo_code": promo_code, + "ticket_wave_id": selected_wave.id, + "ticket_wave_title": selected_wave.title, "refund_address": refund_address, "nostr_identifier": nostr_identifier, "ticket_base_url": str(request.base_url).rstrip("/"), From 3dfea34d2cfe6b6cfb9ab606a537b7ebae8e2380 Mon Sep 17 00:00:00 2001 From: Arc Date: Wed, 13 May 2026 20:12:21 +0100 Subject: [PATCH 4/8] works, but horrible slop --- __init__.py | 3 +- models.py | 36 ++++++-- services.py | 183 ++++++++++++++++++++++++++++++++-------- static/image/ticket.jpg | Bin 0 -> 106602 bytes static/js/display.js | 12 ++- static/js/index.js | 114 ++++++++++++++++++++++--- static/js/index.vue | 120 ++++++++++++++++++++++---- static/js/ticket.js | 29 ++++++- static/js/ticket.vue | 49 +++++++++++ views_api.py | 119 ++++++++++++++++++++++++-- 10 files changed, 579 insertions(+), 86 deletions(-) create mode 100644 static/image/ticket.jpg diff --git a/__init__.py b/__init__.py index ef37528..827b8d5 100644 --- a/__init__.py +++ b/__init__.py @@ -6,12 +6,13 @@ from .crud import db from .tasks import wait_for_paid_invoices from .views import events_generic_router -from .views_api import events_api_router, tickets_api_router +from .views_api import events_api_router, qr_api_router, tickets_api_router events_ext: APIRouter = APIRouter(prefix="/events", tags=["Events"]) events_ext.include_router(events_generic_router) events_ext.include_router(events_api_router) events_ext.include_router(tickets_api_router) +events_ext.include_router(qr_api_router) events_static_files = [ { diff --git a/models.py b/models.py index 21d174d..6911002 100644 --- a/models.py +++ b/models.py @@ -27,6 +27,8 @@ class TicketWave(BaseModel): opening_date: str closing_date: str currency: str = "sat" + use_ticket_image: bool = False + ticket_image_id: str | None = None allow_fiat: bool = False fiat_currency: str = "GBP" amount_tickets: int = Field(default=0, ge=0) @@ -133,6 +135,22 @@ class Ticket(BaseModel): extra: TicketExtra = Field(default_factory=TicketExtra) +class NotificationDeliveryResult(BaseModel): + attempted: bool = False + sent: bool = False + error: str | None = None + + +class TicketResendResult(BaseModel): + ticket: Ticket + email: NotificationDeliveryResult = Field( + default_factory=NotificationDeliveryResult + ) + nostr: NotificationDeliveryResult = Field( + default_factory=NotificationDeliveryResult + ) + + class PublicTicket(BaseModel): event: str name: str @@ -163,19 +181,19 @@ def ensure_ticket_waves(event: Event | PublicEvent | CreateEvent) -> list[Ticket if hasattr(event, "time") and getattr(event, "time", None): fallback_opening_date = event.time.date().isoformat() if not fallback_opening_date: - fallback_opening_date = getattr(event, "closing_date") + fallback_opening_date = event.closing_date return [ TicketWave( id="primary", title="Primary wave", opening_date=fallback_opening_date, - closing_date=getattr(event, "closing_date"), - currency=getattr(event, "currency"), - allow_fiat=getattr(event, "allow_fiat"), - fiat_currency=getattr(event, "fiat_currency"), - amount_tickets=getattr(event, "amount_tickets"), - price_per_ticket=getattr(event, "price_per_ticket"), + closing_date=event.closing_date, + currency=event.currency, + allow_fiat=event.allow_fiat, + fiat_currency=event.fiat_currency, + amount_tickets=event.amount_tickets, + price_per_ticket=event.price_per_ticket, ) ] @@ -202,6 +220,8 @@ def get_active_ticket_waves( return [ wave for wave in ensure_ticket_waves(event) - if _parse_date(wave.opening_date) <= current_day <= _parse_date(wave.closing_date) + if _parse_date(wave.opening_date) + <= current_day + <= _parse_date(wave.closing_date) and wave.amount_tickets > 0 ] diff --git a/services.py b/services.py index f8e6fa0..73e7039 100644 --- a/services.py +++ b/services.py @@ -1,15 +1,15 @@ from __future__ import annotations +import smtplib from asyncio.tasks import create_task +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from html import escape from lnbits.core.models.users import UserNotifications -from lnbits.core.services.nostr import send_nostr_dm -from lnbits.core.services.notifications import ( - send_email_notification, - send_user_notification, -) +from lnbits.core.services.notifications import send_user_notification +from lnbits.helpers import is_valid_email_address from lnbits.settings import settings -from lnbits.utils.nostr import normalize_private_key, normalize_public_key from lnurl import execute from loguru import logger @@ -20,13 +20,13 @@ update_event, update_ticket, ) -from .models import Event, Ticket - -DEFAULT_NOSTR_RELAYS = [ - "wss://relay.damus.io", - "wss://relay.primal.net", - "wss://relay.nostr.band", -] +from .models import ( + Event, + NotificationDeliveryResult, + Ticket, + TicketResendResult, + ensure_ticket_waves, +) async def set_ticket_paid(ticket: Ticket) -> Ticket: @@ -42,11 +42,7 @@ async def set_ticket_paid(ticket: Ticket) -> Ticket: ticket_waves = event.extra.ticket_waves or [] if ticket_waves: selected_wave = next( - ( - wave - for wave in ticket_waves - if wave.id == ticket.extra.ticket_wave_id - ), + (wave for wave in ticket_waves if wave.id == ticket.extra.ticket_wave_id), ticket_waves[0], ) if selected_wave.amount_tickets > 0: @@ -69,6 +65,8 @@ async def _send_ticket_notification(ticket: Ticket) -> None: return subject, message = _ticket_notification_message(ticket, event) + email_message = _ticket_email_message(ticket, event, message) + email_html_message = _ticket_email_html_message(ticket, event, message) updated = False if ( @@ -77,7 +75,9 @@ async def _send_ticket_notification(ticket: Ticket) -> None: and ticket.email ): try: - await send_email_notification([ticket.email], message, subject) + await _send_ticket_email_notification( + [ticket.email], email_message, subject, email_html_message + ) ticket.extra.email_notification_sent = True updated = True except Exception as exc: @@ -89,8 +89,9 @@ async def _send_ticket_notification(ticket: Ticket) -> None: and ticket.extra.nostr_identifier ): try: + nostr_message = _ticket_delivery_message(ticket, event, message) await _send_nostr_ticket_notification( - ticket.extra.nostr_identifier, message + ticket.extra.nostr_identifier, nostr_message ) ticket.extra.nostr_notification_sent = True updated = True @@ -101,7 +102,9 @@ async def _send_ticket_notification(ticket: Ticket) -> None: await update_ticket(ticket) -async def resend_ticket_email_notification(ticket: Ticket) -> Ticket: +async def resend_ticket_email_notification( + ticket: Ticket, base_url: str | None = None +) -> TicketResendResult: event = await get_event(ticket.event) if not event: raise ValueError("Event does not exist.") @@ -109,11 +112,47 @@ async def resend_ticket_email_notification(ticket: Ticket) -> Ticket: raise ValueError("Email notifications are not enabled.") if not ticket.email: raise ValueError("Ticket does not have an email address.") + if base_url: + ticket.extra.ticket_base_url = base_url.rstrip("/") subject, message = _ticket_notification_message(ticket, event) - await send_email_notification([ticket.email], message, subject) - ticket.extra.email_notification_sent = True - return await update_ticket(ticket) + email_message = _ticket_email_message(ticket, event, message) + email_html_message = _ticket_email_html_message(ticket, event, message) + result = TicketResendResult( + ticket=ticket, + email=NotificationDeliveryResult(attempted=True), + ) + + try: + await _send_ticket_email_notification( + [ticket.email], email_message, subject, email_html_message + ) + ticket.extra.email_notification_sent = True + result.email.sent = True + except Exception as exc: + logger.warning(f"Failed to resend email for ticket {ticket.id}: {exc}") + result.email.error = str(exc) + + if ( + event.extra.nostr_notifications + and settings.is_nostr_notifications_configured() + and ticket.extra.nostr_identifier + ): + result.nostr.attempted = True + try: + nostr_message = _ticket_delivery_message(ticket, event, message) + await _send_nostr_ticket_notification( + ticket.extra.nostr_identifier, nostr_message + ) + ticket.extra.nostr_notification_sent = True + result.nostr.sent = True + except Exception as exc: + logger.warning(f"Failed to resend nostr DM for ticket {ticket.id}: {exc}") + result.nostr.error = str(exc) + + updated_ticket = await update_ticket(ticket) + result.ticket = updated_ticket + return result def _ticket_notification_message(ticket: Ticket, event: Event) -> tuple[str, str]: @@ -130,18 +169,81 @@ def _ticket_notification_message(ticket: Ticket, event: Event) -> tuple[str, str return subject, f"{body}\n\nOpen it here: {ticket_url}" +def _ticket_delivery_message(ticket: Ticket, event: Event, base_message: str) -> str: + ticket_image_url = _ticket_image_url(ticket, event) + if not ticket_image_url: + return base_message + + return f"{base_message}\n\nTicket image: {ticket_image_url}" + + +def _ticket_email_message(ticket: Ticket, event: Event, base_message: str) -> str: + return _ticket_delivery_message(ticket, event, base_message) + + +def _ticket_email_html_message(ticket: Ticket, event: Event, base_message: str) -> str: + text_message = _ticket_delivery_message(ticket, event, base_message) + html_message = f"

{escape(text_message).replace(chr(10), '
')}

" + ticket_image_url = _ticket_image_url(ticket, event) + if not ticket_image_url: + return html_message + + return ( + f"{html_message}" + f'

Ticket image

' + ) + + async def _send_nostr_ticket_notification(identifier: str, message: str) -> None: - if "@" in identifier: - await send_user_notification( - UserNotifications(nostr_identifier=identifier), - message, - "text_message", - ) - return + await send_user_notification( + UserNotifications(nostr_identifier=identifier), + message, + "text_message", + ) - private_key = normalize_private_key(settings.lnbits_nostr_notifications_private_key) - public_key = normalize_public_key(identifier) - await send_nostr_dm(private_key, public_key, message, DEFAULT_NOSTR_RELAYS) + +async def _send_ticket_email_notification( + to_emails: list[str], + message: str, + subject: str, + html_message: str | None = None, +) -> None: + if not settings.lnbits_email_notifications_enabled: + raise ValueError("Email notifications are disabled") + if not is_valid_email_address(settings.lnbits_email_notifications_email): + raise ValueError( + f"Invalid from email address: {settings.lnbits_email_notifications_email}" + ) + if not to_emails: + raise ValueError("No email addresses provided") + for email in to_emails: + if not is_valid_email_address(email): + raise ValueError(f"Invalid email address: {email}") + + msg = MIMEMultipart("alternative") + msg["From"] = settings.lnbits_email_notifications_email + msg["To"] = ", ".join(to_emails) + msg["Subject"] = subject + msg.attach(MIMEText(message, "plain")) + if html_message: + msg.attach(MIMEText(html_message, "html")) + + username = ( + settings.lnbits_email_notifications_username + or settings.lnbits_email_notifications_email + ) + with smtplib.SMTP( + settings.lnbits_email_notifications_server, + settings.lnbits_email_notifications_port, + ) as smtp_server: + smtp_server.starttls() + smtp_server.login(username, settings.lnbits_email_notifications_password) + smtp_server.sendmail( + settings.lnbits_email_notifications_email, + to_emails, + msg.as_string(), + ) def _ticket_url(ticket: Ticket) -> str: @@ -149,6 +251,19 @@ def _ticket_url(ticket: Ticket) -> str: return f"{base_url}/events/ticket/{ticket.id}" +def _ticket_image_url(ticket: Ticket, event: Event) -> str | None: + waves = ensure_ticket_waves(event) + wave = next( + (wave for wave in waves if wave.id == ticket.extra.ticket_wave_id), + waves[0], + ) + if not wave.use_ticket_image: + return None + + base_url = (ticket.extra.ticket_base_url or settings.lnbits_baseurl).rstrip("/") + return f"{base_url}/events/api/v1/qr/{ticket.id}" + + async def refund_tickets(event_id: str): """ Refund tickets for an event that has not met the minimum ticket requirement. diff --git a/static/image/ticket.jpg b/static/image/ticket.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e05d9318a5dd38ef26268c0c516f0c057a115473 GIT binary patch literal 106602 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

L2dRO9pfi0}hU%i@`{9;AFY=eU& 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≠72V4L$HJ`y?cZ$v_E$Sjp_ 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 wave.id === this.formDialog.data.ticket_wave_id - ) || this.activeTicketWaves[0] || null + ) || + this.activeTicketWaves[0] || + null ) }, showTicketWaveSelector() { @@ -109,7 +111,9 @@ window.PageEventsDisplay = { this.formDialog.data.refund = '' this.formDialog.data.nostr_identifier = '' this.formDialog.data.ticket_wave_id = - this.activeTicketWaves.length === 1 ? this.activeTicketWaves[0].id : null + this.activeTicketWaves.length === 1 + ? this.activeTicketWaves[0].id + : null this.formDialog.data.payment_method = 'lightning' }, @@ -145,7 +149,9 @@ window.PageEventsDisplay = { this.formDialog.data.refund = '' this.formDialog.data.nostr_identifier = '' this.formDialog.data.ticket_wave_id = - this.activeTicketWaves.length === 1 ? this.activeTicketWaves[0].id : null + this.activeTicketWaves.length === 1 + ? this.activeTicketWaves[0].id + : null this.formDialog.data.payment_method = 'lightning' Quasar.Notify.create({ type: 'positive', diff --git a/static/js/index.js b/static/js/index.js index 617df5e..3083f75 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -5,6 +5,8 @@ window.PageEvents = { events: [], tickets: [], resendingTicketEmails: [], + isUploadingTicketTemplate: false, + ticketImageUploadTarget: null, currencies: [], eventsTable: { columns: [ @@ -59,6 +61,12 @@ window.PageEvents = { label: 'Registered', field: 'registered' }, + { + name: 'nostr', + align: 'left', + label: 'Nostr', + field: row => row.extra?.nostr_identifier || '' + }, { name: 'promo_code', align: 'left', @@ -96,6 +104,8 @@ window.PageEvents = { opening_date: '', closing_date: '', currency: 'sats', + use_ticket_image: false, + ticket_image_id: null, allow_fiat: false, fiat_currency: 'GBP', amount_tickets: 0, @@ -130,6 +140,8 @@ window.PageEvents = { opening_date: data.closing_date || '', closing_date: data.closing_date || '', currency: data.currency || 'sats', + use_ticket_image: false, + ticket_image_id: null, allow_fiat: Boolean(data.allow_fiat), fiat_currency: data.fiat_currency || 'GBP', amount_tickets: data.amount_tickets || 0, @@ -145,6 +157,8 @@ window.PageEvents = { primaryWave.opening_date = primaryWave.opening_date || '' primaryWave.closing_date = data.closing_date || '' primaryWave.currency = data.currency || 'sats' + primaryWave.use_ticket_image = Boolean(primaryWave.use_ticket_image) + primaryWave.ticket_image_id = primaryWave.ticket_image_id || null primaryWave.allow_fiat = Boolean(data.allow_fiat) primaryWave.fiat_currency = data.fiat_currency || 'GBP' primaryWave.amount_tickets = Number(data.amount_tickets || 0) @@ -165,7 +179,8 @@ window.PageEvents = { formData.fiat_currency = primaryWave.fiat_currency || 'GBP' formData.amount_tickets = primaryWave.amount_tickets formData.price_per_ticket = primaryWave.price_per_ticket - formData.closing_date = primaryWave.closing_date || formData.closing_date || '' + formData.closing_date = + primaryWave.closing_date || formData.closing_date || '' return formData }, isFiatCurrency(currency) { @@ -179,12 +194,60 @@ window.PageEvents = { code: code.code.trim().toUpperCase() })) }, + templateDownloadUrl() { + return '/events/static/image/ticket.jpg' + }, + async uploadAssetFile(file) { + const form = new FormData() + form.append('file', file) + form.append('public_asset', 'true') + const {data} = await LNbits.api.request( + 'POST', + '/api/v1/assets?public_asset=true', + null, + form + ) + return data.id + }, + triggerTicketImageUpload(target) { + this.ticketImageUploadTarget = target + this.$refs.ticketImageUpload.value = null + this.$refs.ticketImageUpload.click() + }, + async handleTicketImageSelected(event) { + const file = event.target.files?.[0] + if (!file || !this.ticketImageUploadTarget) return + + this.isUploadingTicketTemplate = true + try { + const assetId = await this.uploadAssetFile(file) + if (this.ticketImageUploadTarget === 'primary') { + const wave = this.primaryTicketWave() + wave.use_ticket_image = true + wave.ticket_image_id = assetId + } else if (this.ticketImageUploadTarget === 'dialog') { + this.ticketWaveDialog.data.use_ticket_image = true + this.ticketWaveDialog.data.ticket_image_id = assetId + } + Quasar.Notify.create({ + type: 'positive', + message: 'Ticket template uploaded.', + icon: null + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } finally { + this.isUploadingTicketTemplate = false + this.ticketImageUploadTarget = null + } + }, soldTicketsForWave(eventId, waveId) { return this.tickets.filter( ticket => ticket.event === eventId && ticket.paid && - ticket.extra?.ticket_wave_id === waveId + (ticket.extra?.ticket_wave_id === waveId || + (!ticket.extra?.ticket_wave_id && waveId === 'primary')) ).length }, getTickets() { @@ -232,14 +295,30 @@ window.PageEvents = { wallet.adminkey ) .then(response => { + const result = response.data this.tickets = this.tickets.map(obj => - obj.id === ticket.id ? response.data : obj + obj.id === ticket.id ? result.ticket : obj ) - Quasar.Notify.create({ - type: 'positive', - message: 'Ticket email resent.', - icon: null - }) + + if (result.email?.attempted) { + Quasar.Notify.create({ + type: result.email.sent ? 'positive' : 'negative', + message: result.email.sent + ? 'Ticket email resent.' + : `Ticket email failed: ${result.email.error || 'Unknown error.'}`, + icon: null + }) + } + + if (result.nostr?.attempted) { + Quasar.Notify.create({ + type: result.nostr.sent ? 'positive' : 'negative', + message: result.nostr.sent + ? 'Ticket Nostr DM resent.' + : `Ticket Nostr DM failed: ${result.nostr.error || 'Unknown error.'}`, + icon: null + }) + } }) .catch(LNbits.utils.notifyApiError) .finally(() => { @@ -270,7 +349,9 @@ window.PageEvents = { const data = this.formDialog.data this.syncPrimaryWaveFromForm(data) if (data.extra?.promo_codes) { - data.extra.promo_codes = this.normalizePromoCodes(data.extra.promo_codes) + data.extra.promo_codes = this.normalizePromoCodes( + data.extra.promo_codes + ) } if (!this.isFiatCurrency(data.currency)) { if (!data.allow_fiat) { @@ -306,6 +387,8 @@ window.PageEvents = { opening_date: '', closing_date: '', currency: 'sats', + use_ticket_image: false, + ticket_image_id: null, allow_fiat: false, fiat_currency: 'GBP', amount_tickets: 0, @@ -338,6 +421,8 @@ window.PageEvents = { opening_date: '', closing_date: '', currency: 'sats', + use_ticket_image: false, + ticket_image_id: null, allow_fiat: false, fiat_currency: 'GBP', amount_tickets: 0, @@ -377,7 +462,10 @@ window.PageEvents = { title: wave?.title || '', opening_date: wave?.opening_date || '', closing_date: wave?.closing_date || '', - currency: wave?.currency || primaryWave.currency || event.currency || 'sats', + currency: + wave?.currency || primaryWave.currency || event.currency || 'sats', + use_ticket_image: Boolean(wave?.use_ticket_image), + ticket_image_id: wave?.ticket_image_id || null, allow_fiat: isEditing ? Boolean(wave?.allow_fiat) : Boolean(primaryWave.allow_fiat ?? event.allow_fiat), @@ -407,6 +495,8 @@ window.PageEvents = { opening_date: '', closing_date: '', currency: 'sats', + use_ticket_image: false, + ticket_image_id: null, allow_fiat: false, fiat_currency: 'GBP', amount_tickets: 0, @@ -415,7 +505,9 @@ window.PageEvents = { } }, saveTicketWave() { - const event = _.findWhere(this.events, {id: this.ticketWaveDialog.eventId}) + const event = _.findWhere(this.events, { + id: this.ticketWaveDialog.eventId + }) const wallet = _.findWhere(this.g.user.wallets, { id: this.ticketWaveDialog.wallet }) diff --git a/static/js/index.vue b/static/js/index.vue index 00428d4..ca0b8d1 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -123,20 +123,20 @@ square clickable class="q-py-xs" - style="height: auto;" + style="height: auto" @click="openTicketWaveDialog(props.row, wave)" >

@@ -175,7 +176,9 @@
code.active )" :key="index" @@ -462,6 +465,43 @@ >
+ +
+
+ + Download template + 400/733 jpg + +
+
+ Replace +
+
+ Custom ticket template uploaded. +
+
@@ -652,7 +694,9 @@ dense flat icon="delete" - @click="promoCodesDialog.data.extra.promo_codes.splice(index, 1)" + @click=" + promoCodesDialog.data.extra.promo_codes.splice(index, 1) + " > @@ -753,6 +797,43 @@ >
+ +
+
+ + Download template + 400/733 jpg + +
+
+ Replace +
+
+ Custom ticket template uploaded. +
+
- {{ - ticketWaveDialog.editingWaveId - ? 'Update Ticket Wave' - : 'Save Ticket Wave' - }} + {{ + ticketWaveDialog.editingWaveId + ? 'Update Ticket Wave' + : 'Save Ticket Wave' + }} +
diff --git a/static/js/ticket.js b/static/js/ticket.js index 82fbd6d..6ff55b6 100644 --- a/static/js/ticket.js +++ b/static/js/ticket.js @@ -3,16 +3,36 @@ window.PageEventsTicket = { data() { return { ticketId: null, - ticket: null + ticket: null, + printMode: false, + qrSrc: '' } }, methods: { - printWindow() { - window.print() + async printWindow() { + this.printMode = true + await this.$nextTick() + await this.waitForPrintAssets() + setTimeout(() => window.print(), 50) + }, + async waitForPrintAssets() { + await this.$nextTick() + const img = document.querySelector('.ticket-print-qr') + if (!img) return + if (img.complete && img.naturalWidth > 0) return + await new Promise(resolve => { + const done = () => resolve() + img.addEventListener('load', done, {once: true}) + img.addEventListener('error', done, {once: true}) + setTimeout(done, 500) + }) } }, async created() { this.ticketId = this.$route.params.id + this.qrSrc = `/api/v1/qrcode?data=${encodeURIComponent( + `ticket://${this.ticketId}` + )}` try { const {data} = await LNbits.api.request( 'GET', @@ -22,5 +42,8 @@ window.PageEventsTicket = { } catch (error) { LNbits.utils.notifyApiError(error) } + window.addEventListener('afterprint', () => { + this.printMode = false + }) } } diff --git a/static/js/ticket.vue b/static/js/ticket.vue index 3c932e1..23a8dc4 100644 --- a/static/js/ticket.vue +++ b/static/js/ticket.vue @@ -36,4 +36,53 @@
+ + +
+ Ticket QR +
+
+ + diff --git a/views_api.py b/views_api.py index 432e3b1..7c009a1 100644 --- a/views_api.py +++ b/views_api.py @@ -1,8 +1,11 @@ import asyncio from datetime import datetime, timezone from http import HTTPStatus +from io import BytesIO +from pathlib import Path from typing import Any +import pyqrcode from fastapi import ( APIRouter, Depends, @@ -12,7 +15,9 @@ WebSocket, WebSocketDisconnect, ) +from fastapi.responses import StreamingResponse from lnbits.core.crud import get_user +from lnbits.core.crud.assets import get_public_asset from lnbits.core.crud.wallets import get_wallet from lnbits.core.models import WalletTypeInfo from lnbits.core.models.payments import CreateInvoice @@ -28,6 +33,7 @@ satoshis_amount_as_fiat, ) from lnbits.utils.nostr import normalize_public_key +from PIL import Image, ImageDraw from .crud import ( create_event, @@ -51,6 +57,8 @@ PublicTicket, Ticket, TicketPaymentRequest, + TicketResendResult, + ensure_ticket_waves, get_active_ticket_waves, ) from .services import refund_tickets, resend_ticket_email_notification @@ -58,12 +66,41 @@ events_api_router = APIRouter(prefix="/api/v1/events") tickets_api_router = APIRouter(prefix="/api/v1/tickets") +qr_api_router = APIRouter(prefix="/api/v1") def _is_fiat_currency(currency: str | None) -> bool: return str(currency or "").lower() not in {"sat", "sats"} +def make_qr_png(data: str, size: int = 235, border: int = 4) -> Image.Image: + qr = pyqrcode.create(data) + matrix = qr.code + modules = len(matrix) + + total_modules = modules + border * 2 + box_size = max(1, size // total_modules) + img_size = total_modules * box_size + + img = Image.new("RGBA", (img_size, img_size), "white") + draw = ImageDraw.Draw(img) + + for y, row in enumerate(matrix): + for x, cell in enumerate(row): + if cell: + x0 = (x + border) * box_size + y0 = (y + border) * box_size + draw.rectangle( + [x0, y0, x0 + box_size - 1, y0 + box_size - 1], + fill="black", + ) + + if img_size != size: + img = img.resize((size, size), Image.Resampling.NEAREST) + + return img + + @events_api_router.get("") async def api_events( all_wallets: bool = Query(False), @@ -217,6 +254,71 @@ async def api_get_ticket(ticket_id: str) -> Ticket: return ticket +@qr_api_router.get("/qr/{ticket_id}", response_class=StreamingResponse) +async def api_ticket_qr(ticket_id: str): + ticket = await get_ticket(ticket_id) + if not ticket: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist." + ) + + event = await get_event(ticket.event) + if not event: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." + ) + + waves = ensure_ticket_waves(event) + wave = next( + (wave for wave in waves if wave.id == ticket.extra.ticket_wave_id), + waves[0], + ) + + qr_img = make_qr_png(f"ticket://{ticket_id}", size=157) + output = BytesIO() + + if not wave.use_ticket_image: + qr_img.save(output, format="PNG") + output.seek(0) + return StreamingResponse( + output, + media_type="image/png", + headers={ + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0", + }, + ) + + background_bytes = None + if wave.ticket_image_id: + asset = await get_public_asset(wave.ticket_image_id) + if asset: + background_bytes = asset.data + + if background_bytes: + ticket_image = Image.open(BytesIO(background_bytes)).convert("RGBA") + else: + default_template = ( + Path(__file__).resolve().parent / "static" / "image" / "ticket.jpg" + ) + ticket_image = Image.open(default_template).convert("RGBA") + + ticket_image.paste(qr_img, (122, 505)) + ticket_image.save(output, format="PNG") + output.seek(0) + + return StreamingResponse( + output, + media_type="image/png", + headers={ + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0", + }, + ) + + @tickets_api_router.post("/{event_id}") async def api_ticket_create( event_id: str, data: CreateTicket, request: Request @@ -430,10 +532,12 @@ async def api_ticket_delete( await delete_ticket(ticket_id) -@tickets_api_router.post("/{ticket_id}/resend-email") +@tickets_api_router.post("/{ticket_id}/resend-email", response_model=TicketResendResult) async def api_ticket_resend_email( - ticket_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) -) -> Ticket: + ticket_id: str, + request: Request, + wallet: WalletTypeInfo = Depends(require_admin_key), +) -> TicketResendResult: ticket = await get_ticket(ticket_id) if not ticket: raise HTTPException( @@ -450,16 +554,13 @@ async def api_ticket_resend_email( ) try: - return await resend_ticket_email_notification(ticket) + return await resend_ticket_email_notification( + ticket, str(request.base_url).rstrip("/") + ) except ValueError as exc: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=str(exc) ) from exc - except Exception as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Failed to resend ticket email.", - ) from exc @tickets_api_router.put("/register/{ticket_id}") From 42f9192d0f5129416a3166532dc2a0afb4dbd599 Mon Sep 17 00:00:00 2001 From: Arc Date: Wed, 13 May 2026 23:25:44 +0100 Subject: [PATCH 5/8] slightly cleaner --- services.py | 131 +++++++++++++++++++++++----------------------------- 1 file changed, 57 insertions(+), 74 deletions(-) diff --git a/services.py b/services.py index 73e7039..3ad4f8f 100644 --- a/services.py +++ b/services.py @@ -64,42 +64,7 @@ async def _send_ticket_notification(ticket: Ticket) -> None: logger.warning(f"Event {ticket.event} not found for ticket notification.") return - subject, message = _ticket_notification_message(ticket, event) - email_message = _ticket_email_message(ticket, event, message) - email_html_message = _ticket_email_html_message(ticket, event, message) - updated = False - - if ( - event.extra.email_notifications - and settings.lnbits_email_notifications_enabled - and ticket.email - ): - try: - await _send_ticket_email_notification( - [ticket.email], email_message, subject, email_html_message - ) - ticket.extra.email_notification_sent = True - updated = True - except Exception as exc: - logger.warning(f"Failed to email ticket {ticket.id}: {exc}") - - if ( - event.extra.nostr_notifications - and settings.is_nostr_notifications_configured() - and ticket.extra.nostr_identifier - ): - try: - nostr_message = _ticket_delivery_message(ticket, event, message) - await _send_nostr_ticket_notification( - ticket.extra.nostr_identifier, nostr_message - ) - ticket.extra.nostr_notification_sent = True - updated = True - except Exception as exc: - logger.warning(f"Failed to send nostr DM for ticket {ticket.id}: {exc}") - - if updated: - await update_ticket(ticket) + await _deliver_ticket_notifications(ticket, event) async def resend_ticket_email_notification( @@ -115,44 +80,7 @@ async def resend_ticket_email_notification( if base_url: ticket.extra.ticket_base_url = base_url.rstrip("/") - subject, message = _ticket_notification_message(ticket, event) - email_message = _ticket_email_message(ticket, event, message) - email_html_message = _ticket_email_html_message(ticket, event, message) - result = TicketResendResult( - ticket=ticket, - email=NotificationDeliveryResult(attempted=True), - ) - - try: - await _send_ticket_email_notification( - [ticket.email], email_message, subject, email_html_message - ) - ticket.extra.email_notification_sent = True - result.email.sent = True - except Exception as exc: - logger.warning(f"Failed to resend email for ticket {ticket.id}: {exc}") - result.email.error = str(exc) - - if ( - event.extra.nostr_notifications - and settings.is_nostr_notifications_configured() - and ticket.extra.nostr_identifier - ): - result.nostr.attempted = True - try: - nostr_message = _ticket_delivery_message(ticket, event, message) - await _send_nostr_ticket_notification( - ticket.extra.nostr_identifier, nostr_message - ) - ticket.extra.nostr_notification_sent = True - result.nostr.sent = True - except Exception as exc: - logger.warning(f"Failed to resend nostr DM for ticket {ticket.id}: {exc}") - result.nostr.error = str(exc) - - updated_ticket = await update_ticket(ticket) - result.ticket = updated_ticket - return result + return await _deliver_ticket_notifications(ticket, event) def _ticket_notification_message(ticket: Ticket, event: Event) -> tuple[str, str]: @@ -195,6 +123,61 @@ def _ticket_email_html_message(ticket: Ticket, event: Event, base_message: str) ) +def _ticket_notification_payload(ticket: Ticket, event: Event) -> tuple[str, str, str]: + subject, base_message = _ticket_notification_message(ticket, event) + text_message = _ticket_email_message(ticket, event, base_message) + html_message = _ticket_email_html_message(ticket, event, base_message) + return subject, text_message, html_message + + +async def _deliver_ticket_notifications( + ticket: Ticket, event: Event +) -> TicketResendResult: + subject, text_message, html_message = _ticket_notification_payload(ticket, event) + result = TicketResendResult( + ticket=ticket, + email=NotificationDeliveryResult( + attempted=bool( + event.extra.email_notifications + and settings.lnbits_email_notifications_enabled + and ticket.email + ) + ), + nostr=NotificationDeliveryResult( + attempted=bool( + event.extra.nostr_notifications + and settings.is_nostr_notifications_configured() + and ticket.extra.nostr_identifier + ) + ), + ) + + if result.email.attempted: + try: + await _send_ticket_email_notification( + [ticket.email], text_message, subject, html_message + ) + ticket.extra.email_notification_sent = True + result.email.sent = True + except Exception as exc: + logger.warning(f"Failed to email ticket {ticket.id}: {exc}") + result.email.error = str(exc) + + if result.nostr.attempted: + try: + await _send_nostr_ticket_notification( + ticket.extra.nostr_identifier, text_message + ) + ticket.extra.nostr_notification_sent = True + result.nostr.sent = True + except Exception as exc: + logger.warning(f"Failed to send nostr DM for ticket {ticket.id}: {exc}") + result.nostr.error = str(exc) + + result.ticket = await update_ticket(ticket) + return result + + async def _send_nostr_ticket_notification(identifier: str, message: str) -> None: await send_user_notification( UserNotifications(nostr_identifier=identifier), From 5e9b0a7ecd21d538fb47486dccc6529ef1a64349 Mon Sep 17 00:00:00 2001 From: Arc Date: Wed, 13 May 2026 23:59:35 +0100 Subject: [PATCH 6/8] filter most recent --- crud.py | 36 +++++++++++++++++++++++++++++-- models.py | 21 ++++++++++++++++++ services.py | 24 ++++++++++++++------- static/js/display.js | 4 ++-- static/js/display.vue | 2 +- static/js/index.js | 50 ++++++++++++++++++++++++++++++++----------- static/js/index.vue | 2 ++ views_api.py | 43 ++++++++++++++++++++++++++++++------- 8 files changed, 148 insertions(+), 34 deletions(-) diff --git a/crud.py b/crud.py index 3f6d93d..9c9774e 100644 --- a/crud.py +++ b/crud.py @@ -1,9 +1,16 @@ from datetime import datetime, timedelta, timezone -from lnbits.db import Database +from lnbits.db import Database, Filters, Page from lnbits.helpers import urlsafe_short_hash -from .models import CreateEvent, Event, Ticket, TicketExtra, sync_event_ticket_waves +from .models import ( + CreateEvent, + Event, + Ticket, + TicketExtra, + TicketFilters, + sync_event_ticket_waves, +) db = Database("ext_events") @@ -51,6 +58,31 @@ async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]: ) +async def get_tickets_paginated( + wallet_ids: str | list[str], filters: Filters[TicketFilters] | None = None +) -> Page[Ticket]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + wallet_where = [] + values = {} + for idx, wallet_id in enumerate(wallet_ids): + key = f"wallet_id_{idx}" + wallet_where.append(f":{key}") + values[key] = wallet_id + + where = [f"wallet IN ({', '.join(wallet_where)})", "paid = true"] + + return await db.fetch_page( + "SELECT * FROM events.ticket", + where=where, + values=values, + filters=filters, + model=Ticket, + table_name="events.ticket", + ) + + async def delete_ticket(payment_hash: str) -> None: await db.execute("DELETE FROM events.ticket WHERE id = :id", {"id": payment_hash}) diff --git a/models.py b/models.py index 6911002..fb9ffe4 100644 --- a/models.py +++ b/models.py @@ -1,7 +1,9 @@ from datetime import date, datetime +from typing import ClassVar from uuid import uuid4 from fastapi import Query +from lnbits.db import FilterModel from pydantic import BaseModel, EmailStr, Field, validator @@ -168,6 +170,25 @@ class TicketPaymentRequest(BaseModel): is_fiat: bool = False +class TicketFilters(FilterModel): + __search_fields__: ClassVar[list[str]] = ["event", "name", "email", "id"] + __sort_fields__: ClassVar[list[str]] = [ + "time", + "event", + "name", + "email", + "registered", + "id", + ] + + event: str | None = None + name: str | None = None + email: str | None = None + registered: bool | None = None + paid: bool | None = None + id: str | None = None + + def _parse_date(value: str) -> date: return datetime.strptime(value, "%Y-%m-%d").date() diff --git a/services.py b/services.py index 3ad4f8f..3c6a17b 100644 --- a/services.py +++ b/services.py @@ -105,10 +105,6 @@ def _ticket_delivery_message(ticket: Ticket, event: Event, base_message: str) -> return f"{base_message}\n\nTicket image: {ticket_image_url}" -def _ticket_email_message(ticket: Ticket, event: Event, base_message: str) -> str: - return _ticket_delivery_message(ticket, event, base_message) - - def _ticket_email_html_message(ticket: Ticket, event: Event, base_message: str) -> str: text_message = _ticket_delivery_message(ticket, event, base_message) html_message = f"

{escape(text_message).replace(chr(10), '
')}

" @@ -119,21 +115,26 @@ def _ticket_email_html_message(ticket: Ticket, event: Event, base_message: str) return ( f"{html_message}" f'

Ticket image

' + 'style="max-width: 200px; height: auto;" />

' ) def _ticket_notification_payload(ticket: Ticket, event: Event) -> tuple[str, str, str]: subject, base_message = _ticket_notification_message(ticket, event) - text_message = _ticket_email_message(ticket, event, base_message) + text_message = _ticket_delivery_message(ticket, event, base_message) html_message = _ticket_email_html_message(ticket, event, base_message) return subject, text_message, html_message +def _supports_nostr_delivery(identifier: str | None) -> bool: + return bool(identifier and "@" in identifier) + + async def _deliver_ticket_notifications( ticket: Ticket, event: Event ) -> TicketResendResult: subject, text_message, html_message = _ticket_notification_payload(ticket, event) + updated = False result = TicketResendResult( ticket=ticket, email=NotificationDeliveryResult( @@ -159,22 +160,29 @@ async def _deliver_ticket_notifications( ) ticket.extra.email_notification_sent = True result.email.sent = True + updated = True except Exception as exc: logger.warning(f"Failed to email ticket {ticket.id}: {exc}") result.email.error = str(exc) - if result.nostr.attempted: + if result.nostr.attempted and not _supports_nostr_delivery( + ticket.extra.nostr_identifier + ): + result.nostr.error = "Only NIP-05 Nostr identifiers are supported." + elif result.nostr.attempted: try: await _send_nostr_ticket_notification( ticket.extra.nostr_identifier, text_message ) ticket.extra.nostr_notification_sent = True result.nostr.sent = True + updated = True except Exception as exc: logger.warning(f"Failed to send nostr DM for ticket {ticket.id}: {exc}") result.nostr.error = str(exc) - result.ticket = await update_ticket(ticket) + if updated: + result.ticket = await update_ticket(ticket) return result diff --git a/static/js/display.js b/static/js/display.js index dce4dd5..6b53d9d 100644 --- a/static/js/display.js +++ b/static/js/display.js @@ -221,7 +221,7 @@ window.PageEventsDisplay = { const url = new URL(window.location) url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:' - url.pathname = `/api/v1/ws/${paymentHash}` + url.pathname = `/events/api/v1/tickets/ws/${paymentHash}` url.search = '' url.hash = '' @@ -230,7 +230,7 @@ window.PageEventsDisplay = { ws.onmessage = event => { const data = JSON.parse(event.data) - if (data.pending === false) { + if (data.paid === true) { this.paymentSuccess(paymentHash) ws.close() } diff --git a/static/js/display.vue b/static/js/display.vue index dc7c921..9051fbf 100644 --- a/static/js/display.vue +++ b/static/js/display.vue @@ -49,7 +49,7 @@ filled dense v-model.trim="formDialog.data.nostr_identifier" - label="(optional) Nostr NIP-05 or npub" + label="(optional) Nostr NIP-05" hint="If provided, we'll DM your ticket link after payment." >
diff --git a/static/js/index.js b/static/js/index.js index 3083f75..9f009a2 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -4,6 +4,7 @@ window.PageEvents = { return { events: [], tickets: [], + allPaidTickets: [], resendingTicketEmails: [], isUploadingTicketTemplate: false, ticketImageUploadTarget: null, @@ -46,6 +47,7 @@ window.PageEvents = { } }, ticketsTable: { + loading: false, columns: [ { name: 'event', @@ -76,7 +78,11 @@ window.PageEvents = { {name: 'id', align: 'left', label: 'ID', field: 'id'} ], pagination: { - rowsPerPage: 10 + sortBy: 'time', + descending: true, + page: 1, + rowsPerPage: 10, + rowsNumber: 10 } }, formDialog: { @@ -242,7 +248,7 @@ window.PageEvents = { } }, soldTicketsForWave(eventId, waveId) { - return this.tickets.filter( + return this.allPaidTickets.filter( ticket => ticket.event === eventId && ticket.paid && @@ -250,16 +256,34 @@ window.PageEvents = { (!ticket.extra?.ticket_wave_id && waveId === 'primary')) ).length }, - getTickets() { - LNbits.api - .request( + async getAllTickets() { + try { + const {data} = await LNbits.api.request( 'GET', '/events/api/v1/tickets?all_wallets=true', this.g.user.wallets[0].adminkey ) - .then(response => { - this.tickets = response.data.filter(e => e.paid) - }) + this.allPaidTickets = data.filter(ticket => ticket.paid) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + async getTickets(props) { + try { + this.ticketsTable.loading = true + const params = LNbits.utils.prepareFilterQuery(this.ticketsTable, props) + const {data} = await LNbits.api.request( + 'GET', + `/events/api/v1/tickets/paginated?all_wallets=true&${params}`, + this.g.user.wallets[0].adminkey + ) + this.tickets = data.data + this.ticketsTable.pagination.rowsNumber = data.total + } catch (error) { + LNbits.utils.notifyApiError(error) + } finally { + this.ticketsTable.loading = false + } }, deleteTicket(ticketId) { const tickets = _.findWhere(this.tickets, {id: ticketId}) @@ -274,10 +298,9 @@ window.PageEvents = { '/events/api/v1/tickets/' + ticketId, wallet.adminkey ) - .then(response => { - this.tickets = _.reject(this.tickets, function (obj) { - return obj.id == ticketId - }) + .then(async () => { + await this.getTickets() + await this.getAllTickets() }) .catch(LNbits.utils.notifyApiError) }) @@ -328,7 +351,7 @@ window.PageEvents = { }) }, exportticketsCSV() { - LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets) + LNbits.utils.exportCSV(this.ticketsTable.columns, this.allPaidTickets) }, getEvents() { LNbits.api @@ -686,6 +709,7 @@ window.PageEvents = { async created() { if (this.g.user.wallets.length) { this.getTickets() + this.getAllTickets() this.getEvents() if (this.g.allowedCurrencies && this.g.allowedCurrencies.length > 0) { this.currencies = ['sats', ...this.g.allowedCurrencies] diff --git a/static/js/index.vue b/static/js/index.vue index ca0b8d1..b90e922 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -222,9 +222,11 @@ dense flat :rows="tickets" + :loading="ticketsTable.loading" row-key="id" :columns="ticketsTable.columns" v-model:pagination="ticketsTable.pagination" + @request="getTickets" >