@frigear-nu/events 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +1 -1
  2. package/dist/module.d.mts +45 -7
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +53 -42
  5. package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +11 -0
  6. package/dist/runtime/app/components/EventAdminAddonDialog.vue +113 -0
  7. package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +11 -0
  8. package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +11 -0
  9. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +79 -0
  10. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +11 -0
  11. package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +19 -0
  12. package/dist/runtime/app/components/EventAdminAddonGroups.vue +123 -0
  13. package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +19 -0
  14. package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +20 -0
  15. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +175 -0
  16. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +20 -0
  17. package/dist/runtime/app/components/EventCard.vue +39 -27
  18. package/dist/runtime/app/components/EventTicketCard.vue +5 -4
  19. package/dist/runtime/app/components/EventTicketPicker.vue +40 -21
  20. package/dist/runtime/app/composables/index.d.ts +1 -1
  21. package/dist/runtime/app/composables/index.js +1 -1
  22. package/dist/runtime/app/composables/useEvents.d.ts +10 -4
  23. package/dist/runtime/app/composables/useEvents.js +21 -10
  24. package/dist/runtime/app/pages/admin-event.d.vue.ts +3 -0
  25. package/dist/runtime/app/pages/admin-event.vue +151 -0
  26. package/dist/runtime/app/pages/admin-event.vue.d.ts +3 -0
  27. package/dist/runtime/app/pages/admin.d.vue.ts +3 -0
  28. package/dist/runtime/app/pages/admin.vue +154 -0
  29. package/dist/runtime/app/pages/admin.vue.d.ts +3 -0
  30. package/dist/runtime/app/pages/event.d.vue.ts +14 -1
  31. package/dist/runtime/app/pages/event.vue +160 -45
  32. package/dist/runtime/app/pages/event.vue.d.ts +14 -1
  33. package/dist/runtime/app/pages/events.vue +12 -10
  34. package/dist/runtime/app/pages/tickets.vue +26 -19
  35. package/dist/runtime/app/tabs/Attendees.d.vue.ts +7 -0
  36. package/dist/runtime/app/tabs/Attendees.vue +88 -0
  37. package/dist/runtime/app/tabs/Attendees.vue.d.ts +7 -0
  38. package/dist/runtime/app/tabs/Details.d.vue.ts +11 -0
  39. package/dist/runtime/app/tabs/Details.vue +74 -0
  40. package/dist/runtime/app/tabs/Details.vue.d.ts +11 -0
  41. package/dist/runtime/app/tabs/Tickets.d.vue.ts +11 -0
  42. package/dist/runtime/app/tabs/Tickets.vue +164 -0
  43. package/dist/runtime/app/tabs/Tickets.vue.d.ts +11 -0
  44. package/dist/runtime/app/utils/admin-event.d.ts +48 -0
  45. package/dist/runtime/app/utils/admin-event.js +22 -0
  46. package/dist/runtime/i18n/lang/da.json +34 -1
  47. package/dist/runtime/i18n/lang/en.json +33 -0
  48. package/dist/runtime/menus.js +17 -1
  49. package/dist/runtime/server/db/schema.sqlite.d.ts +62 -5
  50. package/dist/runtime/server/db/schema.sqlite.js +27 -2
  51. package/dist/runtime/server/mcp/schemas.d.ts +119 -0
  52. package/dist/runtime/server/mcp/schemas.js +58 -0
  53. package/dist/runtime/server/mcp/tools/admin/create-event.d.ts +2 -0
  54. package/dist/runtime/server/mcp/tools/admin/create-event.js +11 -0
  55. package/dist/runtime/server/mcp/tools/admin/get-event-definition.d.ts +2 -0
  56. package/dist/runtime/server/mcp/tools/admin/get-event-definition.js +10 -0
  57. package/dist/runtime/server/mcp/tools/admin/list-all-events.d.ts +2 -0
  58. package/dist/runtime/server/mcp/tools/admin/list-all-events.js +9 -0
  59. package/dist/runtime/server/mcp/tools/admin/list-attendees.d.ts +2 -0
  60. package/dist/runtime/server/mcp/tools/admin/list-attendees.js +25 -0
  61. package/dist/runtime/server/mcp/tools/admin/update-event.d.ts +2 -0
  62. package/dist/runtime/server/mcp/tools/admin/update-event.js +10 -0
  63. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.d.ts +2 -0
  64. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.js +10 -0
  65. package/dist/runtime/server/mcp/tools/get-event.d.ts +2 -0
  66. package/dist/runtime/server/mcp/tools/get-event.js +12 -0
  67. package/dist/runtime/server/mcp/tools/list-events.d.ts +2 -0
  68. package/dist/runtime/server/mcp/tools/list-events.js +6 -0
  69. package/dist/runtime/server/mcp/tools/my-tickets.d.ts +2 -0
  70. package/dist/runtime/server/mcp/tools/my-tickets.js +6 -0
  71. package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
  72. package/dist/runtime/server/plugins/staff-scopes.js +1 -0
  73. package/dist/runtime/server/plugins/subscriptions-membership.d.ts +2 -0
  74. package/dist/runtime/server/plugins/subscriptions-membership.js +11 -0
  75. package/dist/runtime/server/routes/admin/event-clone.post.d.ts +2 -0
  76. package/dist/runtime/server/routes/admin/event-clone.post.js +31 -0
  77. package/dist/runtime/server/routes/admin/event.get.d.ts +2 -0
  78. package/dist/runtime/server/routes/admin/event.get.js +5 -0
  79. package/dist/runtime/server/routes/admin/event.patch.js +3 -8
  80. package/dist/runtime/server/routes/admin/events.post.js +3 -13
  81. package/dist/runtime/server/routes/admin/membership-options.get.d.ts +2 -0
  82. package/dist/runtime/server/routes/admin/membership-options.get.js +4 -0
  83. package/dist/runtime/server/routes/admin/ticket-type.patch.js +3 -8
  84. package/dist/runtime/server/routes/admin/ticket-types.post.js +2 -1
  85. package/dist/runtime/server/routes/public/event.get.js +2 -2
  86. package/dist/runtime/server/routes/public/purchase.post.js +3 -2
  87. package/dist/runtime/server/routes/public/ticket-pay.post.js +1 -1
  88. package/dist/runtime/server/routes/public/ticket.get.js +1 -1
  89. package/dist/runtime/server/routes/public/tickets-me.get.js +1 -1
  90. package/dist/runtime/server/utils/admin-schemas.d.ts +102 -20
  91. package/dist/runtime/server/utils/admin-schemas.js +8 -2
  92. package/dist/runtime/server/utils/admin.d.ts +82 -1
  93. package/dist/runtime/server/utils/admin.js +51 -1
  94. package/dist/runtime/server/utils/auth.d.ts +7 -6
  95. package/dist/runtime/server/utils/auth.js +4 -4
  96. package/dist/runtime/server/utils/catalog.d.ts +14 -0
  97. package/dist/runtime/server/utils/catalog.js +20 -4
  98. package/dist/runtime/server/utils/membership.d.ts +6 -2
  99. package/dist/runtime/server/utils/membership.js +15 -2
  100. package/dist/runtime/server/utils/tickets.js +15 -12
  101. package/dist/runtime/utils/index.d.ts +17 -7
  102. package/dist/runtime/utils/index.js +16 -19
  103. package/dist/types.d.mts +3 -1
  104. package/package.json +21 -7
package/README.md CHANGED
@@ -26,7 +26,7 @@ export default defineNitroPlugin((nitro) => {
26
26
  })
27
27
  ```
28
28
 
29
- Full documentation: `apps/docs/content/4.modules/25.events.md`.
29
+ Full documentation: `apps/docs/content/4.modules/06.events.md`.
30
30
 
31
31
  ## Development
32
32
 
package/dist/module.d.mts CHANGED
@@ -1,14 +1,15 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
  import { H3Event } from 'h3';
3
+ import { Translated } from '@frigear-nu/ui/translated';
4
+ export { Translated } from '@frigear-nu/ui/translated';
3
5
 
4
- /** A text that is either one string or a map of locale -> string. */
5
- type Translated = string | Record<string, string>;
6
6
  /** `null` = open to everyone, `'any'` = any active subscription, or a list of plan keys. */
7
7
  type MembershipRequirement = 'any' | string[] | null;
8
8
  type EventStatus = 'draft' | 'published' | 'archived';
9
9
  type TicketStatus = 'pending' | 'paid' | 'cancelled' | 'refunded' | 'expired';
10
10
  type ModuleOptions = {
11
11
  settings?: {
12
+ mcp?: boolean;
12
13
  pages?: {
13
14
  /** Event list. `true` = `/events`. */
14
15
  events?: string | boolean;
@@ -16,6 +17,10 @@ type ModuleOptions = {
16
17
  event?: string | boolean;
17
18
  /** The signed-in user's tickets. `true` = `/account/tickets`. */
18
19
  tickets?: string | boolean;
20
+ /** Staff event list (needs the `events:authorize` hook to allow staff). `true` = `/admin/events`. Off by default. */
21
+ admin?: string | boolean;
22
+ /** Staff event page (details, ticket types, attendees); must contain an `:id` param. `true` = `/admin/events/:id`. Off by default. */
23
+ adminEvent?: string | boolean;
19
24
  };
20
25
  /** Minutes an unpaid ticket keeps its seat (the Stripe session expires then). Defaults to `30`. */
21
26
  reservationMinutes?: number;
@@ -23,14 +28,14 @@ type ModuleOptions = {
23
28
  };
24
29
  type EventsRuntimeConfig = {
25
30
  reservationMinutes: number;
26
- /** Whether `@frigear-nu/subscriptions` is installed (membership requirements can be evaluated). */
27
- membershipEnabled: boolean;
28
31
  };
29
32
  type PublicModuleOptions = {
30
33
  pages: {
31
34
  events?: string;
32
35
  event?: string;
33
36
  tickets?: string;
37
+ admin?: string;
38
+ adminEvent?: string;
34
39
  };
35
40
  };
36
41
  type AddonDto = {
@@ -57,6 +62,8 @@ type TicketTypeDto = {
57
62
  hidePrice: boolean;
58
63
  isDefault: boolean;
59
64
  requiresMembership: MembershipRequirement;
65
+ /** Holders of these plans cannot buy this ticket (`'any'` = any active subscription). */
66
+ excludesMembership: MembershipRequirement;
60
67
  /** Seats left, or null when unlimited. */
61
68
  remaining: number | null;
62
69
  saleStartsAt: string | null;
@@ -64,7 +71,7 @@ type TicketTypeDto = {
64
71
  /** `open | not_started | ended | sold_out`. */
65
72
  saleState: 'open' | 'not_started' | 'ended' | 'sold_out';
66
73
  /** Only set for signed-in users: `null` when allowed, otherwise why not. */
67
- blocked: 'membership_required' | 'limit_reached' | null;
74
+ blocked: 'membership_required' | 'membership_excluded' | 'limit_reached' | null;
68
75
  addonGroups: AddonGroupDto[];
69
76
  };
70
77
  type EventSummaryDto = {
@@ -73,7 +80,7 @@ type EventSummaryDto = {
73
80
  name: Translated;
74
81
  description: Translated | null;
75
82
  image: string | null;
76
- location: string | null;
83
+ location: Translated | null;
77
84
  startsAt: string;
78
85
  endsAt: string | null;
79
86
  requiresMembership: MembershipRequirement;
@@ -88,6 +95,8 @@ type PurchaseBody = {
88
95
  addonIds?: string[];
89
96
  successUrl?: string;
90
97
  cancelUrl?: string;
98
+ /** The buyer's site language (e.g. `da`), kept on the ticket so later mails and Stripe texts use it. */
99
+ locale?: string;
91
100
  };
92
101
  type PurchaseResponse = {
93
102
  ticket: TicketDto;
@@ -115,6 +124,8 @@ type TicketDto = {
115
124
  code: string | null;
116
125
  checkedInAt: string | null;
117
126
  createdAt: string;
127
+ /** Language the buyer was browsing in when buying, for localised mails; `null` for tickets bought before it was kept. */
128
+ locale: string | null;
118
129
  };
119
130
  type CheckInResponse = {
120
131
  ok: boolean;
@@ -135,6 +146,26 @@ type AuthorizeContext = {
135
146
  /** Set `ctx.allowed = true` to let the user manage events / check tickets in. Denied by default. */
136
147
  allowed: boolean;
137
148
  };
149
+ /** A selectable membership (plan) offered by a membership provider. */
150
+ type MembershipOption = {
151
+ value: string;
152
+ label: string;
153
+ };
154
+ /** Context of the `events:membership:check` hook. */
155
+ type MembershipCheckContext = {
156
+ userId: number;
157
+ /** `'any'` = any active membership, or the plan keys of which the user must hold one. */
158
+ requirement: 'any' | string[];
159
+ /** A provider sets this to `true` when the user holds a matching membership. Defaults to `false`. */
160
+ satisfied: boolean;
161
+ };
162
+ /** Context of the `events:membership:options` hook. */
163
+ type MembershipOptionsContext = {
164
+ /** A provider sets this to `true`: membership rules can then be used on events and tickets. */
165
+ available: boolean;
166
+ /** The memberships staff can pick from. */
167
+ options: MembershipOption[];
168
+ };
138
169
  type TicketHookContext = {
139
170
  ticket: TicketDto;
140
171
  };
@@ -154,6 +185,13 @@ declare module 'nitropack' {
154
185
  interface NitroRuntimeHooks {
155
186
  /** Staff gate (event management, check-in): set `ctx.allowed = true` to let the user in. Denied by default. */
156
187
  'events:authorize': (ctx: AuthorizeContext) => void | Promise<void>;
188
+ /**
189
+ * Membership provider (e.g. `@frigear-nu/subscriptions`): set `ctx.satisfied = true` when the user holds a
190
+ * membership matching `ctx.requirement`. With no provider a requirement can never be met, so it fails closed.
191
+ */
192
+ 'events:membership:check': (ctx: MembershipCheckContext) => void | Promise<void>;
193
+ /** Membership provider: set `ctx.available = true` and add the selectable memberships to `ctx.options`. */
194
+ 'events:membership:options': (ctx: MembershipOptionsContext) => void | Promise<void>;
157
195
  /** Policy hook: throw to deny a purchase. */
158
196
  'events:purchase:before': (ctx: PurchaseBeforeContext) => void | Promise<void>;
159
197
  /** A ticket became `paid` (free tickets immediately, paid ones after the Stripe event). Send confirmations here. */
@@ -165,4 +203,4 @@ declare module 'nitropack' {
165
203
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
166
204
 
167
205
  export { _default as default };
168
- export type { AddonDto, AddonGroupDto, AuthorizeContext, CheckInResponse, EventDetailDto, EventStatus, EventSummaryDto, EventsRuntimeConfig, MembershipRequirement, ModuleOptions, PublicModuleOptions, PurchaseBeforeContext, PurchaseBody, PurchaseResponse, TicketDto, TicketHookContext, TicketStatus, TicketTypeDto, Translated };
206
+ export type { AddonDto, AddonGroupDto, AuthorizeContext, CheckInResponse, EventDetailDto, EventStatus, EventSummaryDto, EventsRuntimeConfig, MembershipCheckContext, MembershipOption, MembershipOptionsContext, MembershipRequirement, ModuleOptions, PublicModuleOptions, PurchaseBeforeContext, PurchaseBody, PurchaseResponse, TicketDto, TicketHookContext, TicketStatus, TicketTypeDto };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frigear-nu/events",
3
3
  "configKey": "events",
4
- "version": "0.0.6",
4
+ "version": "0.0.8",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,12 +1,10 @@
1
- import { defineNuxtModule, createResolver, hasNuxtModule, addServerTemplate, addTypeTemplate, addServerImportsDir, addServerPlugin, addImports, addComponentsDir, extendPages, addServerHandler } from '@nuxt/kit';
2
- import { registerModule, registerLocales, registerDatabaseTable, defineModulePage } from '@frigear-nu/module-kit/utils';
1
+ import { defineNuxtModule, createResolver, hasNuxtModule, addServerImportsDir, addServerPlugin, addImports, addComponentsDir, addServerHandler } from '@nuxt/kit';
2
+ import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, registerDatabaseTable, addModulePages, addOptionalMenuSource } from '@frigear-nu/module-kit/utils';
3
+ import { adminRoute } from '@frigear-nu/admin/utils';
4
+ import { addAuthScope } from '@frigear-nu/auth/utils';
5
+ import { addMcpDefinitions } from '@frigear-nu/mcp/utils';
3
6
 
4
7
  const MODULE_NAME = "@frigear-nu/events";
5
- function resolvePagePath(setting, defaultPath) {
6
- if (typeof setting === "string") return setting;
7
- if (setting === true) return defaultPath;
8
- return void 0;
9
- }
10
8
  const module$1 = defineNuxtModule({
11
9
  meta: { name: MODULE_NAME, configKey: "events" },
12
10
  defaults: {
@@ -18,44 +16,47 @@ const module$1 = defineNuxtModule({
18
16
  defaults: { kv: true }
19
17
  },
20
18
  "@frigear-nu/module-kit": { version: ">=0.0.1" },
19
+ "@frigear-nu/auth": { version: ">=0.0.1" },
20
+ "@frigear-nu/mcp": { version: ">=0.0.1" },
21
21
  "@frigear-nu/users": { version: ">=0.0.1" },
22
+ "@frigear-nu/ui": { version: ">=0.0.1" },
22
23
  "@frigear-nu/stripe": { version: ">=0.0.1" },
23
24
  "@frigear-nu/payments": { version: ">=0.0.1" },
24
25
  "@frigear-nu/subscriptions": { version: ">=0.0.1", optional: true },
25
- "@frigear-nu/menu": { version: ">=0.0.1", optional: true }
26
+ "@frigear-nu/menu": { version: ">=0.0.1", optional: true },
27
+ "@frigear-nu/widget": { version: ">=0.0.0", optional: true },
28
+ // Provides the shared page components of the admin pages (`AdminPage`, `AdminTable`, ...).
29
+ "@frigear-nu/admin": { version: ">=0.0.0", optional: true }
26
30
  },
27
31
  async setup(options, nuxt) {
28
32
  registerModule(MODULE_NAME, nuxt);
33
+ addAuthScope(
34
+ nuxt,
35
+ { name: "events:manage", description: "Manage events, tickets and check-in (staff)" },
36
+ { name: "tickets:read", description: "Read your tickets and event details" },
37
+ { name: "tickets:write", description: "Buy tickets and pay for them" },
38
+ { name: "events:pii", description: "See attendee names and emails through MCP tools (also needs events:manage)" }
39
+ );
29
40
  const resolver = createResolver(import.meta.url);
41
+ registerTailwindSource(nuxt, resolver.resolve("./runtime/app"));
30
42
  registerLocales(nuxt, resolver.resolve("./runtime/i18n/lang"));
31
43
  const settings = options.settings ?? {};
32
- const withMenus = hasNuxtModule("@frigear-nu/menu");
33
- const withMembership = hasNuxtModule("@frigear-nu/subscriptions");
34
44
  const eventsPath = resolvePagePath(settings.pages?.events, "/events");
35
45
  const eventPath = resolvePagePath(settings.pages?.event, "/events/:slug");
36
46
  const ticketsPath = resolvePagePath(settings.pages?.tickets, "/account/tickets");
47
+ const withAdmin = hasNuxtModule("@frigear-nu/admin");
48
+ const adminPath = withAdmin ? resolvePagePath(settings.pages?.admin, adminRoute(nuxt, "events")) : void 0;
49
+ const adminEventPath = withAdmin ? resolvePagePath(settings.pages?.adminEvent, adminRoute(nuxt, "events", ":id")) : void 0;
37
50
  nuxt.options.runtimeConfig.events = {
38
- reservationMinutes: settings.reservationMinutes ?? 30,
39
- membershipEnabled: withMembership
51
+ reservationMinutes: settings.reservationMinutes ?? 30
40
52
  };
41
53
  nuxt.options.runtimeConfig.public.events = {
42
- pages: { events: eventsPath, event: eventPath, tickets: ticketsPath }
54
+ pages: { events: eventsPath, event: eventPath, tickets: ticketsPath, admin: adminPath, adminEvent: adminEventPath }
43
55
  };
44
56
  await registerDatabaseTable(nuxt, {
45
57
  resolver,
46
58
  schema: (dialect) => `./runtime/server/db/schema.${dialect}`
47
59
  });
48
- addServerTemplate({
49
- filename: "#events/membership",
50
- getContents: () => withMembership ? `export { hasActiveSubscription } from '@frigear-nu/subscriptions/utils'` : `export const hasActiveSubscription = async () => false`
51
- });
52
- addTypeTemplate({
53
- filename: "types/events-membership.d.ts",
54
- getContents: () => `declare module '#events/membership' {
55
- export function hasActiveSubscription(userId: number, options?: { plans?: string[], statuses?: string[] }): Promise<boolean>
56
- }
57
- `
58
- });
59
60
  const route = (method, path, file) => addServerHandler({ route: `/api/events${path}`, method, handler: resolver.resolve(`./runtime/server/routes/${file}`) });
60
61
  route("get", "", "public/events.get");
61
62
  route("get", "/tickets/me", "public/tickets-me.get");
@@ -64,10 +65,13 @@ const module$1 = defineNuxtModule({
64
65
  route("post", "/check-in", "public/check-in.post");
65
66
  route("get", "/:slug", "public/event.get");
66
67
  route("post", "/:slug/purchase", "public/purchase.post");
68
+ route("get", "/admin/membership-options", "admin/membership-options.get");
67
69
  route("get", "/admin/events", "admin/events.get");
70
+ route("get", "/admin/events/:id", "admin/event.get");
68
71
  route("post", "/admin/events", "admin/events.post");
69
72
  route("patch", "/admin/events/:id", "admin/event.patch");
70
73
  route("delete", "/admin/events/:id", "admin/event.delete");
74
+ route("post", "/admin/events/:id/clone", "admin/event-clone.post");
71
75
  route("get", "/admin/events/:id/tickets", "admin/attendees.get");
72
76
  route("post", "/admin/events/:id/ticket-types", "admin/ticket-types.post");
73
77
  route("patch", "/admin/ticket-types/:id", "admin/ticket-type.patch");
@@ -81,32 +85,39 @@ const module$1 = defineNuxtModule({
81
85
  route("post", "/admin/tickets/:id/cancel", "admin/ticket-cancel.post");
82
86
  route("post", "/admin/tickets/:id/refund", "admin/ticket-refund.post");
83
87
  addServerImportsDir(resolver.resolve("./runtime/server/utils"));
88
+ if (settings.mcp === true) addMcpDefinitions(nuxt, resolver.resolve("./runtime/server/mcp"));
84
89
  addServerPlugin(resolver.resolve("./runtime/server/plugins/payment-hooks"));
90
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/staff-scopes"));
91
+ nuxt.hook("modules:done", () => {
92
+ if (hasNuxtModule("@frigear-nu/subscriptions", nuxt)) {
93
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/subscriptions-membership"));
94
+ }
95
+ });
85
96
  addImports([
86
97
  { name: "useEvents", from: resolver.resolve("./runtime/app/composables/useEvents") },
87
98
  { name: "useEvent", from: resolver.resolve("./runtime/app/composables/useEvents") },
88
99
  { name: "useMyTickets", from: resolver.resolve("./runtime/app/composables/useEvents") },
89
- { name: "useEventsLocale", from: resolver.resolve("./runtime/app/composables/useEvents") }
100
+ { name: "useEventsLocale", from: resolver.resolve("./runtime/app/composables/useEvents") },
101
+ { name: "useEventsFallbackLocale", from: resolver.resolve("./runtime/app/composables/useEvents") },
102
+ { name: "useEventsTranslate", from: resolver.resolve("./runtime/app/composables/useEvents") }
90
103
  ]);
91
104
  addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
92
- if (eventsPath || eventPath || ticketsPath) {
93
- extendPages((pages) => {
94
- const existingPaths = new Set(pages.map((page) => page.path));
95
- if (eventsPath && !existingPaths.has(eventsPath)) {
96
- pages.push(defineModulePage(MODULE_NAME, { name: "events-list", path: eventsPath, file: resolver.resolve("./runtime/app/pages/events.vue") }));
97
- }
98
- if (eventPath && !existingPaths.has(eventPath)) {
99
- pages.push(defineModulePage(MODULE_NAME, { name: "events-detail", path: eventPath, file: resolver.resolve("./runtime/app/pages/event.vue") }));
100
- }
101
- if (ticketsPath && !existingPaths.has(ticketsPath)) {
102
- pages.push(defineModulePage(MODULE_NAME, { name: "events-tickets", path: ticketsPath, file: resolver.resolve("./runtime/app/pages/tickets.vue") }));
103
- }
104
- });
105
- }
106
- if (withMenus) {
107
- const { registerMenuSource } = await import('@frigear-nu/menu/utils');
108
- registerMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
105
+ addModulePages(MODULE_NAME, resolver, [
106
+ { name: "events-list", path: eventsPath, file: "./runtime/app/pages/events.vue" },
107
+ { name: "events-detail", path: eventPath, file: "./runtime/app/pages/event.vue" },
108
+ { name: "events-admin", path: adminPath, file: "./runtime/app/pages/admin.vue" },
109
+ { name: "events-admin-event", path: adminEventPath, file: "./runtime/app/pages/admin-event.vue" },
110
+ { name: "events-tickets", path: ticketsPath, file: "./runtime/app/pages/tickets.vue" }
111
+ ]);
112
+ if (adminEventPath && hasNuxtModule("@frigear-nu/widget")) {
113
+ const { extendTabs } = await import('@frigear-nu/widget/utils');
114
+ extendTabs(nuxt, "admin.events", [
115
+ { id: "details", component: resolver.resolve("./runtime/app/tabs/Details.vue"), order: 0, label: "Details", labelKey: "events.admin.tabs.details", icon: "i-lucide-file-text" },
116
+ { id: "tickets", component: resolver.resolve("./runtime/app/tabs/Tickets.vue"), order: 10, label: "Tickets", labelKey: "events.admin.tabs.tickets", icon: "i-lucide-ticket" },
117
+ { id: "attendees", component: resolver.resolve("./runtime/app/tabs/Attendees.vue"), order: 20, label: "Attendees", labelKey: "events.admin.tabs.attendees", icon: "i-lucide-users" }
118
+ ]);
109
119
  }
120
+ addOptionalMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
110
121
  }
111
122
  });
112
123
 
@@ -0,0 +1,11 @@
1
+ import { type AdminAddon, type AdminAddonGroup } from '../utils/admin-event.js';
2
+ declare function open(addonGroup: AdminAddonGroup, currency: string, existing?: AdminAddon): void;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {
4
+ open: typeof open;
5
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ refresh: () => any;
7
+ }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
8
+ onRefresh?: (() => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,113 @@
1
+ <script setup>
2
+ import { fromCents, toCents } from "@frigear-nu/ui/money";
3
+ import { DEFAULT_CURRENCY } from "../../utils";
4
+ import { useAdminAction } from "../utils/admin-event";
5
+ defineOptions({ name: "EventAdminAddonDialog" });
6
+ const emit = defineEmits(["refresh"]);
7
+ const t = useModuleT("events");
8
+ const tr = useEventsTranslate();
9
+ const { busy, run } = useAdminAction();
10
+ const dialog = ref(false);
11
+ const groupId = ref("");
12
+ const editingAddon = ref(null);
13
+ const addon = reactive({ label: void 0, description: void 0, price: 0, capacity: "" });
14
+ const addonCurrency = ref(DEFAULT_CURRENCY);
15
+ function open(addonGroup, currency, existing) {
16
+ groupId.value = addonGroup.id;
17
+ addonCurrency.value = currency;
18
+ editingAddon.value = existing ?? null;
19
+ Object.assign(addon, {
20
+ label: existing?.label,
21
+ description: existing?.description ?? void 0,
22
+ price: existing ? fromCents(existing.price, currency) : 0,
23
+ capacity: existing?.capacity ?? ""
24
+ });
25
+ dialog.value = true;
26
+ }
27
+ defineExpose({ open });
28
+ const save = () => run(async () => {
29
+ const body = {
30
+ label: addon.label,
31
+ description: addon.description ?? null,
32
+ // Prices are stored in minor units (øre/cents).
33
+ price: toCents(Number(addon.price), addonCurrency.value),
34
+ capacity: addon.capacity === "" ? null : Number(addon.capacity)
35
+ };
36
+ if (editingAddon.value) {
37
+ await $fetch(`/api/events/admin/addons/${editingAddon.value.id}`, { method: "PATCH", body });
38
+ } else {
39
+ await $fetch(`/api/events/admin/addon-groups/${groupId.value}/addons`, { method: "POST", body });
40
+ }
41
+ dialog.value = false;
42
+ emit("refresh");
43
+ }, t("admin.addonSaved", "Add-on saved."));
44
+ </script>
45
+
46
+ <template>
47
+ <UModal
48
+ v-model:open="dialog"
49
+ :title="editingAddon ? t('admin.editAddon', 'Edit choice') : t('admin.addAddon', 'Add choice')"
50
+ >
51
+ <template #body>
52
+ <form
53
+ id="admin-addon-form"
54
+ class="flex flex-col gap-4"
55
+ @submit.prevent="save"
56
+ >
57
+ <UFormField :label="t('admin.name', 'Name')">
58
+ <TranslatedInput
59
+ v-model="addon.label"
60
+ required
61
+ />
62
+ </UFormField>
63
+ <UFormField :label="t('admin.description', 'Description')">
64
+ <TranslatedInput
65
+ v-model="addon.description"
66
+ multiline
67
+ :rows="2"
68
+ />
69
+ </UFormField>
70
+ <UFormField
71
+ :label="t('admin.price', 'Price')"
72
+ :description="t('admin.addonPriceHelp', 'Added to the ticket price. 0 means included.')"
73
+ >
74
+ <UInput
75
+ v-model="addon.price"
76
+ type="number"
77
+ min="0"
78
+ step="0.01"
79
+ class="w-full"
80
+ />
81
+ </UFormField>
82
+ <UFormField
83
+ :label="t('admin.capacity', 'Capacity')"
84
+ :description="t('admin.capacityHelp', 'Leave empty for unlimited.')"
85
+ >
86
+ <UInput
87
+ v-model="addon.capacity"
88
+ type="number"
89
+ min="0"
90
+ class="w-full"
91
+ />
92
+ </UFormField>
93
+ </form>
94
+ </template>
95
+ <template #footer>
96
+ <div class="flex w-full justify-end gap-2">
97
+ <UButton
98
+ variant="subtle"
99
+ color="neutral"
100
+ :label="t('admin.cancel', 'Cancel')"
101
+ @click="dialog = false"
102
+ />
103
+ <UButton
104
+ type="submit"
105
+ form="admin-addon-form"
106
+ :loading="busy"
107
+ :disabled="!tr(addon.label)"
108
+ :label="t('admin.save', 'Save')"
109
+ />
110
+ </div>
111
+ </template>
112
+ </UModal>
113
+ </template>
@@ -0,0 +1,11 @@
1
+ import { type AdminAddon, type AdminAddonGroup } from '../utils/admin-event.js';
2
+ declare function open(addonGroup: AdminAddonGroup, currency: string, existing?: AdminAddon): void;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {
4
+ open: typeof open;
5
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ refresh: () => any;
7
+ }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
8
+ onRefresh?: (() => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import { type AdminAddonGroup, type AdminTicketType } from '../utils/admin-event.js';
2
+ declare function open(ticketType: AdminTicketType, existing?: AdminAddonGroup): void;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {
4
+ open: typeof open;
5
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ refresh: () => any;
7
+ }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
8
+ onRefresh?: (() => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,79 @@
1
+ <script setup>
2
+ import { useAdminAction } from "../utils/admin-event";
3
+ defineOptions({ name: "EventAdminAddonGroupDialog" });
4
+ const emit = defineEmits(["refresh"]);
5
+ const t = useModuleT("events");
6
+ const tr = useEventsTranslate();
7
+ const { busy, run } = useAdminAction();
8
+ const dialog = ref(false);
9
+ const ticketTypeId = ref("");
10
+ const editingGroup = ref(null);
11
+ const group = reactive({ title: void 0, required: false, multiple: false });
12
+ function open(ticketType, existing) {
13
+ ticketTypeId.value = ticketType.id;
14
+ editingGroup.value = existing ?? null;
15
+ Object.assign(group, { title: existing?.title, required: existing?.required ?? false, multiple: existing?.multiple ?? false });
16
+ dialog.value = true;
17
+ }
18
+ defineExpose({ open });
19
+ const save = () => run(async () => {
20
+ const body = { title: group.title, required: group.required, multiple: group.multiple };
21
+ if (editingGroup.value) {
22
+ await $fetch(`/api/events/admin/addon-groups/${editingGroup.value.id}`, { method: "PATCH", body });
23
+ } else {
24
+ await $fetch(`/api/events/admin/ticket-types/${ticketTypeId.value}/addon-groups`, { method: "POST", body });
25
+ }
26
+ dialog.value = false;
27
+ emit("refresh");
28
+ }, t("admin.groupSaved", "Add-on group saved."));
29
+ </script>
30
+
31
+ <template>
32
+ <UModal
33
+ v-model:open="dialog"
34
+ :title="editingGroup ? t('admin.editGroup', 'Edit add-on group') : t('admin.addGroup', 'Add add-on group')"
35
+ >
36
+ <template #body>
37
+ <form
38
+ id="admin-addon-group-form"
39
+ class="flex flex-col gap-4"
40
+ @submit.prevent="save"
41
+ >
42
+ <UFormField
43
+ :label="t('admin.groupTitle', 'Title')"
44
+ :description="t('admin.groupTitleHelp', 'Shown above the choices, e.g. Food.')"
45
+ >
46
+ <TranslatedInput
47
+ v-model="group.title"
48
+ required
49
+ />
50
+ </UFormField>
51
+ <UCheckbox
52
+ v-model="group.required"
53
+ :label="t('admin.groupRequiredLabel', 'Buyers must choose at least one')"
54
+ />
55
+ <UCheckbox
56
+ v-model="group.multiple"
57
+ :label="t('admin.groupMultipleLabel', 'Buyers can choose several')"
58
+ />
59
+ </form>
60
+ </template>
61
+ <template #footer>
62
+ <div class="flex w-full justify-end gap-2">
63
+ <UButton
64
+ variant="subtle"
65
+ color="neutral"
66
+ :label="t('admin.cancel', 'Cancel')"
67
+ @click="dialog = false"
68
+ />
69
+ <UButton
70
+ type="submit"
71
+ form="admin-addon-group-form"
72
+ :loading="busy"
73
+ :disabled="!tr(group.title)"
74
+ :label="t('admin.save', 'Save')"
75
+ />
76
+ </div>
77
+ </template>
78
+ </UModal>
79
+ </template>
@@ -0,0 +1,11 @@
1
+ import { type AdminAddonGroup, type AdminTicketType } from '../utils/admin-event.js';
2
+ declare function open(ticketType: AdminTicketType, existing?: AdminAddonGroup): void;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {
4
+ open: typeof open;
5
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ refresh: () => any;
7
+ }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
8
+ onRefresh?: (() => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,19 @@
1
+ import { type AdminAddon, type AdminAddonGroup, type AdminTicketType } from '../utils/admin-event.js';
2
+ type __VLS_Props = {
3
+ ticketType: AdminTicketType;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ refresh: () => any;
7
+ addGroup: () => any;
8
+ editGroup: (group: AdminAddonGroup) => any;
9
+ addAddon: (group: AdminAddonGroup) => any;
10
+ editAddon: (group: AdminAddonGroup, addon: AdminAddon) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onRefresh?: (() => any) | undefined;
13
+ onAddGroup?: (() => any) | undefined;
14
+ onEditGroup?: ((group: AdminAddonGroup) => any) | undefined;
15
+ onAddAddon?: ((group: AdminAddonGroup) => any) | undefined;
16
+ onEditAddon?: ((group: AdminAddonGroup, addon: AdminAddon) => any) | undefined;
17
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ declare const _default: typeof __VLS_export;
19
+ export default _default;