@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,4 +1,5 @@
1
1
  <script setup>
2
+ import { computed, useEventLinks, useEvents, useModuleT, useSeoMeta } from "#imports";
2
3
  defineOptions({ name: "EventsListPage" });
3
4
  const t = useModuleT("events");
4
5
  const { grouped } = await useEvents();
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { useEventLinks, useModuleT, useMyTickets } from "#imports";
2
3
  defineOptions({ name: "EventTicketsPage" });
3
4
  const t = useModuleT("events");
4
5
  const { tickets, loading, error, refresh, pay } = await useMyTickets();
@@ -7,6 +8,10 @@ const links = useEventLinks();
7
8
 
8
9
  <template>
9
10
  <div class="flex flex-col gap-4">
11
+ <PageHeading
12
+ :title="t('menu.tickets.label', 'Tickets')"
13
+ :description="t('menu.tickets.description', 'Your event tickets.')"
14
+ />
10
15
  <div
11
16
  v-if="loading"
12
17
  class="grid gap-4 md:grid-cols-2"
@@ -1,4 +1,7 @@
1
1
  <script setup>
2
+ import { refDebounced } from "@vueuse/core";
3
+ import { eventsApi } from "../../utils/api";
4
+ import { $fetch, computed, emptyCell, h, ref, resolveComponent, useAction, useConfirmDialog, useEventsTranslate, useFetch, useFormat, useModuleT, watch } from "#imports";
2
5
  defineOptions({ name: "EventsAdminAttendeesTab" });
3
6
  const props = defineProps({
4
7
  event: { type: Object, required: true }
@@ -8,7 +11,18 @@ const tr = useEventsTranslate();
8
11
  const format = useFormat();
9
12
  const confirmAction = useConfirmDialog();
10
13
  const { run } = useAction();
11
- const { data: attendees, refresh } = await useFetch(() => `/api/events/admin/events/${props.event.id}/tickets`, { default: () => [] });
14
+ const page = ref(1);
15
+ const pageSize = ref(50);
16
+ const search = ref("");
17
+ const q = refDebounced(search, 300);
18
+ watch([q, pageSize], () => {
19
+ page.value = 1;
20
+ });
21
+ const query = computed(() => ({ page: page.value, pageSize: pageSize.value, q: q.value.trim() || void 0 }));
22
+ const { data, status, error, refresh } = await useFetch(() => eventsApi.admin.attendees(props.event.id), { query });
23
+ const rows = computed(() => data.value?.items ?? []);
24
+ const loading = computed(() => status.value === "pending" && !data.value);
25
+ const refreshing = computed(() => status.value === "pending" && !!data.value);
12
26
  const typeName = (ticketTypeId) => tr(props.event.ticketTypes.find((item) => item.id === ticketTypeId)?.name);
13
27
  const who = (ticket) => ticket.userName || ticket.userEmail || "";
14
28
  const ticketAction = async (ticket, action) => {
@@ -20,7 +34,7 @@ const ticketAction = async (ticket, action) => {
20
34
  });
21
35
  if (!confirmed) return;
22
36
  return run(async () => {
23
- await $fetch(`/api/events/admin/tickets/${ticket.id}/${action}`, { method: "POST" });
37
+ await $fetch(action === "refund" ? eventsApi.admin.refundTicket(ticket.id) : eventsApi.admin.cancelTicket(ticket.id), { method: "POST" });
24
38
  await refresh();
25
39
  }, { success: action === "refund" ? t("admin.refunded", "Refunded.") : t("admin.cancelled", "Cancelled.") });
26
40
  };
@@ -33,53 +47,71 @@ const menu = (ticket) => {
33
47
  } }] : [];
34
48
  return [refund, cancel].filter((group) => group.length);
35
49
  };
50
+ const TableIdentity = resolveComponent("TableIdentity");
51
+ const StatusBadge = resolveComponent("StatusBadge");
52
+ const RowMenu = resolveComponent("RowMenu");
36
53
  const columns = computed(() => [
37
- { accessorKey: "userName", header: t("admin.attendee", "Attendee") },
38
- { accessorKey: "ticketTypeId", header: t("admin.ticketType", "Ticket") },
39
- { accessorKey: "status", header: t("admin.status", "Status") },
40
- { accessorKey: "checkedInAt", header: t("admin.checkedIn", "Checked in") },
41
- { id: "actions" }
54
+ {
55
+ accessorKey: "userName",
56
+ header: t("admin.attendee", "Attendee"),
57
+ cell: ({ row }) => h(TableIdentity, { title: who(row.original), description: row.original.userName ? row.original.userEmail : null })
58
+ },
59
+ {
60
+ accessorKey: "ticketTypeId",
61
+ header: t("admin.ticketType", "Ticket"),
62
+ cell: ({ row }) => typeName(row.original.ticketTypeId) || emptyCell()
63
+ },
64
+ {
65
+ accessorKey: "status",
66
+ header: t("admin.status", "Status"),
67
+ cell: ({ row }) => h(StatusBadge, { status: row.original.status, label: t(`tickets.status.${row.original.status}`, row.original.status) })
68
+ },
69
+ {
70
+ accessorKey: "checkedInAt",
71
+ header: t("admin.checkedIn", "Checked in"),
72
+ cell: ({ row }) => row.original.checkedInAt ? h("span", { class: "tabular-nums" }, format.dateTime(row.original.checkedInAt)) : emptyCell()
73
+ },
74
+ {
75
+ id: "actions",
76
+ meta: { class: { th: "w-0", td: "w-0" } },
77
+ cell: ({ row }) => {
78
+ const items = menu(row.original);
79
+ return items.length ? h(RowMenu, { items, subject: who(row.original) }) : null;
80
+ }
81
+ }
42
82
  ]);
43
83
  </script>
44
84
 
45
85
  <template>
46
- <SectionCard
86
+ <TableCard
47
87
  :title="t('admin.attendees', 'Attendees')"
48
- flush
88
+ :loading="loading"
89
+ :refreshing="refreshing"
49
90
  >
91
+ <template #actions>
92
+ <UInput
93
+ v-model="search"
94
+ icon="i-lucide-search"
95
+ size="sm"
96
+ :placeholder="t('admin.searchAttendees', 'Search name or email')"
97
+ :aria-label="t('admin.searchAttendees', 'Search name or email')"
98
+ class="w-full sm:max-w-xs"
99
+ />
100
+ </template>
101
+ <UEmpty
102
+ v-if="error && !data"
103
+ variant="naked"
104
+ icon="i-lucide-circle-alert"
105
+ :title="t('admin.attendeesLoadFailed', 'Attendees could not be loaded')"
106
+ :description="errorMessage(error, '')"
107
+ :actions="[{ label: t('tryAgain', 'Try again'), icon: 'i-lucide-rotate-ccw', color: 'neutral', variant: 'outline', onClick: () => refresh() }]"
108
+ />
50
109
  <UTable
51
- :data="attendees"
110
+ v-else
111
+ :data="rows"
52
112
  :columns="columns"
53
113
  :ui="tableUi({ tr: '' })"
54
114
  >
55
- <template #userName-cell="{ row }">
56
- <span class="text-highlighted font-medium">{{ row.original.userName }}</span>
57
- <span class="text-muted"> · {{ row.original.userEmail }}</span>
58
- </template>
59
- <template #ticketTypeId-cell="{ row }">
60
- {{ typeName(row.original.ticketTypeId) }}
61
- </template>
62
- <template #status-cell="{ row }">
63
- <UBadge
64
- variant="subtle"
65
- :color="statusColor(row.original.status)"
66
- :label="t(`tickets.status.${row.original.status}`, row.original.status)"
67
- />
68
- </template>
69
- <template #checkedInAt-cell="{ row }">
70
- <span v-if="row.original.checkedInAt">{{ format.dateTime(row.original.checkedInAt) }}</span>
71
- <span
72
- v-else
73
- class="text-muted"
74
- >–</span>
75
- </template>
76
- <template #actions-cell="{ row }">
77
- <RowMenu
78
- v-if="menu(row.original).length"
79
- :items="menu(row.original)"
80
- :subject="who(row.original)"
81
- />
82
- </template>
83
115
  <template #empty>
84
116
  <UEmpty
85
117
  variant="naked"
@@ -88,5 +120,13 @@ const columns = computed(() => [
88
120
  />
89
121
  </template>
90
122
  </UTable>
91
- </SectionCard>
123
+ <template #footer>
124
+ <TablePagination
125
+ v-model:page="page"
126
+ v-model:page-size="pageSize"
127
+ :page-sizes="[50, 100]"
128
+ :total="data?.total ?? 0"
129
+ />
130
+ </template>
131
+ </TableCard>
92
132
  </template>
@@ -1,4 +1,4 @@
1
- import type { StaffEvent } from '../utils/admin-event.js';
1
+ import { type StaffEvent } from '../utils/admin-event.js';
2
2
  type __VLS_Props = {
3
3
  event: StaffEvent;
4
4
  };
@@ -1,4 +1,7 @@
1
1
  <script setup>
2
+ import { eventsApi } from "../../utils/api";
3
+ import { EVENT_STATUSES, eventFormSchema, toLocalInput } from "../utils/admin-event";
4
+ import { $fetch, computed, reactive, useAction, useEventsTranslate, useModuleT, watch } from "#imports";
2
5
  defineOptions({ name: "EventsAdminDetailsTab" });
3
6
  const props = defineProps({
4
7
  event: { type: Object, required: true }
@@ -7,65 +10,145 @@ const emit = defineEmits(["refresh"]);
7
10
  const t = useModuleT("events");
8
11
  const tr = useEventsTranslate();
9
12
  const { busy, run } = useAction();
10
- const form = reactive({
11
- name: void 0,
12
- description: void 0,
13
- body: void 0
14
- });
13
+ const schema = eventFormSchema(t, tr);
14
+ const statuses = computed(() => EVENT_STATUSES.map((value) => ({ value, label: t(`admin.statuses.${value}`, value.charAt(0).toUpperCase() + value.slice(1)) })));
15
+ const state = reactive({ name: void 0, slug: "", startsAt: "", endsAt: "", status: "draft" });
15
16
  function fill(value) {
16
- Object.assign(form, { name: value.name, description: value.description ?? void 0, body: value.body ?? void 0 });
17
+ Object.assign(state, {
18
+ name: value.name,
19
+ slug: value.slug,
20
+ status: value.status,
21
+ startsAt: toLocalInput(value.startsAt),
22
+ endsAt: toLocalInput(value.endsAt),
23
+ location: value.location ?? void 0,
24
+ description: value.description ?? void 0,
25
+ body: value.body ?? void 0
26
+ });
17
27
  }
18
28
  fill(props.event);
19
29
  watch(() => props.event, fill);
20
- const save = () => run(async () => {
21
- await $fetch(`/api/events/admin/events/${props.event.id}`, {
22
- method: "PATCH",
23
- body: { name: form.name, description: form.description ?? null, body: form.body ?? null }
24
- });
25
- emit("refresh");
26
- }, { success: t("admin.saved", "Saved.") });
30
+ async function save(event) {
31
+ const data = event.data;
32
+ const saved = await run(
33
+ () => $fetch(eventsApi.admin.event(props.event.id), {
34
+ method: "PATCH",
35
+ body: {
36
+ name: data.name,
37
+ slug: data.slug,
38
+ status: data.status,
39
+ startsAt: data.startsAt,
40
+ endsAt: data.endsAt || null,
41
+ location: data.location ?? null,
42
+ description: data.description ?? null,
43
+ body: data.body ?? null
44
+ }
45
+ }).then(() => true),
46
+ { success: t("admin.saved", "Saved.") }
47
+ );
48
+ if (saved) emit("refresh");
49
+ }
27
50
  </script>
28
51
 
29
52
  <template>
30
53
  <SectionCard :title="t('admin.details', 'Details')">
31
- <form
54
+ <UForm
32
55
  id="admin-event-details"
33
- class="flex flex-col gap-4"
34
- @submit.prevent="save"
56
+ :schema="schema"
57
+ :state="state"
58
+ class="flex flex-col divide-y divide-default"
59
+ @submit="save"
35
60
  >
36
- <UFormField :label="t('admin.name', 'Name')">
61
+ <FormRow
62
+ name="name"
63
+ :label="t('admin.name', 'Name')"
64
+ required
65
+ >
37
66
  <TranslatedInput
38
- v-model="form.name"
39
- required
67
+ v-model="state.name"
68
+ class="sm:max-w-sm"
69
+ />
70
+ </FormRow>
71
+ <FormRow
72
+ name="status"
73
+ :label="t('admin.status', 'Status')"
74
+ >
75
+ <USelect
76
+ v-model="state.status"
77
+ :items="statuses"
78
+ class="w-full sm:max-w-sm"
79
+ />
80
+ </FormRow>
81
+ <FormRow
82
+ name="slug"
83
+ :label="t('admin.slug', 'URL name')"
84
+ :description="t('admin.slugHelp', 'Lowercase letters, numbers and dashes.')"
85
+ required
86
+ >
87
+ <UInput
88
+ v-model="state.slug"
89
+ class="w-full sm:max-w-sm"
40
90
  />
41
- </UFormField>
42
- <UFormField :label="t('admin.description', 'Description')">
91
+ </FormRow>
92
+ <FormRow
93
+ name="startsAt"
94
+ :label="t('admin.starts', 'Starts')"
95
+ required
96
+ >
97
+ <UInput
98
+ v-model="state.startsAt"
99
+ type="datetime-local"
100
+ class="w-full sm:max-w-sm"
101
+ />
102
+ </FormRow>
103
+ <FormRow
104
+ name="endsAt"
105
+ :label="t('admin.ends', 'Ends')"
106
+ >
107
+ <UInput
108
+ v-model="state.endsAt"
109
+ type="datetime-local"
110
+ class="w-full sm:max-w-sm"
111
+ />
112
+ </FormRow>
113
+ <FormRow
114
+ name="location"
115
+ :label="t('admin.location', 'Location')"
116
+ >
117
+ <TranslatedInput
118
+ v-model="state.location"
119
+ class="sm:max-w-sm"
120
+ />
121
+ </FormRow>
122
+ <FormRow
123
+ name="description"
124
+ :label="t('admin.description', 'Description')"
125
+ >
43
126
  <TranslatedInput
44
- v-model="form.description"
127
+ v-model="state.description"
45
128
  multiline
46
129
  mdc
47
130
  />
48
- </UFormField>
49
- <UFormField
131
+ </FormRow>
132
+ <FormRow
133
+ name="body"
50
134
  :label="t('admin.body', 'Details')"
51
135
  :description="t('admin.bodyHelp', 'Shown on the event page. Markdown is supported.')"
52
136
  >
53
137
  <TranslatedInput
54
- v-model="form.body"
138
+ v-model="state.body"
55
139
  multiline
56
140
  mdc
57
141
  :rows="8"
58
142
  />
59
- </UFormField>
60
- </form>
143
+ </FormRow>
144
+ </UForm>
61
145
  <template #footer>
62
- <div class="flex justify-end">
146
+ <div class="flex justify-end gap-2">
63
147
  <UButton
64
148
  type="submit"
65
149
  form="admin-event-details"
66
150
  :loading="busy"
67
- :disabled="!tr(form.name)"
68
- :label="t('admin.save', 'Save')"
151
+ :label="t('admin.saveEvent', 'Save event')"
69
152
  />
70
153
  </div>
71
154
  </template>
@@ -1,4 +1,4 @@
1
- import type { StaffEvent } from '../utils/admin-event.js';
1
+ import { type StaffEvent } from '../utils/admin-event.js';
2
2
  type __VLS_Props = {
3
3
  event: StaffEvent;
4
4
  };
@@ -1,4 +1,6 @@
1
1
  <script setup>
2
+ import { eventsApi } from "../../utils/api";
3
+ import { computed, emptyCell, h, numericColumn, resolveComponent, useCachedFetch, useEventCatalogActions, useEventsTranslate, useFormat, useModuleT } from "#imports";
2
4
  defineOptions({ name: "EventsAdminTicketsTab" });
3
5
  const props = defineProps({
4
6
  event: { type: Object, required: true }
@@ -7,161 +9,123 @@ const emit = defineEmits(["refresh"]);
7
9
  const t = useModuleT("events");
8
10
  const format = useFormat();
9
11
  const tr = useEventsTranslate();
10
- const confirmAction = useConfirmDialog();
11
- const { run } = useAction();
12
12
  const refresh = () => emit("refresh");
13
- const typeColumns = computed(() => [
14
- { id: "expand" },
15
- { accessorKey: "name", header: t("admin.name", "Name") },
16
- { accessorKey: "price", header: t("admin.price", "Price") },
17
- { accessorKey: "capacity", header: t("admin.capacity", "Capacity") },
18
- { id: "access", header: t("admin.access", "Access") },
19
- { id: "addons", header: t("admin.addonGroups", "Add-ons") },
20
- { id: "actions" }
21
- ]);
22
- const { data: membershipOptions, loading: optionsLoading } = useCachedFetch("/api/events/admin/membership-options", {
13
+ const { data: membershipOptions, loading: optionsLoading } = useCachedFetch(eventsApi.admin.membershipOptions, {
23
14
  key: "events:admin:membership-options",
24
15
  ttl: 5 * 6e4
25
16
  });
26
- const membershipEnabled = computed(() => membershipOptions.value?.available === true);
27
17
  const plans = computed(() => (membershipOptions.value?.options ?? []).map((option) => ({ label: option.label, value: option.value })));
28
18
  const planLabel = (key) => plans.value.find((plan) => plan.value === key)?.label ?? key;
29
19
  const describeRule = (value) => value === "any" ? t("admin.ruleAny", "Any membership") : Array.isArray(value) ? value.map(planLabel).join(", ") : "";
30
- const typeDialog = useTemplateRef("typeDialog");
31
- const groupDialog = useTemplateRef("groupDialog");
32
- const addonDialog = useTemplateRef("addonDialog");
33
- const removeType = async (ticketType) => {
34
- const name = tr(ticketType.name);
35
- if (!await confirmAction({
36
- title: t("admin.confirmRemoveType", "Delete {name}?", { name }),
37
- description: t("admin.confirmRemoveTypeDescription", "Its add-on groups and choices are deleted too. A ticket type with active tickets cannot be deleted."),
38
- confirmText: t("admin.removeType", "Delete ticket type"),
39
- destructive: true
40
- })) return;
41
- return run(async () => {
42
- await $fetch(`/api/events/admin/ticket-types/${ticketType.id}`, { method: "DELETE" });
43
- await refresh();
44
- }, { success: t("admin.typeDeleted", "Deleted {name}", { name }) });
45
- };
46
- const menu = (ticketType) => [
47
- [{ label: t("admin.editType", "Edit ticket type"), icon: "i-lucide-pencil", onSelect: () => typeDialog.value?.open(ticketType) }],
48
- [{ label: t("admin.removeType", "Delete ticket type"), icon: "i-lucide-trash-2", color: "error", onSelect: () => {
49
- void removeType(ticketType);
50
- } }]
51
- ];
20
+ const catalog = useEventCatalogActions({
21
+ eventId: () => props.event.id,
22
+ onChanged: refresh,
23
+ membership: () => ({ enabled: membershipOptions.value?.available === true, plans: plans.value })
24
+ });
25
+ const RowMenu = resolveComponent("RowMenu");
26
+ const typeColumns = computed(() => [
27
+ { id: "expand", meta: { class: { th: "w-0", td: "w-0" } } },
28
+ {
29
+ accessorKey: "name",
30
+ header: t("admin.name", "Name"),
31
+ cell: ({ row }) => h("span", { class: "text-highlighted font-medium" }, tr(row.original.name))
32
+ },
33
+ {
34
+ accessorKey: "price",
35
+ header: t("admin.price", "Price"),
36
+ meta: numericColumn,
37
+ cell: ({ row }) => h("span", { class: "whitespace-nowrap" }, format.money(row.original.price, row.original.currency))
38
+ },
39
+ {
40
+ accessorKey: "capacity",
41
+ header: t("admin.capacity", "Capacity"),
42
+ meta: numericColumn,
43
+ cell: ({ row }) => row.original.capacity ?? emptyCell()
44
+ },
45
+ { id: "access", header: t("admin.access", "Access") },
46
+ {
47
+ id: "addons",
48
+ header: t("admin.addonGroups", "Add-ons"),
49
+ meta: numericColumn,
50
+ cell: ({ row }) => row.original.addonGroups.length
51
+ },
52
+ {
53
+ id: "actions",
54
+ meta: { class: { th: "w-0", td: "w-0" } },
55
+ cell: ({ row }) => h(RowMenu, { items: catalog.typeMenu(row.original), subject: tr(row.original.name) })
56
+ }
57
+ ]);
52
58
  </script>
53
59
 
54
60
  <template>
55
- <div class="flex flex-col gap-4">
56
- <SectionCard
57
- :title="t('admin.ticketTypes', 'Ticket types')"
58
- flush
61
+ <TableCard :title="t('admin.ticketTypes', 'Ticket types')">
62
+ <template #actions>
63
+ <UButton
64
+ icon="i-lucide-plus"
65
+ variant="outline"
66
+ color="neutral"
67
+ size="xs"
68
+ :label="t('admin.addType', 'Add ticket type')"
69
+ @click="catalog.openTicketType()"
70
+ />
71
+ </template>
72
+ <UTable
73
+ :data="event.ticketTypes"
74
+ :columns="typeColumns"
75
+ :ui="tableUi({ tr: 'data-[expanded=true]:bg-elevated/30' })"
59
76
  >
60
- <template #actions>
77
+ <template #expand-cell="{ row }">
61
78
  <UButton
62
- icon="i-lucide-plus"
63
- variant="outline"
64
79
  color="neutral"
80
+ variant="ghost"
65
81
  size="xs"
66
- :label="t('admin.addType', 'Add ticket type')"
67
- @click="typeDialog?.open()"
82
+ :icon="row.getIsExpanded() ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
83
+ :aria-label="t('admin.addonGroups', 'Add-ons')"
84
+ :aria-expanded="row.getIsExpanded()"
85
+ @click="row.toggleExpanded()"
68
86
  />
69
87
  </template>
70
- <UTable
71
- :data="event.ticketTypes"
72
- :columns="typeColumns"
73
- :ui="tableUi({ tr: 'data-[expanded=true]:bg-elevated/30' })"
74
- >
75
- <template #expand-cell="{ row }">
76
- <UButton
77
- color="neutral"
78
- variant="ghost"
79
- size="xs"
80
- :icon="row.getIsExpanded() ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
81
- :aria-label="t('admin.addonGroups', 'Add-ons')"
82
- @click="row.toggleExpanded()"
83
- />
84
- </template>
85
- <template #name-cell="{ row }">
86
- <span class="text-highlighted font-medium">{{ tr(row.original.name) }}</span>
87
- </template>
88
- <template #price-cell="{ row }">
89
- <span class="whitespace-nowrap">{{ format.money(row.original.price, row.original.currency) }}</span>
90
- </template>
91
- <template #capacity-cell="{ row }">
92
- {{ row.original.capacity ?? "\u2013" }}
93
- </template>
94
- <template #access-cell="{ row }">
95
- <USkeleton
96
- v-if="optionsLoading && (row.original.requiresMembership || row.original.excludesMembership)"
97
- class="h-5 w-28"
98
- />
99
- <div
100
- v-else
101
- class="flex flex-wrap gap-1"
102
- >
103
- <UBadge
104
- v-if="row.original.requiresMembership"
105
- variant="subtle"
106
- color="success"
107
- :label="`${t('admin.requires', 'Requires')}: ${describeRule(row.original.requiresMembership)}`"
108
- />
109
- <UBadge
110
- v-if="row.original.excludesMembership"
111
- variant="subtle"
112
- color="warning"
113
- :label="`${t('admin.notFor', 'Not for')}: ${describeRule(row.original.excludesMembership)}`"
114
- />
115
- <span
116
- v-if="!row.original.requiresMembership && !row.original.excludesMembership"
117
- class="text-muted"
118
- >–</span>
119
- </div>
120
- </template>
121
- <template #addons-cell="{ row }">
122
- {{ row.original.addonGroups.length }}
123
- </template>
124
- <template #actions-cell="{ row }">
125
- <RowMenu
126
- :items="menu(row.original)"
127
- :subject="tr(row.original.name)"
128
- />
129
- </template>
130
- <template #expanded="{ row }">
131
- <EventAdminAddonGroups
132
- :ticket-type="row.original"
133
- @refresh="refresh"
134
- @add-group="groupDialog?.open(row.original)"
135
- @edit-group="(group) => groupDialog?.open(row.original, group)"
136
- @add-addon="(group) => addonDialog?.open(group, row.original.currency)"
137
- @edit-addon="(group, addon) => addonDialog?.open(group, row.original.currency, addon)"
88
+ <template #access-cell="{ row }">
89
+ <USkeleton
90
+ v-if="optionsLoading && (row.original.requiresMembership || row.original.excludesMembership)"
91
+ class="h-5 w-28"
92
+ />
93
+ <div
94
+ v-else-if="row.original.requiresMembership || row.original.excludesMembership"
95
+ class="flex flex-wrap gap-1"
96
+ >
97
+ <UBadge
98
+ v-if="row.original.requiresMembership"
99
+ variant="subtle"
100
+ color="success"
101
+ :label="`${t('admin.requires', 'Requires')}: ${describeRule(row.original.requiresMembership)}`"
138
102
  />
139
- </template>
140
- <template #empty>
141
- <UEmpty
142
- variant="naked"
143
- icon="i-lucide-ticket"
144
- :title="t('admin.noTypes', 'No ticket types yet: nobody can buy a ticket.')"
145
- :actions="[{ label: t('admin.addType', 'Add ticket type'), icon: 'i-lucide-plus', onClick: () => typeDialog?.open() }]"
103
+ <UBadge
104
+ v-if="row.original.excludesMembership"
105
+ variant="subtle"
106
+ color="warning"
107
+ :label="`${t('admin.notFor', 'Not for')}: ${describeRule(row.original.excludesMembership)}`"
146
108
  />
147
- </template>
148
- </UTable>
149
- </SectionCard>
150
-
151
- <EventAdminTicketTypeDialog
152
- ref="typeDialog"
153
- :event-id="props.event.id"
154
- :membership-enabled="membershipEnabled"
155
- :plans="plans"
156
- @refresh="refresh"
157
- />
158
- <EventAdminAddonGroupDialog
159
- ref="groupDialog"
160
- @refresh="refresh"
161
- />
162
- <EventAdminAddonDialog
163
- ref="addonDialog"
164
- @refresh="refresh"
165
- />
166
- </div>
109
+ </div>
110
+ <component
111
+ :is="emptyCell"
112
+ v-else
113
+ />
114
+ </template>
115
+ <template #expanded="{ row }">
116
+ <EventAdminAddonGroups
117
+ :ticket-type="row.original"
118
+ @refresh="refresh"
119
+ />
120
+ </template>
121
+ <template #empty>
122
+ <UEmpty
123
+ variant="naked"
124
+ icon="i-lucide-ticket"
125
+ :title="t('admin.noTypes', 'No ticket types yet: nobody can buy a ticket.')"
126
+ :actions="[{ label: t('admin.addType', 'Add ticket type'), icon: 'i-lucide-plus', onClick: () => catalog.openTicketType() }]"
127
+ />
128
+ </template>
129
+ </UTable>
130
+ </TableCard>
167
131
  </template>