Some checks failed
lint.yml / fix(nostr): always publish tickets_available, zero is not unlimited (pull_request) Failing after 0s
Omitting the tag used to mean "unlimited capacity". Nothing else in the codebase agreed: `api_get_event` and `api_ticket_create` both treat `amount_tickets < 1` as sold out. So a zero-capacity event advertised "Unlimited tickets" on the card while the detail page and the purchase both returned 410. Observed on aio-demo — three approved, listed, free events in that state. For `PKBVuusKikfJFU4PYGtBTW`: relay tickets_available absent -> webapp renders "Unlimited" GET event 410 "Event is sold out." POST ticket 410 "Event is sold out." The admin form was advertising it too (`min="0"`, `hint="0 = unlimited"`), so organizers were being invited into the broken state. Now the tag is always emitted and zero reads as sold out, which is what every other part of the system already believed. Clients that must handle an absent tag — a NIP-52 event from another publisher — are unaffected, since we simply never omit it. Scoped to removing the contradiction. Making capacity a *required* field is the other half of #34 and is blocked on the webapp: its create dialog uses a falsy check (`if (formValues.amount_tickets)`), so a 0 omits the field entirely and a server-side `ge=1` would 422 it. Refs #34
879 lines
30 KiB
Vue
879 lines
30 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="text-subtitle1 q-mb-md">Promo codes</div>
|
|
<div class="column">
|
|
<div
|
|
v-if="props.row.extra.promo_codes.length == 0"
|
|
class="text-caption"
|
|
>
|
|
No promo codes for this event.
|
|
</div>
|
|
<div
|
|
v-for="(code, index) in props.row.extra.promo_codes"
|
|
:key="index"
|
|
class="row items-center q-col-gutter-sm q-mb-sm"
|
|
>
|
|
<div class="col-auto">
|
|
<q-chip
|
|
square
|
|
size="md"
|
|
clickable
|
|
@click="utils.copyText(code.code.toUpperCase())"
|
|
>
|
|
<q-avatar
|
|
icon="bookmark"
|
|
:color="code.active ? 'green' : 'grey'"
|
|
text-color="white"
|
|
></q-avatar>
|
|
<span v-text="code.code.toUpperCase()"></span>
|
|
</q-chip>
|
|
</div>
|
|
<div class="col-auto">
|
|
Discount:
|
|
<span v-text="code.discount_percent"></span>%
|
|
</div>
|
|
<div class="col-auto">
|
|
Status:
|
|
<span
|
|
:class="code.active ? 'text-green' : 'text-grey'"
|
|
v-text="code.active ? 'Active' : 'Inactive'"
|
|
></span>
|
|
</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"
|
|
row-key="id"
|
|
:columns="ticketsTable.columns"
|
|
v-model:pagination="ticketsTable.pagination"
|
|
>
|
|
<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>
|
|
<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-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">Promo Codes</div>
|
|
<div class="text-caption">
|
|
Allow users to enter a promo code for discounts.
|
|
</div>
|
|
|
|
<div
|
|
v-for="(code, index) in formDialog.data.extra.promo_codes"
|
|
:key="index"
|
|
class="row q-col-gutter-sm q-mt-md"
|
|
>
|
|
<q-input
|
|
class="col-6"
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.extra.promo_codes[index].code"
|
|
type="text"
|
|
label="Promo Code"
|
|
:hint="
|
|
formDialog.data.extra.promo_codes[index].used_count
|
|
? 'Used ' +
|
|
formDialog.data.extra.promo_codes[index].used_count +
|
|
(formDialog.data.extra.promo_codes[index].max_uses
|
|
? ' / ' +
|
|
formDialog.data.extra.promo_codes[index].max_uses
|
|
: '')
|
|
: ''
|
|
"
|
|
>
|
|
<template v-slot:before>
|
|
<q-checkbox
|
|
left-label
|
|
v-model="formDialog.data.extra.promo_codes[index].active"
|
|
checked-icon="radio_button_checked"
|
|
unchecked-icon="radio_button_unchecked"
|
|
></q-checkbox>
|
|
<q-tooltip>
|
|
<span
|
|
v-text="
|
|
formDialog.data.extra.promo_codes[index].active
|
|
? 'Active'
|
|
: 'Inactive'
|
|
"
|
|
></span>
|
|
</q-tooltip>
|
|
</template>
|
|
</q-input>
|
|
<q-input
|
|
class="col-3"
|
|
filled
|
|
dense
|
|
v-model.number="
|
|
formDialog.data.extra.promo_codes[index].discount_percent
|
|
"
|
|
type="number"
|
|
label="Discount (%)"
|
|
min="0"
|
|
max="100"
|
|
></q-input>
|
|
<q-input
|
|
class="col-3"
|
|
filled
|
|
dense
|
|
v-model.number="
|
|
formDialog.data.extra.promo_codes[index].max_uses
|
|
"
|
|
type="number"
|
|
label="Max uses"
|
|
placeholder="∞"
|
|
min="1"
|
|
hint="Blank = unlimited"
|
|
>
|
|
<template v-slot:after>
|
|
<q-btn
|
|
round
|
|
dense
|
|
flat
|
|
icon="delete"
|
|
@click="formDialog.data.extra.promo_codes.splice(index, 1)"
|
|
></q-btn>
|
|
</template>
|
|
</q-input>
|
|
</div>
|
|
<div class="col-12 q-mt-md">
|
|
<q-btn
|
|
@click="
|
|
formDialog.data.extra.promo_codes.push({
|
|
code: '',
|
|
discount_percent: 0,
|
|
active: true,
|
|
max_uses: null
|
|
})
|
|
"
|
|
>Add Promo Code</q-btn
|
|
>
|
|
</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>
|
|
<q-toggle
|
|
v-model="formDialog.data.extra.email_notifications"
|
|
label="Email notifications"
|
|
left-label
|
|
></q-toggle>
|
|
<q-toggle
|
|
v-model="formDialog.data.extra.nostr_notifications"
|
|
label="Nostr notifications"
|
|
left-label
|
|
></q-toggle>
|
|
</q-expansion-item>
|
|
|
|
<q-separator class="q-my-md"></q-separator>
|
|
<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
|
|
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 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
|
|
"
|
|
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>
|
|
</div>
|
|
</template>
|