@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.
- package/README.md +1 -1
- package/dist/module.d.mts +45 -7
- package/dist/module.json +1 -1
- package/dist/module.mjs +53 -42
- package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +11 -0
- package/dist/runtime/app/components/EventAdminAddonDialog.vue +113 -0
- package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +11 -0
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +11 -0
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +79 -0
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +11 -0
- package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +19 -0
- package/dist/runtime/app/components/EventAdminAddonGroups.vue +123 -0
- package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +19 -0
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +20 -0
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +175 -0
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +20 -0
- package/dist/runtime/app/components/EventCard.vue +39 -27
- package/dist/runtime/app/components/EventTicketCard.vue +5 -4
- package/dist/runtime/app/components/EventTicketPicker.vue +40 -21
- package/dist/runtime/app/composables/index.d.ts +1 -1
- package/dist/runtime/app/composables/index.js +1 -1
- package/dist/runtime/app/composables/useEvents.d.ts +10 -4
- package/dist/runtime/app/composables/useEvents.js +21 -10
- package/dist/runtime/app/pages/admin-event.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-event.vue +151 -0
- package/dist/runtime/app/pages/admin-event.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin.vue +154 -0
- package/dist/runtime/app/pages/admin.vue.d.ts +3 -0
- package/dist/runtime/app/pages/event.d.vue.ts +14 -1
- package/dist/runtime/app/pages/event.vue +160 -45
- package/dist/runtime/app/pages/event.vue.d.ts +14 -1
- package/dist/runtime/app/pages/events.vue +12 -10
- package/dist/runtime/app/pages/tickets.vue +26 -19
- package/dist/runtime/app/tabs/Attendees.d.vue.ts +7 -0
- package/dist/runtime/app/tabs/Attendees.vue +88 -0
- package/dist/runtime/app/tabs/Attendees.vue.d.ts +7 -0
- package/dist/runtime/app/tabs/Details.d.vue.ts +11 -0
- package/dist/runtime/app/tabs/Details.vue +74 -0
- package/dist/runtime/app/tabs/Details.vue.d.ts +11 -0
- package/dist/runtime/app/tabs/Tickets.d.vue.ts +11 -0
- package/dist/runtime/app/tabs/Tickets.vue +164 -0
- package/dist/runtime/app/tabs/Tickets.vue.d.ts +11 -0
- package/dist/runtime/app/utils/admin-event.d.ts +48 -0
- package/dist/runtime/app/utils/admin-event.js +22 -0
- package/dist/runtime/i18n/lang/da.json +34 -1
- package/dist/runtime/i18n/lang/en.json +33 -0
- package/dist/runtime/menus.js +17 -1
- package/dist/runtime/server/db/schema.sqlite.d.ts +62 -5
- package/dist/runtime/server/db/schema.sqlite.js +27 -2
- package/dist/runtime/server/mcp/schemas.d.ts +119 -0
- package/dist/runtime/server/mcp/schemas.js +58 -0
- package/dist/runtime/server/mcp/tools/admin/create-event.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/create-event.js +11 -0
- package/dist/runtime/server/mcp/tools/admin/get-event-definition.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/get-event-definition.js +10 -0
- package/dist/runtime/server/mcp/tools/admin/list-all-events.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/list-all-events.js +9 -0
- package/dist/runtime/server/mcp/tools/admin/list-attendees.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/list-attendees.js +25 -0
- package/dist/runtime/server/mcp/tools/admin/update-event.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/update-event.js +10 -0
- package/dist/runtime/server/mcp/tools/admin/update-ticket-type.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/update-ticket-type.js +10 -0
- package/dist/runtime/server/mcp/tools/get-event.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/get-event.js +12 -0
- package/dist/runtime/server/mcp/tools/list-events.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/list-events.js +6 -0
- package/dist/runtime/server/mcp/tools/my-tickets.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/my-tickets.js +6 -0
- package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
- package/dist/runtime/server/plugins/staff-scopes.js +1 -0
- package/dist/runtime/server/plugins/subscriptions-membership.d.ts +2 -0
- package/dist/runtime/server/plugins/subscriptions-membership.js +11 -0
- package/dist/runtime/server/routes/admin/event-clone.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/event-clone.post.js +31 -0
- package/dist/runtime/server/routes/admin/event.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/event.get.js +5 -0
- package/dist/runtime/server/routes/admin/event.patch.js +3 -8
- package/dist/runtime/server/routes/admin/events.post.js +3 -13
- package/dist/runtime/server/routes/admin/membership-options.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/membership-options.get.js +4 -0
- package/dist/runtime/server/routes/admin/ticket-type.patch.js +3 -8
- package/dist/runtime/server/routes/admin/ticket-types.post.js +2 -1
- package/dist/runtime/server/routes/public/event.get.js +2 -2
- package/dist/runtime/server/routes/public/purchase.post.js +3 -2
- package/dist/runtime/server/routes/public/ticket-pay.post.js +1 -1
- package/dist/runtime/server/routes/public/ticket.get.js +1 -1
- package/dist/runtime/server/routes/public/tickets-me.get.js +1 -1
- package/dist/runtime/server/utils/admin-schemas.d.ts +102 -20
- package/dist/runtime/server/utils/admin-schemas.js +8 -2
- package/dist/runtime/server/utils/admin.d.ts +82 -1
- package/dist/runtime/server/utils/admin.js +51 -1
- package/dist/runtime/server/utils/auth.d.ts +7 -6
- package/dist/runtime/server/utils/auth.js +4 -4
- package/dist/runtime/server/utils/catalog.d.ts +14 -0
- package/dist/runtime/server/utils/catalog.js +20 -4
- package/dist/runtime/server/utils/membership.d.ts +6 -2
- package/dist/runtime/server/utils/membership.js +15 -2
- package/dist/runtime/server/utils/tickets.js +15 -12
- package/dist/runtime/utils/index.d.ts +17 -7
- package/dist/runtime/utils/index.js +16 -19
- package/dist/types.d.mts +3 -1
- package/package.json +21 -7
|
@@ -1,3 +1,16 @@
|
|
|
1
|
-
declare
|
|
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
|
|
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-
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
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="
|
|
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-
|
|
30
|
+
class="mb-12"
|
|
29
31
|
>
|
|
30
|
-
<h2 class="mb-
|
|
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-
|
|
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
|
-
<
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
}
|