From f835766935808257c6b4b05477eaed569111f50e Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 10 Sep 2026 13:43:53 +0200 Subject: [PATCH] feat(admin): event form parity with the webapp (1.6.1-aio.13) The LNbits admin form lagged the webapp's CreateEventDialog: - Payment methods never rendered. c2d9a96 wired the template to `paymentMethodOptions` / `acceptsFiat` but never defined them, so the q-option-group got `options=undefined` and the fiat-currency select was gated on `undefined`. Rails are now two q-checkboxes; Card is disabled with an explanatory tooltip when `g.user.fiat_providers` is empty (same rule as the webapp) and names the providers otherwise. - Location (NIP-52 `location` tag) and Categories (NIP-52 `t` tags, same 25-item list as the webapp's category.ts) were missing from the form even though the model, CRUD and publisher already carry them. - Datetimes are stamped with the browser's UTC offset on submit, as the webapp does; `_to_unix` treats naive values as UTC, so 18:00 CEST entered here went out on Nostr as 18:00 UTC. Table columns render "YYYY-MM-DD HH:MM" instead of the raw ISO string. - Validation: title + start date required, end >= start on the folded date+time, fiat currency required when a sat-priced event accepts card. Create is enabled once wallet + title + start are set; info, closing date, tickets and price were all effectively required before because the disable check compared undefined fields to null. - Labels follow the payment-rails vocabulary: "Unit" -> "Price currency", "Fiat checkout currency" -> "Fiat currency"; ticket closing date and end date explain their defaults. - A fiat-priced event mirrors `fiat_currency = currency` on save so the payload and the `tickets_fiat_currency` tag stay coherent. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_018b1bDExMX7W3a47wcgFUjb --- config.json | 2 +- static/js/index.js | 136 +++++++++++++++++++++++++++++++++++++---- static/js/index.vue | 143 ++++++++++++++++++++++++++++++-------------- 3 files changed, 222 insertions(+), 59 deletions(-) diff --git a/config.json b/config.json index 68e908a..6240778 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "id": "events", - "version": "1.6.1-aio.10", + "version": "1.6.1-aio.13", "name": "Events", "repo": "https://git.atitlan.io/aiolabs/events", "short_description": "Sell and register event tickets", diff --git a/static/js/index.js b/static/js/index.js index 646b37b..40f98a4 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -67,13 +67,15 @@ window.PageEvents = { name: 'event_start_date', align: 'left', label: 'Start date', - field: 'event_start_date' + field: 'event_start_date', + format: val => this.formatEventDate(val) }, { name: 'event_end_date', align: 'left', label: 'End date', - field: 'event_end_date' + field: 'event_end_date', + format: val => this.formatEventDate(val) }, { name: 'closing_date', @@ -149,12 +151,58 @@ window.PageEvents = { rowsPerPage: 10 } }, + // Rails an organizer can enable per event. Mirrors the webapp's + // CreateEventDialog; `fiat` is rendered disabled when the LNbits user + // has no fiat provider (see `hasFiatProvider`). + paymentMethodOptions: [ + { + value: 'lightning', + label: 'Lightning', + hint: 'Pay with any Lightning wallet' + }, + { + value: 'fiat', + label: 'Card', + hint: 'Card or bank through your configured fiat provider' + } + ], + // Same list the webapp offers (src/modules/events/types/category.ts); + // published as NIP-52 `t` tags so both clients filter on one vocabulary. + categoryOptions: [ + 'concert', + 'workshop', + 'market', + 'festival', + 'exhibition', + 'sport', + 'theater', + 'cinema', + 'party', + 'talk', + 'conference', + 'meetup', + 'food', + 'outdoor', + 'kids', + 'wellness', + 'technology', + 'art', + 'music', + 'dance', + 'literature', + 'comedy', + 'charity', + 'tradition', + 'other' + ].map(c => ({label: c.charAt(0).toUpperCase() + c.slice(1), value: c})), formDialog: { show: false, data: { currency: 'sats', allow_fiat: false, fiat_currency: 'GBP', + location: '', + categories: [], extra: { payment_methods: ['lightning'], promo_codes: [], @@ -165,6 +213,27 @@ window.PageEvents = { } } }, + computed: { + hasFiatProvider() { + return (this.g.user?.fiat_providers || []).length > 0 + }, + fiatProviderNames() { + return (this.g.user?.fiat_providers || []) + .map(p => p.charAt(0).toUpperCase() + p.slice(1)) + .join(', ') + }, + acceptsFiat() { + return (this.formDialog.data.extra?.payment_methods || []).includes( + 'fiat' + ) + }, + isSatPrice() { + return !this.isFiatCurrency(this.formDialog.data.currency) + }, + fiatCurrencyOptions() { + return this.currencies.filter(c => this.isFiatCurrency(c)) + } + }, methods: { isFiatCurrency(currency) { return !['sat', 'sats'].includes((currency || '').toLowerCase()) @@ -382,29 +451,62 @@ window.PageEvents = { }, splitDateTime(value) { // Inverse of foldDateTime: split a stored string back into the - // day/time pieces the form inputs bind to. + // day/time pieces the form inputs bind to. Slicing to HH:MM also + // drops the seconds + offset suffix withLocalTzOffset stamps on + // submit, so the organizer sees the wall-clock they entered. if (!value) return {day: '', time: ''} const [day, time = ''] = value.split('T') // Time inputs only accept HH:MM, drop any seconds we stored. return {day, time: time.slice(0, 5)} }, + withLocalTzOffset(value) { + // Stamp the browser's UTC offset on a "YYYY-MM-DDTHH:MM" value. + // The publisher's `_to_unix` treats a naive datetime as UTC, so an + // event entered as 18:00 in CEST would otherwise go out on Nostr + // as 18:00 UTC. Same transform the webapp applies; date-only + // values pass through unchanged (they map to NIP-52 kind 31922). + if (!value || !value.includes('T')) return value + const offMin = -new Date(value).getTimezoneOffset() + const sign = offMin >= 0 ? '+' : '-' + const abs = Math.abs(offMin) + const hh = String(Math.floor(abs / 60)).padStart(2, '0') + const mm = String(abs % 60).padStart(2, '0') + return `${value}:00${sign}${hh}:${mm}` + }, + formatEventDate(value) { + // Table display: "YYYY-MM-DD" or "YYYY-MM-DD HH:MM". + if (!value) return '' + const {day, time} = this.splitDateTime(value) + return time ? `${day} ${time}` : day + }, + validateEndDate() { + // Cross-field rule for the end-day input: end >= start, compared + // on the folded date+time so an equal-day earlier time is caught. + const d = this.formDialog.data + const start = this.foldDateTime(d.event_start_day, d.event_start_time) + const end = this.foldDateTime(d.event_end_day, d.event_end_time) + if (!start || !end) return true + return end >= start || 'End must be on or after start' + }, sendEventData() { const wallet = _.findWhere(this.g.user.wallets, { id: this.formDialog.data.wallet }) const data = {...this.formDialog.data} - data.event_start_date = this.foldDateTime( - data.event_start_day, - data.event_start_time + data.event_start_date = this.withLocalTzOffset( + this.foldDateTime(data.event_start_day, data.event_start_time) ) - data.event_end_date = this.foldDateTime( - data.event_end_day, - data.event_end_time + data.event_end_date = this.withLocalTzOffset( + this.foldDateTime(data.event_end_day, data.event_end_time) ) delete data.event_start_day delete data.event_start_time delete data.event_end_day delete data.event_end_time + // Optional NIP-52 fields: blank location is "unset", not "". + data.location = (data.location || '').trim() || null + data.categories = data.categories || [] + data.closing_date = data.closing_date || null if (data.extra?.promo_codes) { data.extra.promo_codes = data.extra.promo_codes @@ -425,10 +527,12 @@ window.PageEvents = { // allow_fiat stays the fiat-currency carrier the backend and the // NIP-52 tags read; keep it in lockstep with the checkbox list. data.allow_fiat = methods.includes('fiat') - if (!this.isFiatCurrency(data.currency)) { - if (!data.allow_fiat) { - data.fiat_currency = 'GBP' - } + if (this.isFiatCurrency(data.currency)) { + // A fiat-priced event settles in its price currency; mirror it so + // the payload (and the tickets_fiat_currency tag) stay coherent. + data.fiat_currency = data.currency + } else if (!data.allow_fiat) { + data.fiat_currency = 'GBP' } if (data.id) { @@ -453,6 +557,8 @@ window.PageEvents = { ? data.extra.payment_methods : ['lightning', ...(data.allow_fiat ? ['fiat'] : [])] }, + location: data.location || '', + categories: [...(data.categories || [])], event_start_day: start.day, event_start_time: start.time, event_end_day: end.day, @@ -463,6 +569,8 @@ window.PageEvents = { currency: 'sats', allow_fiat: false, fiat_currency: 'GBP', + location: '', + categories: [], event_start_day: '', event_start_time: '', event_end_day: '', @@ -487,6 +595,8 @@ window.PageEvents = { currency: 'sats', allow_fiat: false, fiat_currency: 'GBP', + location: '', + categories: [], extra: { payment_methods: ['lightning'], email_notifications: false, diff --git a/static/js/index.vue b/static/js/index.vue index 502d9ad..6e7ec28 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -479,8 +479,10 @@ filled dense v-model.trim="formDialog.data.name" - type="name" - label="Title of event " + type="text" + label="Title of event *" + lazy-rules + :rules="[val => !!val || 'Title is required']" >
@@ -504,6 +506,30 @@ label="Info about the event" hint="Markdown supported" > + + + + -
-
Ticket closing date
-
- -
-
-
Event begins
+
Event begins *
@@ -552,6 +569,10 @@ dense v-model.trim="formDialog.data.event_end_day" type="date" + hint="Defaults to the start date" + lazy-rules + reactive-rules + :rules="[validateEndDate]" >
@@ -564,6 +585,18 @@ >
+
+
Ticket sales close
+
+ +
+
@@ -581,7 +614,9 @@ dense v-model.number="formDialog.data.amount_tickets" type="number" - label="Amount of tickets " + min="0" + label="Amount of tickets" + hint="0 = unlimited" >
@@ -600,34 +635,57 @@
-
Payment methods *
- +
Payment methods *
- Card / fiat checkout goes through the fiat provider configured on - this LNbits instance. Untick Lightning for a card-only event. + Pick the rails buyers can pay with. Untick Lightning for a + card-only sale. +
+
+
+ + + +
+ + +
+ + Your LNbits user has no fiat provider configured. Ask the + instance admin to enable Stripe, PayPal or Square (Admin → + Fiat providers) to accept card payments. + +
+
Create Event