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