@frigear-nu/events 0.1.2 → 0.2.0

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.
Files changed (102) hide show
  1. package/dist/module.d.mts +105 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +35 -31
  4. package/dist/runtime/app/components/EventAddonGroupSlideover.d.vue.ts +18 -0
  5. package/dist/runtime/app/components/EventAddonGroupSlideover.vue +67 -0
  6. package/dist/runtime/app/components/EventAddonGroupSlideover.vue.d.ts +18 -0
  7. package/dist/runtime/app/components/EventAddonSlideover.d.vue.ts +20 -0
  8. package/dist/runtime/app/components/EventAddonSlideover.vue +104 -0
  9. package/dist/runtime/app/components/EventAddonSlideover.vue.d.ts +20 -0
  10. package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +1 -9
  11. package/dist/runtime/app/components/EventAdminAddonGroups.vue +7 -48
  12. package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +1 -9
  13. package/dist/runtime/app/components/EventCard.vue +1 -0
  14. package/dist/runtime/app/components/EventFormSlideover.vue +46 -23
  15. package/dist/runtime/app/components/EventTicketCard.vue +60 -62
  16. package/dist/runtime/app/components/EventTicketPicker.vue +1 -0
  17. package/dist/runtime/app/components/EventTicketTypeSlideover.d.vue.ts +20 -0
  18. package/dist/runtime/app/components/EventTicketTypeSlideover.vue +177 -0
  19. package/dist/runtime/app/components/EventTicketTypeSlideover.vue.d.ts +20 -0
  20. package/dist/runtime/app/composables/index.d.ts +2 -0
  21. package/dist/runtime/app/composables/index.js +1 -0
  22. package/dist/runtime/app/composables/useEventActions.d.ts +3 -3
  23. package/dist/runtime/app/composables/useEventActions.js +10 -4
  24. package/dist/runtime/app/composables/useEventCatalogActions.d.ts +33 -0
  25. package/dist/runtime/app/composables/useEventCatalogActions.js +108 -0
  26. package/dist/runtime/app/composables/useEventLinks.js +1 -0
  27. package/dist/runtime/app/composables/useEvents.d.ts +29 -16
  28. package/dist/runtime/app/composables/useEvents.js +7 -6
  29. package/dist/runtime/app/pages/admin-event.vue +72 -136
  30. package/dist/runtime/app/pages/admin.vue +92 -29
  31. package/dist/runtime/app/pages/event.vue +9 -4
  32. package/dist/runtime/app/pages/events.vue +1 -0
  33. package/dist/runtime/app/pages/tickets.vue +5 -0
  34. package/dist/runtime/app/tabs/Attendees.vue +79 -39
  35. package/dist/runtime/app/tabs/Details.d.vue.ts +1 -1
  36. package/dist/runtime/app/tabs/Details.vue +113 -30
  37. package/dist/runtime/app/tabs/Details.vue.d.ts +1 -1
  38. package/dist/runtime/app/tabs/Tickets.vue +103 -139
  39. package/dist/runtime/app/utils/admin-event.d.ts +53 -38
  40. package/dist/runtime/app/utils/admin-event.js +14 -0
  41. package/dist/runtime/app/widgets/UpcomingEvents.vue +3 -1
  42. package/dist/runtime/app/widgets/UpcomingEventsStat.vue +3 -1
  43. package/dist/runtime/i18n/lang/da.json +22 -3
  44. package/dist/runtime/i18n/lang/en.json +22 -3
  45. package/dist/runtime/menus.js +1 -0
  46. package/dist/runtime/server/bootstrap/my-tickets.d.ts +1 -1
  47. package/dist/runtime/server/mcp/schemas.d.ts +4 -4
  48. package/dist/runtime/server/routes/admin/addon-group.delete.js +6 -3
  49. package/dist/runtime/server/routes/admin/addon-group.patch.js +6 -4
  50. package/dist/runtime/server/routes/admin/addon-groups.post.js +8 -6
  51. package/dist/runtime/server/routes/admin/addon.delete.js +6 -3
  52. package/dist/runtime/server/routes/admin/addon.patch.js +8 -5
  53. package/dist/runtime/server/routes/admin/addons.post.js +8 -6
  54. package/dist/runtime/server/routes/admin/attendees.get.js +38 -8
  55. package/dist/runtime/server/routes/{public → admin}/check-in.post.js +4 -3
  56. package/dist/runtime/server/routes/admin/event-clone.post.js +37 -19
  57. package/dist/runtime/server/routes/admin/event.delete.js +7 -4
  58. package/dist/runtime/server/routes/admin/event.get.js +4 -2
  59. package/dist/runtime/server/routes/admin/event.patch.js +4 -2
  60. package/dist/runtime/server/routes/admin/events.get.js +36 -3
  61. package/dist/runtime/server/routes/admin/events.post.js +3 -4
  62. package/dist/runtime/server/routes/admin/membership-options.get.js +3 -2
  63. package/dist/runtime/server/routes/admin/ticket-cancel.post.js +7 -8
  64. package/dist/runtime/server/routes/admin/ticket-refund.post.js +7 -5
  65. package/dist/runtime/server/routes/admin/ticket-type.delete.js +7 -4
  66. package/dist/runtime/server/routes/admin/ticket-type.patch.js +4 -2
  67. package/dist/runtime/server/routes/admin/ticket-types.post.js +10 -7
  68. package/dist/runtime/server/routes/admin/upcoming.get.js +7 -3
  69. package/dist/runtime/server/routes/me/ticket-pay.post.js +13 -0
  70. package/dist/runtime/server/routes/me/ticket.get.js +6 -0
  71. package/dist/runtime/server/routes/public/event.get.js +5 -2
  72. package/dist/runtime/server/routes/public/purchase.post.js +3 -1
  73. package/dist/runtime/server/utils/admin-dto.d.ts +30 -0
  74. package/dist/runtime/server/utils/admin-dto.js +101 -0
  75. package/dist/runtime/server/utils/admin-schemas.js +2 -1
  76. package/dist/runtime/server/utils/admin.d.ts +10 -8
  77. package/dist/runtime/server/utils/admin.js +20 -6
  78. package/dist/runtime/server/utils/auth.d.ts +5 -4
  79. package/dist/runtime/server/utils/auth.js +6 -7
  80. package/dist/runtime/server/utils/tickets.js +6 -11
  81. package/dist/runtime/utils/api.d.ts +48 -0
  82. package/dist/runtime/utils/api.js +51 -0
  83. package/dist/runtime/utils/index.d.ts +2 -1
  84. package/dist/runtime/utils/index.js +1 -0
  85. package/dist/types.d.mts +1 -1
  86. package/package.json +19 -14
  87. package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +0 -11
  88. package/dist/runtime/app/components/EventAdminAddonDialog.vue +0 -112
  89. package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +0 -11
  90. package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +0 -11
  91. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +0 -78
  92. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +0 -11
  93. package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +0 -20
  94. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +0 -174
  95. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +0 -20
  96. package/dist/runtime/server/routes/public/ticket-pay.post.js +0 -10
  97. package/dist/runtime/server/routes/public/ticket.get.js +0 -4
  98. /package/dist/runtime/server/routes/{public → admin}/check-in.post.d.ts +0 -0
  99. /package/dist/runtime/server/routes/{public → me}/ticket-pay.post.d.ts +0 -0
  100. /package/dist/runtime/server/routes/{public → me}/ticket.get.d.ts +0 -0
  101. /package/dist/runtime/server/routes/{public/tickets-me.get.d.ts → me/tickets.get.d.ts} +0 -0
  102. /package/dist/runtime/server/routes/{public/tickets-me.get.js → me/tickets.get.js} +0 -0
@@ -1,20 +1,25 @@
1
1
  <script setup>
2
+ import { eventFormSchema } from "../utils/admin-event";
3
+ import { eventsApi } from "../../utils/api";
4
+ import { $fetch, computed, reactive, useAction, useEventsTranslate, useModuleT, watch } from "#imports";
2
5
  defineOptions({ name: "EventFormSlideover" });
3
6
  const emit = defineEmits(["close"]);
4
7
  const t = useModuleT("events");
5
8
  const tr = useEventsTranslate();
6
9
  const { busy, run } = useAction();
7
- const draft = reactive({ name: void 0, slug: "", startsAt: "", location: void 0 });
10
+ const schema = eventFormSchema(t, tr);
11
+ const state = reactive({ name: void 0, slug: "", startsAt: "", endsAt: "", status: "draft", location: void 0 });
8
12
  const slugify = (value) => value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
9
- const draftName = computed(() => tr(draft.name));
13
+ const draftName = computed(() => tr(state.name));
10
14
  watch(draftName, (name, previous) => {
11
- if (draft.slug === slugify(previous)) draft.slug = slugify(name);
15
+ if (state.slug === slugify(previous)) state.slug = slugify(name);
12
16
  });
13
- async function save() {
17
+ async function save(event) {
18
+ const { name, slug, startsAt, endsAt, location } = event.data;
14
19
  const created = await run(
15
- () => $fetch("/api/events/admin/events", {
20
+ () => $fetch(eventsApi.admin.events, {
16
21
  method: "POST",
17
- body: { name: draft.name, slug: draft.slug, startsAt: draft.startsAt, location: draft.location || null, status: "draft" }
22
+ body: { name, slug, startsAt, endsAt: endsAt || null, location: location || null, status: "draft" }
18
23
  }),
19
24
  { success: t("admin.created", "Event created") }
20
25
  );
@@ -29,42 +34,60 @@ async function save() {
29
34
  :description="t('admin.createDescription', 'It is saved as a draft until you publish it.')"
30
35
  :submit-label="t('admin.createAction', 'Create draft')"
31
36
  :loading="busy"
32
- :disabled="!draftName"
33
37
  @close="emit('close', false)"
34
38
  >
35
- <form
39
+ <UForm
36
40
  id="event-form"
41
+ :schema="schema"
42
+ :state="state"
37
43
  class="flex flex-col gap-4"
38
- @submit.prevent="save"
44
+ @submit="save"
39
45
  >
40
- <UFormField :label="t('admin.name', 'Name')">
41
- <TranslatedInput
42
- v-model="draft.name"
43
- required
44
- />
46
+ <UFormField
47
+ name="name"
48
+ :label="t('admin.name', 'Name')"
49
+ required
50
+ >
51
+ <TranslatedInput v-model="state.name" />
45
52
  </UFormField>
46
53
  <UFormField
54
+ name="slug"
47
55
  :label="t('admin.slug', 'URL name')"
48
56
  :description="t('admin.slugHelp', 'Lowercase letters, numbers and dashes.')"
57
+ required
58
+ >
59
+ <UInput
60
+ v-model="state.slug"
61
+ class="w-full"
62
+ />
63
+ </UFormField>
64
+ <UFormField
65
+ name="startsAt"
66
+ :label="t('admin.starts', 'Starts')"
67
+ required
49
68
  >
50
69
  <UInput
51
- v-model="draft.slug"
70
+ v-model="state.startsAt"
71
+ type="datetime-local"
52
72
  class="w-full"
53
- required
54
- pattern="[a-z0-9]+(-[a-z0-9]+)*"
55
73
  />
56
74
  </UFormField>
57
- <UFormField :label="t('admin.starts', 'Starts')">
75
+ <UFormField
76
+ name="endsAt"
77
+ :label="t('admin.ends', 'Ends')"
78
+ >
58
79
  <UInput
59
- v-model="draft.startsAt"
80
+ v-model="state.endsAt"
60
81
  type="datetime-local"
61
82
  class="w-full"
62
- required
63
83
  />
64
84
  </UFormField>
65
- <UFormField :label="t('admin.location', 'Location')">
66
- <TranslatedInput v-model="draft.location" />
85
+ <UFormField
86
+ name="location"
87
+ :label="t('admin.location', 'Location')"
88
+ >
89
+ <TranslatedInput v-model="state.location" />
67
90
  </UFormField>
68
- </form>
91
+ </UForm>
69
92
  </FormSlideover>
70
93
  </template>
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import { renderSVG } from "uqr";
3
+ import { computed, ref, useEventsTranslate, useFormat, useModuleT } from "#imports";
3
4
  const props = defineProps({
4
5
  ticket: { type: Object, required: true },
5
6
  to: { type: String, required: false }
@@ -8,74 +9,71 @@ const emit = defineEmits(["pay"]);
8
9
  const t = useModuleT("events");
9
10
  const format = useFormat();
10
11
  const tr = useEventsTranslate();
12
+ const description = computed(() => [props.ticket.ticketTypeName, ...props.ticket.addons.map((addon) => addon.label)].map((value) => tr(value)).filter(Boolean).join(" \xB7 "));
11
13
  const qr = computed(() => props.ticket.code ? `data:image/svg+xml;utf8,${encodeURIComponent(renderSVG(props.ticket.code, { border: 1 }))}` : null);
12
14
  const showQr = ref(false);
15
+ const canPay = computed(() => props.ticket.status === "pending");
16
+ const hasActions = computed(() => !!props.to || !!qr.value || canPay.value);
13
17
  </script>
14
18
 
15
19
  <template>
16
- <UCard :ui="{ body: 'flex flex-col gap-3' }">
17
- <div class="flex items-start gap-2">
18
- <div>
19
- <h3 class="text-highlighted font-semibold">
20
- {{ tr(ticket.eventName) }}
21
- </h3>
22
- <p class="text-sm text-muted">
23
- {{ tr(ticket.ticketTypeName) }}<template
24
- v-for="addon in ticket.addons"
25
- :key="addon.id"
26
- >
27
- · {{ tr(addon.label) }}
28
- </template>
29
- </p>
30
- </div>
31
- <UBadge
32
- class="ml-auto capitalize"
33
- :color="statusColor(ticket.status, { refunded: 'error' })"
34
- variant="subtle"
35
- >
36
- {{ t(`tickets.status.${ticket.status}`, ticket.status) }}
37
- </UBadge>
38
- </div>
39
- <p class="text-sm">
40
- {{ ticket.amount === 0 ? t("detail.tickets.free", "Free") : format.money(ticket.amount, ticket.currency) }}
41
- <UBadge
42
- v-if="ticket.checkedInAt"
43
- color="info"
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"
20
+ <SectionCard
21
+ :title="tr(ticket.eventName)"
22
+ :description="description"
23
+ >
24
+ <template #actions>
25
+ <StatusBadge
26
+ :status="ticket.status"
27
+ :label="t(`tickets.status.${ticket.status}`, ticket.status)"
28
+ :colors="{ refunded: 'error' }"
29
+ />
30
+ </template>
31
+
32
+ <div class="flex flex-col gap-3">
33
+ <p class="flex flex-wrap items-center gap-2 text-sm">
34
+ <span class="tabular-nums">{{ ticket.amount === 0 ? t("detail.tickets.free", "Free") : format.money(ticket.amount, ticket.currency) }}</span>
35
+ <UBadge
36
+ v-if="ticket.checkedInAt"
37
+ color="info"
38
+ variant="subtle"
39
+ icon="i-lucide-check"
40
+ :label="t('checkedIn', 'Checked in')"
41
+ />
42
+ </p>
43
+ <img
44
+ v-if="showQr && qr"
45
+ :src="qr"
46
+ :alt="t('tickets.qrAlt', 'Ticket QR code')"
47
+ class="mx-auto w-56 rounded-md bg-white p-2"
70
48
  >
71
- {{ t("viewEvent", "View event") }}
72
- </UButton>
73
49
  </div>
74
- <img
75
- v-if="showQr && qr"
76
- :src="qr"
77
- :alt="t('tickets.qrAlt', 'Ticket QR code')"
78
- class="mx-auto w-56 rounded-md bg-white p-2"
50
+
51
+ <template
52
+ v-if="hasActions"
53
+ #footer
79
54
  >
80
- </UCard>
55
+ <div class="flex flex-wrap justify-end gap-2">
56
+ <UButton
57
+ v-if="to"
58
+ :to="to"
59
+ color="neutral"
60
+ variant="outline"
61
+ :label="t('viewEvent', 'View event')"
62
+ />
63
+ <UButton
64
+ v-if="qr"
65
+ color="neutral"
66
+ variant="outline"
67
+ icon="i-lucide-qr-code"
68
+ :label="showQr ? t('hideQr', 'Hide QR code') : t('tickets.viewQrCode', 'Show QR code')"
69
+ @click="showQr = !showQr"
70
+ />
71
+ <UButton
72
+ v-if="canPay"
73
+ :label="t('tickets.payNow', 'Pay now')"
74
+ @click="emit('pay', ticket.id)"
75
+ />
76
+ </div>
77
+ </template>
78
+ </SectionCard>
81
79
  </template>
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { withMdcData } from "@frigear-nu/ui/markdown";
3
3
  import { DEFAULT_CURRENCY, ticketMdcData } from "../../utils";
4
+ import { computed, ref, resolveComponent, useEventsTranslate, useFormat, useModuleT, watch } from "#imports";
4
5
  const props = defineProps({
5
6
  ticketTypes: { type: Array, required: true },
6
7
  loggedIn: { type: Boolean, required: true },
@@ -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,177 @@
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
+ import { $fetch, computed, reactive, useAction, useEventsLocale, useEventsTranslate, useModuleT } from "#imports";
6
+ defineOptions({ name: "EventTicketTypeSlideover" });
7
+ const props = defineProps({
8
+ eventId: { type: String, required: true },
9
+ ticketType: { type: Object, required: false },
10
+ membership: { type: Object, required: false }
11
+ });
12
+ const emit = defineEmits(["close"]);
13
+ const t = useModuleT("events");
14
+ const locale = useEventsLocale();
15
+ const tr = useEventsTranslate();
16
+ const { busy, run } = useAction();
17
+ const membershipEnabled = computed(() => props.membership?.enabled === true);
18
+ const plans = computed(() => props.membership?.plans ?? []);
19
+ const modes = computed(() => [
20
+ { label: t("admin.ruleNone", "Nobody"), value: "none" },
21
+ { label: t("admin.ruleAny", "Any membership"), value: "any" },
22
+ { label: t("admin.rulePlans", "Specific memberships"), value: "plans" }
23
+ ]);
24
+ const toRule = (value) => value === "any" ? { mode: "any", plans: [] } : Array.isArray(value) && value.length ? { mode: "plans", plans: [...value] } : { mode: "none", plans: [] };
25
+ const fromRule = (rule) => rule.mode === "any" ? "any" : rule.mode === "plans" && rule.plans.length ? [...rule.plans] : null;
26
+ const currency = computed(() => props.ticketType?.currency ?? DEFAULT_CURRENCY);
27
+ 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);
28
+ const schema = z.object({
29
+ key: props.ticketType ? z.string().optional() : z.string().trim().min(1, t("admin.keyRequired", "Enter a key.")).max(64),
30
+ name: z.custom().refine((value) => !!tr(value).trim(), t("admin.nameRequired", "Enter a name.")),
31
+ description: z.custom().optional(),
32
+ price: z.coerce.number().min(0, t("admin.priceInvalid", "Enter a price of 0 or more.")),
33
+ capacity: count,
34
+ requires: z.custom(),
35
+ excludes: z.custom()
36
+ });
37
+ const state = reactive({
38
+ key: props.ticketType?.key ?? "",
39
+ name: props.ticketType?.name,
40
+ description: props.ticketType?.description ?? void 0,
41
+ price: props.ticketType ? fromCents(props.ticketType.price, props.ticketType.currency) : 0,
42
+ capacity: props.ticketType?.capacity ?? "",
43
+ requires: toRule(props.ticketType?.requiresMembership),
44
+ excludes: toRule(props.ticketType?.excludesMembership)
45
+ });
46
+ const mdcData = computed(() => ticketMdcData({ ...state, price: toCents(Number(state.price) || 0, currency.value) ?? 0, currency: currency.value }, tr(state.name), locale.value));
47
+ async function save(event) {
48
+ const data = event.data;
49
+ const body = {
50
+ name: data.name,
51
+ description: data.description ?? null,
52
+ price: toCents(data.price, currency.value) ?? 0,
53
+ capacity: data.capacity,
54
+ ...membershipEnabled.value ? { requiresMembership: fromRule(data.requires), excludesMembership: fromRule(data.excludes) } : {}
55
+ };
56
+ const saved = await run(
57
+ () => 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 } }),
58
+ { success: t("admin.typeSaved", "Ticket type saved.") }
59
+ );
60
+ if (saved) emit("close", saved);
61
+ }
62
+ </script>
63
+
64
+ <template>
65
+ <FormSlideover
66
+ form-id="event-ticket-type-form"
67
+ :title="ticketType ? t('admin.editType', 'Edit ticket type') : t('admin.addType', 'Add ticket type')"
68
+ :submit-label="ticketType ? t('admin.saveType', 'Save ticket type') : t('admin.addType', 'Add ticket type')"
69
+ :loading="busy"
70
+ @close="emit('close', false)"
71
+ >
72
+ <UForm
73
+ id="event-ticket-type-form"
74
+ :schema="schema"
75
+ :state="state"
76
+ class="flex flex-col gap-4"
77
+ @submit="save"
78
+ >
79
+ <UFormField
80
+ name="name"
81
+ :label="t('admin.name', 'Name')"
82
+ required
83
+ >
84
+ <TranslatedInput v-model="state.name" />
85
+ </UFormField>
86
+ <UFormField
87
+ name="description"
88
+ :label="t('admin.description', 'Description')"
89
+ >
90
+ <TranslatedInput
91
+ v-model="state.description"
92
+ multiline
93
+ mdc
94
+ :mdc-data="mdcData"
95
+ :rows="2"
96
+ />
97
+ </UFormField>
98
+ <UFormField
99
+ v-if="!ticketType"
100
+ name="key"
101
+ :label="t('admin.key', 'Key')"
102
+ :description="t('admin.keyHelp', 'A short unique id, e.g. member.')"
103
+ required
104
+ >
105
+ <UInput
106
+ v-model="state.key"
107
+ class="w-full"
108
+ />
109
+ </UFormField>
110
+ <UFormField
111
+ name="price"
112
+ :label="t('admin.price', 'Price')"
113
+ >
114
+ <UInput
115
+ v-model="state.price"
116
+ type="number"
117
+ min="0"
118
+ step="0.01"
119
+ class="w-full"
120
+ />
121
+ </UFormField>
122
+ <UFormField
123
+ name="capacity"
124
+ :label="t('admin.capacity', 'Capacity')"
125
+ :description="t('admin.capacityHelp', 'Leave empty for unlimited.')"
126
+ >
127
+ <UInput
128
+ v-model="state.capacity"
129
+ type="number"
130
+ min="0"
131
+ class="w-full"
132
+ />
133
+ </UFormField>
134
+
135
+ <template v-if="membershipEnabled">
136
+ <UFormField
137
+ name="requires"
138
+ :label="t('admin.requiresMembership', 'Only for members of')"
139
+ :description="t('admin.requiresHelp', 'Visitors without a matching active membership cannot buy this ticket.')"
140
+ >
141
+ <USelect
142
+ v-model="state.requires.mode"
143
+ :items="modes"
144
+ class="w-full"
145
+ />
146
+ <USelectMenu
147
+ v-if="state.requires.mode === 'plans'"
148
+ v-model="state.requires.plans"
149
+ :items="plans"
150
+ value-key="value"
151
+ multiple
152
+ class="mt-2 w-full"
153
+ />
154
+ </UFormField>
155
+ <UFormField
156
+ name="excludes"
157
+ :label="t('admin.excludesMembership', 'Not for members of')"
158
+ :description="t('admin.excludesHelp', 'Holders of these memberships cannot buy this ticket, so members and non-members can have separate prices.')"
159
+ >
160
+ <USelect
161
+ v-model="state.excludes.mode"
162
+ :items="modes"
163
+ class="w-full"
164
+ />
165
+ <USelectMenu
166
+ v-if="state.excludes.mode === 'plans'"
167
+ v-model="state.excludes.plans"
168
+ :items="plans"
169
+ value-key="value"
170
+ multiple
171
+ class="mt-2 w-full"
172
+ />
173
+ </UFormField>
174
+ </template>
175
+ </UForm>
176
+ </FormSlideover>
177
+ </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 that resolves with the
15
- * new event, or `false` when cancelled), `duplicate`, `remove` (confirms first) and `menu(event)`, the row menu in the
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 | undefined>;
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,6 @@
1
1
  import EventFormSlideover from "../components/EventFormSlideover.vue";
2
+ import { eventsApi } from "../../utils/api.js";
3
+ import { $fetch, navigateTo, useAction, useConfirmDialog, useEventLinks, useEventsTranslate, useModuleT, useOverlay, useRuntimeConfig } from "#imports";
2
4
  export function useEventActions() {
3
5
  const t = useModuleT("events");
4
6
  const overlay = useOverlay();
@@ -8,11 +10,14 @@ export function useEventActions() {
8
10
  const tr = useEventsTranslate();
9
11
  const hasPublicPage = !!useRuntimeConfig().public.events?.pages?.event;
10
12
  async function create() {
11
- return overlay.create(EventFormSlideover, { destroyOnClose: true }).open();
13
+ const created = await overlay.create(EventFormSlideover, { destroyOnClose: true }).open();
14
+ if (!created) return false;
15
+ await navigateTo(links.adminEvent(created.id));
16
+ return created;
12
17
  }
13
18
  async function duplicate(event) {
14
19
  const copy = await run(
15
- () => $fetch(`/api/events/admin/events/${event.id}/clone`, { method: "POST" }),
20
+ () => $fetch(eventsApi.admin.clone(event.id), { method: "POST" }),
16
21
  { success: t("admin.duplicated", "Event duplicated as a draft") }
17
22
  );
18
23
  if (copy) await navigateTo(links.adminEvent(copy.id));
@@ -28,10 +33,11 @@ export function useEventActions() {
28
33
  });
29
34
  if (!confirmed) return false;
30
35
  const done = await run(
31
- () => $fetch(`/api/events/admin/events/${event.id}`, { method: "DELETE" }),
36
+ // 204: resolves with no body, so map success to `true` (`run` resolves `undefined` on failure).
37
+ () => $fetch(eventsApi.admin.event(event.id), { method: "DELETE" }).then(() => true),
32
38
  { success: t("admin.deleted", "Deleted {name}", { name }), error: t("admin.deleteFailed", "Could not delete the event") }
33
39
  );
34
- return done !== void 0;
40
+ return done === true;
35
41
  }
36
42
  function menu(event, options = {}) {
37
43
  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
+ };