Some checks failed
lint.yml / fix: require a capacity on every ticket wave (pull_request) Failing after 0s
"Capacity is always required, there is no unlimited" was settled on #34 and enforced in #62 — but as a single number on the event. Since v1.6.8 capacity is per-wave and `event.amount_tickets` is a derived roll-up (`sync_event_ticket_waves`), so the rule had nothing holding it up at the level organisers actually set: the wave form's capacity input had no minimum, and nothing on the backend checked one at all. A zero-capacity wave can never be active — `get_active_ticket_waves` requires `amount_tickets > 0` — so it is the wave-level form of exactly what #62 removed: an event that looks on sale but refuses every purchase, on the card and at checkout both. `_validate_wave_capacity` now runs on create and update. Two details that matter: - it checks `ensure_ticket_waves(data)` rather than the raw list, so an event submitted with no waves is checked through the primary wave it is about to be given, not vacuously passed - on an edit it only checks waves NEW to the event. Selling out is the legitimate route to zero, and rejecting it would make a sold-out event uneditable — including the legacy zero-capacity rows this rule exists to let organisers fix Frontend: the wave dialog's capacity input gains the `min="1"` and hint the event-level field already had, and a new wave opens at 1 rather than 0. That default uses `??`, not `||` — a sold-out wave holds 0 and must keep showing it instead of silently regaining stock when saved. Also drops the stale `0 = unlimited / not ticketed` contract still documented on `CreateEvent.amount_tickets`, which has not been true since #62. 6 new tests; 120 pass. ruff, black, prettier clean; mypy error set unchanged from baseline.
1202 lines
41 KiB
Vue
1202 lines
41 KiB
Vue
<template id="page-events">
|
|
<div class="row q-col-gutter-md">
|
|
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
|
<q-card v-if="isAdmin">
|
|
<q-card-section>
|
|
<div class="row items-center justify-between">
|
|
<div class="col">
|
|
<span class="text-subtitle1">Settings</span>
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-toggle
|
|
v-model="settings.auto_approve"
|
|
label="Auto-approve events"
|
|
@update:model-value="saveSettings"
|
|
></q-toggle>
|
|
</div>
|
|
</div>
|
|
<q-separator class="q-my-md"></q-separator>
|
|
<div class="row items-center justify-between">
|
|
<div class="col">
|
|
<span class="text-subtitle2">Republish to Nostr</span>
|
|
<div class="text-caption text-grey-7" style="color: #aaa">
|
|
Re-emit every approved event so connected clients pick up the
|
|
latest tag set. Useful after the extension publisher changes
|
|
(e.g. new tickets_* tags) so existing events don't need a
|
|
per-event edit.
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-btn
|
|
outline
|
|
color="primary"
|
|
icon="cloud_upload"
|
|
label="Republish all"
|
|
:loading="republishing"
|
|
@click="republishAllEvents"
|
|
></q-btn>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card>
|
|
<q-card-section>
|
|
<div class="row items-center q-gutter-sm">
|
|
<q-btn unelevated color="primary" @click="openEventDialog"
|
|
>New Event</q-btn
|
|
>
|
|
<q-btn
|
|
outline
|
|
color="primary"
|
|
icon="cloud_upload"
|
|
label="Republish mine"
|
|
:loading="republishingMine"
|
|
@click="republishMyEvents"
|
|
></q-btn>
|
|
</div>
|
|
<div class="text-caption q-mt-sm" style="color: #aaa">
|
|
Re-emit your approved events to Nostr relays. Useful after a
|
|
publisher upgrade or if a relay dropped your events.
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card v-if="pendingEvents.length > 0">
|
|
<q-card-section>
|
|
<div class="row items-center no-wrap q-mb-md">
|
|
<div class="col">
|
|
<h5 class="text-subtitle1 q-my-none">
|
|
<q-icon name="pending" color="orange" class="q-mr-sm"></q-icon>
|
|
Pending Approvals
|
|
<q-badge
|
|
color="orange"
|
|
:label="pendingEvents.length"
|
|
class="q-ml-sm"
|
|
></q-badge>
|
|
</h5>
|
|
</div>
|
|
</div>
|
|
<q-list separator>
|
|
<q-item v-for="event in pendingEvents" :key="event.id">
|
|
<q-item-section>
|
|
<q-item-label v-text="event.name"></q-item-label>
|
|
<q-item-label caption>
|
|
<span v-text="event.event_start_date"></span>
|
|
—
|
|
<span v-text="event.info.substring(0, 80)"></span
|
|
><span v-if="event.info.length > 80">...</span>
|
|
</q-item-label>
|
|
<q-item-label caption>
|
|
<span v-text="event.amount_tickets"></span> tickets •
|
|
<span v-text="event.price_per_ticket"></span>
|
|
<span v-text="event.currency"></span>
|
|
</q-item-label>
|
|
</q-item-section>
|
|
<q-item-section side>
|
|
<div class="row q-gutter-sm">
|
|
<q-btn
|
|
dense
|
|
color="green"
|
|
icon="check_circle"
|
|
label="Approve"
|
|
size="sm"
|
|
@click="approveEvent(event.id)"
|
|
></q-btn>
|
|
<q-btn
|
|
dense
|
|
outline
|
|
color="red"
|
|
icon="block"
|
|
label="Reject"
|
|
size="sm"
|
|
@click="rejectEvent(event.id)"
|
|
></q-btn>
|
|
</div>
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card>
|
|
<q-card-section>
|
|
<div class="row items-center no-wrap q-mb-md">
|
|
<div class="col">
|
|
<h5 class="text-subtitle1 q-my-none">Events</h5>
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-btn flat color="grey" @click="exporteventsCSV"
|
|
>Export to CSV</q-btn
|
|
>
|
|
</div>
|
|
</div>
|
|
<q-table
|
|
dense
|
|
flat
|
|
:rows="events"
|
|
row-key="id"
|
|
:columns="eventsTable.columns"
|
|
v-model:pagination="eventsTable.pagination"
|
|
>
|
|
<template v-slot:header="props">
|
|
<q-tr :props="props">
|
|
<q-th auto-width></q-th>
|
|
<q-th auto-width></q-th>
|
|
<q-th auto-width></q-th>
|
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
<span v-text="col.label"></span>
|
|
</q-th>
|
|
</q-tr>
|
|
</template>
|
|
<template v-slot:body="props">
|
|
<q-tr :props="props">
|
|
<q-td auto-width>
|
|
<q-btn
|
|
size="sm"
|
|
color="accent"
|
|
round
|
|
dense
|
|
@click="props.expand = !props.expand"
|
|
:icon="props.expand ? 'expand_less' : 'expand_more'"
|
|
/>
|
|
</q-td>
|
|
<q-td auto-width>
|
|
<q-btn
|
|
unelevated
|
|
dense
|
|
size="xs"
|
|
icon="link"
|
|
:color="$q.dark.isActive ? 'grey-7' : 'grey-5'"
|
|
type="a"
|
|
:href="'/events/' + props.row.id"
|
|
target="_blank"
|
|
></q-btn>
|
|
<q-btn
|
|
unelevated
|
|
dense
|
|
size="xs"
|
|
icon="how_to_reg"
|
|
:color="$q.dark.isActive ? 'grey-7' : 'grey-5'"
|
|
type="a"
|
|
:href="'/events/register/' + props.row.id"
|
|
target="_blank"
|
|
class="q-ml-xs"
|
|
></q-btn>
|
|
</q-td>
|
|
<q-td auto-width>
|
|
<q-btn
|
|
v-if="isAdmin && props.row.status === 'proposed'"
|
|
flat
|
|
dense
|
|
size="xs"
|
|
@click="approveEvent(props.row.id)"
|
|
icon="check_circle"
|
|
color="green"
|
|
>
|
|
<q-tooltip>Approve</q-tooltip>
|
|
</q-btn>
|
|
<q-btn
|
|
v-if="isAdmin && props.row.status === 'proposed'"
|
|
flat
|
|
dense
|
|
size="xs"
|
|
@click="rejectEvent(props.row.id)"
|
|
icon="block"
|
|
color="red"
|
|
>
|
|
<q-tooltip>Reject</q-tooltip>
|
|
</q-btn>
|
|
<q-btn
|
|
flat
|
|
dense
|
|
size="xs"
|
|
@click="updateformDialog(props.row.id)"
|
|
icon="edit"
|
|
color="light-blue"
|
|
></q-btn>
|
|
<q-btn
|
|
flat
|
|
dense
|
|
size="xs"
|
|
@click="deleteEvent(props.row.id)"
|
|
icon="cancel"
|
|
color="pink"
|
|
class="q-ml-xs"
|
|
></q-btn>
|
|
</q-td>
|
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
<q-badge
|
|
v-if="col.name === 'status'"
|
|
:color="
|
|
col.value === 'approved'
|
|
? 'green'
|
|
: col.value === 'proposed'
|
|
? 'orange'
|
|
: 'red'
|
|
"
|
|
:label="col.value"
|
|
></q-badge>
|
|
<span v-else v-text="col.value"></span>
|
|
</q-td>
|
|
</q-tr>
|
|
<q-tr v-show="props.expand" :props="props">
|
|
<q-td colspan="100%">
|
|
<div class="q-pa-md">
|
|
<div class="row items-center q-mb-md">
|
|
<div class="text-subtitle1">Ticket waves</div>
|
|
<q-btn
|
|
round
|
|
dense
|
|
unelevated
|
|
color="primary"
|
|
icon="add"
|
|
class="q-ml-sm"
|
|
@click="openTicketWaveDialog(props.row)"
|
|
></q-btn>
|
|
</div>
|
|
<div class="column q-mb-lg">
|
|
<div class="column">
|
|
<div
|
|
v-for="(wave, index) in props.row.extra.ticket_waves"
|
|
:key="wave.id || index"
|
|
class="q-mb-sm"
|
|
>
|
|
<q-chip
|
|
square
|
|
clickable
|
|
class="q-py-xs"
|
|
style="height: auto"
|
|
@click="openTicketWaveDialog(props.row, wave)"
|
|
>
|
|
<span
|
|
style="white-space: normal; line-height: 1.3"
|
|
v-text="
|
|
`${wave.title} - ${wave.opening_date} to ${
|
|
wave.closing_date
|
|
} - ${
|
|
isFiatCurrency(wave.currency)
|
|
? LNbits.utils.formatCurrency(
|
|
Number(
|
|
wave.price_per_ticket || 0
|
|
).toFixed(2),
|
|
wave.currency
|
|
)
|
|
: `${wave.price_per_ticket} sats`
|
|
} - ${wave.amount_tickets} tickets - ${soldTicketsForWave(
|
|
props.row.id,
|
|
wave.id
|
|
)} sold`
|
|
"
|
|
></span>
|
|
</q-chip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row items-center q-mb-md">
|
|
<div class="text-subtitle1">Promo codes</div>
|
|
<q-btn
|
|
round
|
|
dense
|
|
unelevated
|
|
color="primary"
|
|
icon="edit"
|
|
class="q-ml-sm"
|
|
@click="openPromoCodesDialog(props.row)"
|
|
></q-btn>
|
|
</div>
|
|
<div class="column">
|
|
<div
|
|
v-if="
|
|
props.row.extra.promo_codes.filter(
|
|
code => code.active
|
|
).length == 0
|
|
"
|
|
class="text-caption"
|
|
>
|
|
No active promo codes for this event.
|
|
</div>
|
|
<div class="row q-col-gutter-sm">
|
|
<div
|
|
v-for="(
|
|
code, index
|
|
) in props.row.extra.promo_codes.filter(
|
|
code => code.active
|
|
)"
|
|
:key="index"
|
|
class="col-auto q-mb-sm"
|
|
>
|
|
<q-chip
|
|
square
|
|
clickable
|
|
@click="utils.copyText(code.code.toUpperCase())"
|
|
>
|
|
<span
|
|
v-text="
|
|
`${code.code.toUpperCase()} - ${code.discount_percent}%`
|
|
"
|
|
></span>
|
|
</q-chip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
</q-table>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card>
|
|
<q-card-section>
|
|
<div class="row items-center no-wrap q-mb-md">
|
|
<div class="col">
|
|
<h5 class="text-subtitle1 q-my-none">Tickets</h5>
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-btn flat color="grey" @click="exportticketsCSV"
|
|
>Export to CSV</q-btn
|
|
>
|
|
</div>
|
|
</div>
|
|
<q-table
|
|
dense
|
|
flat
|
|
:rows="tickets"
|
|
:loading="ticketsTable.loading"
|
|
row-key="id"
|
|
:columns="ticketsTable.columns"
|
|
v-model:pagination="ticketsTable.pagination"
|
|
@request="getTickets"
|
|
>
|
|
<template v-slot:header="props">
|
|
<q-tr :props="props">
|
|
<q-th auto-width></q-th>
|
|
<q-th auto-width></q-th>
|
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
<span v-text="col.label"></span>
|
|
</q-th>
|
|
<q-th auto-width></q-th>
|
|
</q-tr>
|
|
</template>
|
|
<template v-slot:body="props">
|
|
<q-tr :props="props">
|
|
<q-td auto-width>
|
|
<q-btn
|
|
unelevated
|
|
dense
|
|
size="xs"
|
|
icon="local_activity"
|
|
:color="$q.dark.isActive ? 'grey-7' : 'grey-5'"
|
|
type="a"
|
|
:href="'/events/ticket/' + props.row.id"
|
|
target="_blank"
|
|
></q-btn>
|
|
</q-td>
|
|
<q-td auto-width>
|
|
<q-btn
|
|
flat
|
|
dense
|
|
size="xs"
|
|
@click="resendTicketEmail(props.row)"
|
|
icon="email"
|
|
color="primary"
|
|
:disable="!props.row.paid || !props.row.email"
|
|
:loading="resendingTicketEmails.includes(props.row.id)"
|
|
>
|
|
<q-tooltip>Resend ticket email</q-tooltip>
|
|
</q-btn>
|
|
</q-td>
|
|
|
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
<span v-text="col.value"></span>
|
|
</q-td>
|
|
|
|
<q-td auto-width>
|
|
<q-btn
|
|
flat
|
|
dense
|
|
size="xs"
|
|
@click="deleteTicket(props.row.id)"
|
|
icon="cancel"
|
|
color="pink"
|
|
></q-btn>
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
</q-table>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card v-if="isAdmin && allUserEvents.length > 0">
|
|
<q-card-section>
|
|
<div class="row items-center no-wrap q-mb-md">
|
|
<div class="col">
|
|
<h5 class="text-subtitle1 q-my-none">
|
|
All Users' Events
|
|
<q-badge
|
|
color="blue"
|
|
:label="allUserEvents.length"
|
|
class="q-ml-sm"
|
|
></q-badge>
|
|
</h5>
|
|
</div>
|
|
</div>
|
|
<q-table
|
|
dense
|
|
flat
|
|
:rows="allUserEvents"
|
|
row-key="id"
|
|
:columns="allUsersEventsTable.columns"
|
|
:pagination="{rowsPerPage: 10}"
|
|
>
|
|
<template v-slot:header="props">
|
|
<q-tr :props="props">
|
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
<span v-text="col.label"></span>
|
|
</q-th>
|
|
</q-tr>
|
|
</template>
|
|
<template v-slot:body="props">
|
|
<q-tr :props="props">
|
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
<q-badge
|
|
v-if="col.name === 'status'"
|
|
:color="
|
|
col.value === 'approved'
|
|
? 'green'
|
|
: col.value === 'proposed'
|
|
? 'orange'
|
|
: 'red'
|
|
"
|
|
:label="col.value"
|
|
></q-badge>
|
|
<span v-else v-text="col.value"></span>
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
</q-table>
|
|
</q-card-section>
|
|
</q-card>
|
|
</div>
|
|
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
|
<q-card>
|
|
<q-card-section>
|
|
<h6 class="text-subtitle1 ellipsis q-my-none">
|
|
<span v-text="SITE_TITLE"></span>
|
|
Events extension
|
|
</h6>
|
|
</q-card-section>
|
|
<q-card-section class="q-pa-none">
|
|
<q-separator></q-separator>
|
|
<q-list>
|
|
<q-expansion-item
|
|
group="extras"
|
|
icon="swap_vertical_circle"
|
|
label="Info"
|
|
:content-inset-level="0.5"
|
|
>
|
|
<q-card>
|
|
<q-card-section>
|
|
<h5 class="text-subtitle1 q-my-none">
|
|
Events: Sell and register ticket waves for an event
|
|
</h5>
|
|
<p>
|
|
Events allows you to make a wave of tickets for an event,
|
|
each ticket is in the form of a unique QRcode, which the
|
|
user presents at registration. Events comes with a shareable
|
|
ticket scanner, which can be used to register attendees.<br />
|
|
<small>
|
|
Created by,
|
|
<a class="text-secondary" href="https://github.com/benarc"
|
|
>Ben Arc</a
|
|
>
|
|
</small>
|
|
</p>
|
|
</q-card-section>
|
|
</q-card>
|
|
<q-btn
|
|
flat
|
|
label="Swagger API"
|
|
type="a"
|
|
href="../docs#/events"
|
|
></q-btn>
|
|
</q-expansion-item>
|
|
</q-list>
|
|
</q-card-section>
|
|
</q-card>
|
|
</div>
|
|
|
|
<q-dialog v-model="formDialog.show" position="top">
|
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
<q-form @submit="sendEventData" class="q-gutter-md">
|
|
<div class="row">
|
|
<div class="col">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.name"
|
|
type="text"
|
|
label="Title of event *"
|
|
lazy-rules
|
|
:rules="[val => !!val || 'Title is required']"
|
|
></q-input>
|
|
</div>
|
|
<div class="col q-pl-sm">
|
|
<q-select
|
|
filled
|
|
dense
|
|
emit-value
|
|
v-model="formDialog.data.wallet"
|
|
:options="g.user.walletOptions"
|
|
label="Wallet *"
|
|
>
|
|
</q-select>
|
|
</div>
|
|
</div>
|
|
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.info"
|
|
type="textarea"
|
|
label="Info about the event"
|
|
hint="Markdown supported"
|
|
></q-input>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.location"
|
|
type="text"
|
|
label="Location"
|
|
hint="Venue or address, e.g. Salle des fêtes, Foix. Published as the NIP-52 location tag."
|
|
>
|
|
<template v-slot:prepend>
|
|
<q-icon name="place"></q-icon>
|
|
</template>
|
|
</q-input>
|
|
<q-select
|
|
filled
|
|
dense
|
|
multiple
|
|
use-chips
|
|
emit-value
|
|
map-options
|
|
v-model="formDialog.data.categories"
|
|
:options="categoryOptions"
|
|
label="Categories"
|
|
hint="Published as NIP-52 hashtags so clients can filter the feed."
|
|
></q-select>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.banner"
|
|
type="url"
|
|
label="Image URL"
|
|
hint="Optional banner image to display on the event page"
|
|
></q-input>
|
|
<div class="row q-col-gutter-sm">
|
|
<div class="col-4">Event begins *</div>
|
|
<div class="col-5">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.event_start_day"
|
|
type="date"
|
|
lazy-rules
|
|
:rules="[val => !!val || 'Start date is required']"
|
|
></q-input>
|
|
</div>
|
|
<div class="col-3">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.event_start_time"
|
|
type="time"
|
|
hint="Optional"
|
|
></q-input>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row q-col-gutter-sm">
|
|
<div class="col-4">Event ends</div>
|
|
<div class="col-5">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.event_end_day"
|
|
type="date"
|
|
hint="Defaults to the start date"
|
|
lazy-rules
|
|
reactive-rules
|
|
:rules="[validateEndDate]"
|
|
></q-input>
|
|
</div>
|
|
<div class="col-3">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.event_end_time"
|
|
type="time"
|
|
hint="Optional"
|
|
></q-input>
|
|
</div>
|
|
</div>
|
|
<div class="row q-col-gutter-sm">
|
|
<div class="col-4">Ticket sales close</div>
|
|
<div class="col-8">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.closing_date"
|
|
type="date"
|
|
hint="Optional. Defaults to the event end date."
|
|
></q-input>
|
|
</div>
|
|
</div>
|
|
<q-separator class="q-my-md"></q-separator>
|
|
<div class="text-subtitle1 q-mt-lg q-mb-md">
|
|
Primary ticket wave (you can add other waves later)
|
|
</div>
|
|
<div class="row q-col-gutter-sm">
|
|
<div class="col-12 col-md-4">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="primaryTicketWave().title"
|
|
type="text"
|
|
label="Wave title"
|
|
></q-input>
|
|
</div>
|
|
<div class="col-12 col-md-4">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="primaryTicketWave().opening_date"
|
|
type="date"
|
|
label="Ticket opening date"
|
|
></q-input>
|
|
</div>
|
|
<div class="col-12 col-md-4">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.closing_date"
|
|
type="date"
|
|
label="Ticket closing date"
|
|
></q-input>
|
|
</div>
|
|
</div>
|
|
<div class="row q-col-gutter-sm">
|
|
<div class="col">
|
|
<q-select
|
|
filled
|
|
dense
|
|
v-model="formDialog.data.currency"
|
|
type="text"
|
|
label="Price currency"
|
|
:options="currencies"
|
|
></q-select>
|
|
</div>
|
|
<div class="col">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.number="formDialog.data.amount_tickets"
|
|
type="number"
|
|
min="1"
|
|
label="Amount of tickets"
|
|
hint="Total tickets on sale"
|
|
></q-input>
|
|
</div>
|
|
<div class="col">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.number="formDialog.data.price_per_ticket"
|
|
type="number"
|
|
:label="'Price (' + formDialog.data.currency + ') *'"
|
|
:step="formDialog.data.currency != 'sats' ? '0.01' : '1'"
|
|
:mask="formDialog.data.currency != 'sats' ? '#.##' : '#'"
|
|
fill-mask="0"
|
|
reverse-fill-mask
|
|
:disable="formDialog.data.currency == null"
|
|
></q-input>
|
|
</div>
|
|
</div>
|
|
<div class="q-mt-sm">
|
|
<div class="text-subtitle2">Payment methods *</div>
|
|
<div class="text-caption text-grey-7">
|
|
Pick the rails buyers can pay with. Untick Lightning for a
|
|
card-only sale.
|
|
</div>
|
|
<div class="row q-col-gutter-md q-mt-xs">
|
|
<div
|
|
v-for="opt in paymentMethodOptions"
|
|
:key="opt.value"
|
|
class="col-12 col-sm-6"
|
|
>
|
|
<!-- The span carries the tooltip: a disabled checkbox
|
|
swallows pointer events, the wrapper still hovers. -->
|
|
<span class="inline-block full-width">
|
|
<q-checkbox
|
|
v-model="formDialog.data.extra.payment_methods"
|
|
:val="opt.value"
|
|
:label="opt.label"
|
|
:disable="opt.value === 'fiat' && !hasFiatProvider"
|
|
></q-checkbox>
|
|
<div class="text-caption text-grey-7 q-pl-lg">
|
|
<span v-text="opt.hint"></span>
|
|
<span
|
|
v-if="opt.value === 'fiat' && hasFiatProvider"
|
|
v-text="' (' + fiatProviderNames + ')'"
|
|
></span>
|
|
</div>
|
|
<q-tooltip
|
|
v-if="opt.value === 'fiat' && !hasFiatProvider"
|
|
max-width="280px"
|
|
>
|
|
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.
|
|
</q-tooltip>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<q-toggle
|
|
v-model="primaryTicketWave().use_ticket_image"
|
|
label="Use ticket image"
|
|
left-label
|
|
></q-toggle>
|
|
<div
|
|
v-if="primaryTicketWave().use_ticket_image"
|
|
class="row items-center q-col-gutter-sm q-mb-sm"
|
|
>
|
|
<div class="col-auto">
|
|
<q-btn
|
|
unelevated
|
|
color="primary"
|
|
type="a"
|
|
:href="templateDownloadUrl()"
|
|
download="ticket.jpg"
|
|
>
|
|
Download template
|
|
<q-tooltip>400/733 jpg</q-tooltip>
|
|
</q-btn>
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-btn
|
|
outline
|
|
color="primary"
|
|
:loading="isUploadingTicketTemplate"
|
|
@click="triggerTicketImageUpload('primary')"
|
|
>Replace</q-btn
|
|
>
|
|
</div>
|
|
<div
|
|
v-if="primaryTicketWave().ticket_image_id"
|
|
class="col-12 text-caption"
|
|
>
|
|
Custom ticket template uploaded.
|
|
</div>
|
|
</div>
|
|
<q-toggle
|
|
v-model="formDialog.data.allow_fiat"
|
|
label="Allow fiat checkout"
|
|
left-label
|
|
hint="Lets attendees pay through a configured fiat provider using the event currency."
|
|
></q-toggle>
|
|
<q-select
|
|
v-if="acceptsFiat && isSatPrice"
|
|
filled
|
|
dense
|
|
v-model="formDialog.data.fiat_currency"
|
|
label="Fiat currency"
|
|
hint="Currency card buyers are charged in."
|
|
:options="fiatCurrencyOptions"
|
|
lazy-rules
|
|
:rules="[
|
|
val => !!val || 'Pick a fiat currency for buyers paying by card'
|
|
]"
|
|
></q-select>
|
|
<q-expansion-item
|
|
group="advanced"
|
|
icon="settings"
|
|
label="Advanced options"
|
|
>
|
|
<div class="row q-mt-lg">
|
|
<div class="text-subtitle1 q-mb-md">Conditional Events</div>
|
|
<div class="text-caption">
|
|
Make this event conditional if
|
|
<strong>minimum tickets</strong> are sold. User will be asked to
|
|
provide a Lightning Address or LNURL pay for refunds.
|
|
</div>
|
|
<div class="col-8">
|
|
<q-toggle
|
|
v-model="formDialog.data.extra.conditional"
|
|
label="Conditional Event"
|
|
left-label
|
|
></q-toggle>
|
|
</div>
|
|
<div class="col-4">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.number="formDialog.data.extra.min_tickets"
|
|
type="number"
|
|
label="Minimum Tickets"
|
|
:disable="!formDialog.data.extra.conditional"
|
|
></q-input>
|
|
</div>
|
|
</div>
|
|
<q-separator class="q-my-md"></q-separator>
|
|
<div class="text-subtitle1 q-mb-md">Ticket Delivery</div>
|
|
<div class="text-caption">
|
|
Send the paid ticket link automatically by email or Nostr DM.
|
|
</div>
|
|
<div class="row items-center q-col-gutter-md">
|
|
<div class="col-auto">
|
|
<q-toggle
|
|
v-model="formDialog.data.extra.email_notifications"
|
|
label="Email notifications"
|
|
left-label
|
|
></q-toggle>
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-toggle
|
|
v-model="formDialog.data.extra.nostr_notifications"
|
|
label="Nostr notifications"
|
|
left-label
|
|
></q-toggle>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="formDialog.data.extra.email_notifications"
|
|
class="q-mt-md"
|
|
>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.extra.notification_subject"
|
|
type="text"
|
|
label="Ticket notification subject"
|
|
hint="Used as the email subject when sending paid ticket links."
|
|
></q-input>
|
|
<q-input
|
|
class="q-mt-md"
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.extra.notification_body"
|
|
type="textarea"
|
|
label="Ticket notification body"
|
|
hint="Shown before the ticket link in the paid ticket notification."
|
|
></q-input>
|
|
</div>
|
|
</q-expansion-item>
|
|
|
|
<div class="row q-mt-lg">
|
|
<q-btn
|
|
v-if="formDialog.data.id"
|
|
unelevated
|
|
color="primary"
|
|
type="submit"
|
|
>Update Event</q-btn
|
|
>
|
|
<q-btn
|
|
v-else
|
|
unelevated
|
|
color="primary"
|
|
:disable="
|
|
!formDialog.data.wallet ||
|
|
!formDialog.data.name ||
|
|
!formDialog.data.event_start_day ||
|
|
primaryTicketWave().title == null ||
|
|
primaryTicketWave().opening_date == null
|
|
"
|
|
type="submit"
|
|
>Create Event</q-btn
|
|
>
|
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
>Cancel</q-btn
|
|
>
|
|
</div>
|
|
</q-form>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<q-dialog v-model="promoCodesDialog.show" position="top">
|
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
<q-form @submit="savePromoCodes" class="q-gutter-md">
|
|
<div class="text-subtitle1">Promo Codes</div>
|
|
<div class="text-caption">
|
|
Allow users to enter a promo code for discounts.
|
|
</div>
|
|
|
|
<div
|
|
v-for="(code, index) in promoCodesDialog.data.extra.promo_codes"
|
|
:key="index"
|
|
class="row q-col-gutter-sm q-mt-md"
|
|
>
|
|
<q-input
|
|
class="col-8"
|
|
filled
|
|
dense
|
|
v-model.trim="promoCodesDialog.data.extra.promo_codes[index].code"
|
|
type="text"
|
|
label="Promo Code"
|
|
>
|
|
<template v-slot:before>
|
|
<q-checkbox
|
|
left-label
|
|
v-model="
|
|
promoCodesDialog.data.extra.promo_codes[index].active
|
|
"
|
|
checked-icon="radio_button_checked"
|
|
unchecked-icon="radio_button_unchecked"
|
|
></q-checkbox>
|
|
<q-tooltip>
|
|
<span
|
|
v-text="
|
|
promoCodesDialog.data.extra.promo_codes[index].active
|
|
? 'Active'
|
|
: 'Inactive'
|
|
"
|
|
></span>
|
|
</q-tooltip>
|
|
</template>
|
|
</q-input>
|
|
<q-input
|
|
class="col-2"
|
|
filled
|
|
dense
|
|
v-model.number="
|
|
promoCodesDialog.data.extra.promo_codes[index].discount_percent
|
|
"
|
|
type="number"
|
|
label="Discount (%)"
|
|
:hint="
|
|
promoCodesDialog.data.extra.promo_codes[index].used_count
|
|
? 'Used ' +
|
|
promoCodesDialog.data.extra.promo_codes[index].used_count +
|
|
(promoCodesDialog.data.extra.promo_codes[index].max_uses
|
|
? ' / ' +
|
|
promoCodesDialog.data.extra.promo_codes[index].max_uses
|
|
: '')
|
|
: ''
|
|
"
|
|
min="0"
|
|
max="100"
|
|
>
|
|
<template v-slot:after>
|
|
<q-btn
|
|
round
|
|
dense
|
|
flat
|
|
icon="delete"
|
|
@click="
|
|
promoCodesDialog.data.extra.promo_codes.splice(index, 1)
|
|
"
|
|
></q-btn>
|
|
</template>
|
|
</q-input>
|
|
<q-input
|
|
class="col-2"
|
|
filled
|
|
dense
|
|
v-model.number="
|
|
promoCodesDialog.data.extra.promo_codes[index].max_uses
|
|
"
|
|
type="number"
|
|
label="Max uses"
|
|
placeholder="∞"
|
|
min="1"
|
|
hint="Blank = unlimited"
|
|
></q-input>
|
|
</div>
|
|
|
|
<div class="col-12 q-mt-md">
|
|
<q-btn @click="addPromoCodeToDialog">Add Promo Code</q-btn>
|
|
</div>
|
|
|
|
<div class="row q-mt-lg">
|
|
<q-btn unelevated color="primary" type="submit"
|
|
>Save Promo Codes</q-btn
|
|
>
|
|
<q-btn
|
|
flat
|
|
color="grey"
|
|
class="q-ml-auto"
|
|
@click="resetPromoCodesDialog"
|
|
>Cancel</q-btn
|
|
>
|
|
</div>
|
|
</q-form>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<q-dialog v-model="ticketWaveDialog.show" position="top">
|
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
<q-form @submit="saveTicketWave" class="q-gutter-md">
|
|
<div
|
|
class="text-subtitle1"
|
|
v-text="
|
|
ticketWaveDialog.editingWaveId
|
|
? 'Edit Ticket Wave'
|
|
: 'Add Ticket Wave'
|
|
"
|
|
></div>
|
|
<div class="row q-col-gutter-sm">
|
|
<div class="col-12 col-md-4">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="ticketWaveDialog.data.title"
|
|
type="text"
|
|
label="Wave title"
|
|
></q-input>
|
|
</div>
|
|
<div class="col-12 col-md-4">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="ticketWaveDialog.data.opening_date"
|
|
type="date"
|
|
label="Ticket opening date"
|
|
></q-input>
|
|
</div>
|
|
<div class="col-12 col-md-4">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="ticketWaveDialog.data.closing_date"
|
|
type="date"
|
|
label="Ticket closing date"
|
|
></q-input>
|
|
</div>
|
|
</div>
|
|
<div class="row q-col-gutter-sm">
|
|
<div class="col">
|
|
<q-select
|
|
filled
|
|
dense
|
|
v-model="ticketWaveDialog.data.currency"
|
|
type="text"
|
|
label="Unit"
|
|
:options="currencies"
|
|
></q-select>
|
|
</div>
|
|
<div class="col">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.number="ticketWaveDialog.data.amount_tickets"
|
|
type="number"
|
|
min="1"
|
|
label="Amount of tickets"
|
|
hint="Tickets on sale in this wave"
|
|
></q-input>
|
|
</div>
|
|
<div class="col">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.number="ticketWaveDialog.data.price_per_ticket"
|
|
type="number"
|
|
:label="'Price (' + ticketWaveDialog.data.currency + ') *'"
|
|
:step="ticketWaveDialog.data.currency != 'sats' ? '0.01' : '1'"
|
|
:mask="ticketWaveDialog.data.currency != 'sats' ? '#.##' : '#'"
|
|
fill-mask="0"
|
|
reverse-fill-mask
|
|
:disable="ticketWaveDialog.data.currency == null"
|
|
></q-input>
|
|
</div>
|
|
</div>
|
|
<q-toggle
|
|
v-model="ticketWaveDialog.data.use_ticket_image"
|
|
label="Use ticket image"
|
|
left-label
|
|
></q-toggle>
|
|
<div
|
|
v-if="ticketWaveDialog.data.use_ticket_image"
|
|
class="row items-center q-col-gutter-sm q-mb-sm"
|
|
>
|
|
<div class="col-auto">
|
|
<q-btn
|
|
unelevated
|
|
color="primary"
|
|
type="a"
|
|
:href="templateDownloadUrl()"
|
|
download="ticket.jpg"
|
|
>
|
|
Download template
|
|
<q-tooltip>400/733 jpg</q-tooltip>
|
|
</q-btn>
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-btn
|
|
outline
|
|
color="primary"
|
|
:loading="isUploadingTicketTemplate"
|
|
@click="triggerTicketImageUpload('dialog')"
|
|
>Replace</q-btn
|
|
>
|
|
</div>
|
|
<div
|
|
v-if="ticketWaveDialog.data.ticket_image_id"
|
|
class="col-12 text-caption"
|
|
>
|
|
Custom ticket template uploaded.
|
|
</div>
|
|
</div>
|
|
<q-toggle
|
|
v-model="ticketWaveDialog.data.allow_fiat"
|
|
label="Allow fiat checkout"
|
|
left-label
|
|
hint="Lets attendees pay through a configured fiat provider using this wave currency."
|
|
></q-toggle>
|
|
<q-select
|
|
v-if="
|
|
ticketWaveDialog.data.allow_fiat &&
|
|
['sat', 'sats'].includes(
|
|
(ticketWaveDialog.data.currency || '').toLowerCase()
|
|
)
|
|
"
|
|
filled
|
|
dense
|
|
v-model="ticketWaveDialog.data.fiat_currency"
|
|
label="Fiat checkout currency"
|
|
:options="
|
|
currencies.filter(
|
|
c => !['sat', 'sats'].includes((c || '').toLowerCase())
|
|
)
|
|
"
|
|
></q-select>
|
|
<div class="row q-mt-lg">
|
|
<q-btn unelevated color="primary" type="submit">{{
|
|
ticketWaveDialog.editingWaveId
|
|
? 'Update Ticket Wave'
|
|
: 'Save Ticket Wave'
|
|
}}</q-btn>
|
|
<q-btn
|
|
flat
|
|
color="grey"
|
|
class="q-ml-auto"
|
|
@click="resetTicketWaveDialog"
|
|
>Cancel</q-btn
|
|
>
|
|
</div>
|
|
</q-form>
|
|
</q-card>
|
|
</q-dialog>
|
|
<input
|
|
ref="ticketImageUpload"
|
|
type="file"
|
|
accept="image/png,image/jpeg,image/webp"
|
|
style="display: none"
|
|
@change="handleTicketImageSelected"
|
|
/>
|
|
</div>
|
|
</template>
|