@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.
Files changed (97) hide show
  1. package/dist/module.d.mts +105 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +40 -31
  4. package/dist/runtime/app/components/EventAddonGroupSlideover.d.vue.ts +18 -0
  5. package/dist/runtime/app/components/EventAddonGroupSlideover.vue +66 -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 +103 -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 +6 -48
  12. package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +1 -9
  13. package/dist/runtime/app/components/EventFormSlideover.vue +45 -23
  14. package/dist/runtime/app/components/EventTicketCard.vue +59 -62
  15. package/dist/runtime/app/components/EventTicketTypeSlideover.d.vue.ts +20 -0
  16. package/dist/runtime/app/components/EventTicketTypeSlideover.vue +176 -0
  17. package/dist/runtime/app/components/EventTicketTypeSlideover.vue.d.ts +20 -0
  18. package/dist/runtime/app/composables/index.d.ts +2 -0
  19. package/dist/runtime/app/composables/index.js +1 -0
  20. package/dist/runtime/app/composables/useEventActions.d.ts +3 -3
  21. package/dist/runtime/app/composables/useEventActions.js +9 -4
  22. package/dist/runtime/app/composables/useEventCatalogActions.d.ts +33 -0
  23. package/dist/runtime/app/composables/useEventCatalogActions.js +107 -0
  24. package/dist/runtime/app/composables/useEvents.d.ts +7 -6
  25. package/dist/runtime/app/composables/useEvents.js +17 -10
  26. package/dist/runtime/app/pages/admin-event.vue +71 -136
  27. package/dist/runtime/app/pages/admin.vue +91 -29
  28. package/dist/runtime/app/pages/event.vue +8 -4
  29. package/dist/runtime/app/pages/tickets.vue +24 -2
  30. package/dist/runtime/app/tabs/Attendees.vue +78 -39
  31. package/dist/runtime/app/tabs/Details.d.vue.ts +1 -1
  32. package/dist/runtime/app/tabs/Details.vue +112 -30
  33. package/dist/runtime/app/tabs/Details.vue.d.ts +1 -1
  34. package/dist/runtime/app/tabs/Tickets.vue +105 -132
  35. package/dist/runtime/app/utils/admin-event.d.ts +53 -38
  36. package/dist/runtime/app/utils/admin-event.js +14 -0
  37. package/dist/runtime/app/widgets/UpcomingEvents.vue +2 -1
  38. package/dist/runtime/app/widgets/UpcomingEventsStat.vue +2 -1
  39. package/dist/runtime/i18n/lang/da.json +24 -3
  40. package/dist/runtime/i18n/lang/en.json +24 -3
  41. package/dist/runtime/server/bootstrap/my-tickets.d.ts +7 -0
  42. package/dist/runtime/server/{routes/public/tickets-me.get.js → bootstrap/my-tickets.js} +2 -2
  43. package/dist/runtime/server/routes/admin/addon-group.delete.js +6 -3
  44. package/dist/runtime/server/routes/admin/addon-group.patch.js +6 -4
  45. package/dist/runtime/server/routes/admin/addon-groups.post.js +8 -6
  46. package/dist/runtime/server/routes/admin/addon.delete.js +6 -3
  47. package/dist/runtime/server/routes/admin/addon.patch.js +8 -5
  48. package/dist/runtime/server/routes/admin/addons.post.js +8 -6
  49. package/dist/runtime/server/routes/admin/attendees.get.js +38 -8
  50. package/dist/runtime/server/routes/{public → admin}/check-in.post.js +4 -3
  51. package/dist/runtime/server/routes/admin/event-clone.post.js +37 -19
  52. package/dist/runtime/server/routes/admin/event.delete.js +7 -4
  53. package/dist/runtime/server/routes/admin/event.get.js +4 -2
  54. package/dist/runtime/server/routes/admin/event.patch.js +4 -2
  55. package/dist/runtime/server/routes/admin/events.get.js +36 -3
  56. package/dist/runtime/server/routes/admin/events.post.js +3 -4
  57. package/dist/runtime/server/routes/admin/membership-options.get.js +3 -2
  58. package/dist/runtime/server/routes/admin/ticket-cancel.post.js +7 -8
  59. package/dist/runtime/server/routes/admin/ticket-refund.post.js +7 -5
  60. package/dist/runtime/server/routes/admin/ticket-type.delete.js +7 -4
  61. package/dist/runtime/server/routes/admin/ticket-type.patch.js +4 -2
  62. package/dist/runtime/server/routes/admin/ticket-types.post.js +10 -7
  63. package/dist/runtime/server/routes/admin/upcoming.get.js +7 -3
  64. package/dist/runtime/server/routes/me/ticket-pay.post.js +13 -0
  65. package/dist/runtime/server/routes/me/ticket.get.js +6 -0
  66. package/dist/runtime/server/routes/me/tickets.get.js +2 -0
  67. package/dist/runtime/server/routes/public/event.get.js +5 -2
  68. package/dist/runtime/server/routes/public/purchase.post.js +3 -1
  69. package/dist/runtime/server/utils/admin-dto.d.ts +30 -0
  70. package/dist/runtime/server/utils/admin-dto.js +101 -0
  71. package/dist/runtime/server/utils/admin-schemas.js +2 -1
  72. package/dist/runtime/server/utils/admin.d.ts +3 -1
  73. package/dist/runtime/server/utils/admin.js +20 -6
  74. package/dist/runtime/server/utils/auth.d.ts +5 -4
  75. package/dist/runtime/server/utils/auth.js +6 -7
  76. package/dist/runtime/server/utils/tickets.js +6 -11
  77. package/dist/runtime/utils/api.d.ts +48 -0
  78. package/dist/runtime/utils/api.js +51 -0
  79. package/dist/runtime/utils/index.d.ts +2 -1
  80. package/dist/runtime/utils/index.js +1 -0
  81. package/dist/types.d.mts +1 -1
  82. package/package.json +21 -16
  83. package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +0 -11
  84. package/dist/runtime/app/components/EventAdminAddonDialog.vue +0 -112
  85. package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +0 -11
  86. package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +0 -11
  87. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +0 -78
  88. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +0 -11
  89. package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +0 -20
  90. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +0 -174
  91. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +0 -20
  92. package/dist/runtime/server/routes/public/ticket-pay.post.js +0 -10
  93. package/dist/runtime/server/routes/public/ticket.get.js +0 -4
  94. /package/dist/runtime/server/routes/{public → admin}/check-in.post.d.ts +0 -0
  95. /package/dist/runtime/server/routes/{public → me}/ticket-pay.post.d.ts +0 -0
  96. /package/dist/runtime/server/routes/{public → me}/ticket.get.d.ts +0 -0
  97. /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 draft = reactive({ name: void 0, slug: "", startsAt: "", location: void 0 });
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(draft.name));
12
+ const draftName = computed(() => tr(state.name));
10
13
  watch(draftName, (name, previous) => {
11
- if (draft.slug === slugify(previous)) draft.slug = slugify(name);
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("/api/events/admin/events", {
19
+ () => $fetch(eventsApi.admin.events, {
16
20
  method: "POST",
17
- body: { name: draft.name, slug: draft.slug, startsAt: draft.startsAt, location: draft.location || null, status: "draft" }
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
- <form
38
+ <UForm
36
39
  id="event-form"
40
+ :schema="schema"
41
+ :state="state"
37
42
  class="flex flex-col gap-4"
38
- @submit.prevent="save"
43
+ @submit="save"
39
44
  >
40
- <UFormField :label="t('admin.name', 'Name')">
41
- <TranslatedInput
42
- v-model="draft.name"
43
- required
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="draft.slug"
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 :label="t('admin.starts', 'Starts')">
74
+ <UFormField
75
+ name="endsAt"
76
+ :label="t('admin.ends', 'Ends')"
77
+ >
58
78
  <UInput
59
- v-model="draft.startsAt"
79
+ v-model="state.endsAt"
60
80
  type="datetime-local"
61
81
  class="w-full"
62
- required
63
82
  />
64
83
  </UFormField>
65
- <UFormField :label="t('admin.location', 'Location')">
66
- <TranslatedInput v-model="draft.location" />
84
+ <UFormField
85
+ name="location"
86
+ :label="t('admin.location', 'Location')"
87
+ >
88
+ <TranslatedInput v-model="state.location" />
67
89
  </UFormField>
68
- </form>
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
- <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"
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
- <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"
49
+
50
+ <template
51
+ v-if="hasActions"
52
+ #footer
79
53
  >
80
- </UCard>
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 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,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
- return overlay.create(EventFormSlideover, { destroyOnClose: true }).open();
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(`/api/events/admin/events/${event.id}/clone`, { method: "POST" }),
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
- () => $fetch(`/api/events/admin/events/${event.id}`, { method: "DELETE" }),
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 !== void 0;
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
+ };