@vicacha-devs/strapi-plugin-events 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +121 -2
  3. package/dist/admin/CalendarPage-CwCvPDRx.js +1215 -0
  4. package/dist/admin/CalendarPage-Dz7g19qp.mjs +1215 -0
  5. package/dist/admin/RRuleInput-BQl-siRj.js +11 -0
  6. package/dist/admin/RRuleInput-C8bR37X9.mjs +6 -0
  7. package/dist/admin/SettingsPage-CNG9AEO_.mjs +415 -0
  8. package/dist/admin/SettingsPage-DhjtKDmY.js +415 -0
  9. package/dist/admin/ar-mMKUXxch.js +216 -0
  10. package/dist/admin/ar-tWKEvJLB.mjs +216 -0
  11. package/dist/admin/de-CGk8UZ3W.js +216 -0
  12. package/dist/admin/de-OdbJEFs0.mjs +216 -0
  13. package/dist/admin/en-Dskn9ZPa.mjs +216 -0
  14. package/dist/admin/en-imdSlLhJ.js +216 -0
  15. package/dist/admin/es-DcMsMHrI.mjs +216 -0
  16. package/dist/admin/es-DkqqxUJQ.js +216 -0
  17. package/dist/admin/fr-BTEIPOFw.mjs +216 -0
  18. package/dist/admin/fr-D92B_ZTv.js +216 -0
  19. package/dist/admin/index-BTq_X4uT.js +1005 -0
  20. package/dist/admin/index-BatOiwBu.mjs +1006 -0
  21. package/dist/admin/index.js +6 -0
  22. package/dist/admin/index.mjs +6 -0
  23. package/dist/admin/it-CAxPpL_9.mjs +216 -0
  24. package/dist/admin/it-ClKaSCgv.js +216 -0
  25. package/dist/admin/nl-CS1aUfT7.mjs +216 -0
  26. package/dist/admin/nl-F6FIi7zw.js +216 -0
  27. package/dist/admin/pl-BfmBU9FW.mjs +216 -0
  28. package/dist/admin/pl-p8VLXi02.js +216 -0
  29. package/dist/admin/pt-BUdH0uSw.js +216 -0
  30. package/dist/admin/pt-DdulKcbk.mjs +216 -0
  31. package/dist/admin/src/components/EventSidePanel.d.ts +13 -0
  32. package/dist/admin/src/components/Initializer.d.ts +5 -0
  33. package/dist/admin/src/components/PluginIcon.d.ts +2 -0
  34. package/dist/admin/src/components/RRuleInput.d.ts +3 -0
  35. package/dist/admin/src/components/events/AttendeesManagerPanel.d.ts +15 -0
  36. package/dist/admin/src/components/events/EventsDirectoryPanel.d.ts +11 -0
  37. package/dist/admin/src/components/events/EventsKpiCards.d.ts +16 -0
  38. package/dist/admin/src/components/events/QuickCreateEventPanel.d.ts +21 -0
  39. package/dist/admin/src/components/events/calendarPageHelpers.d.ts +8 -0
  40. package/dist/admin/src/fields/rruleField.d.ts +41 -0
  41. package/dist/admin/src/index.d.ts +15 -0
  42. package/dist/admin/src/pages/CalendarPage.d.ts +3 -0
  43. package/dist/admin/src/pages/SettingsPage.d.ts +3 -0
  44. package/dist/admin/src/permissions.d.ts +15 -0
  45. package/dist/admin/src/translations/cmLabels.d.ts +1 -0
  46. package/dist/admin/src/translations/cmPlaceholders.d.ts +1 -0
  47. package/dist/admin/tr-Bp50qvL8.mjs +216 -0
  48. package/dist/admin/tr-C7G9mPtg.js +216 -0
  49. package/dist/server/index.js +3709 -0
  50. package/dist/server/index.mjs +3705 -0
  51. package/dist/server/src/bootstrap.d.ts +5 -0
  52. package/dist/server/src/config/index.d.ts +7 -0
  53. package/dist/server/src/content-types/event/schema.json.d.ts +279 -0
  54. package/dist/server/src/content-types/index.d.ts +366 -0
  55. package/dist/server/src/content-types/registration/schema.json.d.ts +86 -0
  56. package/dist/server/src/controllers/admin.d.ts +17 -0
  57. package/dist/server/src/controllers/content-api.d.ts +14 -0
  58. package/dist/server/src/controllers/index.d.ts +28 -0
  59. package/dist/server/src/destroy.d.ts +5 -0
  60. package/dist/server/src/index.d.ts +551 -0
  61. package/dist/server/src/middlewares/index.d.ts +2 -0
  62. package/dist/server/src/policies/index.d.ts +2 -0
  63. package/dist/server/src/register.d.ts +5 -0
  64. package/dist/server/src/routes/admin.d.ts +17 -0
  65. package/dist/server/src/routes/content-api.d.ts +12 -0
  66. package/dist/server/src/routes/index.d.ts +30 -0
  67. package/dist/server/src/services/ical.d.ts +18 -0
  68. package/dist/server/src/services/index.d.ts +110 -0
  69. package/dist/server/src/services/occurrence.d.ts +44 -0
  70. package/dist/server/src/services/occurrenceHelpers.d.ts +16 -0
  71. package/dist/server/src/services/occurrencePayloadHelpers.d.ts +30 -0
  72. package/dist/server/src/services/occurrenceRsvpBatch.d.ts +25 -0
  73. package/dist/server/src/services/rsvp.d.ts +51 -0
  74. package/dist/server/src/services/rsvpHelpers.d.ts +52 -0
  75. package/dist/server/src/services/rsvpMutations.d.ts +34 -0
  76. package/dist/server/src/services/rsvpSecurity.d.ts +22 -0
  77. package/dist/server/src/services/settings.d.ts +10 -0
  78. package/dist/server/src/types.d.ts +171 -0
  79. package/dist/shared/calendarSystems.d.ts +6 -0
  80. package/dist/shared/icalGenerator.d.ts +25 -0
  81. package/dist/shared/index.d.ts +6 -0
  82. package/dist/shared/index.js +753 -0
  83. package/dist/shared/index.mjs +727 -0
  84. package/dist/shared/pluginId.d.ts +4 -0
  85. package/dist/shared/rruleEngine.d.ts +20 -0
  86. package/dist/shared/spotlightHelpers.d.ts +17 -0
  87. package/dist/shared/types.d.ts +309 -0
  88. package/dist/shared/urlAndPosterHelpers.d.ts +18 -0
  89. package/package.json +109 -3
@@ -0,0 +1,1215 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Box, Flex, Field, TextInput, SingleSelect, SingleSelectOption, Button, Typography, Badge, Table, Thead, Tr, Th, Tbody, Td, Checkbox, NumberInput } from "@strapi/design-system";
3
+ import { useFetchClient, useNotification, Layouts } from "@strapi/strapi/admin";
4
+ import { useState, useCallback, useMemo, useRef, useEffect } from "react";
5
+ import { useIntl } from "react-intl";
6
+ import { useNavigate } from "react-router-dom";
7
+ import { P as PLUGIN_ID } from "./index-BatOiwBu.mjs";
8
+ import "@vicacha-devs/strapi-plugin-calendar/shared";
9
+ const STATUS_BADGE_COLORS = {
10
+ checked_in: { bg: "success100", text: "success700" },
11
+ confirmed: { bg: "primary100", text: "primary700" },
12
+ waitlisted: { bg: "warning100", text: "warning700" },
13
+ cancelled: { bg: "danger100", text: "danger700" }
14
+ };
15
+ function getStatusBadgeColors(status) {
16
+ return Object.hasOwn(STATUS_BADGE_COLORS, status) ? STATUS_BADGE_COLORS[status] : STATUS_BADGE_COLORS.cancelled;
17
+ }
18
+ const AttendeeRegistrationRow = ({
19
+ reg,
20
+ t,
21
+ onCheckIn,
22
+ onStatusChange
23
+ }) => {
24
+ const colors = getStatusBadgeColors(reg.status);
25
+ const seatWord = reg.guestsCount === 1 ? t("attendees.seatSingular", "seat") : t("attendees.seatPlural", "seats");
26
+ const eventDisplay = reg.eventTitle || reg.eventDocumentId;
27
+ const phoneSuffix = reg.attendeePhone ? ` • ${reg.attendeePhone}` : "";
28
+ const canCheckIn = reg.status === "confirmed";
29
+ return /* @__PURE__ */ jsx(Box, { padding: 3, background: "neutral100", hasRadius: true, children: /* @__PURE__ */ jsxs(
30
+ Flex,
31
+ {
32
+ justifyContent: "space-between",
33
+ alignItems: "center",
34
+ wrap: "wrap",
35
+ gap: 3,
36
+ children: [
37
+ /* @__PURE__ */ jsxs(Box, { children: [
38
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
39
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "bold", children: reg.attendeeName }),
40
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
41
+ "(",
42
+ reg.attendeeEmail,
43
+ ")"
44
+ ] }),
45
+ /* @__PURE__ */ jsxs(Badge, { backgroundColor: "neutral200", textColor: "neutral800", children: [
46
+ reg.guestsCount,
47
+ " ",
48
+ seatWord
49
+ ] }),
50
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: colors.bg, textColor: colors.text, children: t(`status.${reg.status}`, reg.status.toUpperCase()) })
51
+ ] }),
52
+ /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
53
+ eventDisplay,
54
+ " • ",
55
+ reg.occurrenceDate,
56
+ phoneSuffix
57
+ ] }) })
58
+ ] }),
59
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "flex-end", children: [
60
+ canCheckIn && /* @__PURE__ */ jsxs(
61
+ Button,
62
+ {
63
+ size: "S",
64
+ variant: "default",
65
+ onClick: () => onCheckIn(reg.documentId),
66
+ children: [
67
+ "✓ ",
68
+ t("attendees.checkIn", "Check In")
69
+ ]
70
+ }
71
+ ),
72
+ /* @__PURE__ */ jsx(Box, { style: { minWidth: 155 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
73
+ /* @__PURE__ */ jsx(Field.Label, { children: t("attendees.rowStatus", "Registration Status") }),
74
+ /* @__PURE__ */ jsxs(
75
+ SingleSelect,
76
+ {
77
+ placeholder: t("placeholder.select_status", "Select status"),
78
+ value: reg.status,
79
+ onChange: (nextSt) => onStatusChange(reg.documentId, nextSt),
80
+ children: [
81
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "confirmed", children: t("status.confirmed", "Confirmed") }),
82
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "checked_in", children: t("status.checked_in", "Checked In") }),
83
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "waitlisted", children: t("status.waitlisted", "Waitlisted") }),
84
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "cancelled", children: t("status.cancelled", "Cancelled") })
85
+ ]
86
+ }
87
+ )
88
+ ] }) })
89
+ ] })
90
+ ]
91
+ }
92
+ ) });
93
+ };
94
+ const AttendeesManagerPanel = ({
95
+ t,
96
+ registrations,
97
+ rsvpSearch,
98
+ onRsvpSearchChange,
99
+ rsvpStatusFilter,
100
+ onRsvpStatusFilterChange,
101
+ rsvpEventFilter,
102
+ onClearRsvpEventFilter,
103
+ onCheckIn,
104
+ onStatusChange
105
+ }) => {
106
+ const { get } = useFetchClient();
107
+ const { toggleNotification } = useNotification();
108
+ const [exportingCsv, setExportingCsv] = useState(false);
109
+ const handleExportCsv = useCallback(async () => {
110
+ setExportingCsv(true);
111
+ try {
112
+ const params = new URLSearchParams();
113
+ if (rsvpEventFilter.trim()) {
114
+ params.set("eventDocumentId", rsvpEventFilter.trim());
115
+ }
116
+ if (rsvpStatusFilter && rsvpStatusFilter !== "all") {
117
+ params.set("status", rsvpStatusFilter);
118
+ }
119
+ const qs = params.toString();
120
+ const suffix = qs ? `?${qs}` : "";
121
+ const res = await get(
122
+ `/${PLUGIN_ID}/registrations/export.csv${suffix}`
123
+ );
124
+ const csvText = typeof res?.data === "string" ? res.data : String(res?.data ?? "");
125
+ const blob = new Blob([csvText], { type: "text/csv;charset=utf-8;" });
126
+ const blobUrl = URL.createObjectURL(blob);
127
+ const anchor = document.createElement("a");
128
+ anchor.href = blobUrl;
129
+ anchor.setAttribute("download", "event-registrations.csv");
130
+ document.body.appendChild(anchor);
131
+ anchor.click();
132
+ document.body.removeChild(anchor);
133
+ URL.revokeObjectURL(blobUrl);
134
+ } catch {
135
+ toggleNotification({
136
+ type: "danger",
137
+ message: t("attendees.error.load", "Could not load registrations.")
138
+ });
139
+ } finally {
140
+ setExportingCsv(false);
141
+ }
142
+ }, [get, rsvpEventFilter, rsvpStatusFilter, t, toggleNotification]);
143
+ return /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 4, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
144
+ /* @__PURE__ */ jsxs(
145
+ Flex,
146
+ {
147
+ justifyContent: "space-between",
148
+ alignItems: "flex-end",
149
+ wrap: "wrap",
150
+ gap: 3,
151
+ children: [
152
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, wrap: "wrap", alignItems: "flex-end", children: [
153
+ /* @__PURE__ */ jsx(Box, { style: { minWidth: 240 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
154
+ /* @__PURE__ */ jsx(Field.Label, { children: t("attendees.search", "Search Attendee or Event") }),
155
+ /* @__PURE__ */ jsx(
156
+ TextInput,
157
+ {
158
+ value: rsvpSearch,
159
+ onChange: (e) => onRsvpSearchChange(e.target.value),
160
+ placeholder: t(
161
+ "placeholder.name_email_or_event_title",
162
+ "Name, email, or event title..."
163
+ )
164
+ }
165
+ )
166
+ ] }) }),
167
+ /* @__PURE__ */ jsx(Box, { style: { minWidth: 160 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
168
+ /* @__PURE__ */ jsx(Field.Label, { children: t("attendees.statusFilter", "Status") }),
169
+ /* @__PURE__ */ jsxs(
170
+ SingleSelect,
171
+ {
172
+ placeholder: t("placeholder.all_statuses", "All Statuses"),
173
+ value: rsvpStatusFilter,
174
+ onChange: (val) => onRsvpStatusFilterChange(val),
175
+ children: [
176
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "all", children: t("placeholder.all_statuses", "All Statuses") }),
177
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "confirmed", children: t("status.confirmed", "Confirmed") }),
178
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "checked_in", children: t("status.checked_in", "Checked In") }),
179
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "waitlisted", children: t("status.waitlisted", "Waitlisted") }),
180
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "cancelled", children: t("status.cancelled", "Cancelled") })
181
+ ]
182
+ }
183
+ )
184
+ ] }) }),
185
+ rsvpEventFilter && /* @__PURE__ */ jsxs(
186
+ Button,
187
+ {
188
+ size: "S",
189
+ variant: "tertiary",
190
+ onClick: onClearRsvpEventFilter,
191
+ children: [
192
+ t("attendees.clearEventFilter", "Clear Event Filter"),
193
+ " (",
194
+ rsvpEventFilter,
195
+ ")"
196
+ ]
197
+ }
198
+ )
199
+ ] }),
200
+ /* @__PURE__ */ jsx(
201
+ Button,
202
+ {
203
+ size: "S",
204
+ variant: "secondary",
205
+ loading: exportingCsv,
206
+ onClick: handleExportCsv,
207
+ children: t("attendees.exportCsv", "Export CSV")
208
+ }
209
+ )
210
+ ]
211
+ }
212
+ ),
213
+ registrations.length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", children: t(
214
+ "attendees.empty",
215
+ "No registrations found matching current filters."
216
+ ) }) : /* @__PURE__ */ jsx(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: registrations.map((reg) => /* @__PURE__ */ jsx(
217
+ AttendeeRegistrationRow,
218
+ {
219
+ reg,
220
+ t,
221
+ onCheckIn,
222
+ onStatusChange
223
+ },
224
+ reg.documentId
225
+ )) })
226
+ ] }) });
227
+ };
228
+ const DEFAULT_RECURRENCE_PRESETS = [
229
+ { id: "daily", label: "Daily", rrule: "FREQ=DAILY;INTERVAL=1" },
230
+ {
231
+ id: "weekly-mon",
232
+ label: "Weekly on Mondays",
233
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO"
234
+ },
235
+ {
236
+ id: "weekly-tue",
237
+ label: "Weekly on Tuesdays",
238
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=TU"
239
+ },
240
+ {
241
+ id: "weekly-wed",
242
+ label: "Weekly on Wednesdays",
243
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=WE"
244
+ },
245
+ {
246
+ id: "weekly-thu",
247
+ label: "Weekly on Thursdays",
248
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=TH"
249
+ },
250
+ {
251
+ id: "weekly-fri",
252
+ label: "Weekly on Fridays",
253
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=FR"
254
+ },
255
+ {
256
+ id: "weekly-sat",
257
+ label: "Weekly on Saturdays",
258
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=SA"
259
+ },
260
+ {
261
+ id: "weekly-sun",
262
+ label: "Weekly on Sundays",
263
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=SU"
264
+ },
265
+ { id: "monthly", label: "Monthly", rrule: "FREQ=MONTHLY;INTERVAL=1" },
266
+ { id: "yearly", label: "Yearly", rrule: "FREQ=YEARLY;INTERVAL=1" }
267
+ ];
268
+ function formatCategoryLabel(category) {
269
+ const trimmed = category.trim();
270
+ if (!trimmed) return "";
271
+ return trimmed.split(/[\s_-]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
272
+ }
273
+ const EVENT_STATUS_COLORS = {
274
+ scheduled: { bg: "success100", text: "success700" },
275
+ cancelled: { bg: "danger100", text: "danger700" }
276
+ };
277
+ function getEventStatusColors(status) {
278
+ return EVENT_STATUS_COLORS[status] ?? { bg: "warning100", text: "warning700" };
279
+ }
280
+ function matchesCategoryFilter(ev, categoryFilter) {
281
+ if (categoryFilter === "all") return true;
282
+ return (ev.category ?? "").toLowerCase() === categoryFilter.toLowerCase();
283
+ }
284
+ function matchesRsvpModeFilter(ev, rsvpModeFilter) {
285
+ const enabled = Boolean(ev.rsvp?.rsvpEnabled);
286
+ if (rsvpModeFilter === "rsvp") return enabled;
287
+ if (rsvpModeFilter === "info") return !enabled;
288
+ return true;
289
+ }
290
+ function matchesSearchQuery(ev, query) {
291
+ if (!query) return true;
292
+ const haystack = [ev.title, ev.location, ev.category, ev.summary].filter(Boolean).join(" ").toLowerCase();
293
+ return haystack.includes(query);
294
+ }
295
+ function formatEventDateLabel(ev) {
296
+ const endKey = ev.endDate ? ev.endDate.slice(0, 10) : "";
297
+ return endKey && endKey !== ev.occurrenceDate ? `${ev.occurrenceDate} → ${endKey}` : ev.occurrenceDate;
298
+ }
299
+ const EventRsvpCell = ({ rsvp, t }) => {
300
+ if (!rsvp?.rsvpEnabled) {
301
+ return /* @__PURE__ */ jsx(Badge, { backgroundColor: "neutral150", textColor: "neutral600", children: t("events.directory.noRsvp", "Info / External Link") });
302
+ }
303
+ const badgeBg = rsvp.isFull ? "warning100" : "primary100";
304
+ const badgeText = rsvp.isFull ? "warning700" : "primary700";
305
+ const seatsLabel = rsvp.remainingSeats !== null ? `${rsvp.remainingSeats} ${t("events.directory.seatsLeft", "seats available")}` : t("sidePanel.open", "Open");
306
+ return /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
307
+ /* @__PURE__ */ jsxs(Badge, { backgroundColor: badgeBg, textColor: badgeText, children: [
308
+ rsvp.confirmedSeats,
309
+ " / ",
310
+ rsvp.capacity ?? "∞",
311
+ " ",
312
+ t("sidePanel.confirmedSeats", "Confirmed Seats")
313
+ ] }),
314
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: seatsLabel })
315
+ ] });
316
+ };
317
+ const EventDirectoryRow = ({
318
+ ev,
319
+ isSelectedForRsvp,
320
+ t,
321
+ onSelectEventDocId,
322
+ onEditInContentManager,
323
+ onOpenInCalendar
324
+ }) => {
325
+ const dateLabel = formatEventDateLabel(ev);
326
+ const statusColors = getEventStatusColors(ev.status);
327
+ const rsvpEnabled = Boolean(ev.rsvp?.rsvpEnabled);
328
+ const rsvpBtnVariant = isSelectedForRsvp ? "default" : "secondary";
329
+ const rsvpBtnLabel = isSelectedForRsvp ? t("attendees.clearEventFilter", "Clear Filter") : t("events.directory.filterRsvps", "View RSVPs");
330
+ return /* @__PURE__ */ jsxs(Tr, { children: [
331
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
332
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
333
+ /* @__PURE__ */ jsx(
334
+ Box,
335
+ {
336
+ style: {
337
+ width: 10,
338
+ height: 10,
339
+ borderRadius: "50%",
340
+ background: ev.color || "#0D9488",
341
+ flexShrink: 0
342
+ }
343
+ }
344
+ ),
345
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: ev.title }),
346
+ ev.isRecurring && /* @__PURE__ */ jsx(Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: t("events.directory.recurring", "Recurring") })
347
+ ] }),
348
+ ev.summary && /* @__PURE__ */ jsx(
349
+ Typography,
350
+ {
351
+ variant: "pi",
352
+ textColor: "neutral600",
353
+ style: {
354
+ maxWidth: 360,
355
+ whiteSpace: "nowrap",
356
+ overflow: "hidden",
357
+ textOverflow: "ellipsis"
358
+ },
359
+ children: ev.summary
360
+ }
361
+ )
362
+ ] }) }),
363
+ /* @__PURE__ */ jsx(Td, { children: ev.category ? /* @__PURE__ */ jsx(Badge, { children: formatCategoryLabel(ev.category) }) : /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "—" }) }),
364
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
365
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: dateLabel }),
366
+ /* @__PURE__ */ jsx(
367
+ Badge,
368
+ {
369
+ backgroundColor: statusColors.bg,
370
+ textColor: statusColors.text,
371
+ children: t(`status.${ev.status}`, ev.status.toUpperCase())
372
+ }
373
+ )
374
+ ] }) }),
375
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral700", children: ev.location || "—" }) }),
376
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(EventRsvpCell, { rsvp: ev.rsvp, t }) }),
377
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 2, wrap: "wrap", children: [
378
+ rsvpEnabled && /* @__PURE__ */ jsx(
379
+ Button,
380
+ {
381
+ size: "S",
382
+ variant: rsvpBtnVariant,
383
+ onClick: () => onSelectEventDocId(isSelectedForRsvp ? "" : ev.documentId),
384
+ children: rsvpBtnLabel
385
+ }
386
+ ),
387
+ /* @__PURE__ */ jsx(
388
+ Button,
389
+ {
390
+ size: "S",
391
+ variant: "tertiary",
392
+ onClick: () => onEditInContentManager(ev.uid, ev.documentId, ev.locale),
393
+ children: t("events.directory.editEvent", "Edit Event")
394
+ }
395
+ ),
396
+ /* @__PURE__ */ jsx(
397
+ Button,
398
+ {
399
+ size: "S",
400
+ variant: "tertiary",
401
+ onClick: () => onOpenInCalendar(ev.documentId),
402
+ children: t("calendar.openUnifiedCalendar", "Calendar")
403
+ }
404
+ )
405
+ ] }) })
406
+ ] });
407
+ };
408
+ const EventsDirectoryPanel = ({
409
+ t,
410
+ events,
411
+ selectedEventDocId,
412
+ onSelectEventDocId,
413
+ onEditInContentManager,
414
+ onOpenInCalendar
415
+ }) => {
416
+ const [eventSearch, setEventSearch] = useState("");
417
+ const [categoryFilter, setCategoryFilter] = useState("all");
418
+ const [rsvpModeFilter, setRsvpModeFilter] = useState("all");
419
+ const categories = useMemo(() => {
420
+ const set = /* @__PURE__ */ new Set();
421
+ for (const ev of events) {
422
+ if (ev.category) set.add(ev.category);
423
+ }
424
+ return Array.from(set);
425
+ }, [events]);
426
+ const filteredEvents = useMemo(() => {
427
+ const q = eventSearch.trim().toLowerCase();
428
+ return events.filter(
429
+ (ev) => matchesCategoryFilter(ev, categoryFilter) && matchesRsvpModeFilter(ev, rsvpModeFilter) && matchesSearchQuery(ev, q)
430
+ );
431
+ }, [events, eventSearch, categoryFilter, rsvpModeFilter]);
432
+ return /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 4, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
433
+ /* @__PURE__ */ jsxs(
434
+ Flex,
435
+ {
436
+ justifyContent: "space-between",
437
+ alignItems: "flex-end",
438
+ wrap: "wrap",
439
+ gap: 3,
440
+ children: [
441
+ /* @__PURE__ */ jsxs(Box, { children: [
442
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
443
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", fontWeight: "bold", children: t("events.directory.title", "Registered Events") }),
444
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: "primary100", textColor: "primary700", children: filteredEvents.length })
445
+ ] }),
446
+ /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
447
+ "events.directory.subtitle",
448
+ "All scheduled and recurring events across your configured sources, including RSVP seat capacity and Content Manager links."
449
+ ) }) })
450
+ ] }),
451
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, wrap: "wrap", alignItems: "flex-end", children: [
452
+ /* @__PURE__ */ jsx(Box, { style: { minWidth: 220 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
453
+ /* @__PURE__ */ jsx(Field.Label, { children: t("events.directory.search", "Search Events") }),
454
+ /* @__PURE__ */ jsx(
455
+ TextInput,
456
+ {
457
+ value: eventSearch,
458
+ onChange: (e) => setEventSearch(e.target.value),
459
+ placeholder: t(
460
+ "events.directory.searchPlaceholder",
461
+ "Title, location, category..."
462
+ )
463
+ }
464
+ )
465
+ ] }) }),
466
+ /* @__PURE__ */ jsx(Box, { style: { minWidth: 160 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
467
+ /* @__PURE__ */ jsx(Field.Label, { children: t("calendar.categoryFilter", "Category") }),
468
+ /* @__PURE__ */ jsxs(
469
+ SingleSelect,
470
+ {
471
+ value: categoryFilter,
472
+ onChange: (val) => setCategoryFilter(String(val)),
473
+ children: [
474
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "all", children: t("calendar.allCategories", "All Categories") }),
475
+ categories.map((cat) => /* @__PURE__ */ jsx(SingleSelectOption, { value: cat, children: formatCategoryLabel(cat) }, cat))
476
+ ]
477
+ }
478
+ )
479
+ ] }) }),
480
+ /* @__PURE__ */ jsx(Box, { style: { minWidth: 160 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
481
+ /* @__PURE__ */ jsx(Field.Label, { children: t("events.directory.rsvpFilter", "RSVP Mode") }),
482
+ /* @__PURE__ */ jsxs(
483
+ SingleSelect,
484
+ {
485
+ value: rsvpModeFilter,
486
+ onChange: (val) => setRsvpModeFilter(String(val)),
487
+ children: [
488
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "all", children: t("events.directory.allEvents", "All Events") }),
489
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "rsvp", children: t("events.directory.rsvpOnly", "With RSVPs Enabled") }),
490
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "info", children: t("events.directory.infoOnly", "Informational Only") })
491
+ ]
492
+ }
493
+ )
494
+ ] }) })
495
+ ] })
496
+ ]
497
+ }
498
+ ),
499
+ filteredEvents.length === 0 ? /* @__PURE__ */ jsx(Box, { padding: 4, background: "neutral100", hasRadius: true, children: /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", children: t(
500
+ "events.directory.empty",
501
+ "No events found matching the current filters."
502
+ ) }) }) : /* @__PURE__ */ jsxs(Table, { colCount: 6, rowCount: filteredEvents.length + 1, children: [
503
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
504
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("calendar.field.title", "Event Title") }) }),
505
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("calendar.categoryFilter", "Category") }) }),
506
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("calendar.field.startDate", "Start Date") }) }),
507
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("calendar.field.location", "Location") }) }),
508
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("sidePanel.rsvpStatus", "RSVP & Capacity") }) }),
509
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("events.directory.actions", "Actions") }) })
510
+ ] }) }),
511
+ /* @__PURE__ */ jsx(Tbody, { children: filteredEvents.map((ev) => /* @__PURE__ */ jsx(
512
+ EventDirectoryRow,
513
+ {
514
+ ev,
515
+ isSelectedForRsvp: selectedEventDocId === ev.documentId,
516
+ t,
517
+ onSelectEventDocId,
518
+ onEditInContentManager,
519
+ onOpenInCalendar
520
+ },
521
+ `${ev.uid}:${ev.documentId}`
522
+ )) })
523
+ ] })
524
+ ] }) });
525
+ };
526
+ function computeDashboardRsvpStats(uniqueEvents, registrations) {
527
+ let confirmedSeats = 0;
528
+ let checkedInSeats = 0;
529
+ let waitlistedSeats = 0;
530
+ const eventsSet = /* @__PURE__ */ new Set();
531
+ for (const ev of uniqueEvents) {
532
+ if (ev.rsvp?.rsvpEnabled) {
533
+ eventsSet.add(ev.documentId);
534
+ }
535
+ }
536
+ for (const reg of registrations) {
537
+ const seats = Number(reg.guestsCount) || 1;
538
+ if (reg.eventDocumentId) eventsSet.add(reg.eventDocumentId);
539
+ if (reg.status === "confirmed") confirmedSeats += seats;
540
+ else if (reg.status === "checked_in") checkedInSeats += seats;
541
+ else if (reg.status === "waitlisted") waitlistedSeats += seats;
542
+ }
543
+ return {
544
+ totalEvents: uniqueEvents.length,
545
+ totalRegistrations: registrations.length,
546
+ confirmedSeats,
547
+ checkedInSeats,
548
+ waitlistedSeats,
549
+ distinctEvents: eventsSet.size
550
+ };
551
+ }
552
+ const EventsKpiCards = ({ stats, t }) => {
553
+ const cards = [
554
+ {
555
+ label: t("stats.totalEvents", "Registered Events"),
556
+ value: stats.totalEvents,
557
+ badge: "primary100"
558
+ },
559
+ {
560
+ label: t("stats.eventsWithRsvps", "Events with RSVPs"),
561
+ value: stats.distinctEvents,
562
+ badge: "secondary100"
563
+ },
564
+ {
565
+ label: t("stats.registrations", "Total Registrations"),
566
+ value: stats.totalRegistrations,
567
+ badge: "neutral100"
568
+ },
569
+ {
570
+ label: t("stats.confirmedSeats", "Confirmed Seats"),
571
+ value: stats.confirmedSeats,
572
+ badge: "primary100"
573
+ },
574
+ {
575
+ label: t("stats.checkedInSeats", "Checked-In Seats"),
576
+ value: stats.checkedInSeats,
577
+ badge: "success100"
578
+ },
579
+ {
580
+ label: t("stats.waitlistedSeats", "Waitlisted Seats"),
581
+ value: stats.waitlistedSeats,
582
+ badge: "warning100"
583
+ }
584
+ ];
585
+ return /* @__PURE__ */ jsx(Flex, { gap: 4, wrap: "wrap", children: cards.map((card) => /* @__PURE__ */ jsx(
586
+ Box,
587
+ {
588
+ background: "neutral0",
589
+ hasRadius: true,
590
+ shadow: "tableShadow",
591
+ padding: 4,
592
+ style: { flex: "1 1 160px" },
593
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
594
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: card.label }),
595
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: card.badge, children: card.value })
596
+ ] })
597
+ },
598
+ card.label
599
+ )) });
600
+ };
601
+ const WEEKDAYS = [
602
+ { code: "MO", labelKey: "rrule.weekday.mo", defaultLabel: "Mon", jsDay: 1 },
603
+ { code: "TU", labelKey: "rrule.weekday.tu", defaultLabel: "Tue", jsDay: 2 },
604
+ { code: "WE", labelKey: "rrule.weekday.we", defaultLabel: "Wed", jsDay: 3 },
605
+ { code: "TH", labelKey: "rrule.weekday.th", defaultLabel: "Thu", jsDay: 4 },
606
+ { code: "FR", labelKey: "rrule.weekday.fr", defaultLabel: "Fri", jsDay: 5 },
607
+ { code: "SA", labelKey: "rrule.weekday.sa", defaultLabel: "Sat", jsDay: 6 },
608
+ { code: "SU", labelKey: "rrule.weekday.su", defaultLabel: "Sun", jsDay: 0 }
609
+ ];
610
+ function extractByDays(rrule) {
611
+ const match = rrule.match(/BYDAY=([A-Z,]+)/i);
612
+ if (!match?.[1]) return [];
613
+ const validCodes = /* @__PURE__ */ new Set([
614
+ "MO",
615
+ "TU",
616
+ "WE",
617
+ "TH",
618
+ "FR",
619
+ "SA",
620
+ "SU"
621
+ ]);
622
+ return match[1].toUpperCase().split(",").map((d) => d.trim()).filter((d) => validCodes.has(d));
623
+ }
624
+ function alignStartDateToWeekday(startStr, targetCode) {
625
+ if (!startStr || !/^\d{4}-\d{2}-\d{2}$/.test(startStr)) return startStr;
626
+ const target = WEEKDAYS.find((w) => w.code === targetCode);
627
+ if (!target) return startStr;
628
+ const dt = /* @__PURE__ */ new Date(`${startStr}T00:00:00.000Z`);
629
+ if (Number.isNaN(dt.getTime())) return startStr;
630
+ const currentDay = dt.getUTCDay();
631
+ const diff = (target.jsDay - currentDay + 7) % 7;
632
+ if (diff === 0) return startStr;
633
+ dt.setUTCDate(dt.getUTCDate() + diff);
634
+ return dt.toISOString().slice(0, 10);
635
+ }
636
+ const QuickCreateEventPanel = ({
637
+ t,
638
+ quickTitle,
639
+ onQuickTitleChange,
640
+ quickStart,
641
+ onQuickStartChange,
642
+ quickLocation,
643
+ onQuickLocationChange,
644
+ quickRRule,
645
+ onQuickRRuleChange,
646
+ quickRsvpEnabled,
647
+ onQuickRsvpEnabledChange,
648
+ quickCapacity,
649
+ onQuickCapacityChange,
650
+ recurrencePresets,
651
+ onCancel,
652
+ onSubmit
653
+ }) => {
654
+ const normalizedPresets = useMemo(() => {
655
+ const byRRule = /* @__PURE__ */ new Map();
656
+ for (const def of DEFAULT_RECURRENCE_PRESETS) {
657
+ byRRule.set(def.rrule, def);
658
+ }
659
+ for (const p of recurrencePresets) {
660
+ if (p?.rrule) {
661
+ byRRule.set(p.rrule, p);
662
+ }
663
+ }
664
+ return Array.from(byRRule.values());
665
+ }, [recurrencePresets]);
666
+ const isWeekly = quickRRule.toUpperCase().includes("FREQ=WEEKLY");
667
+ const selectedWeekdays = useMemo(
668
+ () => extractByDays(quickRRule),
669
+ [quickRRule]
670
+ );
671
+ const hasCustomWeeklyOption = isWeekly && Boolean(quickRRule) && !normalizedPresets.some((p) => p.rrule === quickRRule);
672
+ const handleRecurrenceSelect = (val) => {
673
+ const nextRRule = val === "NONE" ? "" : val;
674
+ onQuickRRuleChange(nextRRule);
675
+ if (nextRRule.toUpperCase().includes("FREQ=WEEKLY")) {
676
+ const days = extractByDays(nextRRule);
677
+ if (days.length === 1 && quickStart) {
678
+ const aligned = alignStartDateToWeekday(quickStart, days[0]);
679
+ if (aligned !== quickStart) {
680
+ onQuickStartChange(aligned);
681
+ }
682
+ }
683
+ }
684
+ };
685
+ const handleToggleWeekday = (code) => {
686
+ const exists = selectedWeekdays.includes(code);
687
+ let nextDays;
688
+ if (exists) {
689
+ nextDays = selectedWeekdays.length > 1 ? selectedWeekdays.filter((d) => d !== code) : [code];
690
+ } else {
691
+ nextDays = WEEKDAYS.map((w) => w.code).filter(
692
+ (c) => c === code || selectedWeekdays.includes(c)
693
+ );
694
+ }
695
+ const nextRRule = `FREQ=WEEKLY;INTERVAL=1;BYDAY=${nextDays.join(",")}`;
696
+ onQuickRRuleChange(nextRRule);
697
+ if (nextDays.length === 1 && quickStart) {
698
+ const aligned = alignStartDateToWeekday(quickStart, nextDays[0]);
699
+ if (aligned !== quickStart) {
700
+ onQuickStartChange(aligned);
701
+ }
702
+ }
703
+ };
704
+ const handleStartDateChange = (nextDateStr) => {
705
+ onQuickStartChange(nextDateStr);
706
+ if (isWeekly && selectedWeekdays.length === 1 && /^\d{4}-\d{2}-\d{2}$/.test(nextDateStr)) {
707
+ const dt = /* @__PURE__ */ new Date(`${nextDateStr}T00:00:00.000Z`);
708
+ if (!Number.isNaN(dt.getTime())) {
709
+ const match = WEEKDAYS.find((w) => w.jsDay === dt.getUTCDay());
710
+ if (match && match.code !== selectedWeekdays[0]) {
711
+ onQuickRRuleChange(`FREQ=WEEKLY;INTERVAL=1;BYDAY=${match.code}`);
712
+ }
713
+ }
714
+ }
715
+ };
716
+ return /* @__PURE__ */ jsx(Box, { padding: 4, background: "neutral0", hasRadius: true, shadow: "tableShadow", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 3, children: [
717
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", children: t("calendar.quickCreateTitle", "Quick-Create Event") }),
718
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, wrap: "wrap", children: [
719
+ /* @__PURE__ */ jsx(Box, { style: { flex: 2, minWidth: 220 }, children: /* @__PURE__ */ jsxs(Field.Root, { required: true, children: [
720
+ /* @__PURE__ */ jsx(Field.Label, { children: t("calendar.field.title", "Event Title") }),
721
+ /* @__PURE__ */ jsx(
722
+ TextInput,
723
+ {
724
+ value: quickTitle,
725
+ onChange: (e) => onQuickTitleChange(e.target.value),
726
+ placeholder: t(
727
+ "placeholder.e_g_product_launch_webinar_weekly_workshop",
728
+ "e.g., Product Launch Webinar / Weekly Workshop"
729
+ )
730
+ }
731
+ )
732
+ ] }) }),
733
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 180 }, children: /* @__PURE__ */ jsxs(Field.Root, { required: true, children: [
734
+ /* @__PURE__ */ jsx(Field.Label, { children: t("calendar.field.startDate", "Start Date") }),
735
+ /* @__PURE__ */ jsx(
736
+ TextInput,
737
+ {
738
+ type: "date",
739
+ placeholder: t("placeholder.yyyy_mm_dd", "YYYY-MM-DD"),
740
+ value: quickStart,
741
+ onChange: (e) => handleStartDateChange(e.target.value)
742
+ }
743
+ )
744
+ ] }) }),
745
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 180 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
746
+ /* @__PURE__ */ jsx(Field.Label, { children: t("calendar.field.location", "Location") }),
747
+ /* @__PURE__ */ jsx(
748
+ TextInput,
749
+ {
750
+ value: quickLocation,
751
+ onChange: (e) => onQuickLocationChange(e.target.value),
752
+ placeholder: t(
753
+ "placeholder.main_auditorium_online",
754
+ "Main Auditorium / Online"
755
+ )
756
+ }
757
+ )
758
+ ] }) }),
759
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 210 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
760
+ /* @__PURE__ */ jsx(Field.Label, { children: t("calendar.field.recurrence", "Recurrence Preset") }),
761
+ /* @__PURE__ */ jsxs(
762
+ SingleSelect,
763
+ {
764
+ placeholder: t(
765
+ "placeholder.select_recurrence_preset",
766
+ "Select recurrence preset"
767
+ ),
768
+ value: quickRRule || "NONE",
769
+ onChange: handleRecurrenceSelect,
770
+ children: [
771
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "NONE", children: t("rrule.freq.none", "Does not repeat (Single Event)") }),
772
+ normalizedPresets.map((preset) => /* @__PURE__ */ jsx(SingleSelectOption, { value: preset.rrule, children: preset.label }, preset.id)),
773
+ hasCustomWeeklyOption && /* @__PURE__ */ jsx(SingleSelectOption, { value: quickRRule, children: `${t("rrule.freq.weekly", "Weekly")} (${selectedWeekdays.join(", ")})` })
774
+ ]
775
+ }
776
+ )
777
+ ] }) })
778
+ ] }),
779
+ isWeekly && /* @__PURE__ */ jsx(Box, { padding: 3, background: "neutral100", hasRadius: true, children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, wrap: "wrap", children: [
780
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral700", children: t(
781
+ "calendar.field.repeatWeekdays",
782
+ "Repeat on Day(s) of Week:"
783
+ ) }),
784
+ WEEKDAYS.map((day) => {
785
+ const active = selectedWeekdays.includes(day.code);
786
+ return /* @__PURE__ */ jsx(
787
+ Button,
788
+ {
789
+ size: "S",
790
+ variant: active ? "default" : "tertiary",
791
+ onClick: () => handleToggleWeekday(day.code),
792
+ children: t(day.labelKey, day.defaultLabel)
793
+ },
794
+ day.code
795
+ );
796
+ })
797
+ ] }) }),
798
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, alignItems: "flex-end", wrap: "wrap", children: [
799
+ /* @__PURE__ */ jsx(Box, { style: { minHeight: 40, display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx(
800
+ Checkbox,
801
+ {
802
+ checked: quickRsvpEnabled,
803
+ onCheckedChange: (checked) => onQuickRsvpEnabledChange(Boolean(checked)),
804
+ children: t("calendar.field.enableRsvp", "Enable RSVP & Seat Capacity")
805
+ }
806
+ ) }),
807
+ quickRsvpEnabled && /* @__PURE__ */ jsx(Box, { style: { width: 180 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
808
+ /* @__PURE__ */ jsx(Field.Label, { children: t("calendar.field.capacity", "Max Capacity (Seats)") }),
809
+ /* @__PURE__ */ jsx(
810
+ NumberInput,
811
+ {
812
+ placeholder: t("placeholder.50", "50"),
813
+ value: quickCapacity,
814
+ onValueChange: (val) => onQuickCapacityChange(val && val >= 1 ? val : 50)
815
+ }
816
+ )
817
+ ] }) }),
818
+ /* @__PURE__ */ jsxs(
819
+ Flex,
820
+ {
821
+ gap: 2,
822
+ alignItems: "center",
823
+ style: { marginLeft: "auto", minHeight: 40 },
824
+ children: [
825
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: onCancel, children: t("common.cancel", "Cancel") }),
826
+ /* @__PURE__ */ jsx(Button, { variant: "default", onClick: onSubmit, children: t("common.create", "Create Event") })
827
+ ]
828
+ }
829
+ )
830
+ ] })
831
+ ] }) });
832
+ };
833
+ function shouldReplaceRepresentativeOccurrence(existing, candidate, todayKey) {
834
+ const existingIsFuture = existing.occurrenceDate >= todayKey;
835
+ const currentIsFuture = candidate.occurrenceDate >= todayKey;
836
+ if (!existingIsFuture && currentIsFuture) return true;
837
+ if (existingIsFuture && currentIsFuture) {
838
+ return candidate.occurrenceDate < existing.occurrenceDate;
839
+ }
840
+ if (!existingIsFuture && !currentIsFuture) {
841
+ return candidate.occurrenceDate > existing.occurrenceDate;
842
+ }
843
+ return false;
844
+ }
845
+ const CalendarPage = () => {
846
+ const { formatMessage } = useIntl();
847
+ const navigate = useNavigate();
848
+ const { get, post } = useFetchClient();
849
+ const isMountedRef = useRef(true);
850
+ useEffect(() => {
851
+ isMountedRef.current = true;
852
+ return () => {
853
+ isMountedRef.current = false;
854
+ };
855
+ }, []);
856
+ const t = useCallback(
857
+ (id, defaultMessage) => formatMessage({ id: `${PLUGIN_ID}.${id}`, defaultMessage }),
858
+ [formatMessage]
859
+ );
860
+ const [settings, setSettings] = useState(null);
861
+ const [occurrences, setOccurrences] = useState([]);
862
+ const [bannerMessage, setBannerMessage] = useState(null);
863
+ const [showQuickCreate, setShowQuickCreate] = useState(false);
864
+ const [quickTitle, setQuickTitle] = useState("");
865
+ const [quickStart, setQuickStart] = useState("");
866
+ const [quickLocation, setQuickLocation] = useState("");
867
+ const [quickCategory] = useState("general");
868
+ const [quickRRule, setQuickRRule] = useState("");
869
+ const [quickRsvpEnabled, setQuickRsvpEnabled] = useState(true);
870
+ const [quickCapacity, setQuickCapacity] = useState(50);
871
+ const [registrations, setRegistrations] = useState(
872
+ []
873
+ );
874
+ const [rsvpSearch, setRsvpSearch] = useState("");
875
+ const [debouncedRsvpSearch, setDebouncedRsvpSearch] = useState("");
876
+ const [rsvpStatusFilter, setRsvpStatusFilter] = useState("all");
877
+ const [rsvpEventFilter, setRsvpEventFilter] = useState(() => {
878
+ if (typeof window === "undefined") return "";
879
+ return new URLSearchParams(window.location.search).get("eventDocumentId") || "";
880
+ });
881
+ useEffect(() => {
882
+ const timer = window.setTimeout(() => {
883
+ setDebouncedRsvpSearch(rsvpSearch);
884
+ }, 250);
885
+ return () => {
886
+ window.clearTimeout(timer);
887
+ };
888
+ }, [rsvpSearch]);
889
+ const todayKey = useMemo(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), []);
890
+ const fetchCalendarData = useCallback(async () => {
891
+ try {
892
+ const now = /* @__PURE__ */ new Date();
893
+ const fromIso = `${now.getFullYear() - 1}-01-01T00:00:00.000Z`;
894
+ const toIso = `${now.getFullYear() + 2}-12-31T23:59:59.000Z`;
895
+ const res = await get(
896
+ `/${PLUGIN_ID}/calendar?from=${encodeURIComponent(fromIso)}&to=${encodeURIComponent(toIso)}`
897
+ );
898
+ if (!isMountedRef.current) return;
899
+ const payload = res?.data?.data;
900
+ if (payload?.settings) {
901
+ setSettings(payload.settings);
902
+ }
903
+ if (Array.isArray(payload?.occurrences)) {
904
+ setOccurrences(payload.occurrences);
905
+ }
906
+ } catch {
907
+ try {
908
+ const res = await get(`/${PLUGIN_ID}/settings`);
909
+ if (!isMountedRef.current) return;
910
+ const payload = res?.data?.data;
911
+ if (payload) {
912
+ setSettings(payload);
913
+ }
914
+ } catch {
915
+ }
916
+ }
917
+ }, [get]);
918
+ const fetchRegistrations = useCallback(async () => {
919
+ try {
920
+ const params = new URLSearchParams();
921
+ if (debouncedRsvpSearch.trim()) {
922
+ params.set("search", debouncedRsvpSearch.trim());
923
+ }
924
+ if (rsvpStatusFilter !== "all") params.set("status", rsvpStatusFilter);
925
+ if (rsvpEventFilter.trim()) {
926
+ params.set("eventDocumentId", rsvpEventFilter.trim());
927
+ }
928
+ const res = await get(`/${PLUGIN_ID}/registrations?${params.toString()}`);
929
+ if (!isMountedRef.current) return;
930
+ const data = res?.data?.data;
931
+ if (Array.isArray(data)) {
932
+ setRegistrations(data);
933
+ }
934
+ } catch {
935
+ if (isMountedRef.current) {
936
+ setBannerMessage(
937
+ t("attendees.error.load", "Could not load registrations.")
938
+ );
939
+ }
940
+ }
941
+ }, [get, debouncedRsvpSearch, rsvpStatusFilter, rsvpEventFilter, t]);
942
+ useEffect(() => {
943
+ void fetchCalendarData();
944
+ }, [fetchCalendarData]);
945
+ useEffect(() => {
946
+ void fetchRegistrations();
947
+ }, [fetchRegistrations]);
948
+ const uniqueEvents = useMemo(() => {
949
+ const byDoc = /* @__PURE__ */ new Map();
950
+ for (const occ of occurrences) {
951
+ const key = `${occ.uid}:${occ.documentId}`;
952
+ const existing = byDoc.get(key);
953
+ if (!existing || shouldReplaceRepresentativeOccurrence(existing, occ, todayKey)) {
954
+ byDoc.set(key, occ);
955
+ }
956
+ }
957
+ return Array.from(byDoc.values()).sort(
958
+ (a, b) => a.occurrenceDate.localeCompare(b.occurrenceDate)
959
+ );
960
+ }, [occurrences, todayKey]);
961
+ const recurrencePresets = useMemo(
962
+ () => settings?.recurrencePresets && settings.recurrencePresets.length > 0 ? settings.recurrencePresets : DEFAULT_RECURRENCE_PRESETS,
963
+ [settings?.recurrencePresets]
964
+ );
965
+ const rsvpStats = useMemo(
966
+ () => computeDashboardRsvpStats(uniqueEvents, registrations),
967
+ [registrations, uniqueEvents]
968
+ );
969
+ const handleQuickCreateSubmit = useCallback(async () => {
970
+ if (!quickTitle.trim() || !quickStart) return;
971
+ try {
972
+ const startIso = quickStart.includes("T") ? new Date(quickStart).toISOString() : `${quickStart}T10:00:00.000Z`;
973
+ const endIso = new Date(
974
+ new Date(startIso).getTime() + 36e5
975
+ ).toISOString();
976
+ await post(`/${PLUGIN_ID}/calendar/quick-create`, {
977
+ title: quickTitle.trim(),
978
+ startDate: startIso,
979
+ endDate: endIso,
980
+ location: quickLocation.trim() || void 0,
981
+ category: quickCategory,
982
+ rrule: quickRRule || void 0,
983
+ rsvpEnabled: quickRsvpEnabled,
984
+ capacity: quickRsvpEnabled ? quickCapacity : void 0
985
+ });
986
+ if (!isMountedRef.current) return;
987
+ setShowQuickCreate(false);
988
+ setQuickTitle("");
989
+ setQuickLocation("");
990
+ setQuickRRule("");
991
+ setBannerMessage(t("calendar.created", "Event created and published."));
992
+ await Promise.all([fetchCalendarData(), fetchRegistrations()]);
993
+ } catch {
994
+ if (isMountedRef.current) {
995
+ setBannerMessage(t("calendar.error.create", "Could not create event."));
996
+ }
997
+ }
998
+ }, [
999
+ quickTitle,
1000
+ quickStart,
1001
+ quickLocation,
1002
+ quickCategory,
1003
+ quickRRule,
1004
+ quickRsvpEnabled,
1005
+ quickCapacity,
1006
+ post,
1007
+ t,
1008
+ fetchCalendarData,
1009
+ fetchRegistrations
1010
+ ]);
1011
+ const handleCheckIn = useCallback(
1012
+ async (regDocId) => {
1013
+ try {
1014
+ await post(
1015
+ `/${PLUGIN_ID}/registrations/${encodeURIComponent(regDocId)}/check-in`,
1016
+ {}
1017
+ );
1018
+ if (!isMountedRef.current) return;
1019
+ setBannerMessage(
1020
+ t("attendees.checkedInSuccess", "Attendee checked in.")
1021
+ );
1022
+ await Promise.all([fetchCalendarData(), fetchRegistrations()]);
1023
+ } catch {
1024
+ if (isMountedRef.current) {
1025
+ setBannerMessage(t("attendees.error.checkIn", "Check-in failed."));
1026
+ }
1027
+ }
1028
+ },
1029
+ [post, t, fetchCalendarData, fetchRegistrations]
1030
+ );
1031
+ const handleStatusChange = useCallback(
1032
+ async (regDocId, status) => {
1033
+ try {
1034
+ const res = await post(
1035
+ `/${PLUGIN_ID}/registrations/${encodeURIComponent(regDocId)}/status`,
1036
+ { status }
1037
+ );
1038
+ if (!isMountedRef.current) return;
1039
+ const promotedCount = res?.data?.data?.promotedCount ?? 0;
1040
+ const baseMsg = t("attendees.statusUpdated", "Status updated.");
1041
+ const promoSuffix = t(
1042
+ "attendees.waitlistPromoted",
1043
+ "attendee(s) automatically promoted from waitlist!"
1044
+ );
1045
+ setBannerMessage(
1046
+ promotedCount > 0 ? `${baseMsg} ${promotedCount} ${promoSuffix}` : baseMsg
1047
+ );
1048
+ await Promise.all([fetchCalendarData(), fetchRegistrations()]);
1049
+ } catch {
1050
+ if (isMountedRef.current) {
1051
+ setBannerMessage(
1052
+ t("attendees.error.status", "Could not update status.")
1053
+ );
1054
+ }
1055
+ }
1056
+ },
1057
+ [post, t, fetchCalendarData, fetchRegistrations]
1058
+ );
1059
+ return /* @__PURE__ */ jsx(Layouts.Root, { children: /* @__PURE__ */ jsx(Box, { padding: 6, background: "neutral100", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 5, children: [
1060
+ /* @__PURE__ */ jsxs(
1061
+ Flex,
1062
+ {
1063
+ justifyContent: "space-between",
1064
+ alignItems: "center",
1065
+ wrap: "wrap",
1066
+ gap: 3,
1067
+ children: [
1068
+ /* @__PURE__ */ jsxs(Box, { children: [
1069
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", fontWeight: "bold", children: t("calendar.title", "Events, RSVPs & Check-In") }),
1070
+ /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", children: t(
1071
+ "calendar.subtitle",
1072
+ "Manage event registrations, waitlist auto-promotion, and attendee check-ins. Schedule views are unified in Calendar."
1073
+ ) }) })
1074
+ ] }),
1075
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, wrap: "wrap", children: [
1076
+ /* @__PURE__ */ jsx(
1077
+ Button,
1078
+ {
1079
+ variant: "secondary",
1080
+ size: "S",
1081
+ onClick: () => navigate("/plugins/calendar?source=events"),
1082
+ children: t("calendar.openUnifiedCalendar", "Open in Calendar")
1083
+ }
1084
+ ),
1085
+ /* @__PURE__ */ jsxs(
1086
+ Button,
1087
+ {
1088
+ variant: "default",
1089
+ size: "S",
1090
+ onClick: () => {
1091
+ setQuickStart(todayKey);
1092
+ setShowQuickCreate(true);
1093
+ },
1094
+ children: [
1095
+ "+ ",
1096
+ t("calendar.quickCreate", "New Event")
1097
+ ]
1098
+ }
1099
+ ),
1100
+ /* @__PURE__ */ jsx(
1101
+ Button,
1102
+ {
1103
+ variant: "secondary",
1104
+ size: "S",
1105
+ tag: "a",
1106
+ href: "/api/events/feed.ics",
1107
+ target: "_blank",
1108
+ rel: "noopener noreferrer",
1109
+ children: t("calendar.downloadIcsFeed", "iCal Feed (.ics)")
1110
+ }
1111
+ ),
1112
+ /* @__PURE__ */ jsx(
1113
+ Button,
1114
+ {
1115
+ variant: "tertiary",
1116
+ size: "S",
1117
+ onClick: () => navigate(`/settings/${PLUGIN_ID}`),
1118
+ children: t("calendar.settings", "Settings")
1119
+ }
1120
+ )
1121
+ ] })
1122
+ ]
1123
+ }
1124
+ ),
1125
+ /* @__PURE__ */ jsx(EventsKpiCards, { stats: rsvpStats, t }),
1126
+ bannerMessage && /* @__PURE__ */ jsx(
1127
+ Box,
1128
+ {
1129
+ padding: 3,
1130
+ background: "primary100",
1131
+ hasRadius: true,
1132
+ style: { border: "1px solid #0284C7" },
1133
+ children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
1134
+ /* @__PURE__ */ jsx(
1135
+ Typography,
1136
+ {
1137
+ variant: "omega",
1138
+ fontWeight: "bold",
1139
+ textColor: "primary700",
1140
+ children: bannerMessage
1141
+ }
1142
+ ),
1143
+ /* @__PURE__ */ jsx(
1144
+ Button,
1145
+ {
1146
+ variant: "tertiary",
1147
+ size: "S",
1148
+ "aria-label": t("common.close", "Close"),
1149
+ onClick: () => setBannerMessage(null),
1150
+ children: "×"
1151
+ }
1152
+ )
1153
+ ] })
1154
+ }
1155
+ ),
1156
+ showQuickCreate && /* @__PURE__ */ jsx(
1157
+ QuickCreateEventPanel,
1158
+ {
1159
+ t,
1160
+ quickTitle,
1161
+ onQuickTitleChange: setQuickTitle,
1162
+ quickStart,
1163
+ onQuickStartChange: setQuickStart,
1164
+ quickLocation,
1165
+ onQuickLocationChange: setQuickLocation,
1166
+ quickRRule,
1167
+ onQuickRRuleChange: setQuickRRule,
1168
+ quickRsvpEnabled,
1169
+ onQuickRsvpEnabledChange: setQuickRsvpEnabled,
1170
+ quickCapacity,
1171
+ onQuickCapacityChange: setQuickCapacity,
1172
+ recurrencePresets,
1173
+ onCancel: () => setShowQuickCreate(false),
1174
+ onSubmit: handleQuickCreateSubmit
1175
+ }
1176
+ ),
1177
+ /* @__PURE__ */ jsx(
1178
+ EventsDirectoryPanel,
1179
+ {
1180
+ t,
1181
+ events: uniqueEvents,
1182
+ selectedEventDocId: rsvpEventFilter,
1183
+ onSelectEventDocId: setRsvpEventFilter,
1184
+ onEditInContentManager: (uid, docId, locale) => {
1185
+ const query = locale ? `?plugins[i18n][locale]=${encodeURIComponent(locale)}` : "";
1186
+ navigate(
1187
+ `/content-manager/collection-types/${uid}/${docId}${query}`
1188
+ );
1189
+ },
1190
+ onOpenInCalendar: (docId) => navigate(
1191
+ `/plugins/calendar?source=events${docId ? `&documentId=${encodeURIComponent(docId)}` : ""}`
1192
+ )
1193
+ }
1194
+ ),
1195
+ /* @__PURE__ */ jsx(
1196
+ AttendeesManagerPanel,
1197
+ {
1198
+ t,
1199
+ registrations,
1200
+ rsvpSearch,
1201
+ onRsvpSearchChange: setRsvpSearch,
1202
+ rsvpStatusFilter,
1203
+ onRsvpStatusFilterChange: setRsvpStatusFilter,
1204
+ rsvpEventFilter,
1205
+ onClearRsvpEventFilter: () => setRsvpEventFilter(""),
1206
+ onCheckIn: handleCheckIn,
1207
+ onStatusChange: handleStatusChange
1208
+ }
1209
+ )
1210
+ ] }) }) });
1211
+ };
1212
+ export {
1213
+ CalendarPage,
1214
+ CalendarPage as default
1215
+ };