@frigear-nu/events 0.0.6 → 0.0.8

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 (104) hide show
  1. package/README.md +1 -1
  2. package/dist/module.d.mts +45 -7
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +53 -42
  5. package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +11 -0
  6. package/dist/runtime/app/components/EventAdminAddonDialog.vue +113 -0
  7. package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +11 -0
  8. package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +11 -0
  9. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +79 -0
  10. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +11 -0
  11. package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +19 -0
  12. package/dist/runtime/app/components/EventAdminAddonGroups.vue +123 -0
  13. package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +19 -0
  14. package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +20 -0
  15. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +175 -0
  16. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +20 -0
  17. package/dist/runtime/app/components/EventCard.vue +39 -27
  18. package/dist/runtime/app/components/EventTicketCard.vue +5 -4
  19. package/dist/runtime/app/components/EventTicketPicker.vue +40 -21
  20. package/dist/runtime/app/composables/index.d.ts +1 -1
  21. package/dist/runtime/app/composables/index.js +1 -1
  22. package/dist/runtime/app/composables/useEvents.d.ts +10 -4
  23. package/dist/runtime/app/composables/useEvents.js +21 -10
  24. package/dist/runtime/app/pages/admin-event.d.vue.ts +3 -0
  25. package/dist/runtime/app/pages/admin-event.vue +151 -0
  26. package/dist/runtime/app/pages/admin-event.vue.d.ts +3 -0
  27. package/dist/runtime/app/pages/admin.d.vue.ts +3 -0
  28. package/dist/runtime/app/pages/admin.vue +154 -0
  29. package/dist/runtime/app/pages/admin.vue.d.ts +3 -0
  30. package/dist/runtime/app/pages/event.d.vue.ts +14 -1
  31. package/dist/runtime/app/pages/event.vue +160 -45
  32. package/dist/runtime/app/pages/event.vue.d.ts +14 -1
  33. package/dist/runtime/app/pages/events.vue +12 -10
  34. package/dist/runtime/app/pages/tickets.vue +26 -19
  35. package/dist/runtime/app/tabs/Attendees.d.vue.ts +7 -0
  36. package/dist/runtime/app/tabs/Attendees.vue +88 -0
  37. package/dist/runtime/app/tabs/Attendees.vue.d.ts +7 -0
  38. package/dist/runtime/app/tabs/Details.d.vue.ts +11 -0
  39. package/dist/runtime/app/tabs/Details.vue +74 -0
  40. package/dist/runtime/app/tabs/Details.vue.d.ts +11 -0
  41. package/dist/runtime/app/tabs/Tickets.d.vue.ts +11 -0
  42. package/dist/runtime/app/tabs/Tickets.vue +164 -0
  43. package/dist/runtime/app/tabs/Tickets.vue.d.ts +11 -0
  44. package/dist/runtime/app/utils/admin-event.d.ts +48 -0
  45. package/dist/runtime/app/utils/admin-event.js +22 -0
  46. package/dist/runtime/i18n/lang/da.json +34 -1
  47. package/dist/runtime/i18n/lang/en.json +33 -0
  48. package/dist/runtime/menus.js +17 -1
  49. package/dist/runtime/server/db/schema.sqlite.d.ts +62 -5
  50. package/dist/runtime/server/db/schema.sqlite.js +27 -2
  51. package/dist/runtime/server/mcp/schemas.d.ts +119 -0
  52. package/dist/runtime/server/mcp/schemas.js +58 -0
  53. package/dist/runtime/server/mcp/tools/admin/create-event.d.ts +2 -0
  54. package/dist/runtime/server/mcp/tools/admin/create-event.js +11 -0
  55. package/dist/runtime/server/mcp/tools/admin/get-event-definition.d.ts +2 -0
  56. package/dist/runtime/server/mcp/tools/admin/get-event-definition.js +10 -0
  57. package/dist/runtime/server/mcp/tools/admin/list-all-events.d.ts +2 -0
  58. package/dist/runtime/server/mcp/tools/admin/list-all-events.js +9 -0
  59. package/dist/runtime/server/mcp/tools/admin/list-attendees.d.ts +2 -0
  60. package/dist/runtime/server/mcp/tools/admin/list-attendees.js +25 -0
  61. package/dist/runtime/server/mcp/tools/admin/update-event.d.ts +2 -0
  62. package/dist/runtime/server/mcp/tools/admin/update-event.js +10 -0
  63. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.d.ts +2 -0
  64. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.js +10 -0
  65. package/dist/runtime/server/mcp/tools/get-event.d.ts +2 -0
  66. package/dist/runtime/server/mcp/tools/get-event.js +12 -0
  67. package/dist/runtime/server/mcp/tools/list-events.d.ts +2 -0
  68. package/dist/runtime/server/mcp/tools/list-events.js +6 -0
  69. package/dist/runtime/server/mcp/tools/my-tickets.d.ts +2 -0
  70. package/dist/runtime/server/mcp/tools/my-tickets.js +6 -0
  71. package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
  72. package/dist/runtime/server/plugins/staff-scopes.js +1 -0
  73. package/dist/runtime/server/plugins/subscriptions-membership.d.ts +2 -0
  74. package/dist/runtime/server/plugins/subscriptions-membership.js +11 -0
  75. package/dist/runtime/server/routes/admin/event-clone.post.d.ts +2 -0
  76. package/dist/runtime/server/routes/admin/event-clone.post.js +31 -0
  77. package/dist/runtime/server/routes/admin/event.get.d.ts +2 -0
  78. package/dist/runtime/server/routes/admin/event.get.js +5 -0
  79. package/dist/runtime/server/routes/admin/event.patch.js +3 -8
  80. package/dist/runtime/server/routes/admin/events.post.js +3 -13
  81. package/dist/runtime/server/routes/admin/membership-options.get.d.ts +2 -0
  82. package/dist/runtime/server/routes/admin/membership-options.get.js +4 -0
  83. package/dist/runtime/server/routes/admin/ticket-type.patch.js +3 -8
  84. package/dist/runtime/server/routes/admin/ticket-types.post.js +2 -1
  85. package/dist/runtime/server/routes/public/event.get.js +2 -2
  86. package/dist/runtime/server/routes/public/purchase.post.js +3 -2
  87. package/dist/runtime/server/routes/public/ticket-pay.post.js +1 -1
  88. package/dist/runtime/server/routes/public/ticket.get.js +1 -1
  89. package/dist/runtime/server/routes/public/tickets-me.get.js +1 -1
  90. package/dist/runtime/server/utils/admin-schemas.d.ts +102 -20
  91. package/dist/runtime/server/utils/admin-schemas.js +8 -2
  92. package/dist/runtime/server/utils/admin.d.ts +82 -1
  93. package/dist/runtime/server/utils/admin.js +51 -1
  94. package/dist/runtime/server/utils/auth.d.ts +7 -6
  95. package/dist/runtime/server/utils/auth.js +4 -4
  96. package/dist/runtime/server/utils/catalog.d.ts +14 -0
  97. package/dist/runtime/server/utils/catalog.js +20 -4
  98. package/dist/runtime/server/utils/membership.d.ts +6 -2
  99. package/dist/runtime/server/utils/membership.js +15 -2
  100. package/dist/runtime/server/utils/tickets.js +15 -12
  101. package/dist/runtime/utils/index.d.ts +17 -7
  102. package/dist/runtime/utils/index.js +16 -19
  103. package/dist/types.d.mts +3 -1
  104. package/package.json +21 -7
@@ -1,3 +1,16 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
1
+ declare var __VLS_82: {
2
+ event: any;
3
+ body: any;
4
+ };
5
+ type __VLS_Slots = {} & {
6
+ body?: (props: typeof __VLS_82) => any;
7
+ };
8
+ declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
9
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
2
10
  declare const _default: typeof __VLS_export;
3
11
  export default _default;
12
+ type __VLS_WithSlots<T, S> = T & {
13
+ new (): {
14
+ $slots: S;
15
+ };
16
+ };
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  const t = useModuleT("events");
3
- const { grouped, status } = useEvents();
3
+ const { grouped } = await useEvents();
4
4
  const pages = useRuntimeConfig().public.events?.pages;
5
5
  const eventTo = (slug) => (pages?.event || "/events/:slug").replace(":slug", encodeURIComponent(slug));
6
6
  const sections = computed(() => [
@@ -12,25 +12,27 @@ defineOptions({ name: "EventsListPage" });
12
12
  </script>
13
13
 
14
14
  <template>
15
- <UContainer class="py-10">
16
- <h1 class="mb-6 text-2xl font-semibold">
17
- {{ t("title", "Events") }}
18
- </h1>
15
+ <UContainer class="py-8 sm:py-12">
16
+ <div class="mb-10 space-y-2 text-center">
17
+ <h1 class="text-3xl font-bold tracking-tight text-highlighted sm:text-5xl">
18
+ {{ t("title", "Events") }}
19
+ </h1>
20
+ </div>
19
21
  <p
20
- v-if="status === 'success' && !sections.length"
21
- class="text-muted"
22
+ v-if="!sections.length"
23
+ class="text-center text-muted"
22
24
  >
23
25
  {{ t("empty", "There are no events right now.") }}
24
26
  </p>
25
27
  <section
26
28
  v-for="section in sections"
27
29
  :key="section.title"
28
- class="mb-10"
30
+ class="mb-12"
29
31
  >
30
- <h2 class="mb-3 text-lg font-medium">
32
+ <h2 class="mb-4 text-xl font-semibold tracking-tight text-highlighted">
31
33
  {{ section.title }}
32
34
  </h2>
33
- <div class="grid gap-4 md:grid-cols-2">
35
+ <div class="grid gap-6 md:grid-cols-2">
34
36
  <EventCard
35
37
  v-for="item in section.events"
36
38
  :key="item.id"
@@ -3,28 +3,35 @@ const t = useModuleT("events");
3
3
  const { tickets, pay } = useMyTickets();
4
4
  const pages = useRuntimeConfig().public.events?.pages;
5
5
  const eventTo = (slug) => (pages?.event || "/events/:slug").replace(":slug", encodeURIComponent(slug));
6
+ const AccountHeader = resolveComponent("AccountHeader");
7
+ const hasAccountHeader = typeof AccountHeader !== "string";
6
8
  defineOptions({ name: "EventTicketsPage" });
7
9
  </script>
8
10
 
9
11
  <template>
10
- <UContainer class="py-10">
11
- <h1 class="mb-6 text-2xl font-semibold">
12
- {{ t("myTickets", "My tickets") }}
13
- </h1>
14
- <p
15
- v-if="!tickets.length"
16
- class="text-muted"
17
- >
18
- {{ t("noTickets", "You do not have any tickets yet.") }}
19
- </p>
20
- <div class="grid gap-4 md:grid-cols-2">
21
- <EventTicketCard
22
- v-for="ticket in tickets"
23
- :key="ticket.id"
24
- :ticket="ticket"
25
- :to="eventTo(ticket.eventSlug)"
26
- @pay="pay"
12
+ <div>
13
+ <AccountHeader v-if="hasAccountHeader" />
14
+ <UContainer :class="hasAccountHeader ? 'pb-10' : 'py-10'">
15
+ <UPageHeader
16
+ v-if="!hasAccountHeader"
17
+ :title="t('myTickets', 'My tickets')"
18
+ :ui="{ root: 'pt-0 pb-6 mb-6', title: 'text-2xl sm:text-3xl' }"
27
19
  />
28
- </div>
29
- </UContainer>
20
+ <p
21
+ v-if="!tickets.length"
22
+ class="text-muted"
23
+ >
24
+ {{ t("noTickets", "You do not have any tickets yet.") }}
25
+ </p>
26
+ <div class="grid gap-4 md:grid-cols-2">
27
+ <EventTicketCard
28
+ v-for="ticket in tickets"
29
+ :key="ticket.id"
30
+ :ticket="ticket"
31
+ :to="eventTo(ticket.eventSlug)"
32
+ @pay="pay"
33
+ />
34
+ </div>
35
+ </UContainer>
36
+ </div>
30
37
  </template>
@@ -0,0 +1,7 @@
1
+ import type { AdminEvent } from '../utils/admin-event.js';
2
+ type __VLS_Props = {
3
+ event: AdminEvent;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,88 @@
1
+ <script setup>
2
+ import { useAdminAction } from "../utils/admin-event";
3
+ defineOptions({ name: "EventsAdminAttendeesTab" });
4
+ const props = defineProps({
5
+ event: { type: Object, required: true }
6
+ });
7
+ const t = useModuleT("events");
8
+ const tr = useEventsTranslate();
9
+ const confirmAction = useConfirmDialog();
10
+ const { run } = useAdminAction();
11
+ const { data: attendees, refresh } = await useFetch(() => `/api/events/admin/events/${props.event.id}/tickets`, { default: () => [] });
12
+ const typeName = (ticketTypeId) => tr(props.event.ticketTypes.find((item) => item.id === ticketTypeId)?.name);
13
+ const statusColor = { paid: "success", pending: "warning", cancelled: "neutral", refunded: "neutral", expired: "neutral" };
14
+ const ticketAction = async (ticket, action) => {
15
+ const confirmed = await confirmAction({
16
+ title: action === "refund" ? t("admin.confirmRefund", "Refund this ticket?") : t("admin.confirmCancel", "Cancel this ticket?"),
17
+ confirmText: action === "refund" ? t("admin.refund", "Refund") : t("admin.cancel", "Cancel"),
18
+ destructive: true
19
+ });
20
+ if (!confirmed) return;
21
+ return run(async () => {
22
+ await $fetch(`/api/events/admin/tickets/${ticket.id}/${action}`, { method: "POST" });
23
+ await refresh();
24
+ }, action === "refund" ? t("admin.refunded", "Refunded.") : t("admin.cancelled", "Cancelled."));
25
+ };
26
+ const columns = [
27
+ { accessorKey: "userName", header: t("admin.attendee", "Attendee") },
28
+ { accessorKey: "ticketTypeId", header: t("admin.ticketType", "Ticket") },
29
+ { accessorKey: "status", header: t("admin.status", "Status") },
30
+ { accessorKey: "checkedInAt", header: t("admin.checkedIn", "Checked in") },
31
+ { id: "actions", header: "" }
32
+ ];
33
+ </script>
34
+
35
+ <template>
36
+ <AdminSection :title="t('admin.attendees', 'Attendees')">
37
+ <template #flush>
38
+ <UTable
39
+ v-if="attendees.length"
40
+ :data="attendees"
41
+ :columns="columns"
42
+ >
43
+ <template #userName-cell="{ row }">
44
+ <span class="font-medium">{{ row.original.userName }}</span>
45
+ <span class="text-muted"> · {{ row.original.userEmail }}</span>
46
+ </template>
47
+ <template #ticketTypeId-cell="{ row }">
48
+ {{ typeName(row.original.ticketTypeId) }}
49
+ </template>
50
+ <template #status-cell="{ row }">
51
+ <UBadge
52
+ variant="subtle"
53
+ :color="statusColor[row.original.status] ?? 'neutral'"
54
+ :label="row.original.status"
55
+ />
56
+ </template>
57
+ <template #checkedInAt-cell="{ row }">
58
+ {{ row.original.checkedInAt ? "\u2713" : "\u2013" }}
59
+ </template>
60
+ <template #actions-cell="{ row }">
61
+ <div class="flex justify-end gap-1">
62
+ <UButton
63
+ v-if="row.original.status === 'paid' && row.original.paymentId"
64
+ size="xs"
65
+ variant="outline"
66
+ color="neutral"
67
+ :label="t('admin.refund', 'Refund')"
68
+ @click="ticketAction(row.original, 'refund')"
69
+ />
70
+ <UButton
71
+ v-if="row.original.status === 'paid' || row.original.status === 'pending'"
72
+ size="xs"
73
+ variant="ghost"
74
+ color="error"
75
+ :label="t('admin.cancel', 'Cancel')"
76
+ @click="ticketAction(row.original, 'cancel')"
77
+ />
78
+ </div>
79
+ </template>
80
+ </UTable>
81
+ </template>
82
+ <AdminEmpty
83
+ v-if="!attendees.length"
84
+ icon="i-lucide-users"
85
+ :title="t('admin.noAttendees', 'No tickets yet.')"
86
+ />
87
+ </AdminSection>
88
+ </template>
@@ -0,0 +1,7 @@
1
+ import type { AdminEvent } from '../utils/admin-event.js';
2
+ type __VLS_Props = {
3
+ event: AdminEvent;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import { type AdminEvent } from '../utils/admin-event.js';
2
+ type __VLS_Props = {
3
+ event: AdminEvent;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ refresh: () => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
8
+ onRefresh?: (() => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,74 @@
1
+ <script setup>
2
+ import { useAdminAction } from "../utils/admin-event";
3
+ defineOptions({ name: "EventsAdminDetailsTab" });
4
+ const props = defineProps({
5
+ event: { type: Object, required: true }
6
+ });
7
+ const emit = defineEmits(["refresh"]);
8
+ const t = useModuleT("events");
9
+ const tr = useEventsTranslate();
10
+ const { busy, run } = useAdminAction();
11
+ const form = reactive({
12
+ name: void 0,
13
+ description: void 0,
14
+ body: void 0
15
+ });
16
+ function fill(value) {
17
+ Object.assign(form, { name: value.name, description: value.description ?? void 0, body: value.body ?? void 0 });
18
+ }
19
+ fill(props.event);
20
+ watch(() => props.event, fill);
21
+ const save = () => run(async () => {
22
+ await $fetch(`/api/events/admin/events/${props.event.id}`, {
23
+ method: "PATCH",
24
+ body: { name: form.name, description: form.description ?? null, body: form.body ?? null }
25
+ });
26
+ emit("refresh");
27
+ }, t("admin.saved", "Saved."));
28
+ </script>
29
+
30
+ <template>
31
+ <AdminSection :title="t('admin.details', 'Details')">
32
+ <form
33
+ id="admin-event-details"
34
+ class="flex flex-col gap-4"
35
+ @submit.prevent="save"
36
+ >
37
+ <UFormField :label="t('admin.name', 'Name')">
38
+ <TranslatedInput
39
+ v-model="form.name"
40
+ required
41
+ />
42
+ </UFormField>
43
+ <UFormField :label="t('admin.description', 'Description')">
44
+ <TranslatedInput
45
+ v-model="form.description"
46
+ multiline
47
+ mdc
48
+ />
49
+ </UFormField>
50
+ <UFormField
51
+ :label="t('admin.body', 'Details')"
52
+ :description="t('admin.bodyHelp', 'Shown on the event page. Markdown is supported.')"
53
+ >
54
+ <TranslatedInput
55
+ v-model="form.body"
56
+ multiline
57
+ mdc
58
+ :rows="8"
59
+ />
60
+ </UFormField>
61
+ </form>
62
+ <template #footer>
63
+ <div class="flex justify-end">
64
+ <UButton
65
+ type="submit"
66
+ form="admin-event-details"
67
+ :loading="busy"
68
+ :disabled="!tr(form.name)"
69
+ :label="t('admin.save', 'Save')"
70
+ />
71
+ </div>
72
+ </template>
73
+ </AdminSection>
74
+ </template>
@@ -0,0 +1,11 @@
1
+ import { type AdminEvent } from '../utils/admin-event.js';
2
+ type __VLS_Props = {
3
+ event: AdminEvent;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ refresh: () => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
8
+ onRefresh?: (() => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import { type AdminEvent } from '../utils/admin-event.js';
2
+ type __VLS_Props = {
3
+ event: AdminEvent;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ refresh: () => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
8
+ onRefresh?: (() => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,164 @@
1
+ <script setup>
2
+ import { formatMoney as formatPrice } from "@frigear-nu/ui/money";
3
+ import { useAdminAction } from "../utils/admin-event";
4
+ defineOptions({ name: "EventsAdminTicketsTab" });
5
+ const props = defineProps({
6
+ event: { type: Object, required: true }
7
+ });
8
+ const emit = defineEmits(["refresh"]);
9
+ const t = useModuleT("events");
10
+ const locale = useEventsLocale();
11
+ const tr = useEventsTranslate();
12
+ const confirmAction = useConfirmDialog();
13
+ const { run } = useAdminAction();
14
+ const refresh = () => emit("refresh");
15
+ const typeColumns = computed(() => [
16
+ { id: "expand" },
17
+ { accessorKey: "name", header: t("admin.name", "Name") },
18
+ { accessorKey: "price", header: t("admin.price", "Price") },
19
+ { accessorKey: "capacity", header: t("admin.capacity", "Capacity") },
20
+ { id: "access", header: t("admin.access", "Access") },
21
+ { id: "addons", header: t("admin.addonGroups", "Add-ons") },
22
+ { id: "actions" }
23
+ ]);
24
+ const { data: membershipOptions } = useLazyFetch("/api/events/admin/membership-options", { default: () => ({ available: false, options: [] }) });
25
+ const membershipEnabled = computed(() => membershipOptions.value.available);
26
+ const plans = computed(() => membershipOptions.value.options.map((option) => ({ label: option.label, value: option.value })));
27
+ const planLabel = (key) => plans.value.find((plan) => plan.value === key)?.label ?? key;
28
+ const describeRule = (value) => value === "any" ? t("admin.ruleAny", "Any membership") : Array.isArray(value) ? value.map(planLabel).join(", ") : "";
29
+ const typeDialog = useTemplateRef("typeDialog");
30
+ const groupDialog = useTemplateRef("groupDialog");
31
+ const addonDialog = useTemplateRef("addonDialog");
32
+ const removeType = async (ticketType) => {
33
+ if (!await confirmAction({ title: t("admin.confirmRemoveType", "Delete this ticket type?"), confirmText: t("admin.remove", "Delete"), cancelText: t("admin.cancel", "Cancel"), destructive: true })) return;
34
+ return run(async () => {
35
+ await $fetch(`/api/events/admin/ticket-types/${ticketType.id}`, { method: "DELETE" });
36
+ await refresh();
37
+ });
38
+ };
39
+ </script>
40
+
41
+ <template>
42
+ <div class="flex flex-col gap-6">
43
+ <AdminSection :title="t('admin.ticketTypes', 'Ticket types')">
44
+ <template #actions>
45
+ <UButton
46
+ icon="i-lucide-plus"
47
+ variant="outline"
48
+ color="neutral"
49
+ size="xs"
50
+ :label="t('admin.addType', 'Add ticket type')"
51
+ @click="typeDialog?.open()"
52
+ />
53
+ </template>
54
+ <template #flush>
55
+ <UTable
56
+ :data="event.ticketTypes"
57
+ :columns="typeColumns"
58
+ :ui="{ tr: 'data-[expanded=true]:bg-elevated/30' }"
59
+ >
60
+ <template #expand-cell="{ row }">
61
+ <UButton
62
+ color="neutral"
63
+ variant="ghost"
64
+ size="xs"
65
+ :icon="row.getIsExpanded() ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
66
+ :aria-label="t('admin.addonGroups', 'Add-ons')"
67
+ @click="row.toggleExpanded()"
68
+ />
69
+ </template>
70
+ <template #name-cell="{ row }">
71
+ <span class="text-highlighted font-medium">{{ tr(row.original.name) }}</span>
72
+ </template>
73
+ <template #price-cell="{ row }">
74
+ <span class="whitespace-nowrap">{{ formatPrice(row.original.price, row.original.currency, locale) }}</span>
75
+ </template>
76
+ <template #capacity-cell="{ row }">
77
+ {{ row.original.capacity ?? "\u2013" }}
78
+ </template>
79
+ <template #access-cell="{ row }">
80
+ <div class="flex flex-wrap gap-1">
81
+ <UBadge
82
+ v-if="row.original.requiresMembership"
83
+ variant="subtle"
84
+ color="success"
85
+ :label="`${t('admin.requires', 'Requires')}: ${describeRule(row.original.requiresMembership)}`"
86
+ />
87
+ <UBadge
88
+ v-if="row.original.excludesMembership"
89
+ variant="subtle"
90
+ color="warning"
91
+ :label="`${t('admin.notFor', 'Not for')}: ${describeRule(row.original.excludesMembership)}`"
92
+ />
93
+ <span
94
+ v-if="!row.original.requiresMembership && !row.original.excludesMembership"
95
+ class="text-muted"
96
+ >–</span>
97
+ </div>
98
+ </template>
99
+ <template #addons-cell="{ row }">
100
+ {{ row.original.addonGroups.length }}
101
+ </template>
102
+ <template #actions-cell="{ row }">
103
+ <div class="flex justify-end gap-1">
104
+ <UButton
105
+ icon="i-lucide-pencil"
106
+ color="neutral"
107
+ variant="ghost"
108
+ size="xs"
109
+ :aria-label="t('admin.edit', 'Edit')"
110
+ @click="typeDialog?.open(row.original)"
111
+ />
112
+ <UButton
113
+ icon="i-lucide-trash"
114
+ color="error"
115
+ variant="ghost"
116
+ size="xs"
117
+ :aria-label="t('admin.remove', 'Delete')"
118
+ @click="removeType(row.original)"
119
+ />
120
+ </div>
121
+ </template>
122
+ <template #expanded="{ row }">
123
+ <EventAdminAddonGroups
124
+ :ticket-type="row.original"
125
+ @refresh="refresh"
126
+ @add-group="groupDialog?.open(row.original)"
127
+ @edit-group="(group) => groupDialog?.open(row.original, group)"
128
+ @add-addon="(group) => addonDialog?.open(group, row.original.currency)"
129
+ @edit-addon="(group, addon) => addonDialog?.open(group, row.original.currency, addon)"
130
+ />
131
+ </template>
132
+ <template #empty>
133
+ <AdminEmpty
134
+ icon="i-lucide-ticket"
135
+ :title="t('admin.noTypes', 'No ticket types yet: nobody can buy a ticket.')"
136
+ >
137
+ <UButton
138
+ icon="i-lucide-plus"
139
+ :label="t('admin.addType', 'Add ticket type')"
140
+ @click="typeDialog?.open()"
141
+ />
142
+ </AdminEmpty>
143
+ </template>
144
+ </UTable>
145
+ </template>
146
+ </AdminSection>
147
+
148
+ <EventAdminTicketTypeDialog
149
+ ref="typeDialog"
150
+ :event-id="props.event.id"
151
+ :membership-enabled="membershipEnabled"
152
+ :plans="plans"
153
+ @refresh="refresh"
154
+ />
155
+ <EventAdminAddonGroupDialog
156
+ ref="groupDialog"
157
+ @refresh="refresh"
158
+ />
159
+ <EventAdminAddonDialog
160
+ ref="addonDialog"
161
+ @refresh="refresh"
162
+ />
163
+ </div>
164
+ </template>
@@ -0,0 +1,11 @@
1
+ import { type AdminEvent } from '../utils/admin-event.js';
2
+ type __VLS_Props = {
3
+ event: AdminEvent;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ refresh: () => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
8
+ onRefresh?: (() => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,48 @@
1
+ import type { Translated } from '../../../types.js';
2
+ export type AdminAddon = {
3
+ id: string;
4
+ label: Translated;
5
+ description: Translated | null;
6
+ price: number;
7
+ capacity: number | null;
8
+ };
9
+ export type AdminAddonGroup = {
10
+ id: string;
11
+ title: Translated;
12
+ required: boolean;
13
+ multiple: boolean;
14
+ addons: AdminAddon[];
15
+ };
16
+ export type AdminTicketType = {
17
+ id: string;
18
+ key: string;
19
+ name: Translated;
20
+ description: Translated | null;
21
+ price: number;
22
+ currency: string;
23
+ capacity: number | null;
24
+ saleStartsAt: string | null;
25
+ saleEndsAt: string | null;
26
+ requiresMembership: 'any' | string[] | null;
27
+ excludesMembership: 'any' | string[] | null;
28
+ addonGroups: AdminAddonGroup[];
29
+ };
30
+ export type AdminEvent = {
31
+ id: string;
32
+ slug: string;
33
+ name: Translated;
34
+ description: Translated | null;
35
+ body: Translated | null;
36
+ status: 'draft' | 'published' | 'archived';
37
+ startsAt: string;
38
+ endsAt: string | null;
39
+ location: Translated | null;
40
+ ticketTypes: AdminTicketType[];
41
+ };
42
+ /** `datetime-local` inputs want the local time without a zone, not the stored ISO instant. */
43
+ export declare const toLocalInput: (value: string | null) => string;
44
+ /** Runs an admin action with a shared busy flag, toasting the success message or the server's error. */
45
+ export declare function useAdminAction(): {
46
+ busy: any;
47
+ run: (action: () => Promise<unknown>, success?: string) => Promise<void>;
48
+ };
@@ -0,0 +1,22 @@
1
+ export const toLocalInput = (value) => {
2
+ if (!value) return "";
3
+ const date = new Date(value);
4
+ return new Date(date.getTime() - date.getTimezoneOffset() * 6e4).toISOString().slice(0, 16);
5
+ };
6
+ export function useAdminAction() {
7
+ const t = useModuleT("events");
8
+ const toast = useToast();
9
+ const busy = ref(false);
10
+ async function run(action, success) {
11
+ busy.value = true;
12
+ try {
13
+ await action();
14
+ if (success) toast.add({ title: success, color: "success" });
15
+ } catch (cause) {
16
+ toast.add({ title: cause.statusMessage ?? t("admin.error", "Something went wrong."), color: "error" });
17
+ } finally {
18
+ busy.value = false;
19
+ }
20
+ }
21
+ return { busy, run };
22
+ }
@@ -73,6 +73,22 @@
73
73
  "loading": "Henter…",
74
74
  "members": "Medlemmer",
75
75
  "viewEvent": "Se event",
76
+ "picker": {
77
+ "soldOut": "Udsolgt",
78
+ "notStarted": "Salget er ikke startet",
79
+ "ended": "Salget er slut",
80
+ "membersOnly": "Kun for medlemmer",
81
+ "notForMembers": "Ikke for medlemmer",
82
+ "booked": "Allerede booket",
83
+ "left": "tilbage",
84
+ "free": "Gratis",
85
+ "included": "Inkluderet",
86
+ "getFree": "Få gratis billet",
87
+ "buy": "Køb billet",
88
+ "signIn": "Log ind for at få en billet.",
89
+ "noTickets": "Der er ingen billetter til dette event."
90
+ },
91
+ "backToEvents": "Alle events",
76
92
  "hideQr": "Skjul QR-kode",
77
93
  "checkedIn": "Tjekket ind",
78
94
  "myTickets": "Mine billetter",
@@ -82,6 +98,23 @@
82
98
  "paymentCancelled": "Betaling annulleret. Din reservation holdes et kort stykke tid; du kan betale fra dine billetter.",
83
99
  "ticketReady": "Din billet er klar",
84
100
  "purchaseFailed": "Kunne ikke købe billetten",
85
- "notFound": "Event ikke fundet"
101
+ "notFound": "Event ikke fundet",
102
+ "menu": {
103
+ "tickets": {
104
+ "label": "Billetter",
105
+ "description": "Dine billetter til arrangementer."
106
+ },
107
+ "admin": {
108
+ "label": "Arrangementer",
109
+ "description": "Administrér arrangementer, billettyper og deltagere."
110
+ }
111
+ },
112
+ "admin": {
113
+ "tabs": {
114
+ "details": "Detaljer",
115
+ "tickets": "Billetter",
116
+ "attendees": "Deltagere"
117
+ }
118
+ }
86
119
  }
87
120
  }