@frigear-nu/events 0.0.9 → 0.1.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 (58) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +6 -4
  3. package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +2 -2
  4. package/dist/runtime/app/components/EventAdminAddonDialog.vue +3 -4
  5. package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +2 -2
  6. package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +2 -2
  7. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +3 -4
  8. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +2 -2
  9. package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +8 -8
  10. package/dist/runtime/app/components/EventAdminAddonGroups.vue +46 -55
  11. package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +8 -8
  12. package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +2 -2
  13. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +3 -4
  14. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +2 -2
  15. package/dist/runtime/app/components/EventCard.vue +7 -12
  16. package/dist/runtime/app/components/EventFormSlideover.d.vue.ts +13 -0
  17. package/dist/runtime/app/components/EventFormSlideover.vue +70 -0
  18. package/dist/runtime/app/components/EventFormSlideover.vue.d.ts +13 -0
  19. package/dist/runtime/app/components/EventTicketCard.vue +5 -7
  20. package/dist/runtime/app/components/EventTicketPicker.vue +5 -6
  21. package/dist/runtime/app/composables/index.d.ts +2 -0
  22. package/dist/runtime/app/composables/index.js +2 -0
  23. package/dist/runtime/app/composables/useEventActions.d.ts +26 -0
  24. package/dist/runtime/app/composables/useEventActions.js +61 -0
  25. package/dist/runtime/app/composables/useEventLinks.d.ts +12 -0
  26. package/dist/runtime/app/composables/useEventLinks.js +5 -0
  27. package/dist/runtime/app/composables/useEvents.d.ts +5 -2
  28. package/dist/runtime/app/pages/admin-event.vue +33 -21
  29. package/dist/runtime/app/pages/admin.vue +61 -135
  30. package/dist/runtime/app/pages/event.d.vue.ts +2 -2
  31. package/dist/runtime/app/pages/event.vue +113 -152
  32. package/dist/runtime/app/pages/event.vue.d.ts +2 -2
  33. package/dist/runtime/app/pages/events.vue +33 -31
  34. package/dist/runtime/app/pages/tickets.vue +21 -28
  35. package/dist/runtime/app/tabs/Attendees.d.vue.ts +2 -2
  36. package/dist/runtime/app/tabs/Attendees.vue +64 -60
  37. package/dist/runtime/app/tabs/Attendees.vue.d.ts +2 -2
  38. package/dist/runtime/app/tabs/Details.d.vue.ts +2 -2
  39. package/dist/runtime/app/tabs/Details.vue +4 -5
  40. package/dist/runtime/app/tabs/Details.vue.d.ts +2 -2
  41. package/dist/runtime/app/tabs/Tickets.d.vue.ts +2 -2
  42. package/dist/runtime/app/tabs/Tickets.vue +92 -99
  43. package/dist/runtime/app/tabs/Tickets.vue.d.ts +2 -2
  44. package/dist/runtime/app/utils/admin-event.d.ts +9 -12
  45. package/dist/runtime/app/utils/admin-event.js +0 -17
  46. package/dist/runtime/app/widgets/UpcomingEvents.vue +7 -9
  47. package/dist/runtime/app/widgets/UpcomingEventsStat.vue +5 -6
  48. package/dist/runtime/i18n/lang/da.json +111 -3
  49. package/dist/runtime/i18n/lang/en.json +111 -3
  50. package/dist/runtime/menus.js +2 -4
  51. package/dist/runtime/server/db/schema.postgresql.d.ts +1110 -0
  52. package/dist/runtime/server/db/schema.postgresql.js +125 -0
  53. package/dist/runtime/server/utils/admin.d.ts +5 -5
  54. package/dist/runtime/utils/index.d.ts +1 -0
  55. package/dist/runtime/utils/index.js +1 -0
  56. package/dist/runtime/utils/links.d.ts +21 -0
  57. package/dist/runtime/utils/links.js +12 -0
  58. package/package.json +21 -21
@@ -5,25 +5,18 @@ const route = useRoute();
5
5
  const slug = computed(() => String(route.params.slug));
6
6
  const { loggedIn } = useUserSession();
7
7
  const { event, error, purchase, refresh } = await useEvent(slug);
8
- const locale = useEventsLocale();
9
8
  const tr = useEventsTranslate();
9
+ const format = useFormat();
10
+ const links = useEventLinks();
10
11
  const toast = useToast();
12
+ const { busy: buying, run } = useAction();
11
13
  if (!event.value) {
12
14
  throw createError({ statusCode: error.value?.statusCode ?? 404, statusMessage: t("notFound", "Event not found"), fatal: true });
13
15
  }
14
- const eventsPath = useRuntimeConfig().public.events?.pages?.events || "/events";
15
- const buying = ref(false);
16
- async function onPurchase(ticketTypeId, addonIds) {
17
- buying.value = true;
18
- try {
19
- const result = await purchase(ticketTypeId, addonIds);
20
- if (!result.url) toast.add({ title: t("ticketReady", "Your ticket is ready"), color: "success" });
21
- } catch (e) {
22
- toast.add({ title: e.statusMessage ?? t("purchaseFailed", "Could not buy the ticket"), color: "error" });
23
- } finally {
24
- buying.value = false;
25
- }
26
- }
16
+ const onPurchase = (ticketTypeId, addonIds) => run(async () => {
17
+ const result = await purchase(ticketTypeId, addonIds);
18
+ if (!result.url) toast.add({ title: t("ticketReady", "Your ticket is ready"), color: "success", icon: "i-lucide-check" });
19
+ }, { error: t("purchaseFailed", "Could not buy the ticket") });
27
20
  const payment = computed(() => route.query.payment);
28
21
  const returnedTicket = computed(() => route.query.ticket);
29
22
  const paidState = ref(null);
@@ -41,83 +34,24 @@ onMounted(async () => {
41
34
  }
42
35
  paidState.value = "timeout";
43
36
  });
44
- const ticketsPath = useRuntimeConfig().public.events?.pages?.tickets;
37
+ const hasTicketsPage = !!useRuntimeConfig().public.events?.pages?.tickets;
45
38
  useSeoMeta({ title: () => event.value ? tr(event.value.name) : "Event" });
46
39
  defineOptions({ name: "EventDetailPage" });
47
40
  </script>
48
41
 
49
42
  <template>
50
- <UContainer class="py-8 sm:py-12">
51
- <template v-if="event">
52
- <UAlert
53
- v-if="paidState === 'waiting'"
54
- class="mb-6"
55
- color="info"
56
- variant="subtle"
57
- :title="t('detail.tickets.payment.verifying.description', 'Confirming your payment\u2026')"
58
- />
59
- <UAlert
60
- v-else-if="paidState === 'paid'"
61
- class="mb-6"
62
- color="success"
63
- variant="subtle"
64
- :title="t('paymentReceived', 'Payment received - your ticket is ready.')"
43
+ <UContainer>
44
+ <UPage v-if="event">
45
+ <UPageHeader
46
+ :title="tr(event.name)"
47
+ :description="event.description ? tr(event.description) : void 0"
65
48
  >
66
- <template
67
- v-if="ticketsPath"
68
- #actions
69
- >
70
- <UButton
71
- :to="ticketsPath"
72
- size="xs"
73
- >
74
- {{ t("detail.tickets.view", "View ticket") }}
75
- </UButton>
49
+ <template #headline>
50
+ <UBreadcrumb :items="[{ label: t('title', 'Events'), to: links.events() }, { label: tr(event.name) }]" />
76
51
  </template>
77
- </UAlert>
78
- <UAlert
79
- v-else-if="paidState === 'timeout'"
80
- class="mb-6"
81
- color="warning"
82
- variant="subtle"
83
- :title="t('paymentTimeout', 'We have not received the payment confirmation yet. It will show up under your tickets shortly.')"
84
- />
85
- <UAlert
86
- v-else-if="payment === 'cancel'"
87
- class="mb-6"
88
- color="neutral"
89
- variant="subtle"
90
- :title="t('paymentCancelled', 'Payment cancelled. Your reservation is held for a short while; you can pay from your tickets.')"
91
- />
92
-
93
- <UButton
94
- :to="eventsPath"
95
- color="neutral"
96
- variant="link"
97
- size="sm"
98
- class="mb-4 -ml-2.5"
99
- leading-icon="i-lucide-arrow-left"
100
- :label="t('backToEvents', 'All events')"
101
- />
102
-
103
- <!-- Hero -->
104
- <div class="relative overflow-hidden rounded-xl ring ring-default">
105
- <img
106
- v-if="event.image"
107
- :src="event.image"
108
- :alt="tr(event.name)"
109
- class="aspect-[2/1] w-full object-cover sm:aspect-[3/1]"
110
- >
111
- <div
112
- v-else
113
- class="gradient-linear-br-teal-purple aspect-[3/1] w-full sm:aspect-[4/1]"
114
- />
115
- </div>
116
-
117
- <header class="mt-8 space-y-4">
118
52
  <div
119
53
  v-if="event.requiresMembership"
120
- class="flex flex-wrap items-center gap-2"
54
+ class="mt-3"
121
55
  >
122
56
  <UBadge
123
57
  color="warning"
@@ -126,41 +60,72 @@ defineOptions({ name: "EventDetailPage" });
126
60
  :label="t('members', 'Members')"
127
61
  />
128
62
  </div>
129
- <h1 class="text-3xl font-bold tracking-tight text-highlighted sm:text-5xl">
130
- {{ tr(event.name) }}
131
- </h1>
132
- <p
133
- v-if="event.description"
134
- class="max-w-3xl text-lg text-muted"
63
+ </UPageHeader>
64
+
65
+ <UPageBody>
66
+ <UAlert
67
+ v-if="paidState === 'waiting'"
68
+ color="info"
69
+ variant="subtle"
70
+ :title="t('detail.tickets.payment.verifying.description', 'Confirming your payment\u2026')"
71
+ />
72
+ <UAlert
73
+ v-else-if="paidState === 'paid'"
74
+ color="success"
75
+ variant="subtle"
76
+ :title="t('paymentReceived', 'Payment received - your ticket is ready.')"
135
77
  >
136
- {{ tr(event.description) }}
137
- </p>
78
+ <template
79
+ v-if="hasTicketsPage"
80
+ #actions
81
+ >
82
+ <UButton
83
+ :to="links.tickets()"
84
+ size="xs"
85
+ >
86
+ {{ t("detail.tickets.view", "View ticket") }}
87
+ </UButton>
88
+ </template>
89
+ </UAlert>
90
+ <UAlert
91
+ v-else-if="paidState === 'timeout'"
92
+ color="warning"
93
+ variant="subtle"
94
+ :title="t('paymentTimeout', 'We have not received the payment confirmation yet. It will show up under your tickets shortly.')"
95
+ />
96
+ <UAlert
97
+ v-else-if="payment === 'cancel'"
98
+ color="neutral"
99
+ variant="subtle"
100
+ :title="t('paymentCancelled', 'Payment cancelled. Your reservation is held for a short while; you can pay from your tickets.')"
101
+ />
102
+
103
+ <!-- Hero -->
104
+ <div class="ring-default relative overflow-hidden rounded-lg ring">
105
+ <img
106
+ v-if="event.image"
107
+ :src="event.image"
108
+ :alt="tr(event.name)"
109
+ class="aspect-[2/1] w-full object-cover sm:aspect-[3/1]"
110
+ >
111
+ <div
112
+ v-else
113
+ class="gradient-linear-br-teal-purple aspect-[3/1] w-full sm:aspect-[4/1]"
114
+ />
115
+ </div>
138
116
 
139
- <div class="grid gap-3 pt-2 sm:grid-cols-2">
140
- <div class="flex items-start gap-3 rounded-xl bg-elevated/50 p-4 ring ring-default">
117
+ <div class="grid gap-3 sm:grid-cols-2">
118
+ <div class="bg-elevated/50 ring-default flex items-start gap-3 rounded-lg p-4 ring">
141
119
  <UIcon
142
120
  name="i-lucide-calendar"
143
- class="mt-0.5 size-5 shrink-0 text-primary"
121
+ class="text-primary mt-0.5 size-5 shrink-0"
144
122
  />
145
123
  <div class="min-w-0">
146
- <div class="text-xs font-semibold uppercase tracking-wide text-muted">
124
+ <div class="text-muted text-xs font-semibold uppercase tracking-wide">
147
125
  {{ t("detail.when", "When?") }}
148
126
  </div>
149
- <div class="font-medium text-highlighted">
150
- <NuxtTime
151
- :datetime="event.startsAt"
152
- :locale="locale"
153
- date-style="full"
154
- time-style="short"
155
- />
156
- <template v-if="event.endsAt">
157
- –
158
- <NuxtTime
159
- :datetime="event.endsAt"
160
- :locale="locale"
161
- time-style="short"
162
- />
163
- </template>
127
+ <div class="text-highlighted font-medium">
128
+ {{ format.dateRange(event.startsAt, event.endsAt, { dateStyle: "full", timeStyle: "short" }) }}
164
129
  </div>
165
130
  </div>
166
131
  </div>
@@ -170,23 +135,23 @@ defineOptions({ name: "EventDetailPage" });
170
135
  :href="event.locationUrl || void 0"
171
136
  :target="event.locationUrl ? '_blank' : void 0"
172
137
  :rel="event.locationUrl ? 'noopener' : void 0"
173
- class="flex items-start gap-3 rounded-xl bg-elevated/50 p-4 ring ring-default"
174
- :class="event.locationUrl ? 'transition hover:bg-elevated' : ''"
138
+ class="bg-elevated/50 ring-default flex items-start gap-3 rounded-lg p-4 ring"
139
+ :class="event.locationUrl ? 'hover:bg-elevated transition' : ''"
175
140
  >
176
141
  <UIcon
177
142
  name="i-lucide-map-pin"
178
- class="mt-0.5 size-5 shrink-0 text-primary"
143
+ class="text-primary mt-0.5 size-5 shrink-0"
179
144
  />
180
145
  <div class="min-w-0">
181
- <div class="text-xs font-semibold uppercase tracking-wide text-muted">
146
+ <div class="text-muted text-xs font-semibold uppercase tracking-wide">
182
147
  {{ t("detail.where", "Where?") }}
183
148
  </div>
184
- <div class="flex items-center gap-1.5 font-medium text-highlighted">
149
+ <div class="text-highlighted flex items-center gap-1.5 font-medium">
185
150
  {{ tr(event.location) }}
186
151
  <UIcon
187
152
  v-if="event.locationUrl"
188
153
  name="i-lucide-external-link"
189
- class="size-4 text-muted"
154
+ class="text-muted size-4"
190
155
  />
191
156
  </div>
192
157
  </div>
@@ -205,46 +170,42 @@ defineOptions({ name: "EventDetailPage" });
205
170
  :label="t('detail.tickets.view', 'Show tickets')"
206
171
  />
207
172
  </div>
208
- </header>
209
173
 
210
- <div class="mt-10 grid gap-10 lg:grid-cols-[2fr_1fr] lg:items-start">
211
- <!-- Body renders as MDC when the host has it (Nuxt Content), else plain text; override via the `body` slot. -->
212
- <div>
213
- <slot
214
- name="body"
215
- :event="event"
216
- :body="tr(event.body ?? event.description)"
174
+ <div class="grid gap-10 lg:grid-cols-[2fr_1fr] lg:items-start">
175
+ <!-- Body renders as MDC when the host has it (Nuxt Content), else plain text; override via the `body` slot. -->
176
+ <div>
177
+ <slot
178
+ name="body"
179
+ :event="event"
180
+ :body="tr(event.body ?? event.description)"
181
+ >
182
+ <MDC
183
+ v-if="hasMdc"
184
+ :value="tr(event.body ?? event.description)"
185
+ class="prose text-muted max-w-none"
186
+ />
187
+ <div
188
+ v-else
189
+ class="text-muted whitespace-pre-line"
190
+ >
191
+ {{ tr(event.body ?? event.description) }}
192
+ </div>
193
+ </slot>
194
+ </div>
195
+ <SectionCard
196
+ id="tickets"
197
+ :title="t('detail.tickets.title', 'Tickets')"
198
+ class="scroll-mt-24 lg:sticky lg:top-24"
217
199
  >
218
- <MDC
219
- v-if="hasMdc"
220
- :value="tr(event.body ?? event.description)"
221
- class="prose max-w-none text-muted"
200
+ <EventTicketPicker
201
+ :ticket-types="event.ticketTypes"
202
+ :logged-in="loggedIn"
203
+ :loading="buying"
204
+ @purchase="onPurchase"
222
205
  />
223
- <div
224
- v-else
225
- class="whitespace-pre-line text-muted"
226
- >
227
- {{ tr(event.body ?? event.description) }}
228
- </div>
229
- </slot>
206
+ </SectionCard>
230
207
  </div>
231
- <UCard
232
- id="tickets"
233
- :ui="{ root: 'rounded-xl shadow-sm scroll-mt-24 lg:sticky lg:top-24' }"
234
- >
235
- <template #header>
236
- <h2 class="text-lg font-semibold">
237
- {{ t("detail.tickets.title", "Tickets") }}
238
- </h2>
239
- </template>
240
- <EventTicketPicker
241
- :ticket-types="event.ticketTypes"
242
- :logged-in="loggedIn"
243
- :loading="buying"
244
- @purchase="onPurchase"
245
- />
246
- </UCard>
247
- </div>
248
- </template>
208
+ </UPageBody>
209
+ </UPage>
249
210
  </UContainer>
250
211
  </template>
@@ -1,9 +1,9 @@
1
- declare var __VLS_82: {
1
+ declare var __VLS_91: {
2
2
  event: any;
3
3
  body: any;
4
4
  };
5
5
  type __VLS_Slots = {} & {
6
- body?: (props: typeof __VLS_82) => any;
6
+ body?: (props: typeof __VLS_91) => any;
7
7
  };
8
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
9
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,45 +1,47 @@
1
1
  <script setup>
2
+ defineOptions({ name: "EventsListPage" });
2
3
  const t = useModuleT("events");
3
4
  const { grouped } = await useEvents();
4
- const pages = useRuntimeConfig().public.events?.pages;
5
- const eventTo = (slug) => (pages?.event || "/events/:slug").replace(":slug", encodeURIComponent(slug));
5
+ const links = useEventLinks();
6
6
  const sections = computed(() => [
7
7
  { title: t("ongoing.title", "Happening now"), events: grouped.value.ongoing },
8
8
  { title: t("upcoming.title", "Upcoming"), events: grouped.value.upcoming },
9
9
  { title: t("past.title", "Past events"), events: grouped.value.past }
10
10
  ].filter((section) => section.events.length));
11
- defineOptions({ name: "EventsListPage" });
11
+ useSeoMeta({ title: () => t("title", "Events") });
12
12
  </script>
13
13
 
14
14
  <template>
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>
21
- <p
22
- v-if="!sections.length"
23
- class="text-center text-muted"
24
- >
25
- {{ t("empty", "There are no events right now.") }}
26
- </p>
27
- <section
28
- v-for="section in sections"
29
- :key="section.title"
30
- class="mb-12"
31
- >
32
- <h2 class="mb-4 text-xl font-semibold tracking-tight text-highlighted">
33
- {{ section.title }}
34
- </h2>
35
- <div class="grid gap-6 md:grid-cols-2">
36
- <EventCard
37
- v-for="item in section.events"
38
- :key="item.id"
39
- :event="item"
40
- :to="eventTo(item.slug)"
15
+ <UContainer>
16
+ <UPage>
17
+ <UPageHeader
18
+ :title="t('title', 'Events')"
19
+ :description="t('listDescription', 'Find an event and get your ticket.')"
20
+ />
21
+ <UPageBody>
22
+ <UEmpty
23
+ v-if="!sections.length"
24
+ icon="i-lucide-calendar"
25
+ :title="t('empty', 'There are no events right now.')"
41
26
  />
42
- </div>
43
- </section>
27
+ <section
28
+ v-for="section in sections"
29
+ :key="section.title"
30
+ class="space-y-4"
31
+ >
32
+ <h2 class="text-highlighted text-lg font-semibold">
33
+ {{ section.title }}
34
+ </h2>
35
+ <div class="grid gap-6 md:grid-cols-2">
36
+ <EventCard
37
+ v-for="item in section.events"
38
+ :key="item.id"
39
+ :event="item"
40
+ :to="links.event(item.slug)"
41
+ />
42
+ </div>
43
+ </section>
44
+ </UPageBody>
45
+ </UPage>
44
46
  </UContainer>
45
47
  </template>
@@ -1,37 +1,30 @@
1
1
  <script setup>
2
+ defineOptions({ name: "EventTicketsPage" });
2
3
  const t = useModuleT("events");
3
4
  const { tickets, pay } = useMyTickets();
4
- const pages = useRuntimeConfig().public.events?.pages;
5
- const eventTo = (slug) => (pages?.event || "/events/:slug").replace(":slug", encodeURIComponent(slug));
6
- const AccountHeader = resolveComponent("AccountHeader");
7
- const hasAccountHeader = typeof AccountHeader !== "string";
8
- defineOptions({ name: "EventTicketsPage" });
5
+ const links = useEventLinks();
9
6
  </script>
10
7
 
11
8
  <template>
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' }"
9
+ <div class="flex flex-col gap-4">
10
+ <UEmpty
11
+ v-if="!tickets.length"
12
+ icon="i-lucide-ticket"
13
+ :title="t('noTicketsTitle', 'No tickets yet')"
14
+ :description="t('noTickets', 'You do not have any tickets yet.')"
15
+ :actions="[{ label: t('browseEvents', 'Browse events'), to: links.events(), icon: 'i-lucide-calendar' }]"
16
+ />
17
+ <div
18
+ v-else
19
+ class="grid gap-4 md:grid-cols-2"
20
+ >
21
+ <EventTicketCard
22
+ v-for="ticket in tickets"
23
+ :key="ticket.id"
24
+ :ticket="ticket"
25
+ :to="links.event(ticket.eventSlug)"
26
+ @pay="pay"
19
27
  />
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>
28
+ </div>
36
29
  </div>
37
30
  </template>
@@ -1,6 +1,6 @@
1
- import type { AdminEvent } from '../utils/admin-event.js';
1
+ import type { StaffEvent } from '../utils/admin-event.js';
2
2
  type __VLS_Props = {
3
- event: AdminEvent;
3
+ event: StaffEvent;
4
4
  };
5
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
6
  declare const _default: typeof __VLS_export;
@@ -1,88 +1,92 @@
1
1
  <script setup>
2
- import { useAdminAction } from "../utils/admin-event";
3
2
  defineOptions({ name: "EventsAdminAttendeesTab" });
4
3
  const props = defineProps({
5
4
  event: { type: Object, required: true }
6
5
  });
7
6
  const t = useModuleT("events");
8
7
  const tr = useEventsTranslate();
8
+ const format = useFormat();
9
9
  const confirmAction = useConfirmDialog();
10
- const { run } = useAdminAction();
10
+ const { run } = useAction();
11
11
  const { data: attendees, refresh } = await useFetch(() => `/api/events/admin/events/${props.event.id}/tickets`, { default: () => [] });
12
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" };
13
+ const who = (ticket) => ticket.userName || ticket.userEmail || "";
14
14
  const ticketAction = async (ticket, action) => {
15
15
  const confirmed = await confirmAction({
16
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"),
17
+ description: action === "refund" ? t("admin.confirmRefundDescription", "The full amount goes back to {name} and the ticket is cancelled.", { name: who(ticket) }) : t("admin.confirmCancelDescription", "The ticket of {name} stops working and its seat is freed.", { name: who(ticket) }),
18
+ confirmText: action === "refund" ? t("admin.refund", "Refund ticket") : t("admin.cancelTicket", "Cancel ticket"),
18
19
  destructive: true
19
20
  });
20
21
  if (!confirmed) return;
21
22
  return run(async () => {
22
23
  await $fetch(`/api/events/admin/tickets/${ticket.id}/${action}`, { method: "POST" });
23
24
  await refresh();
24
- }, action === "refund" ? t("admin.refunded", "Refunded.") : t("admin.cancelled", "Cancelled."));
25
+ }, { success: action === "refund" ? t("admin.refunded", "Refunded.") : t("admin.cancelled", "Cancelled.") });
25
26
  };
26
- const columns = [
27
+ const menu = (ticket) => {
28
+ const refund = ticket.status === "paid" && ticket.paymentId ? [{ label: t("admin.refund", "Refund ticket"), icon: "i-lucide-undo-2", onSelect: () => {
29
+ void ticketAction(ticket, "refund");
30
+ } }] : [];
31
+ const cancel = ticket.status === "paid" || ticket.status === "pending" ? [{ label: t("admin.cancelTicket", "Cancel ticket"), icon: "i-lucide-ban", color: "error", onSelect: () => {
32
+ void ticketAction(ticket, "cancel");
33
+ } }] : [];
34
+ return [refund, cancel].filter((group) => group.length);
35
+ };
36
+ const columns = computed(() => [
27
37
  { accessorKey: "userName", header: t("admin.attendee", "Attendee") },
28
38
  { accessorKey: "ticketTypeId", header: t("admin.ticketType", "Ticket") },
29
39
  { accessorKey: "status", header: t("admin.status", "Status") },
30
40
  { accessorKey: "checkedInAt", header: t("admin.checkedIn", "Checked in") },
31
- { id: "actions", header: "" }
32
- ];
41
+ { id: "actions" }
42
+ ]);
33
43
  </script>
34
44
 
35
45
  <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>
46
+ <SectionCard
47
+ :title="t('admin.attendees', 'Attendees')"
48
+ flush
49
+ >
50
+ <UTable
51
+ :data="attendees"
52
+ :columns="columns"
53
+ :ui="tableUi({ tr: '' })"
54
+ >
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
+ <template #empty>
84
+ <UEmpty
85
+ variant="naked"
86
+ icon="i-lucide-users"
87
+ :title="t('admin.noAttendees', 'No tickets yet.')"
88
+ />
89
+ </template>
90
+ </UTable>
91
+ </SectionCard>
88
92
  </template>