@frigear-nu/events 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.d.mts +105 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +40 -31
- package/dist/runtime/app/components/EventAddonGroupSlideover.d.vue.ts +18 -0
- package/dist/runtime/app/components/EventAddonGroupSlideover.vue +66 -0
- package/dist/runtime/app/components/EventAddonGroupSlideover.vue.d.ts +18 -0
- package/dist/runtime/app/components/EventAddonSlideover.d.vue.ts +20 -0
- package/dist/runtime/app/components/EventAddonSlideover.vue +103 -0
- package/dist/runtime/app/components/EventAddonSlideover.vue.d.ts +20 -0
- package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +1 -9
- package/dist/runtime/app/components/EventAdminAddonGroups.vue +6 -48
- package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +1 -9
- package/dist/runtime/app/components/EventFormSlideover.vue +45 -23
- package/dist/runtime/app/components/EventTicketCard.vue +59 -62
- package/dist/runtime/app/components/EventTicketTypeSlideover.d.vue.ts +20 -0
- package/dist/runtime/app/components/EventTicketTypeSlideover.vue +176 -0
- package/dist/runtime/app/components/EventTicketTypeSlideover.vue.d.ts +20 -0
- package/dist/runtime/app/composables/index.d.ts +2 -0
- package/dist/runtime/app/composables/index.js +1 -0
- package/dist/runtime/app/composables/useEventActions.d.ts +3 -3
- package/dist/runtime/app/composables/useEventActions.js +9 -4
- package/dist/runtime/app/composables/useEventCatalogActions.d.ts +33 -0
- package/dist/runtime/app/composables/useEventCatalogActions.js +107 -0
- package/dist/runtime/app/composables/useEvents.d.ts +7 -6
- package/dist/runtime/app/composables/useEvents.js +17 -10
- package/dist/runtime/app/pages/admin-event.vue +71 -136
- package/dist/runtime/app/pages/admin.vue +91 -29
- package/dist/runtime/app/pages/event.vue +8 -4
- package/dist/runtime/app/pages/tickets.vue +24 -2
- package/dist/runtime/app/tabs/Attendees.vue +78 -39
- package/dist/runtime/app/tabs/Details.d.vue.ts +1 -1
- package/dist/runtime/app/tabs/Details.vue +112 -30
- package/dist/runtime/app/tabs/Details.vue.d.ts +1 -1
- package/dist/runtime/app/tabs/Tickets.vue +105 -132
- package/dist/runtime/app/utils/admin-event.d.ts +53 -38
- package/dist/runtime/app/utils/admin-event.js +14 -0
- package/dist/runtime/app/widgets/UpcomingEvents.vue +2 -1
- package/dist/runtime/app/widgets/UpcomingEventsStat.vue +2 -1
- package/dist/runtime/i18n/lang/da.json +24 -3
- package/dist/runtime/i18n/lang/en.json +24 -3
- package/dist/runtime/server/bootstrap/my-tickets.d.ts +7 -0
- package/dist/runtime/server/{routes/public/tickets-me.get.js → bootstrap/my-tickets.js} +2 -2
- package/dist/runtime/server/routes/admin/addon-group.delete.js +6 -3
- package/dist/runtime/server/routes/admin/addon-group.patch.js +6 -4
- package/dist/runtime/server/routes/admin/addon-groups.post.js +8 -6
- package/dist/runtime/server/routes/admin/addon.delete.js +6 -3
- package/dist/runtime/server/routes/admin/addon.patch.js +8 -5
- package/dist/runtime/server/routes/admin/addons.post.js +8 -6
- package/dist/runtime/server/routes/admin/attendees.get.js +38 -8
- package/dist/runtime/server/routes/{public → admin}/check-in.post.js +4 -3
- package/dist/runtime/server/routes/admin/event-clone.post.js +37 -19
- package/dist/runtime/server/routes/admin/event.delete.js +7 -4
- package/dist/runtime/server/routes/admin/event.get.js +4 -2
- package/dist/runtime/server/routes/admin/event.patch.js +4 -2
- package/dist/runtime/server/routes/admin/events.get.js +36 -3
- package/dist/runtime/server/routes/admin/events.post.js +3 -4
- package/dist/runtime/server/routes/admin/membership-options.get.js +3 -2
- package/dist/runtime/server/routes/admin/ticket-cancel.post.js +7 -8
- package/dist/runtime/server/routes/admin/ticket-refund.post.js +7 -5
- package/dist/runtime/server/routes/admin/ticket-type.delete.js +7 -4
- package/dist/runtime/server/routes/admin/ticket-type.patch.js +4 -2
- package/dist/runtime/server/routes/admin/ticket-types.post.js +10 -7
- package/dist/runtime/server/routes/admin/upcoming.get.js +7 -3
- package/dist/runtime/server/routes/me/ticket-pay.post.js +13 -0
- package/dist/runtime/server/routes/me/ticket.get.js +6 -0
- package/dist/runtime/server/routes/me/tickets.get.js +2 -0
- package/dist/runtime/server/routes/public/event.get.js +5 -2
- package/dist/runtime/server/routes/public/purchase.post.js +3 -1
- package/dist/runtime/server/utils/admin-dto.d.ts +30 -0
- package/dist/runtime/server/utils/admin-dto.js +101 -0
- package/dist/runtime/server/utils/admin-schemas.js +2 -1
- package/dist/runtime/server/utils/admin.d.ts +3 -1
- package/dist/runtime/server/utils/admin.js +20 -6
- package/dist/runtime/server/utils/auth.d.ts +5 -4
- package/dist/runtime/server/utils/auth.js +6 -7
- package/dist/runtime/server/utils/tickets.js +6 -11
- package/dist/runtime/utils/api.d.ts +48 -0
- package/dist/runtime/utils/api.js +51 -0
- package/dist/runtime/utils/index.d.ts +2 -1
- package/dist/runtime/utils/index.js +1 -0
- package/dist/types.d.mts +1 -1
- package/package.json +21 -16
- package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +0 -11
- package/dist/runtime/app/components/EventAdminAddonDialog.vue +0 -112
- package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +0 -11
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +0 -11
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +0 -78
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +0 -11
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +0 -20
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +0 -174
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +0 -20
- package/dist/runtime/server/routes/public/ticket-pay.post.js +0 -10
- package/dist/runtime/server/routes/public/ticket.get.js +0 -4
- /package/dist/runtime/server/routes/{public → admin}/check-in.post.d.ts +0 -0
- /package/dist/runtime/server/routes/{public → me}/ticket-pay.post.d.ts +0 -0
- /package/dist/runtime/server/routes/{public → me}/ticket.get.d.ts +0 -0
- /package/dist/runtime/server/routes/{public/tickets-me.get.d.ts → me/tickets.get.d.ts} +0 -0
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { eventFormSchema } from "../utils/admin-event";
|
|
3
|
+
import { eventsApi } from "../../utils/api";
|
|
2
4
|
defineOptions({ name: "EventFormSlideover" });
|
|
3
5
|
const emit = defineEmits(["close"]);
|
|
4
6
|
const t = useModuleT("events");
|
|
5
7
|
const tr = useEventsTranslate();
|
|
6
8
|
const { busy, run } = useAction();
|
|
7
|
-
const
|
|
9
|
+
const schema = eventFormSchema(t, tr);
|
|
10
|
+
const state = reactive({ name: void 0, slug: "", startsAt: "", endsAt: "", status: "draft", location: void 0 });
|
|
8
11
|
const slugify = (value) => value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
9
|
-
const draftName = computed(() => tr(
|
|
12
|
+
const draftName = computed(() => tr(state.name));
|
|
10
13
|
watch(draftName, (name, previous) => {
|
|
11
|
-
if (
|
|
14
|
+
if (state.slug === slugify(previous)) state.slug = slugify(name);
|
|
12
15
|
});
|
|
13
|
-
async function save() {
|
|
16
|
+
async function save(event) {
|
|
17
|
+
const { name, slug, startsAt, endsAt, location } = event.data;
|
|
14
18
|
const created = await run(
|
|
15
|
-
() => $fetch(
|
|
19
|
+
() => $fetch(eventsApi.admin.events, {
|
|
16
20
|
method: "POST",
|
|
17
|
-
body: { name
|
|
21
|
+
body: { name, slug, startsAt, endsAt: endsAt || null, location: location || null, status: "draft" }
|
|
18
22
|
}),
|
|
19
23
|
{ success: t("admin.created", "Event created") }
|
|
20
24
|
);
|
|
@@ -29,42 +33,60 @@ async function save() {
|
|
|
29
33
|
:description="t('admin.createDescription', 'It is saved as a draft until you publish it.')"
|
|
30
34
|
:submit-label="t('admin.createAction', 'Create draft')"
|
|
31
35
|
:loading="busy"
|
|
32
|
-
:disabled="!draftName"
|
|
33
36
|
@close="emit('close', false)"
|
|
34
37
|
>
|
|
35
|
-
<
|
|
38
|
+
<UForm
|
|
36
39
|
id="event-form"
|
|
40
|
+
:schema="schema"
|
|
41
|
+
:state="state"
|
|
37
42
|
class="flex flex-col gap-4"
|
|
38
|
-
@submit
|
|
43
|
+
@submit="save"
|
|
39
44
|
>
|
|
40
|
-
<UFormField
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
<UFormField
|
|
46
|
+
name="name"
|
|
47
|
+
:label="t('admin.name', 'Name')"
|
|
48
|
+
required
|
|
49
|
+
>
|
|
50
|
+
<TranslatedInput v-model="state.name" />
|
|
45
51
|
</UFormField>
|
|
46
52
|
<UFormField
|
|
53
|
+
name="slug"
|
|
47
54
|
:label="t('admin.slug', 'URL name')"
|
|
48
55
|
:description="t('admin.slugHelp', 'Lowercase letters, numbers and dashes.')"
|
|
56
|
+
required
|
|
57
|
+
>
|
|
58
|
+
<UInput
|
|
59
|
+
v-model="state.slug"
|
|
60
|
+
class="w-full"
|
|
61
|
+
/>
|
|
62
|
+
</UFormField>
|
|
63
|
+
<UFormField
|
|
64
|
+
name="startsAt"
|
|
65
|
+
:label="t('admin.starts', 'Starts')"
|
|
66
|
+
required
|
|
49
67
|
>
|
|
50
68
|
<UInput
|
|
51
|
-
v-model="
|
|
69
|
+
v-model="state.startsAt"
|
|
70
|
+
type="datetime-local"
|
|
52
71
|
class="w-full"
|
|
53
|
-
required
|
|
54
|
-
pattern="[a-z0-9]+(-[a-z0-9]+)*"
|
|
55
72
|
/>
|
|
56
73
|
</UFormField>
|
|
57
|
-
<UFormField
|
|
74
|
+
<UFormField
|
|
75
|
+
name="endsAt"
|
|
76
|
+
:label="t('admin.ends', 'Ends')"
|
|
77
|
+
>
|
|
58
78
|
<UInput
|
|
59
|
-
v-model="
|
|
79
|
+
v-model="state.endsAt"
|
|
60
80
|
type="datetime-local"
|
|
61
81
|
class="w-full"
|
|
62
|
-
required
|
|
63
82
|
/>
|
|
64
83
|
</UFormField>
|
|
65
|
-
<UFormField
|
|
66
|
-
|
|
84
|
+
<UFormField
|
|
85
|
+
name="location"
|
|
86
|
+
:label="t('admin.location', 'Location')"
|
|
87
|
+
>
|
|
88
|
+
<TranslatedInput v-model="state.location" />
|
|
67
89
|
</UFormField>
|
|
68
|
-
</
|
|
90
|
+
</UForm>
|
|
69
91
|
</FormSlideover>
|
|
70
92
|
</template>
|
|
@@ -8,74 +8,71 @@ const emit = defineEmits(["pay"]);
|
|
|
8
8
|
const t = useModuleT("events");
|
|
9
9
|
const format = useFormat();
|
|
10
10
|
const tr = useEventsTranslate();
|
|
11
|
+
const description = computed(() => [props.ticket.ticketTypeName, ...props.ticket.addons.map((addon) => addon.label)].map((value) => tr(value)).filter(Boolean).join(" \xB7 "));
|
|
11
12
|
const qr = computed(() => props.ticket.code ? `data:image/svg+xml;utf8,${encodeURIComponent(renderSVG(props.ticket.code, { border: 1 }))}` : null);
|
|
12
13
|
const showQr = ref(false);
|
|
14
|
+
const canPay = computed(() => props.ticket.status === "pending");
|
|
15
|
+
const hasActions = computed(() => !!props.to || !!qr.value || canPay.value);
|
|
13
16
|
</script>
|
|
14
17
|
|
|
15
18
|
<template>
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
variant="subtle"
|
|
45
|
-
class="ml-2"
|
|
46
|
-
>
|
|
47
|
-
{{ t("checkedIn", "Checked in") }}
|
|
48
|
-
</UBadge>
|
|
49
|
-
</p>
|
|
50
|
-
<div class="flex gap-2">
|
|
51
|
-
<UButton
|
|
52
|
-
v-if="ticket.status === 'pending'"
|
|
53
|
-
@click="emit('pay', ticket.id)"
|
|
54
|
-
>
|
|
55
|
-
{{ t("tickets.payNow", "Pay now") }}
|
|
56
|
-
</UButton>
|
|
57
|
-
<UButton
|
|
58
|
-
v-if="qr"
|
|
59
|
-
variant="subtle"
|
|
60
|
-
icon="i-lucide-qr-code"
|
|
61
|
-
@click="showQr = !showQr"
|
|
62
|
-
>
|
|
63
|
-
{{ showQr ? t("hideQr", "Hide QR code") : t("tickets.viewQrCode", "Show QR code") }}
|
|
64
|
-
</UButton>
|
|
65
|
-
<UButton
|
|
66
|
-
v-if="to"
|
|
67
|
-
:to="to"
|
|
68
|
-
variant="ghost"
|
|
69
|
-
class="ml-auto"
|
|
19
|
+
<SectionCard
|
|
20
|
+
:title="tr(ticket.eventName)"
|
|
21
|
+
:description="description"
|
|
22
|
+
>
|
|
23
|
+
<template #actions>
|
|
24
|
+
<StatusBadge
|
|
25
|
+
:status="ticket.status"
|
|
26
|
+
:label="t(`tickets.status.${ticket.status}`, ticket.status)"
|
|
27
|
+
:colors="{ refunded: 'error' }"
|
|
28
|
+
/>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<div class="flex flex-col gap-3">
|
|
32
|
+
<p class="flex flex-wrap items-center gap-2 text-sm">
|
|
33
|
+
<span class="tabular-nums">{{ ticket.amount === 0 ? t("detail.tickets.free", "Free") : format.money(ticket.amount, ticket.currency) }}</span>
|
|
34
|
+
<UBadge
|
|
35
|
+
v-if="ticket.checkedInAt"
|
|
36
|
+
color="info"
|
|
37
|
+
variant="subtle"
|
|
38
|
+
icon="i-lucide-check"
|
|
39
|
+
:label="t('checkedIn', 'Checked in')"
|
|
40
|
+
/>
|
|
41
|
+
</p>
|
|
42
|
+
<img
|
|
43
|
+
v-if="showQr && qr"
|
|
44
|
+
:src="qr"
|
|
45
|
+
:alt="t('tickets.qrAlt', 'Ticket QR code')"
|
|
46
|
+
class="mx-auto w-56 rounded-md bg-white p-2"
|
|
70
47
|
>
|
|
71
|
-
{{ t("viewEvent", "View event") }}
|
|
72
|
-
</UButton>
|
|
73
48
|
</div>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class="mx-auto w-56 rounded-md bg-white p-2"
|
|
49
|
+
|
|
50
|
+
<template
|
|
51
|
+
v-if="hasActions"
|
|
52
|
+
#footer
|
|
79
53
|
>
|
|
80
|
-
|
|
54
|
+
<div class="flex flex-wrap justify-end gap-2">
|
|
55
|
+
<UButton
|
|
56
|
+
v-if="to"
|
|
57
|
+
:to="to"
|
|
58
|
+
color="neutral"
|
|
59
|
+
variant="outline"
|
|
60
|
+
:label="t('viewEvent', 'View event')"
|
|
61
|
+
/>
|
|
62
|
+
<UButton
|
|
63
|
+
v-if="qr"
|
|
64
|
+
color="neutral"
|
|
65
|
+
variant="outline"
|
|
66
|
+
icon="i-lucide-qr-code"
|
|
67
|
+
:label="showQr ? t('hideQr', 'Hide QR code') : t('tickets.viewQrCode', 'Show QR code')"
|
|
68
|
+
@click="showQr = !showQr"
|
|
69
|
+
/>
|
|
70
|
+
<UButton
|
|
71
|
+
v-if="canPay"
|
|
72
|
+
:label="t('tickets.payNow', 'Pay now')"
|
|
73
|
+
@click="emit('pay', ticket.id)"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
</SectionCard>
|
|
81
78
|
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MembershipPlans, StaffTicketType } from '../utils/admin-event.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** The event a new ticket type is added to. */
|
|
4
|
+
eventId: string;
|
|
5
|
+
/** The ticket type to edit; leave out to add one. */
|
|
6
|
+
ticketType?: StaffTicketType;
|
|
7
|
+
/** Membership rules are only offered when @frigear-nu/subscriptions is installed (`enabled`). */
|
|
8
|
+
membership?: MembershipPlans;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
close: (saved: false | {
|
|
12
|
+
id: string;
|
|
13
|
+
}) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
15
|
+
onClose?: ((saved: false | {
|
|
16
|
+
id: string;
|
|
17
|
+
}) => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { toCents, fromCents } from "@frigear-nu/ui/money";
|
|
4
|
+
import { DEFAULT_CURRENCY, eventsApi, ticketMdcData } from "../../utils";
|
|
5
|
+
defineOptions({ name: "EventTicketTypeSlideover" });
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
eventId: { type: String, required: true },
|
|
8
|
+
ticketType: { type: Object, required: false },
|
|
9
|
+
membership: { type: Object, required: false }
|
|
10
|
+
});
|
|
11
|
+
const emit = defineEmits(["close"]);
|
|
12
|
+
const t = useModuleT("events");
|
|
13
|
+
const locale = useEventsLocale();
|
|
14
|
+
const tr = useEventsTranslate();
|
|
15
|
+
const { busy, run } = useAction();
|
|
16
|
+
const membershipEnabled = computed(() => props.membership?.enabled === true);
|
|
17
|
+
const plans = computed(() => props.membership?.plans ?? []);
|
|
18
|
+
const modes = computed(() => [
|
|
19
|
+
{ label: t("admin.ruleNone", "Nobody"), value: "none" },
|
|
20
|
+
{ label: t("admin.ruleAny", "Any membership"), value: "any" },
|
|
21
|
+
{ label: t("admin.rulePlans", "Specific memberships"), value: "plans" }
|
|
22
|
+
]);
|
|
23
|
+
const toRule = (value) => value === "any" ? { mode: "any", plans: [] } : Array.isArray(value) && value.length ? { mode: "plans", plans: [...value] } : { mode: "none", plans: [] };
|
|
24
|
+
const fromRule = (rule) => rule.mode === "any" ? "any" : rule.mode === "plans" && rule.plans.length ? [...rule.plans] : null;
|
|
25
|
+
const currency = computed(() => props.ticketType?.currency ?? DEFAULT_CURRENCY);
|
|
26
|
+
const count = z.union([z.literal(""), z.null(), z.undefined(), z.coerce.number().int(t("admin.countInvalid", "Enter a whole number of 0 or more.")).min(0, t("admin.countInvalid", "Enter a whole number of 0 or more."))]).transform((value) => typeof value === "number" ? value : null);
|
|
27
|
+
const schema = z.object({
|
|
28
|
+
key: props.ticketType ? z.string().optional() : z.string().trim().min(1, t("admin.keyRequired", "Enter a key.")).max(64),
|
|
29
|
+
name: z.custom().refine((value) => !!tr(value).trim(), t("admin.nameRequired", "Enter a name.")),
|
|
30
|
+
description: z.custom().optional(),
|
|
31
|
+
price: z.coerce.number().min(0, t("admin.priceInvalid", "Enter a price of 0 or more.")),
|
|
32
|
+
capacity: count,
|
|
33
|
+
requires: z.custom(),
|
|
34
|
+
excludes: z.custom()
|
|
35
|
+
});
|
|
36
|
+
const state = reactive({
|
|
37
|
+
key: props.ticketType?.key ?? "",
|
|
38
|
+
name: props.ticketType?.name,
|
|
39
|
+
description: props.ticketType?.description ?? void 0,
|
|
40
|
+
price: props.ticketType ? fromCents(props.ticketType.price, props.ticketType.currency) : 0,
|
|
41
|
+
capacity: props.ticketType?.capacity ?? "",
|
|
42
|
+
requires: toRule(props.ticketType?.requiresMembership),
|
|
43
|
+
excludes: toRule(props.ticketType?.excludesMembership)
|
|
44
|
+
});
|
|
45
|
+
const mdcData = computed(() => ticketMdcData({ ...state, price: toCents(Number(state.price) || 0, currency.value) ?? 0, currency: currency.value }, tr(state.name), locale.value));
|
|
46
|
+
async function save(event) {
|
|
47
|
+
const data = event.data;
|
|
48
|
+
const body = {
|
|
49
|
+
name: data.name,
|
|
50
|
+
description: data.description ?? null,
|
|
51
|
+
price: toCents(data.price, currency.value) ?? 0,
|
|
52
|
+
capacity: data.capacity,
|
|
53
|
+
...membershipEnabled.value ? { requiresMembership: fromRule(data.requires), excludesMembership: fromRule(data.excludes) } : {}
|
|
54
|
+
};
|
|
55
|
+
const saved = await run(
|
|
56
|
+
() => props.ticketType ? $fetch(eventsApi.admin.ticketType(props.ticketType.id), { method: "PATCH", body }) : $fetch(eventsApi.admin.ticketTypes(props.eventId), { method: "POST", body: { key: data.key, ...body } }),
|
|
57
|
+
{ success: t("admin.typeSaved", "Ticket type saved.") }
|
|
58
|
+
);
|
|
59
|
+
if (saved) emit("close", saved);
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<FormSlideover
|
|
65
|
+
form-id="event-ticket-type-form"
|
|
66
|
+
:title="ticketType ? t('admin.editType', 'Edit ticket type') : t('admin.addType', 'Add ticket type')"
|
|
67
|
+
:submit-label="ticketType ? t('admin.saveType', 'Save ticket type') : t('admin.addType', 'Add ticket type')"
|
|
68
|
+
:loading="busy"
|
|
69
|
+
@close="emit('close', false)"
|
|
70
|
+
>
|
|
71
|
+
<UForm
|
|
72
|
+
id="event-ticket-type-form"
|
|
73
|
+
:schema="schema"
|
|
74
|
+
:state="state"
|
|
75
|
+
class="flex flex-col gap-4"
|
|
76
|
+
@submit="save"
|
|
77
|
+
>
|
|
78
|
+
<UFormField
|
|
79
|
+
name="name"
|
|
80
|
+
:label="t('admin.name', 'Name')"
|
|
81
|
+
required
|
|
82
|
+
>
|
|
83
|
+
<TranslatedInput v-model="state.name" />
|
|
84
|
+
</UFormField>
|
|
85
|
+
<UFormField
|
|
86
|
+
name="description"
|
|
87
|
+
:label="t('admin.description', 'Description')"
|
|
88
|
+
>
|
|
89
|
+
<TranslatedInput
|
|
90
|
+
v-model="state.description"
|
|
91
|
+
multiline
|
|
92
|
+
mdc
|
|
93
|
+
:mdc-data="mdcData"
|
|
94
|
+
:rows="2"
|
|
95
|
+
/>
|
|
96
|
+
</UFormField>
|
|
97
|
+
<UFormField
|
|
98
|
+
v-if="!ticketType"
|
|
99
|
+
name="key"
|
|
100
|
+
:label="t('admin.key', 'Key')"
|
|
101
|
+
:description="t('admin.keyHelp', 'A short unique id, e.g. member.')"
|
|
102
|
+
required
|
|
103
|
+
>
|
|
104
|
+
<UInput
|
|
105
|
+
v-model="state.key"
|
|
106
|
+
class="w-full"
|
|
107
|
+
/>
|
|
108
|
+
</UFormField>
|
|
109
|
+
<UFormField
|
|
110
|
+
name="price"
|
|
111
|
+
:label="t('admin.price', 'Price')"
|
|
112
|
+
>
|
|
113
|
+
<UInput
|
|
114
|
+
v-model="state.price"
|
|
115
|
+
type="number"
|
|
116
|
+
min="0"
|
|
117
|
+
step="0.01"
|
|
118
|
+
class="w-full"
|
|
119
|
+
/>
|
|
120
|
+
</UFormField>
|
|
121
|
+
<UFormField
|
|
122
|
+
name="capacity"
|
|
123
|
+
:label="t('admin.capacity', 'Capacity')"
|
|
124
|
+
:description="t('admin.capacityHelp', 'Leave empty for unlimited.')"
|
|
125
|
+
>
|
|
126
|
+
<UInput
|
|
127
|
+
v-model="state.capacity"
|
|
128
|
+
type="number"
|
|
129
|
+
min="0"
|
|
130
|
+
class="w-full"
|
|
131
|
+
/>
|
|
132
|
+
</UFormField>
|
|
133
|
+
|
|
134
|
+
<template v-if="membershipEnabled">
|
|
135
|
+
<UFormField
|
|
136
|
+
name="requires"
|
|
137
|
+
:label="t('admin.requiresMembership', 'Only for members of')"
|
|
138
|
+
:description="t('admin.requiresHelp', 'Visitors without a matching active membership cannot buy this ticket.')"
|
|
139
|
+
>
|
|
140
|
+
<USelect
|
|
141
|
+
v-model="state.requires.mode"
|
|
142
|
+
:items="modes"
|
|
143
|
+
class="w-full"
|
|
144
|
+
/>
|
|
145
|
+
<USelectMenu
|
|
146
|
+
v-if="state.requires.mode === 'plans'"
|
|
147
|
+
v-model="state.requires.plans"
|
|
148
|
+
:items="plans"
|
|
149
|
+
value-key="value"
|
|
150
|
+
multiple
|
|
151
|
+
class="mt-2 w-full"
|
|
152
|
+
/>
|
|
153
|
+
</UFormField>
|
|
154
|
+
<UFormField
|
|
155
|
+
name="excludes"
|
|
156
|
+
:label="t('admin.excludesMembership', 'Not for members of')"
|
|
157
|
+
:description="t('admin.excludesHelp', 'Holders of these memberships cannot buy this ticket, so members and non-members can have separate prices.')"
|
|
158
|
+
>
|
|
159
|
+
<USelect
|
|
160
|
+
v-model="state.excludes.mode"
|
|
161
|
+
:items="modes"
|
|
162
|
+
class="w-full"
|
|
163
|
+
/>
|
|
164
|
+
<USelectMenu
|
|
165
|
+
v-if="state.excludes.mode === 'plans'"
|
|
166
|
+
v-model="state.excludes.plans"
|
|
167
|
+
:items="plans"
|
|
168
|
+
value-key="value"
|
|
169
|
+
multiple
|
|
170
|
+
class="mt-2 w-full"
|
|
171
|
+
/>
|
|
172
|
+
</UFormField>
|
|
173
|
+
</template>
|
|
174
|
+
</UForm>
|
|
175
|
+
</FormSlideover>
|
|
176
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MembershipPlans, StaffTicketType } from '../utils/admin-event.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** The event a new ticket type is added to. */
|
|
4
|
+
eventId: string;
|
|
5
|
+
/** The ticket type to edit; leave out to add one. */
|
|
6
|
+
ticketType?: StaffTicketType;
|
|
7
|
+
/** Membership rules are only offered when @frigear-nu/subscriptions is installed (`enabled`). */
|
|
8
|
+
membership?: MembershipPlans;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
close: (saved: false | {
|
|
12
|
+
id: string;
|
|
13
|
+
}) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
15
|
+
onClose?: ((saved: false | {
|
|
16
|
+
id: string;
|
|
17
|
+
}) => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { useEvents, useEvent, useMyTickets, useEventsLocale, useEventsFallbackLocale, useEventsTranslate } from './useEvents.js';
|
|
2
2
|
export { useEventLinks } from './useEventLinks.js';
|
|
3
3
|
export { useEventActions } from './useEventActions.js';
|
|
4
|
+
export { useEventCatalogActions } from './useEventCatalogActions.js';
|
|
5
|
+
export type { EventCatalogActionsOptions, CatalogSaved } from './useEventCatalogActions.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { useEvents, useEvent, useMyTickets, useEventsLocale, useEventsFallbackLocale, useEventsTranslate } from "./useEvents.js";
|
|
2
2
|
export { useEventLinks } from "./useEventLinks.js";
|
|
3
3
|
export { useEventActions } from "./useEventActions.js";
|
|
4
|
+
export { useEventCatalogActions } from "./useEventCatalogActions.js";
|
|
@@ -11,15 +11,15 @@ export type EventMenuOptions = {
|
|
|
11
11
|
viewPublic?: boolean;
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
|
-
* What staff can do with an event, shared by the list and the event page: `create` (a slideover
|
|
15
|
-
*
|
|
14
|
+
* What staff can do with an event, shared by the list and the event page: `create` (a slideover; navigates to the new
|
|
15
|
+
* event and resolves with it, or `false` when cancelled), `duplicate`, `remove` (confirms first) and `menu(event)`, the row menu in the
|
|
16
16
|
* same order everywhere: Open, View public page, Duplicate | extras | Delete.
|
|
17
17
|
*/
|
|
18
18
|
export declare function useEventActions(): {
|
|
19
19
|
create: () => Promise<{
|
|
20
20
|
id: string;
|
|
21
21
|
slug: string;
|
|
22
|
-
} | false
|
|
22
|
+
} | false>;
|
|
23
23
|
duplicate: (event: StaffEventSummary) => Promise<any>;
|
|
24
24
|
remove: (event: StaffEventSummary) => Promise<boolean>;
|
|
25
25
|
menu: (event: StaffEventSummary, options?: EventMenuOptions) => DropdownMenuItem[][];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import EventFormSlideover from "../components/EventFormSlideover.vue";
|
|
2
|
+
import { eventsApi } from "../../utils/api.js";
|
|
2
3
|
export function useEventActions() {
|
|
3
4
|
const t = useModuleT("events");
|
|
4
5
|
const overlay = useOverlay();
|
|
@@ -8,11 +9,14 @@ export function useEventActions() {
|
|
|
8
9
|
const tr = useEventsTranslate();
|
|
9
10
|
const hasPublicPage = !!useRuntimeConfig().public.events?.pages?.event;
|
|
10
11
|
async function create() {
|
|
11
|
-
|
|
12
|
+
const created = await overlay.create(EventFormSlideover, { destroyOnClose: true }).open();
|
|
13
|
+
if (!created) return false;
|
|
14
|
+
await navigateTo(links.adminEvent(created.id));
|
|
15
|
+
return created;
|
|
12
16
|
}
|
|
13
17
|
async function duplicate(event) {
|
|
14
18
|
const copy = await run(
|
|
15
|
-
() => $fetch(
|
|
19
|
+
() => $fetch(eventsApi.admin.clone(event.id), { method: "POST" }),
|
|
16
20
|
{ success: t("admin.duplicated", "Event duplicated as a draft") }
|
|
17
21
|
);
|
|
18
22
|
if (copy) await navigateTo(links.adminEvent(copy.id));
|
|
@@ -28,10 +32,11 @@ export function useEventActions() {
|
|
|
28
32
|
});
|
|
29
33
|
if (!confirmed) return false;
|
|
30
34
|
const done = await run(
|
|
31
|
-
|
|
35
|
+
// 204: resolves with no body, so map success to `true` (`run` resolves `undefined` on failure).
|
|
36
|
+
() => $fetch(eventsApi.admin.event(event.id), { method: "DELETE" }).then(() => true),
|
|
32
37
|
{ success: t("admin.deleted", "Deleted {name}", { name }), error: t("admin.deleteFailed", "Could not delete the event") }
|
|
33
38
|
);
|
|
34
|
-
return done
|
|
39
|
+
return done === true;
|
|
35
40
|
}
|
|
36
41
|
function menu(event, options = {}) {
|
|
37
42
|
const main = [];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DropdownMenuItem } from '@nuxt/ui';
|
|
2
|
+
import { type MaybeRefOrGetter } from 'vue';
|
|
3
|
+
import type { MembershipPlans, StaffAddon, StaffAddonGroup, StaffTicketType } from '../utils/admin-event.js';
|
|
4
|
+
export type EventCatalogActionsOptions = {
|
|
5
|
+
/** The event new ticket types are added to. Only needed to add ticket types. */
|
|
6
|
+
eventId?: MaybeRefOrGetter<string | undefined>;
|
|
7
|
+
/** Runs after a ticket type, add-on group or choice was saved or deleted, e.g. to refresh the event. */
|
|
8
|
+
onChanged?: () => unknown;
|
|
9
|
+
/** The memberships the ticket type form offers (`@frigear-nu/subscriptions`), read when the form opens. */
|
|
10
|
+
membership?: () => MembershipPlans | undefined;
|
|
11
|
+
};
|
|
12
|
+
/** What a catalog form resolves with: the saved record, or `false` when cancelled or failed. */
|
|
13
|
+
export type CatalogSaved = {
|
|
14
|
+
id: string;
|
|
15
|
+
} | false;
|
|
16
|
+
/**
|
|
17
|
+
* What staff can do with an event's catalog: its ticket types, their add-on groups and the choices in them.
|
|
18
|
+
* `openTicketType` / `openGroup` / `openAddon` open the create (no record) or edit form in a slideover and resolve
|
|
19
|
+
* with the saved record or `false`; `removeTicketType` / `removeGroup` / `removeAddon` confirm first and resolve
|
|
20
|
+
* `true` once deleted; `typeMenu` / `groupMenu` / `addonMenu` are the `RowMenu` items (edits, then the delete in its
|
|
21
|
+
* own red group). Every save and delete runs `onChanged` afterwards.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useEventCatalogActions(options?: EventCatalogActionsOptions): {
|
|
24
|
+
openTicketType: (ticketType?: StaffTicketType) => Promise<CatalogSaved>;
|
|
25
|
+
openGroup: (ticketType: StaffTicketType, group?: StaffAddonGroup) => Promise<CatalogSaved>;
|
|
26
|
+
openAddon: (ticketType: StaffTicketType, group: StaffAddonGroup, addon?: StaffAddon) => Promise<CatalogSaved>;
|
|
27
|
+
removeTicketType: (ticketType: StaffTicketType) => Promise<boolean>;
|
|
28
|
+
removeGroup: (group: StaffAddonGroup) => Promise<boolean>;
|
|
29
|
+
removeAddon: (addon: StaffAddon) => Promise<boolean>;
|
|
30
|
+
typeMenu: (ticketType: StaffTicketType) => DropdownMenuItem[][];
|
|
31
|
+
groupMenu: (ticketType: StaffTicketType, group: StaffAddonGroup) => DropdownMenuItem[][];
|
|
32
|
+
addonMenu: (ticketType: StaffTicketType, group: StaffAddonGroup, addon: StaffAddon) => DropdownMenuItem[][];
|
|
33
|
+
};
|