@vicacha-devs/strapi-plugin-calendar 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.
- package/LICENSE.md +21 -0
- package/README.md +163 -2
- package/dist/admin/CalendarPage-CVqowKst.cjs +1015 -0
- package/dist/admin/CalendarPage-Dq6YfSXF.js +1015 -0
- package/dist/admin/SettingsPage-7SecVQEi.js +2088 -0
- package/dist/admin/SettingsPage-D0aCDb9w.cjs +2088 -0
- package/dist/admin/ar-O6QL_nJZ.js +331 -0
- package/dist/admin/ar-yKsEpX_y.cjs +331 -0
- package/dist/admin/de-BpDxjctz.js +331 -0
- package/dist/admin/de-ryVtwWOz.cjs +331 -0
- package/dist/admin/en-BHYSUqXv.cjs +331 -0
- package/dist/admin/en-BoRq-9QD.js +331 -0
- package/dist/admin/es-BZ7WX8Ev.js +331 -0
- package/dist/admin/es-T5v93hXW.cjs +331 -0
- package/dist/admin/fr-BnDr9DC8.js +331 -0
- package/dist/admin/fr-DTdTwx7e.cjs +331 -0
- package/dist/admin/index-DO0QbnXX.cjs +3223 -0
- package/dist/admin/index-USqxqyAm.js +3216 -0
- package/dist/admin/index.cjs +9 -0
- package/dist/admin/index.js +9 -0
- package/dist/admin/index.mjs +9 -0
- package/dist/admin/it-BkJNzy27.cjs +331 -0
- package/dist/admin/it-DkdUInap.js +331 -0
- package/dist/admin/nl-C2NFORLS.js +331 -0
- package/dist/admin/nl-CjnlbeUM.cjs +331 -0
- package/dist/admin/pl-BJArPgCQ.cjs +331 -0
- package/dist/admin/pl-mv5eXA8W.js +331 -0
- package/dist/admin/pt-CSYqSGnz.cjs +331 -0
- package/dist/admin/pt-WpvZGj3b.js +331 -0
- package/dist/admin/src/components/CalendarSidePanel.d.ts +13 -0
- package/dist/admin/src/components/Initializer.d.ts +6 -0
- package/dist/admin/src/components/PluginIcon.d.ts +3 -0
- package/dist/admin/src/components/RRuleInput.d.ts +20 -0
- package/dist/admin/src/components/calendar/CalendarToolbar.d.ts +32 -0
- package/dist/admin/src/components/calendar/FullCalendarCanvas.d.ts +21 -0
- package/dist/admin/src/components/calendar/FullCalendarDayCell.d.ts +11 -0
- package/dist/admin/src/components/calendar/OccurrenceDetailDrawer.d.ts +15 -0
- package/dist/admin/src/components/calendar/QuickCreatePanel.d.ts +29 -0
- package/dist/admin/src/components/calendar/QuickCreateSubcomponents.d.ts +40 -0
- package/dist/admin/src/components/calendar/calendarPageHelpers.d.ts +52 -0
- package/dist/admin/src/components/calendar/calendarThemeStyles.d.ts +2 -0
- package/dist/admin/src/components/settings/CalendarThemeSection.d.ts +9 -0
- package/dist/admin/src/components/settings/CalendarViewsSection.d.ts +10 -0
- package/dist/admin/src/components/settings/CollectionSourcesSection.d.ts +12 -0
- package/dist/admin/src/components/settings/CompanionSuiteSettingsSection.d.ts +29 -0
- package/dist/admin/src/components/settings/SecondaryCalendarsSection.d.ts +11 -0
- package/dist/admin/src/fields/rruleField.d.ts +19 -0
- package/dist/admin/src/index.d.ts +18 -0
- package/dist/admin/src/pages/CalendarPage.d.ts +3 -0
- package/dist/admin/src/pages/SettingsPage.d.ts +3 -0
- package/dist/admin/src/pages/calendarPageSupport.d.ts +8 -0
- package/dist/admin/tr-BhZKvFoK.js +331 -0
- package/dist/admin/tr-zUVxuw9X.cjs +331 -0
- package/dist/package.json +3 -0
- package/dist/server/index.cjs +2585 -0
- package/dist/server/index.js +2585 -0
- package/dist/server/index.mjs +2585 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +7 -0
- package/dist/server/src/controllers/admin.d.ts +15 -0
- package/dist/server/src/controllers/content-api.d.ts +10 -0
- package/dist/server/src/controllers/index.d.ts +21 -0
- package/dist/server/src/destroy.d.ts +5 -0
- package/dist/server/src/index.d.ts +85 -0
- package/dist/server/src/register.d.ts +5 -0
- package/dist/server/src/routes/admin.d.ts +17 -0
- package/dist/server/src/routes/content-api.d.ts +10 -0
- package/dist/server/src/routes/index.d.ts +28 -0
- package/dist/server/src/services/builtinSources.d.ts +3 -0
- package/dist/server/src/services/ical.d.ts +6 -0
- package/dist/server/src/services/index.d.ts +23 -0
- package/dist/server/src/services/occurrence.d.ts +6 -0
- package/dist/server/src/services/occurrenceDates.d.ts +7 -0
- package/dist/server/src/services/quickCreateHelpers.d.ts +20 -0
- package/dist/server/src/services/registry.d.ts +19 -0
- package/dist/server/src/services/settings.d.ts +6 -0
- package/dist/server/src/types.d.ts +98 -0
- package/dist/shared/calendarSystems.d.ts +41 -0
- package/dist/shared/icalGenerator.d.ts +18 -0
- package/dist/shared/index.cjs +1612 -0
- package/dist/shared/index.d.ts +6 -0
- package/dist/shared/index.js +1612 -0
- package/dist/shared/index.mjs +1612 -0
- package/dist/shared/localization.d.ts +14 -0
- package/dist/shared/pluginId.d.ts +2 -0
- package/dist/shared/rruleEngine.d.ts +22 -0
- package/dist/shared/themePalettes.d.ts +27 -0
- package/dist/shared/themePresets.d.ts +32 -0
- package/dist/shared/types.d.ts +311 -0
- package/package.json +120 -3
|
@@ -0,0 +1,2585 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
3
|
+
const node_crypto = require("node:crypto");
|
|
4
|
+
const PLUGIN_ID = "calendar";
|
|
5
|
+
function getCalendarService(strapi, pluginId, serviceName) {
|
|
6
|
+
return strapi.plugin(pluginId).service(serviceName);
|
|
7
|
+
}
|
|
8
|
+
function formatErrorMessage(err, fallback) {
|
|
9
|
+
if (err instanceof Error && err.message) {
|
|
10
|
+
return err.message;
|
|
11
|
+
}
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
const bootstrap = async ({ strapi }) => {
|
|
15
|
+
const actions = [
|
|
16
|
+
{
|
|
17
|
+
section: "plugins",
|
|
18
|
+
displayName: "Read Calendar & Occurrences",
|
|
19
|
+
uid: "read",
|
|
20
|
+
pluginName: PLUGIN_ID
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
section: "plugins",
|
|
24
|
+
displayName: "Create & Reschedule Calendar Entries",
|
|
25
|
+
uid: "update",
|
|
26
|
+
pluginName: PLUGIN_ID
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
section: "plugins",
|
|
30
|
+
displayName: "Manage Calendar Settings",
|
|
31
|
+
uid: "settings.update",
|
|
32
|
+
pluginName: PLUGIN_ID
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
const actionProvider = strapi.admin?.services?.permission?.actionProvider;
|
|
36
|
+
if (actionProvider && typeof actionProvider.registerMany === "function") {
|
|
37
|
+
await actionProvider.registerMany(actions);
|
|
38
|
+
}
|
|
39
|
+
const settingsService = getCalendarService(strapi, PLUGIN_ID, "settings");
|
|
40
|
+
if (settingsService && typeof settingsService.ensureInitialized === "function") {
|
|
41
|
+
await settingsService.ensureInitialized();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const DEFAULT_CALENDAR_SETTINGS = {
|
|
45
|
+
theme: "breeze",
|
|
46
|
+
colorScheme: "auto",
|
|
47
|
+
themePalette: "default",
|
|
48
|
+
secondaryCalendarLanguage: "local",
|
|
49
|
+
defaultTimezone: "UTC",
|
|
50
|
+
defaultLocale: "auto",
|
|
51
|
+
direction: "auto",
|
|
52
|
+
defaultView: "dayGridMonth",
|
|
53
|
+
firstDayOfWeek: 1,
|
|
54
|
+
weekNumbers: false,
|
|
55
|
+
weekNumberCalculation: "ISO",
|
|
56
|
+
timeFormat24h: true,
|
|
57
|
+
startHour: "07:00:00",
|
|
58
|
+
endHour: "21:00:00",
|
|
59
|
+
slotDuration: "00:30:00",
|
|
60
|
+
primaryColor: "#0284C7",
|
|
61
|
+
showWeekends: true,
|
|
62
|
+
showTodayButton: true,
|
|
63
|
+
showCreateButton: true,
|
|
64
|
+
monthView: true,
|
|
65
|
+
weekView: true,
|
|
66
|
+
dayView: true,
|
|
67
|
+
agendaView: true,
|
|
68
|
+
yearView: true,
|
|
69
|
+
businessHours: [
|
|
70
|
+
{
|
|
71
|
+
daysOfWeek: [1, 2, 3, 4, 5],
|
|
72
|
+
startTime: "08:00",
|
|
73
|
+
endTime: "18:00"
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
feedTitle: "Organization Calendar",
|
|
77
|
+
sources: [],
|
|
78
|
+
secondaryCalendars: [],
|
|
79
|
+
recurrencePresets: [
|
|
80
|
+
{ id: "daily", label: "Daily", rrule: "FREQ=DAILY;INTERVAL=1" },
|
|
81
|
+
{
|
|
82
|
+
id: "weekly-mon",
|
|
83
|
+
label: "Weekly on Mondays",
|
|
84
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "weekly-tue",
|
|
88
|
+
label: "Weekly on Tuesdays",
|
|
89
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=TU"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "weekly-wed",
|
|
93
|
+
label: "Weekly on Wednesdays",
|
|
94
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=WE"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "weekly-thu",
|
|
98
|
+
label: "Weekly on Thursdays",
|
|
99
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=TH"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "weekly-fri",
|
|
103
|
+
label: "Weekly on Fridays",
|
|
104
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=FR"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "weekly-sat",
|
|
108
|
+
label: "Weekly on Saturdays",
|
|
109
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=SA"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "weekly-sun",
|
|
113
|
+
label: "Weekly on Sundays",
|
|
114
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=SU"
|
|
115
|
+
},
|
|
116
|
+
{ id: "monthly", label: "Monthly", rrule: "FREQ=MONTHLY;INTERVAL=1" },
|
|
117
|
+
{ id: "yearly", label: "Yearly", rrule: "FREQ=YEARLY;INTERVAL=1" }
|
|
118
|
+
]
|
|
119
|
+
};
|
|
120
|
+
const config = {
|
|
121
|
+
default: DEFAULT_CALENDAR_SETTINGS,
|
|
122
|
+
validator(config2) {
|
|
123
|
+
if (config2.firstDayOfWeek !== void 0 && (config2.firstDayOfWeek < 0 || config2.firstDayOfWeek > 6)) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"calendar plugin config: firstDayOfWeek must be between 0 and 6"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
function readOptString(val) {
|
|
131
|
+
return typeof val === "string" && val ? val : void 0;
|
|
132
|
+
}
|
|
133
|
+
function parseQuickCreateBody(body) {
|
|
134
|
+
const statusRaw = body.bookingStatus;
|
|
135
|
+
const bookingStatus = statusRaw === "pending" || statusRaw === "confirmed" ? statusRaw : void 0;
|
|
136
|
+
return {
|
|
137
|
+
sourceId: readOptString(body.sourceId),
|
|
138
|
+
title: String(body.title ?? ""),
|
|
139
|
+
startDate: String(body.startDate ?? ""),
|
|
140
|
+
endDate: readOptString(body.endDate),
|
|
141
|
+
location: readOptString(body.location),
|
|
142
|
+
category: readOptString(body.category),
|
|
143
|
+
summary: readOptString(body.summary),
|
|
144
|
+
rrule: readOptString(body.rrule),
|
|
145
|
+
customerName: readOptString(body.customerName),
|
|
146
|
+
customerEmail: readOptString(body.customerEmail),
|
|
147
|
+
customerPhone: readOptString(body.customerPhone),
|
|
148
|
+
partySize: body.partySize !== void 0 ? Number(body.partySize) : void 0,
|
|
149
|
+
bookingStatus
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const admin$1 = ({ strapi }) => {
|
|
153
|
+
const getSettingsService = () => getCalendarService(strapi, PLUGIN_ID, "settings");
|
|
154
|
+
const getOccurrenceService = () => getCalendarService(strapi, PLUGIN_ID, "occurrence");
|
|
155
|
+
return {
|
|
156
|
+
async getCalendar(ctx) {
|
|
157
|
+
try {
|
|
158
|
+
const query = ctx.query ?? {};
|
|
159
|
+
const occurrences = await getOccurrenceService().getOccurrences({
|
|
160
|
+
from: readOptString(query.from),
|
|
161
|
+
to: readOptString(query.to),
|
|
162
|
+
locale: readOptString(query.locale),
|
|
163
|
+
category: readOptString(query.category),
|
|
164
|
+
sourceId: readOptString(query.sourceId),
|
|
165
|
+
includeDrafts: query.includeDrafts !== "false"
|
|
166
|
+
});
|
|
167
|
+
const settings2 = await getSettingsService().getSettings();
|
|
168
|
+
ctx.body = {
|
|
169
|
+
data: {
|
|
170
|
+
occurrences,
|
|
171
|
+
settings: settings2
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
} catch (err) {
|
|
175
|
+
ctx.status = 400;
|
|
176
|
+
ctx.body = {
|
|
177
|
+
error: {
|
|
178
|
+
message: formatErrorMessage(err, "Failed to load calendar data.")
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
async reschedule(ctx) {
|
|
184
|
+
try {
|
|
185
|
+
const body = ctx.request?.body ?? {};
|
|
186
|
+
const updated = await getOccurrenceService().rescheduleOccurrence({
|
|
187
|
+
sourceId: readOptString(body.sourceId),
|
|
188
|
+
uid: readOptString(body.uid),
|
|
189
|
+
documentId: String(body.documentId ?? ""),
|
|
190
|
+
locale: readOptString(body.locale),
|
|
191
|
+
occurrenceDate: String(body.occurrenceDate ?? ""),
|
|
192
|
+
newStartDate: String(body.newStartDate ?? ""),
|
|
193
|
+
newEndDate: readOptString(body.newEndDate),
|
|
194
|
+
applyToSeries: Boolean(body.applyToSeries),
|
|
195
|
+
cancelOccurrence: Boolean(body.cancelOccurrence)
|
|
196
|
+
});
|
|
197
|
+
ctx.body = { data: updated };
|
|
198
|
+
} catch (err) {
|
|
199
|
+
ctx.status = 400;
|
|
200
|
+
ctx.body = {
|
|
201
|
+
error: {
|
|
202
|
+
message: formatErrorMessage(
|
|
203
|
+
err,
|
|
204
|
+
"Failed to reschedule occurrence."
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
async quickCreate(ctx) {
|
|
211
|
+
try {
|
|
212
|
+
const body = ctx.request?.body ?? {};
|
|
213
|
+
const created = await getOccurrenceService().quickCreateEntry(
|
|
214
|
+
parseQuickCreateBody(body)
|
|
215
|
+
);
|
|
216
|
+
ctx.status = 201;
|
|
217
|
+
ctx.body = { data: created };
|
|
218
|
+
} catch (err) {
|
|
219
|
+
ctx.status = 400;
|
|
220
|
+
ctx.body = {
|
|
221
|
+
error: {
|
|
222
|
+
message: formatErrorMessage(
|
|
223
|
+
err,
|
|
224
|
+
"Failed to create calendar entry."
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
async getSettings(ctx) {
|
|
231
|
+
try {
|
|
232
|
+
const settings2 = await getSettingsService().getSettings();
|
|
233
|
+
ctx.body = { data: settings2 };
|
|
234
|
+
} catch (err) {
|
|
235
|
+
ctx.status = 400;
|
|
236
|
+
ctx.body = {
|
|
237
|
+
error: {
|
|
238
|
+
message: formatErrorMessage(err, "Failed to fetch settings.")
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
async updateSettings(ctx) {
|
|
244
|
+
try {
|
|
245
|
+
const body = ctx.request?.body ?? {};
|
|
246
|
+
const updated = await getSettingsService().updateSettings(body);
|
|
247
|
+
ctx.body = { data: updated };
|
|
248
|
+
} catch (err) {
|
|
249
|
+
ctx.status = 400;
|
|
250
|
+
ctx.body = {
|
|
251
|
+
error: {
|
|
252
|
+
message: formatErrorMessage(err, "Failed to update settings.")
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
async clearSettings(ctx) {
|
|
258
|
+
try {
|
|
259
|
+
const reset = await getSettingsService().clearSettings();
|
|
260
|
+
ctx.body = { data: reset };
|
|
261
|
+
} catch (err) {
|
|
262
|
+
ctx.status = 400;
|
|
263
|
+
ctx.body = {
|
|
264
|
+
error: {
|
|
265
|
+
message: formatErrorMessage(err, "Failed to reset settings.")
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
async getContentTypes(ctx) {
|
|
271
|
+
try {
|
|
272
|
+
const contentTypes = getSettingsService().discoverContentTypes();
|
|
273
|
+
ctx.body = { data: contentTypes };
|
|
274
|
+
} catch (err) {
|
|
275
|
+
ctx.status = 400;
|
|
276
|
+
ctx.body = {
|
|
277
|
+
error: {
|
|
278
|
+
message: formatErrorMessage(
|
|
279
|
+
err,
|
|
280
|
+
"Failed to discover content types."
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
const contentApi$1 = ({ strapi }) => {
|
|
289
|
+
const getOccurrenceService = () => getCalendarService(strapi, PLUGIN_ID, "occurrence");
|
|
290
|
+
const getIcalService = () => getCalendarService(strapi, PLUGIN_ID, "ical");
|
|
291
|
+
return {
|
|
292
|
+
async getOccurrences(ctx) {
|
|
293
|
+
try {
|
|
294
|
+
const query = ctx.query ?? {};
|
|
295
|
+
const occurrences = await getOccurrenceService().getOccurrences({
|
|
296
|
+
from: typeof query.from === "string" ? query.from : void 0,
|
|
297
|
+
to: typeof query.to === "string" ? query.to : void 0,
|
|
298
|
+
locale: typeof query.locale === "string" ? query.locale : void 0,
|
|
299
|
+
category: typeof query.category === "string" ? query.category : void 0,
|
|
300
|
+
sourceId: typeof query.sourceId === "string" ? query.sourceId : void 0,
|
|
301
|
+
documentId: typeof query.documentId === "string" ? query.documentId : void 0,
|
|
302
|
+
includeDrafts: false,
|
|
303
|
+
publicOnly: true
|
|
304
|
+
});
|
|
305
|
+
ctx.body = {
|
|
306
|
+
data: occurrences,
|
|
307
|
+
meta: {
|
|
308
|
+
total: occurrences.length
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
} catch (err) {
|
|
312
|
+
ctx.status = 400;
|
|
313
|
+
ctx.body = {
|
|
314
|
+
error: {
|
|
315
|
+
message: formatErrorMessage(err, "Could not fetch occurrences.")
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
async downloadFeedIcs(ctx) {
|
|
321
|
+
try {
|
|
322
|
+
const query = ctx.query ?? {};
|
|
323
|
+
const ics = await getIcalService().buildFeedIcs({
|
|
324
|
+
from: typeof query.from === "string" ? query.from : void 0,
|
|
325
|
+
to: typeof query.to === "string" ? query.to : void 0,
|
|
326
|
+
locale: typeof query.locale === "string" ? query.locale : void 0,
|
|
327
|
+
category: typeof query.category === "string" ? query.category : void 0,
|
|
328
|
+
sourceId: typeof query.sourceId === "string" ? query.sourceId : void 0,
|
|
329
|
+
publicOnly: true
|
|
330
|
+
});
|
|
331
|
+
ctx.set?.("Content-Type", "text/calendar; charset=utf-8");
|
|
332
|
+
ctx.set?.("Content-Disposition", 'attachment; filename="calendar.ics"');
|
|
333
|
+
ctx.set?.("X-Content-Type-Options", "nosniff");
|
|
334
|
+
ctx.body = ics;
|
|
335
|
+
} catch (err) {
|
|
336
|
+
ctx.status = 400;
|
|
337
|
+
ctx.body = {
|
|
338
|
+
error: {
|
|
339
|
+
message: formatErrorMessage(err, "Could not generate iCal feed.")
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
async downloadEventIcs(ctx) {
|
|
345
|
+
try {
|
|
346
|
+
const documentId = String(ctx.params?.documentId ?? "");
|
|
347
|
+
const query = ctx.query ?? {};
|
|
348
|
+
const ics = await getIcalService().buildSingleInviteIcs({
|
|
349
|
+
documentId,
|
|
350
|
+
occurrenceDate: typeof query.occurrenceDate === "string" ? query.occurrenceDate : void 0,
|
|
351
|
+
locale: typeof query.locale === "string" ? query.locale : void 0,
|
|
352
|
+
publicOnly: true
|
|
353
|
+
});
|
|
354
|
+
ctx.set?.("Content-Type", "text/calendar; charset=utf-8");
|
|
355
|
+
ctx.set?.(
|
|
356
|
+
"Content-Disposition",
|
|
357
|
+
`attachment; filename="invite-${documentId.replace(/[^a-zA-Z0-9_-]/g, "")}.ics"`
|
|
358
|
+
);
|
|
359
|
+
ctx.set?.("X-Content-Type-Options", "nosniff");
|
|
360
|
+
ctx.body = ics;
|
|
361
|
+
} catch (err) {
|
|
362
|
+
ctx.status = 400;
|
|
363
|
+
ctx.body = {
|
|
364
|
+
error: {
|
|
365
|
+
message: formatErrorMessage(err, "Could not generate invite .ics.")
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
const controllers = {
|
|
373
|
+
admin: admin$1,
|
|
374
|
+
"content-api": contentApi$1
|
|
375
|
+
};
|
|
376
|
+
const destroy = ({ strapi: _strapi }) => {
|
|
377
|
+
};
|
|
378
|
+
const register = ({ strapi }) => {
|
|
379
|
+
strapi.customFields.register({
|
|
380
|
+
name: "rrule",
|
|
381
|
+
plugin: PLUGIN_ID,
|
|
382
|
+
type: "json",
|
|
383
|
+
inputSize: {
|
|
384
|
+
default: 12,
|
|
385
|
+
isResizable: true
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
const admin = {
|
|
390
|
+
type: "admin",
|
|
391
|
+
routes: [
|
|
392
|
+
{
|
|
393
|
+
method: "GET",
|
|
394
|
+
path: "/calendar",
|
|
395
|
+
handler: "admin.getCalendar",
|
|
396
|
+
config: {
|
|
397
|
+
policies: [
|
|
398
|
+
"admin::isAuthenticatedAdmin",
|
|
399
|
+
{
|
|
400
|
+
name: "admin::hasPermissions",
|
|
401
|
+
config: { actions: [`plugin::${PLUGIN_ID}.read`] }
|
|
402
|
+
}
|
|
403
|
+
]
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
method: "POST",
|
|
408
|
+
path: "/calendar/reschedule",
|
|
409
|
+
handler: "admin.reschedule",
|
|
410
|
+
config: {
|
|
411
|
+
policies: [
|
|
412
|
+
"admin::isAuthenticatedAdmin",
|
|
413
|
+
{
|
|
414
|
+
name: "admin::hasPermissions",
|
|
415
|
+
config: { actions: [`plugin::${PLUGIN_ID}.update`] }
|
|
416
|
+
}
|
|
417
|
+
]
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
method: "POST",
|
|
422
|
+
path: "/calendar/quick-create",
|
|
423
|
+
handler: "admin.quickCreate",
|
|
424
|
+
config: {
|
|
425
|
+
policies: [
|
|
426
|
+
"admin::isAuthenticatedAdmin",
|
|
427
|
+
{
|
|
428
|
+
name: "admin::hasPermissions",
|
|
429
|
+
config: { actions: [`plugin::${PLUGIN_ID}.update`] }
|
|
430
|
+
}
|
|
431
|
+
]
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
method: "GET",
|
|
436
|
+
path: "/content-types",
|
|
437
|
+
handler: "admin.getContentTypes",
|
|
438
|
+
config: {
|
|
439
|
+
policies: [
|
|
440
|
+
"admin::isAuthenticatedAdmin",
|
|
441
|
+
{
|
|
442
|
+
name: "admin::hasPermissions",
|
|
443
|
+
config: { actions: [`plugin::${PLUGIN_ID}.read`] }
|
|
444
|
+
}
|
|
445
|
+
]
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
method: "GET",
|
|
450
|
+
path: "/settings",
|
|
451
|
+
handler: "admin.getSettings",
|
|
452
|
+
config: {
|
|
453
|
+
policies: [
|
|
454
|
+
"admin::isAuthenticatedAdmin",
|
|
455
|
+
{
|
|
456
|
+
name: "admin::hasPermissions",
|
|
457
|
+
config: { actions: [`plugin::${PLUGIN_ID}.read`] }
|
|
458
|
+
}
|
|
459
|
+
]
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
method: "PUT",
|
|
464
|
+
path: "/settings",
|
|
465
|
+
handler: "admin.updateSettings",
|
|
466
|
+
config: {
|
|
467
|
+
policies: [
|
|
468
|
+
"admin::isAuthenticatedAdmin",
|
|
469
|
+
{
|
|
470
|
+
name: "admin::hasPermissions",
|
|
471
|
+
config: { actions: [`plugin::${PLUGIN_ID}.settings.update`] }
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
method: "POST",
|
|
478
|
+
path: "/clear-settings",
|
|
479
|
+
handler: "admin.clearSettings",
|
|
480
|
+
config: {
|
|
481
|
+
policies: [
|
|
482
|
+
"admin::isAuthenticatedAdmin",
|
|
483
|
+
{
|
|
484
|
+
name: "admin::hasPermissions",
|
|
485
|
+
config: { actions: [`plugin::${PLUGIN_ID}.settings.update`] }
|
|
486
|
+
}
|
|
487
|
+
]
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
]
|
|
491
|
+
};
|
|
492
|
+
const contentApi = {
|
|
493
|
+
type: "content-api",
|
|
494
|
+
routes: [
|
|
495
|
+
{
|
|
496
|
+
method: "GET",
|
|
497
|
+
path: "/occurrences",
|
|
498
|
+
handler: "content-api.getOccurrences",
|
|
499
|
+
config: {}
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
method: "GET",
|
|
503
|
+
path: "/feed.ics",
|
|
504
|
+
handler: "content-api.downloadFeedIcs",
|
|
505
|
+
config: {}
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
method: "GET",
|
|
509
|
+
path: "/:documentId/invite.ics",
|
|
510
|
+
handler: "content-api.downloadEventIcs",
|
|
511
|
+
config: {}
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
};
|
|
515
|
+
const routes = {
|
|
516
|
+
admin,
|
|
517
|
+
"content-api": contentApi
|
|
518
|
+
};
|
|
519
|
+
function escapeIcsText(value) {
|
|
520
|
+
if (!value) return "";
|
|
521
|
+
return String(value).replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\r?\n/g, "\\n");
|
|
522
|
+
}
|
|
523
|
+
const utf8Encoder = new TextEncoder();
|
|
524
|
+
function foldIcsLine(line) {
|
|
525
|
+
if (utf8Encoder.encode(line).byteLength <= 75) {
|
|
526
|
+
return line;
|
|
527
|
+
}
|
|
528
|
+
const chunks = [];
|
|
529
|
+
let current = "";
|
|
530
|
+
let currentBytes = 0;
|
|
531
|
+
for (const codePoint of line) {
|
|
532
|
+
const cpBytes = utf8Encoder.encode(codePoint).byteLength;
|
|
533
|
+
if (currentBytes + cpBytes > 75) {
|
|
534
|
+
chunks.push(current);
|
|
535
|
+
current = ` ${codePoint}`;
|
|
536
|
+
currentBytes = 1 + cpBytes;
|
|
537
|
+
} else {
|
|
538
|
+
current += codePoint;
|
|
539
|
+
currentBytes += cpBytes;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (current) {
|
|
543
|
+
chunks.push(current);
|
|
544
|
+
}
|
|
545
|
+
return chunks.join("\r\n");
|
|
546
|
+
}
|
|
547
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
548
|
+
function toIcsTimestamp(iso, allDay = false) {
|
|
549
|
+
const date = new Date(iso);
|
|
550
|
+
if (Number.isNaN(date.getTime())) {
|
|
551
|
+
return "19700101T000000Z";
|
|
552
|
+
}
|
|
553
|
+
const y = date.getUTCFullYear();
|
|
554
|
+
const m = pad(date.getUTCMonth() + 1);
|
|
555
|
+
const d = pad(date.getUTCDate());
|
|
556
|
+
if (allDay) {
|
|
557
|
+
return `${y}${m}${d}`;
|
|
558
|
+
}
|
|
559
|
+
const hh = pad(date.getUTCHours());
|
|
560
|
+
const mm = pad(date.getUTCMinutes());
|
|
561
|
+
const ss = pad(date.getUTCSeconds());
|
|
562
|
+
return `${y}${m}${d}T${hh}${mm}${ss}Z`;
|
|
563
|
+
}
|
|
564
|
+
function toExclusiveAllDayEndStamp(startIso, endIso) {
|
|
565
|
+
const startDate = new Date(startIso);
|
|
566
|
+
const endDate = new Date(endIso);
|
|
567
|
+
if (Number.isNaN(startDate.getTime())) {
|
|
568
|
+
return "19700102";
|
|
569
|
+
}
|
|
570
|
+
const startMidnight = Date.UTC(
|
|
571
|
+
startDate.getUTCFullYear(),
|
|
572
|
+
startDate.getUTCMonth(),
|
|
573
|
+
startDate.getUTCDate()
|
|
574
|
+
);
|
|
575
|
+
const endMidnight = Number.isNaN(endDate.getTime()) ? startMidnight : Date.UTC(
|
|
576
|
+
endDate.getUTCFullYear(),
|
|
577
|
+
endDate.getUTCMonth(),
|
|
578
|
+
endDate.getUTCDate()
|
|
579
|
+
);
|
|
580
|
+
const isAlreadyExclusiveMidnight = !Number.isNaN(endDate.getTime()) && endMidnight > startMidnight && endDate.getUTCHours() === 0 && endDate.getUTCMinutes() === 0 && endDate.getUTCSeconds() === 0;
|
|
581
|
+
const exclusiveDate = new Date(
|
|
582
|
+
isAlreadyExclusiveMidnight ? endMidnight : Math.max(startMidnight, endMidnight) + 864e5
|
|
583
|
+
);
|
|
584
|
+
return toIcsTimestamp(exclusiveDate.toISOString(), true);
|
|
585
|
+
}
|
|
586
|
+
function buildCalendarLinks(params) {
|
|
587
|
+
const startStamp = toIcsTimestamp(params.startDate, params.allDay);
|
|
588
|
+
const endStamp = params.allDay ? toExclusiveAllDayEndStamp(params.startDate, params.endDate) : toIcsTimestamp(params.endDate, false);
|
|
589
|
+
const googleParams = new URLSearchParams({
|
|
590
|
+
action: "TEMPLATE",
|
|
591
|
+
text: params.title,
|
|
592
|
+
dates: `${startStamp}/${endStamp}`
|
|
593
|
+
});
|
|
594
|
+
if (params.summary) googleParams.set("details", params.summary);
|
|
595
|
+
if (params.location) googleParams.set("location", params.location);
|
|
596
|
+
const outlookParams = new URLSearchParams({
|
|
597
|
+
path: "/calendar/action/compose",
|
|
598
|
+
rru: "addevent",
|
|
599
|
+
subject: params.title,
|
|
600
|
+
startdt: params.startDate,
|
|
601
|
+
enddt: params.endDate
|
|
602
|
+
});
|
|
603
|
+
if (params.summary) outlookParams.set("body", params.summary);
|
|
604
|
+
if (params.location) outlookParams.set("location", params.location);
|
|
605
|
+
const yahooParams = new URLSearchParams({
|
|
606
|
+
v: "60",
|
|
607
|
+
title: params.title,
|
|
608
|
+
st: startStamp,
|
|
609
|
+
et: endStamp
|
|
610
|
+
});
|
|
611
|
+
if (params.summary) yahooParams.set("desc", params.summary);
|
|
612
|
+
if (params.location) yahooParams.set("in_loc", params.location);
|
|
613
|
+
const querySuffix = params.occurrenceDate ? `?occurrenceDate=${encodeURIComponent(params.occurrenceDate)}` : "";
|
|
614
|
+
return {
|
|
615
|
+
google: `https://calendar.google.com/calendar/render?${googleParams.toString()}`,
|
|
616
|
+
outlook: `https://outlook.live.com/calendar/0/deeplink/compose?${outlookParams.toString()}`,
|
|
617
|
+
yahoo: `https://calendar.yahoo.com/?${yahooParams.toString()}`,
|
|
618
|
+
icsPath: `/api/calendar/${encodeURIComponent(params.documentId)}/invite.ics${querySuffix}`
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function generateIcsCalendar(params) {
|
|
622
|
+
const nowStamp = toIcsTimestamp((/* @__PURE__ */ new Date()).toISOString());
|
|
623
|
+
const lines = [
|
|
624
|
+
"BEGIN:VCALENDAR",
|
|
625
|
+
"VERSION:2.0",
|
|
626
|
+
"PRODID:-//Vicacha Devs//Strapi Plugin Calendar//EN",
|
|
627
|
+
"CALSCALE:GREGORIAN",
|
|
628
|
+
"METHOD:PUBLISH",
|
|
629
|
+
`X-WR-CALNAME:${escapeIcsText(params.calendarTitle)}`,
|
|
630
|
+
`X-WR-TIMEZONE:${escapeIcsText(params.timezone)}`
|
|
631
|
+
];
|
|
632
|
+
for (const occ of params.occurrences) {
|
|
633
|
+
lines.push("BEGIN:VEVENT");
|
|
634
|
+
lines.push(`UID:${escapeIcsText(occ.occurrenceId)}@strapi-plugin-calendar`);
|
|
635
|
+
lines.push(`DTSTAMP:${nowStamp}`);
|
|
636
|
+
if (occ.allDay) {
|
|
637
|
+
lines.push(`DTSTART;VALUE=DATE:${toIcsTimestamp(occ.startDate, true)}`);
|
|
638
|
+
lines.push(
|
|
639
|
+
`DTEND;VALUE=DATE:${toExclusiveAllDayEndStamp(occ.startDate, occ.endDate)}`
|
|
640
|
+
);
|
|
641
|
+
} else {
|
|
642
|
+
lines.push(`DTSTART:${toIcsTimestamp(occ.startDate, false)}`);
|
|
643
|
+
lines.push(`DTEND:${toIcsTimestamp(occ.endDate, false)}`);
|
|
644
|
+
}
|
|
645
|
+
lines.push(`SUMMARY:${escapeIcsText(occ.title)}`);
|
|
646
|
+
if (occ.summary) {
|
|
647
|
+
lines.push(`DESCRIPTION:${escapeIcsText(occ.summary)}`);
|
|
648
|
+
}
|
|
649
|
+
if (occ.location) {
|
|
650
|
+
lines.push(`LOCATION:${escapeIcsText(occ.location)}`);
|
|
651
|
+
}
|
|
652
|
+
if (occ.category) {
|
|
653
|
+
lines.push(`CATEGORIES:${escapeIcsText(occ.category)}`);
|
|
654
|
+
}
|
|
655
|
+
if (occ.alternateDates) {
|
|
656
|
+
for (const alt of Object.values(occ.alternateDates)) {
|
|
657
|
+
const safeCalId = alt.calendarId.replace(/[^A-Za-z0-9-]/g, "").toUpperCase();
|
|
658
|
+
if (!safeCalId) continue;
|
|
659
|
+
lines.push(`X-ALT-DATE-${safeCalId}:${escapeIcsText(alt.formatted)}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const icsStatus = occ.status === "cancelled" ? "CANCELLED" : occ.status === "postponed" ? "TENTATIVE" : "CONFIRMED";
|
|
663
|
+
lines.push(`STATUS:${icsStatus}`);
|
|
664
|
+
lines.push("END:VEVENT");
|
|
665
|
+
}
|
|
666
|
+
lines.push("END:VCALENDAR");
|
|
667
|
+
return `${lines.map(foldIcsLine).join("\r\n")}\r
|
|
668
|
+
`;
|
|
669
|
+
}
|
|
670
|
+
const ical = ({ strapi }) => {
|
|
671
|
+
const getSettingsService = () => getCalendarService(strapi, PLUGIN_ID, "settings");
|
|
672
|
+
const getOccurrenceService = () => getCalendarService(strapi, PLUGIN_ID, "occurrence");
|
|
673
|
+
return {
|
|
674
|
+
async buildFeedIcs(params) {
|
|
675
|
+
const settings2 = await getSettingsService().getSettings();
|
|
676
|
+
const occurrences = await getOccurrenceService().getOccurrences({
|
|
677
|
+
...params,
|
|
678
|
+
includeDrafts: false
|
|
679
|
+
});
|
|
680
|
+
return generateIcsCalendar({
|
|
681
|
+
calendarTitle: settings2.feedTitle || "Calendar",
|
|
682
|
+
timezone: settings2.defaultTimezone || "UTC",
|
|
683
|
+
occurrences
|
|
684
|
+
});
|
|
685
|
+
},
|
|
686
|
+
async buildSingleInviteIcs(params) {
|
|
687
|
+
const settings2 = await getSettingsService().getSettings();
|
|
688
|
+
const refYear = params.occurrenceDate && /^\d{4}-\d{2}-\d{2}$/.test(params.occurrenceDate) ? Number.parseInt(params.occurrenceDate.slice(0, 4), 10) : (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
689
|
+
const from = new Date(Date.UTC(refYear - 1, 0, 1)).toISOString();
|
|
690
|
+
const to = new Date(
|
|
691
|
+
Date.UTC(refYear + 2, 11, 31, 23, 59, 59)
|
|
692
|
+
).toISOString();
|
|
693
|
+
let occurrences = await getOccurrenceService().getOccurrences({
|
|
694
|
+
documentId: params.documentId,
|
|
695
|
+
from,
|
|
696
|
+
to,
|
|
697
|
+
locale: params.locale,
|
|
698
|
+
includeDrafts: false,
|
|
699
|
+
publicOnly: params.publicOnly
|
|
700
|
+
});
|
|
701
|
+
if (params.occurrenceDate) {
|
|
702
|
+
occurrences = occurrences.filter(
|
|
703
|
+
(o) => o.occurrenceDate === params.occurrenceDate
|
|
704
|
+
);
|
|
705
|
+
} else if (occurrences.length > 1) {
|
|
706
|
+
occurrences = [occurrences[0]];
|
|
707
|
+
}
|
|
708
|
+
return generateIcsCalendar({
|
|
709
|
+
calendarTitle: occurrences[0]?.title || settings2.feedTitle || "Calendar Invite",
|
|
710
|
+
timezone: settings2.defaultTimezone || "UTC",
|
|
711
|
+
occurrences
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
};
|
|
716
|
+
const customFormatters = /* @__PURE__ */ new Map();
|
|
717
|
+
function registerCalendarSystemFormatter(calendarId, formatter) {
|
|
718
|
+
if (!calendarId || typeof formatter !== "function") {
|
|
719
|
+
throw new Error("Both calendarId and formatter function are required.");
|
|
720
|
+
}
|
|
721
|
+
customFormatters.set(calendarId, formatter);
|
|
722
|
+
}
|
|
723
|
+
function unregisterCalendarSystemFormatter(calendarId) {
|
|
724
|
+
customFormatters.delete(calendarId);
|
|
725
|
+
}
|
|
726
|
+
function applyDayOffset(date, dayOffset) {
|
|
727
|
+
if (!dayOffset || !Number.isFinite(dayOffset)) {
|
|
728
|
+
return date;
|
|
729
|
+
}
|
|
730
|
+
const offsetDays = Math.trunc(dayOffset);
|
|
731
|
+
return new Date(date.getTime() + offsetDays * 864e5);
|
|
732
|
+
}
|
|
733
|
+
function parseDigits(raw) {
|
|
734
|
+
const n = Number.parseInt(raw.replace(/[^0-9]/g, ""), 10);
|
|
735
|
+
return Number.isFinite(n) ? n : 0;
|
|
736
|
+
}
|
|
737
|
+
const INTL_PART_FIELD_MAP = {
|
|
738
|
+
year: "year",
|
|
739
|
+
relatedYear: "year",
|
|
740
|
+
month: "month",
|
|
741
|
+
day: "day"
|
|
742
|
+
};
|
|
743
|
+
function parseIntlPartsToYmd(parts) {
|
|
744
|
+
const ymd = { year: 0, month: 0, day: 0 };
|
|
745
|
+
for (const part of parts) {
|
|
746
|
+
const key = INTL_PART_FIELD_MAP[String(part.type)];
|
|
747
|
+
if (!key) continue;
|
|
748
|
+
const val = parseDigits(part.value);
|
|
749
|
+
if (val > 0) ymd[key] = val;
|
|
750
|
+
}
|
|
751
|
+
return ymd.month >= 1 && ymd.day >= 1 ? ymd : null;
|
|
752
|
+
}
|
|
753
|
+
const intlFormatterCache = /* @__PURE__ */ new Map();
|
|
754
|
+
function getCachedIntlFormatter(locale, options) {
|
|
755
|
+
const key = `${locale}|${options.year ?? ""}|${options.month ?? ""}|${options.day ?? ""}|${options.timeZone ?? "UTC"}`;
|
|
756
|
+
let fmt = intlFormatterCache.get(key);
|
|
757
|
+
if (!fmt) {
|
|
758
|
+
if (intlFormatterCache.size >= 200) {
|
|
759
|
+
const oldestKey = intlFormatterCache.keys().next().value;
|
|
760
|
+
if (oldestKey !== void 0) intlFormatterCache.delete(oldestKey);
|
|
761
|
+
}
|
|
762
|
+
fmt = new Intl.DateTimeFormat(locale, options);
|
|
763
|
+
intlFormatterCache.set(key, fmt);
|
|
764
|
+
}
|
|
765
|
+
return fmt;
|
|
766
|
+
}
|
|
767
|
+
function extractCalendarParts(date, intlCalendar, dayOffset = 0) {
|
|
768
|
+
if (Number.isNaN(date.getTime())) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
const effectiveDate = applyDayOffset(date, dayOffset);
|
|
772
|
+
const calendarTag = (intlCalendar || "gregory").trim().toLowerCase();
|
|
773
|
+
try {
|
|
774
|
+
const formatter = getCachedIntlFormatter(`en-u-ca-${calendarTag}-nu-latn`, {
|
|
775
|
+
year: "numeric",
|
|
776
|
+
month: "numeric",
|
|
777
|
+
day: "numeric",
|
|
778
|
+
timeZone: "UTC"
|
|
779
|
+
});
|
|
780
|
+
const parsed = parseIntlPartsToYmd(formatter.formatToParts(effectiveDate));
|
|
781
|
+
if (parsed) return parsed;
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
year: effectiveDate.getUTCFullYear(),
|
|
786
|
+
month: effectiveDate.getUTCMonth() + 1,
|
|
787
|
+
day: effectiveDate.getUTCDate()
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function resolveCalendarLocaleTag(systemLocale, fallbackLocale) {
|
|
791
|
+
const rawLocale = systemLocale && systemLocale !== "auto" ? systemLocale : fallbackLocale && fallbackLocale !== "auto" ? fallbackLocale : "en";
|
|
792
|
+
return rawLocale.split("-u-")[0];
|
|
793
|
+
}
|
|
794
|
+
function tryFormatWithCustomFormatter(effectiveDate, system, effectiveLocale, compact) {
|
|
795
|
+
const customFn = customFormatters.get(system.id) ?? customFormatters.get(system.intlCalendar);
|
|
796
|
+
if (!customFn) return null;
|
|
797
|
+
try {
|
|
798
|
+
return customFn(effectiveDate, {
|
|
799
|
+
locale: effectiveLocale,
|
|
800
|
+
system,
|
|
801
|
+
compact
|
|
802
|
+
});
|
|
803
|
+
} catch {
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function formatWithIntlFallback(effectiveDate, isoOrDateKey, intlLocale, compact) {
|
|
808
|
+
try {
|
|
809
|
+
const formatter = getCachedIntlFormatter(
|
|
810
|
+
intlLocale,
|
|
811
|
+
compact ? { day: "numeric", month: "short", timeZone: "UTC" } : { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" }
|
|
812
|
+
);
|
|
813
|
+
return formatter.format(effectiveDate);
|
|
814
|
+
} catch {
|
|
815
|
+
try {
|
|
816
|
+
const fallback = getCachedIntlFormatter("en-u-ca-gregory", {
|
|
817
|
+
day: "numeric",
|
|
818
|
+
month: "short",
|
|
819
|
+
year: "numeric",
|
|
820
|
+
timeZone: "UTC"
|
|
821
|
+
});
|
|
822
|
+
return fallback.format(effectiveDate);
|
|
823
|
+
} catch {
|
|
824
|
+
return isoOrDateKey.slice(0, 10);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function formatDateInCalendarSystem(isoOrDateKey, system, locale = "en", options) {
|
|
829
|
+
const rawDate = /^\d{4}-\d{2}-\d{2}$/.test(isoOrDateKey) ? /* @__PURE__ */ new Date(`${isoOrDateKey}T12:00:00.000Z`) : new Date(isoOrDateKey);
|
|
830
|
+
if (Number.isNaN(rawDate.getTime())) {
|
|
831
|
+
return "";
|
|
832
|
+
}
|
|
833
|
+
const effectiveDate = applyDayOffset(rawDate, system.dayOffset);
|
|
834
|
+
const effectiveLocale = resolveCalendarLocaleTag(system.locale, locale);
|
|
835
|
+
const compact = Boolean(options?.compact);
|
|
836
|
+
const customResult = tryFormatWithCustomFormatter(
|
|
837
|
+
effectiveDate,
|
|
838
|
+
system,
|
|
839
|
+
effectiveLocale,
|
|
840
|
+
compact
|
|
841
|
+
);
|
|
842
|
+
if (customResult !== null) {
|
|
843
|
+
return customResult;
|
|
844
|
+
}
|
|
845
|
+
const calendarTag = (system.intlCalendar || "gregory").trim();
|
|
846
|
+
return formatWithIntlFallback(
|
|
847
|
+
effectiveDate,
|
|
848
|
+
isoOrDateKey,
|
|
849
|
+
`${effectiveLocale}-u-ca-${calendarTag}`,
|
|
850
|
+
compact
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
function readTrimmedString(val) {
|
|
854
|
+
return typeof val === "string" ? val.trim() : "";
|
|
855
|
+
}
|
|
856
|
+
function extractDocAltDatesMap(doc) {
|
|
857
|
+
if (doc.alternateDates && typeof doc.alternateDates === "object" && !Array.isArray(doc.alternateDates)) {
|
|
858
|
+
return doc.alternateDates;
|
|
859
|
+
}
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
function resolveSystemOverrideText(sys, doc, sourceAltFields, docAltDatesMap) {
|
|
863
|
+
const fieldName = sourceAltFields[sys.id] || sys.overrideField;
|
|
864
|
+
const rawFieldOverride = fieldName ? readTrimmedString(doc[fieldName]) : "";
|
|
865
|
+
const rawJsonOverride = docAltDatesMap ? readTrimmedString(docAltDatesMap[sys.id]) : "";
|
|
866
|
+
return rawFieldOverride || rawJsonOverride;
|
|
867
|
+
}
|
|
868
|
+
function buildSingleSecondaryCalendarEntry(args) {
|
|
869
|
+
const {
|
|
870
|
+
sys,
|
|
871
|
+
occurrenceDate,
|
|
872
|
+
rawDateObj,
|
|
873
|
+
isBaseOccurrence,
|
|
874
|
+
doc,
|
|
875
|
+
sourceAltFields,
|
|
876
|
+
docAltDatesMap,
|
|
877
|
+
locale
|
|
878
|
+
} = args;
|
|
879
|
+
const rawOverride = resolveSystemOverrideText(
|
|
880
|
+
sys,
|
|
881
|
+
doc,
|
|
882
|
+
sourceAltFields,
|
|
883
|
+
docAltDatesMap
|
|
884
|
+
);
|
|
885
|
+
const parts = extractCalendarParts(
|
|
886
|
+
rawDateObj,
|
|
887
|
+
sys.intlCalendar,
|
|
888
|
+
sys.dayOffset ?? 0
|
|
889
|
+
);
|
|
890
|
+
const label = sys.label || sys.id;
|
|
891
|
+
if (rawOverride && isBaseOccurrence) {
|
|
892
|
+
return {
|
|
893
|
+
calendarId: sys.id,
|
|
894
|
+
label,
|
|
895
|
+
formatted: rawOverride,
|
|
896
|
+
compact: rawOverride,
|
|
897
|
+
intlCalendar: sys.intlCalendar,
|
|
898
|
+
year: parts?.year,
|
|
899
|
+
month: parts?.month,
|
|
900
|
+
day: parts?.day,
|
|
901
|
+
isOverridden: true
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
const formatted = formatDateInCalendarSystem(occurrenceDate, sys, locale, {
|
|
905
|
+
compact: false
|
|
906
|
+
});
|
|
907
|
+
if (!formatted) return null;
|
|
908
|
+
const compact = formatDateInCalendarSystem(occurrenceDate, sys, locale, {
|
|
909
|
+
compact: true
|
|
910
|
+
});
|
|
911
|
+
return {
|
|
912
|
+
calendarId: sys.id,
|
|
913
|
+
label,
|
|
914
|
+
formatted,
|
|
915
|
+
compact: compact || formatted,
|
|
916
|
+
intlCalendar: sys.intlCalendar,
|
|
917
|
+
year: parts?.year,
|
|
918
|
+
month: parts?.month,
|
|
919
|
+
day: parts?.day,
|
|
920
|
+
isOverridden: false
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function appendDirectSourceFieldOverrides(result, sourceAltFields, doc) {
|
|
924
|
+
for (const [calendarId, fieldName] of Object.entries(sourceAltFields)) {
|
|
925
|
+
if (result[calendarId] || !fieldName) continue;
|
|
926
|
+
const rawVal = readTrimmedString(doc[fieldName]);
|
|
927
|
+
if (rawVal) {
|
|
928
|
+
result[calendarId] = {
|
|
929
|
+
calendarId,
|
|
930
|
+
label: calendarId,
|
|
931
|
+
formatted: rawVal,
|
|
932
|
+
compact: rawVal,
|
|
933
|
+
isOverridden: true
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function appendJsonMapOverrides(result, docAltDatesMap) {
|
|
939
|
+
for (const [calendarId, val] of Object.entries(docAltDatesMap)) {
|
|
940
|
+
if (result[calendarId]) continue;
|
|
941
|
+
const trimmed = readTrimmedString(val);
|
|
942
|
+
if (trimmed) {
|
|
943
|
+
result[calendarId] = {
|
|
944
|
+
calendarId,
|
|
945
|
+
label: calendarId,
|
|
946
|
+
formatted: trimmed,
|
|
947
|
+
compact: trimmed,
|
|
948
|
+
isOverridden: true
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function populateSecondarySystems(args) {
|
|
954
|
+
for (const sys of args.secondaryCalendars) {
|
|
955
|
+
if (!sys?.enabled || !sys.id) continue;
|
|
956
|
+
const entry = buildSingleSecondaryCalendarEntry({ ...args, sys });
|
|
957
|
+
if (entry) args.result[sys.id] = entry;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function resolveOccurrenceAlternateDates(params) {
|
|
961
|
+
const { occurrenceDate, baseDateKey, isRecurring, doc, source } = params;
|
|
962
|
+
const result = {};
|
|
963
|
+
const sourceAltFields = source.fields.alternateDateFields ?? {};
|
|
964
|
+
const isBaseOccurrence = !isRecurring || occurrenceDate === baseDateKey;
|
|
965
|
+
const docAltDatesMap = extractDocAltDatesMap(doc);
|
|
966
|
+
populateSecondarySystems({
|
|
967
|
+
result,
|
|
968
|
+
secondaryCalendars: params.secondaryCalendars ?? [],
|
|
969
|
+
occurrenceDate,
|
|
970
|
+
rawDateObj: /* @__PURE__ */ new Date(`${occurrenceDate}T12:00:00.000Z`),
|
|
971
|
+
isBaseOccurrence,
|
|
972
|
+
doc,
|
|
973
|
+
sourceAltFields,
|
|
974
|
+
docAltDatesMap,
|
|
975
|
+
locale: params.locale ?? "en"
|
|
976
|
+
});
|
|
977
|
+
if (isBaseOccurrence) {
|
|
978
|
+
appendDirectSourceFieldOverrides(result, sourceAltFields, doc);
|
|
979
|
+
if (docAltDatesMap) appendJsonMapOverrides(result, docAltDatesMap);
|
|
980
|
+
}
|
|
981
|
+
return result;
|
|
982
|
+
}
|
|
983
|
+
const VALID_FREQUENCIES = [
|
|
984
|
+
"NONE",
|
|
985
|
+
"DAILY",
|
|
986
|
+
"WEEKLY",
|
|
987
|
+
"MONTHLY",
|
|
988
|
+
"YEARLY"
|
|
989
|
+
];
|
|
990
|
+
const JS_DAY_TO_RRULE_DAY = [
|
|
991
|
+
"SU",
|
|
992
|
+
"MO",
|
|
993
|
+
"TU",
|
|
994
|
+
"WE",
|
|
995
|
+
"TH",
|
|
996
|
+
"FR",
|
|
997
|
+
"SA"
|
|
998
|
+
];
|
|
999
|
+
function parseBoundedInt(val, min, max) {
|
|
1000
|
+
const n = Number.parseInt(val, 10);
|
|
1001
|
+
return Number.isFinite(n) && n >= min && n <= max ? n : void 0;
|
|
1002
|
+
}
|
|
1003
|
+
function parseUntilDate(val) {
|
|
1004
|
+
if (/^\d{8}(?:T\d{6}Z?)?$/.test(val)) {
|
|
1005
|
+
return `${val.slice(0, 4)}-${val.slice(4, 6)}-${val.slice(6, 8)}`;
|
|
1006
|
+
}
|
|
1007
|
+
return /^\d{4}-\d{2}-\d{2}/.test(val) ? val.slice(0, 10) : void 0;
|
|
1008
|
+
}
|
|
1009
|
+
function parseByDayList(val) {
|
|
1010
|
+
return val.split(",").map((d) => d.trim().slice(-2)).filter((d) => JS_DAY_TO_RRULE_DAY.includes(d));
|
|
1011
|
+
}
|
|
1012
|
+
const intAssigner = (key, min, max) => (p, v) => {
|
|
1013
|
+
const n = parseBoundedInt(v, min, max);
|
|
1014
|
+
if (n !== void 0) p[key] = n;
|
|
1015
|
+
};
|
|
1016
|
+
const RRULE_PART_ASSIGNERS = {
|
|
1017
|
+
RSCALE: (p, v) => {
|
|
1018
|
+
if (v) p.rscale = v.toLowerCase();
|
|
1019
|
+
},
|
|
1020
|
+
FREQ: (p, v) => {
|
|
1021
|
+
if (VALID_FREQUENCIES.includes(v)) {
|
|
1022
|
+
p.freq = v;
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
INTERVAL: intAssigner("interval", 1, 365),
|
|
1026
|
+
BYDAY: (p, v) => {
|
|
1027
|
+
p.byDay = parseByDayList(v);
|
|
1028
|
+
},
|
|
1029
|
+
BYMONTH: intAssigner("byMonth", 1, 13),
|
|
1030
|
+
BYMONTHDAY: intAssigner("byMonthDay", 1, 31),
|
|
1031
|
+
COUNT: intAssigner("count", 1, 1e3),
|
|
1032
|
+
UNTIL: (p, v) => {
|
|
1033
|
+
const u = parseUntilDate(v);
|
|
1034
|
+
if (u !== void 0) p.until = u;
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
function parseRRule(rawRule) {
|
|
1038
|
+
const defaultParsed = { freq: "NONE", interval: 1, byDay: [] };
|
|
1039
|
+
if (!rawRule || typeof rawRule !== "string") return defaultParsed;
|
|
1040
|
+
const cleaned = rawRule.trim().replace(/^RRULE:/i, "");
|
|
1041
|
+
if (!cleaned) return defaultParsed;
|
|
1042
|
+
const parsed = { ...defaultParsed };
|
|
1043
|
+
for (const part of cleaned.split(";")) {
|
|
1044
|
+
const [rawKey, rawVal] = part.split("=");
|
|
1045
|
+
if (!rawKey || !rawVal) continue;
|
|
1046
|
+
RRULE_PART_ASSIGNERS[rawKey.trim().toUpperCase()]?.(
|
|
1047
|
+
parsed,
|
|
1048
|
+
rawVal.trim().toUpperCase()
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
return parsed;
|
|
1052
|
+
}
|
|
1053
|
+
function normalizeRRuleObject(obj, defaultTimezone) {
|
|
1054
|
+
const rrule = typeof obj.rrule === "string" ? obj.rrule : "";
|
|
1055
|
+
const parsed = parseRRule(rrule);
|
|
1056
|
+
const timezone = typeof obj.timezone === "string" && obj.timezone ? obj.timezone : defaultTimezone;
|
|
1057
|
+
const rscale = typeof obj.rscale === "string" ? obj.rscale : parsed.rscale;
|
|
1058
|
+
const exdates = Array.isArray(obj.exdates) ? obj.exdates.filter((d) => typeof d === "string") : [];
|
|
1059
|
+
const overrides = obj.overrides && typeof obj.overrides === "object" ? obj.overrides : {};
|
|
1060
|
+
return {
|
|
1061
|
+
rrule,
|
|
1062
|
+
timezone,
|
|
1063
|
+
rscale,
|
|
1064
|
+
allDay: Boolean(obj.allDay),
|
|
1065
|
+
exdates,
|
|
1066
|
+
overrides
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
function parseRRuleFieldValue(raw, defaultTimezone = "UTC") {
|
|
1070
|
+
const fallback = normalizeRRuleObject({}, defaultTimezone);
|
|
1071
|
+
if (!raw) return fallback;
|
|
1072
|
+
if (typeof raw === "string") {
|
|
1073
|
+
const trimmed = raw.trim();
|
|
1074
|
+
if (trimmed.startsWith("{")) {
|
|
1075
|
+
try {
|
|
1076
|
+
const obj = JSON.parse(trimmed);
|
|
1077
|
+
return normalizeRRuleObject(obj, defaultTimezone);
|
|
1078
|
+
} catch {
|
|
1079
|
+
return fallback;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const parsed = parseRRule(trimmed);
|
|
1083
|
+
return { ...fallback, rrule: trimmed, rscale: parsed.rscale };
|
|
1084
|
+
}
|
|
1085
|
+
return typeof raw === "object" ? normalizeRRuleObject(raw, defaultTimezone) : fallback;
|
|
1086
|
+
}
|
|
1087
|
+
const toDateKeyUTC = (date) => date.toISOString().slice(0, 10);
|
|
1088
|
+
function buildEffectiveSlot(candidateDate, dateKey, ctx, isRecurring) {
|
|
1089
|
+
if (ctx.exdatesSet.has(dateKey)) return null;
|
|
1090
|
+
const override = ctx.overridesMap[dateKey];
|
|
1091
|
+
if (override?.cancelled) return null;
|
|
1092
|
+
const effStart = override?.startDate ? new Date(override.startDate) : candidateDate;
|
|
1093
|
+
const effEnd = override?.endDate ? new Date(override.endDate) : new Date(effStart.getTime() + ctx.durationMs);
|
|
1094
|
+
if (effEnd.getTime() < ctx.winStartMs || effStart.getTime() > ctx.winEndMs)
|
|
1095
|
+
return null;
|
|
1096
|
+
return {
|
|
1097
|
+
occurrenceDate: dateKey,
|
|
1098
|
+
startDate: effStart.toISOString(),
|
|
1099
|
+
endDate: effEnd.toISOString(),
|
|
1100
|
+
isRecurring,
|
|
1101
|
+
isOverridden: Boolean(override),
|
|
1102
|
+
...isRecurring ? { rscale: ctx.effectiveRScale } : {},
|
|
1103
|
+
override
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
function matchesNonGregorianFrequency(candidateDate, ctx) {
|
|
1107
|
+
const calParts = extractCalendarParts(candidateDate, ctx.effectiveRScale);
|
|
1108
|
+
if (!calParts || !ctx.baseParts) return false;
|
|
1109
|
+
const interval = Math.max(1, ctx.parsed.interval);
|
|
1110
|
+
if (ctx.parsed.freq === "MONTHLY") {
|
|
1111
|
+
const monthsDiff = (calParts.year - ctx.baseParts.year) * 12 + (calParts.month - ctx.baseParts.month);
|
|
1112
|
+
return monthsDiff >= 0 && monthsDiff % interval === 0 && calParts.day === ctx.targetDayOfMonth;
|
|
1113
|
+
}
|
|
1114
|
+
const yearsDiff = calParts.year - ctx.baseParts.year;
|
|
1115
|
+
return yearsDiff >= 0 && yearsDiff % interval === 0 && calParts.month === ctx.targetMonth && calParts.day === ctx.targetDayOfMonth;
|
|
1116
|
+
}
|
|
1117
|
+
function matchesGregorianFrequency(candidateDate, dayOffset, ctx) {
|
|
1118
|
+
const { freq, interval } = ctx.parsed;
|
|
1119
|
+
if (freq === "DAILY") return dayOffset % interval === 0;
|
|
1120
|
+
if (freq === "WEEKLY") {
|
|
1121
|
+
const rruleDay = JS_DAY_TO_RRULE_DAY[candidateDate.getUTCDay()];
|
|
1122
|
+
const startWeekDay = (ctx.startObj.getUTCDay() + 6) % 7;
|
|
1123
|
+
const weekIdx = Math.floor((dayOffset + startWeekDay) / 7);
|
|
1124
|
+
return weekIdx % interval === 0 && ctx.targetWeekdays.includes(rruleDay);
|
|
1125
|
+
}
|
|
1126
|
+
if (freq === "MONTHLY") {
|
|
1127
|
+
const monthsDiff = (candidateDate.getUTCFullYear() - ctx.startObj.getUTCFullYear()) * 12 + (candidateDate.getUTCMonth() - ctx.startObj.getUTCMonth());
|
|
1128
|
+
return monthsDiff >= 0 && monthsDiff % interval === 0 && candidateDate.getUTCDate() === ctx.targetDayOfMonth;
|
|
1129
|
+
}
|
|
1130
|
+
const yearsDiff = candidateDate.getUTCFullYear() - ctx.startObj.getUTCFullYear();
|
|
1131
|
+
return yearsDiff >= 0 && yearsDiff % interval === 0 && candidateDate.getUTCMonth() + 1 === ctx.targetMonth && candidateDate.getUTCDate() === ctx.targetDayOfMonth;
|
|
1132
|
+
}
|
|
1133
|
+
function matchesCandidateRule(candidateDate, dayOffset, ctx) {
|
|
1134
|
+
const isMonthOrYear = ctx.parsed.freq === "MONTHLY" || ctx.parsed.freq === "YEARLY";
|
|
1135
|
+
if (ctx.isNonGregorianRScale && ctx.effectiveRScale && isMonthOrYear) {
|
|
1136
|
+
return matchesNonGregorianFrequency(candidateDate, ctx);
|
|
1137
|
+
}
|
|
1138
|
+
return matchesGregorianFrequency(candidateDate, dayOffset, ctx);
|
|
1139
|
+
}
|
|
1140
|
+
function resolveEndDate(startObj, baseEndDate) {
|
|
1141
|
+
if (baseEndDate) {
|
|
1142
|
+
const parsedEnd = new Date(baseEndDate);
|
|
1143
|
+
if (!Number.isNaN(parsedEnd.getTime())) return parsedEnd;
|
|
1144
|
+
}
|
|
1145
|
+
return new Date(startObj.getTime() + 60 * 60 * 1e3);
|
|
1146
|
+
}
|
|
1147
|
+
function resolveCalendarTargets(startObj, parsed, effectiveRScale) {
|
|
1148
|
+
const isNonGregorianRScale = Boolean(effectiveRScale) && effectiveRScale !== "gregory" && effectiveRScale !== "iso8601";
|
|
1149
|
+
const baseParts = isNonGregorianRScale && effectiveRScale ? extractCalendarParts(startObj, effectiveRScale) : null;
|
|
1150
|
+
const targetMonth = parsed.byMonth ?? (baseParts ? baseParts.month : startObj.getUTCMonth() + 1);
|
|
1151
|
+
const targetDayOfMonth = parsed.byMonthDay ?? (baseParts ? baseParts.day : startObj.getUTCDate());
|
|
1152
|
+
const targetWeekdays = parsed.byDay.length > 0 ? parsed.byDay : [JS_DAY_TO_RRULE_DAY[startObj.getUTCDay()]];
|
|
1153
|
+
return {
|
|
1154
|
+
isNonGregorianRScale,
|
|
1155
|
+
baseParts,
|
|
1156
|
+
targetMonth,
|
|
1157
|
+
targetDayOfMonth,
|
|
1158
|
+
targetWeekdays
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function createSlotContext(params) {
|
|
1162
|
+
const { startObj, baseEndDate, recurrence, windowStart, windowEnd } = params;
|
|
1163
|
+
const endObj = resolveEndDate(startObj, baseEndDate);
|
|
1164
|
+
const parsed = parseRRule(recurrence?.rrule);
|
|
1165
|
+
const effectiveRScale = recurrence?.rscale || parsed.rscale;
|
|
1166
|
+
const targets = resolveCalendarTargets(startObj, parsed, effectiveRScale);
|
|
1167
|
+
const untilMs = parsed.until ? (/* @__PURE__ */ new Date(`${parsed.until}T23:59:59.999Z`)).getTime() : Number.POSITIVE_INFINITY;
|
|
1168
|
+
return {
|
|
1169
|
+
startObj,
|
|
1170
|
+
durationMs: Math.max(0, endObj.getTime() - startObj.getTime()),
|
|
1171
|
+
winStartMs: new Date(windowStart).getTime(),
|
|
1172
|
+
winEndMs: new Date(windowEnd).getTime(),
|
|
1173
|
+
untilMs,
|
|
1174
|
+
maxCount: parsed.count ?? params.maxOccurrences,
|
|
1175
|
+
maxOccurrences: params.maxOccurrences,
|
|
1176
|
+
parsed,
|
|
1177
|
+
exdatesSet: new Set(recurrence?.exdates ?? []),
|
|
1178
|
+
overridesMap: recurrence?.overrides ?? {},
|
|
1179
|
+
effectiveRScale,
|
|
1180
|
+
...targets,
|
|
1181
|
+
anchorMidnightMs: Date.UTC(
|
|
1182
|
+
startObj.getUTCFullYear(),
|
|
1183
|
+
startObj.getUTCMonth(),
|
|
1184
|
+
startObj.getUTCDate()
|
|
1185
|
+
),
|
|
1186
|
+
timeOffsetMs: startObj.getUTCHours() * 36e5 + startObj.getUTCMinutes() * 6e4 + startObj.getUTCSeconds() * 1e3
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
function scanRecurringSlots(ctx) {
|
|
1190
|
+
const results = [];
|
|
1191
|
+
let generatedCount = 0;
|
|
1192
|
+
const daysToWinEnd = Math.ceil((ctx.winEndMs - ctx.anchorMidnightMs) / 864e5) + 1;
|
|
1193
|
+
const maxDayOffset = Math.min(Math.max(daysToWinEnd, 0), 36500);
|
|
1194
|
+
const startDayOffset = ctx.parsed.count === void 0 && !ctx.isNonGregorianRScale ? Math.max(
|
|
1195
|
+
0,
|
|
1196
|
+
Math.floor(
|
|
1197
|
+
(ctx.winStartMs - ctx.durationMs - ctx.anchorMidnightMs) / 864e5
|
|
1198
|
+
) - 1
|
|
1199
|
+
) : 0;
|
|
1200
|
+
for (let dayOffset = startDayOffset; dayOffset <= maxDayOffset; dayOffset++) {
|
|
1201
|
+
if (generatedCount >= ctx.maxCount || results.length >= ctx.maxOccurrences)
|
|
1202
|
+
break;
|
|
1203
|
+
const candidateDate = new Date(
|
|
1204
|
+
ctx.anchorMidnightMs + dayOffset * 864e5 + ctx.timeOffsetMs
|
|
1205
|
+
);
|
|
1206
|
+
const candidateMs = candidateDate.getTime();
|
|
1207
|
+
if (candidateMs > ctx.untilMs || candidateMs > ctx.winEndMs) break;
|
|
1208
|
+
if (!matchesCandidateRule(candidateDate, dayOffset, ctx)) continue;
|
|
1209
|
+
generatedCount++;
|
|
1210
|
+
const slot = buildEffectiveSlot(
|
|
1211
|
+
candidateDate,
|
|
1212
|
+
toDateKeyUTC(candidateDate),
|
|
1213
|
+
ctx,
|
|
1214
|
+
true
|
|
1215
|
+
);
|
|
1216
|
+
if (slot) results.push(slot);
|
|
1217
|
+
}
|
|
1218
|
+
return results;
|
|
1219
|
+
}
|
|
1220
|
+
function expandRuleSlots(params) {
|
|
1221
|
+
const startObj = new Date(params.baseStartDate);
|
|
1222
|
+
if (Number.isNaN(startObj.getTime())) return [];
|
|
1223
|
+
const ctx = createSlotContext({
|
|
1224
|
+
...params,
|
|
1225
|
+
startObj,
|
|
1226
|
+
maxOccurrences: params.maxOccurrences ?? 366
|
|
1227
|
+
});
|
|
1228
|
+
if (ctx.parsed.freq === "NONE") {
|
|
1229
|
+
const slot = buildEffectiveSlot(
|
|
1230
|
+
startObj,
|
|
1231
|
+
toDateKeyUTC(startObj),
|
|
1232
|
+
ctx,
|
|
1233
|
+
false
|
|
1234
|
+
);
|
|
1235
|
+
return slot ? [slot] : [];
|
|
1236
|
+
}
|
|
1237
|
+
return scanRecurringSlots(ctx);
|
|
1238
|
+
}
|
|
1239
|
+
const DEFAULT_TIME_SLOTS = {
|
|
1240
|
+
morning: { start: "09:00:00", end: "11:00:00" },
|
|
1241
|
+
afternoon: { start: "14:00:00", end: "16:00:00" },
|
|
1242
|
+
evening: { start: "18:00:00", end: "20:00:00" }
|
|
1243
|
+
};
|
|
1244
|
+
function resolveDateOnlyWithSlot(rawStart, rawSlot, source) {
|
|
1245
|
+
if (typeof rawSlot === "string" && rawSlot) {
|
|
1246
|
+
const mapped = { ...DEFAULT_TIME_SLOTS, ...source.timeSlotMap }[rawSlot];
|
|
1247
|
+
if (mapped) {
|
|
1248
|
+
return {
|
|
1249
|
+
startDate: `${rawStart}T${mapped.start}.000Z`,
|
|
1250
|
+
endDate: `${rawStart}T${mapped.end}.000Z`,
|
|
1251
|
+
allDay: false
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
if (/^\d{2}:\d{2}(:\d{2})?$/.test(rawSlot)) {
|
|
1255
|
+
const normalizedTime = rawSlot.length === 5 ? `${rawSlot}:00` : rawSlot;
|
|
1256
|
+
return {
|
|
1257
|
+
startDate: `${rawStart}T${normalizedTime}.000Z`,
|
|
1258
|
+
endDate: null,
|
|
1259
|
+
allDay: false
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
startDate: `${rawStart}T00:00:00.000Z`,
|
|
1265
|
+
endDate: `${rawStart}T23:59:59.000Z`,
|
|
1266
|
+
allDay: source.defaultAllDay ?? true
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
function resolveStartEndFromDocument(doc, source) {
|
|
1270
|
+
const rawStart = doc[source.fields.startDate];
|
|
1271
|
+
if (!rawStart || typeof rawStart !== "string") return null;
|
|
1272
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(rawStart)) {
|
|
1273
|
+
const rawSlot = source.fields.timeSlot ? doc[source.fields.timeSlot] : null;
|
|
1274
|
+
return resolveDateOnlyWithSlot(rawStart, rawSlot, source);
|
|
1275
|
+
}
|
|
1276
|
+
const rawEnd = source.fields.endDate ? doc[source.fields.endDate] : null;
|
|
1277
|
+
const allDay = typeof doc.allDay === "boolean" ? doc.allDay : Boolean(source.defaultAllDay);
|
|
1278
|
+
return {
|
|
1279
|
+
startDate: rawStart,
|
|
1280
|
+
endDate: typeof rawEnd === "string" && rawEnd ? rawEnd : null,
|
|
1281
|
+
allDay
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
const STATUS_MAP = {
|
|
1285
|
+
cancelled: "cancelled",
|
|
1286
|
+
canceled: "cancelled",
|
|
1287
|
+
rejected: "cancelled",
|
|
1288
|
+
postponed: "postponed",
|
|
1289
|
+
pending: "postponed",
|
|
1290
|
+
rescheduled: "rescheduled"
|
|
1291
|
+
};
|
|
1292
|
+
function mapStatus(rawStatus) {
|
|
1293
|
+
if (typeof rawStatus !== "string") return "scheduled";
|
|
1294
|
+
return STATUS_MAP[rawStatus.toLowerCase()] ?? "scheduled";
|
|
1295
|
+
}
|
|
1296
|
+
function matchesKeyword(source, keyword, canonicalUid) {
|
|
1297
|
+
if (source.uid === canonicalUid) return true;
|
|
1298
|
+
return source.id.toLowerCase().includes(keyword) || source.uid.toLowerCase().includes(keyword);
|
|
1299
|
+
}
|
|
1300
|
+
function matchesSourceFilter(source, filter) {
|
|
1301
|
+
if (!filter || filter === "all") return true;
|
|
1302
|
+
if (source.id === filter || source.uid === filter) return true;
|
|
1303
|
+
const lowerFilter = filter.toLowerCase();
|
|
1304
|
+
if (lowerFilter === "events" || lowerFilter === "event") {
|
|
1305
|
+
return matchesKeyword(source, "event", "plugin::events.event");
|
|
1306
|
+
}
|
|
1307
|
+
if (lowerFilter === "bookings" || lowerFilter === "booking") {
|
|
1308
|
+
return matchesKeyword(source, "booking", "plugin::bookings.booking");
|
|
1309
|
+
}
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
function resolveTargetSource(sources, sourceId) {
|
|
1313
|
+
if (!Array.isArray(sources) || sources.length === 0) {
|
|
1314
|
+
return void 0;
|
|
1315
|
+
}
|
|
1316
|
+
if (sourceId && sourceId !== "all") {
|
|
1317
|
+
const exact = sources.find((s) => s.id === sourceId || s.uid === sourceId);
|
|
1318
|
+
if (exact) return exact;
|
|
1319
|
+
const aliasMatch = sources.find((s) => matchesSourceFilter(s, sourceId));
|
|
1320
|
+
if (aliasMatch) return aliasMatch;
|
|
1321
|
+
}
|
|
1322
|
+
return sources.find((s) => s.enabled) ?? sources[0];
|
|
1323
|
+
}
|
|
1324
|
+
function isBookingCollectionSource(source, schema) {
|
|
1325
|
+
if (source.uid === "plugin::bookings.booking") return true;
|
|
1326
|
+
const attrs = schema?.attributes ?? {};
|
|
1327
|
+
return "bookingCode" in attrs && "customerEmail" in attrs;
|
|
1328
|
+
}
|
|
1329
|
+
function generateBookingCode(startIso) {
|
|
1330
|
+
const dateCompact = startIso.slice(0, 10).replace(/-/g, "") || "20260101";
|
|
1331
|
+
const rand = node_crypto.randomInt(1e3, 9999);
|
|
1332
|
+
return `BK-${dateCompact}-${rand}`;
|
|
1333
|
+
}
|
|
1334
|
+
function computeEffectiveEnd(rawStart, endDate) {
|
|
1335
|
+
const normalizedStart = rawStart.includes("T") ? rawStart : `${rawStart}T10:00:00.000Z`;
|
|
1336
|
+
const startMs = new Date(normalizedStart).getTime();
|
|
1337
|
+
const defaultEndIso = Number.isNaN(startMs) ? normalizedStart : new Date(startMs + 36e5).toISOString();
|
|
1338
|
+
const trimmedEnd = endDate?.trim();
|
|
1339
|
+
return {
|
|
1340
|
+
normalizedStart,
|
|
1341
|
+
effectiveEnd: trimmedEnd || defaultEndIso
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function setTrimmedIfMapped(data, fieldName, rawVal) {
|
|
1345
|
+
if (fieldName && rawVal) {
|
|
1346
|
+
data[fieldName] = rawVal.trim();
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function applyOptionalSourceFields(data, params, source, defaultTimezone, slug, effectiveEnd, isPluginBooking) {
|
|
1350
|
+
const shouldSetEnd = Boolean(params.endDate) || isPluginBooking;
|
|
1351
|
+
if (source.fields.endDate && shouldSetEnd) {
|
|
1352
|
+
data[source.fields.endDate] = effectiveEnd;
|
|
1353
|
+
}
|
|
1354
|
+
if (source.fields.slug) {
|
|
1355
|
+
data[source.fields.slug] = `${slug}-${Date.now().toString(36)}`;
|
|
1356
|
+
}
|
|
1357
|
+
setTrimmedIfMapped(data, source.fields.location, params.location);
|
|
1358
|
+
setTrimmedIfMapped(data, source.fields.summary, params.summary);
|
|
1359
|
+
if (source.fields.rrule && params.rrule) {
|
|
1360
|
+
data[source.fields.rrule] = {
|
|
1361
|
+
rrule: params.rrule,
|
|
1362
|
+
timezone: defaultTimezone,
|
|
1363
|
+
allDay: false,
|
|
1364
|
+
exdates: [],
|
|
1365
|
+
overrides: {}
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
function applyCategoryIfApplicable(data, params, source, isPluginBooking) {
|
|
1370
|
+
const catField = source.fields.category;
|
|
1371
|
+
if (!catField || !params.category || isPluginBooking) return;
|
|
1372
|
+
if (catField !== source.fields.status) {
|
|
1373
|
+
data[catField] = params.category;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
function hasFieldOrBookingUid(field, attrs, source) {
|
|
1377
|
+
return field in attrs || source.uid === "plugin::bookings.booking";
|
|
1378
|
+
}
|
|
1379
|
+
function setIfBookingFieldPresent(data, field, value, attrs, source) {
|
|
1380
|
+
if (value !== void 0 && hasFieldOrBookingUid(field, attrs, source)) {
|
|
1381
|
+
data[field] = value;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
function applyBookingCollectionFields(data, params, source, attrs, title, normalizedStart, effectiveEnd) {
|
|
1385
|
+
setIfBookingFieldPresent(
|
|
1386
|
+
data,
|
|
1387
|
+
"bookingCode",
|
|
1388
|
+
generateBookingCode(normalizedStart),
|
|
1389
|
+
attrs,
|
|
1390
|
+
source
|
|
1391
|
+
);
|
|
1392
|
+
setIfBookingFieldPresent(
|
|
1393
|
+
data,
|
|
1394
|
+
"customerName",
|
|
1395
|
+
params.customerName?.trim() || title,
|
|
1396
|
+
attrs,
|
|
1397
|
+
source
|
|
1398
|
+
);
|
|
1399
|
+
setIfBookingFieldPresent(
|
|
1400
|
+
data,
|
|
1401
|
+
"customerEmail",
|
|
1402
|
+
params.customerEmail?.trim() || "noreply@example.com",
|
|
1403
|
+
attrs,
|
|
1404
|
+
source
|
|
1405
|
+
);
|
|
1406
|
+
const phone = params.customerPhone?.trim();
|
|
1407
|
+
setIfBookingFieldPresent(
|
|
1408
|
+
data,
|
|
1409
|
+
"customerPhone",
|
|
1410
|
+
phone || void 0,
|
|
1411
|
+
attrs,
|
|
1412
|
+
source
|
|
1413
|
+
);
|
|
1414
|
+
const partySize = Math.max(1, Math.floor(Number(params.partySize) || 1));
|
|
1415
|
+
setIfBookingFieldPresent(data, "partySize", partySize, attrs, source);
|
|
1416
|
+
const statusField = source.fields.status || "status";
|
|
1417
|
+
const statusVal = params.bookingStatus === "pending" ? "pending" : "confirmed";
|
|
1418
|
+
setIfBookingFieldPresent(data, statusField, statusVal, attrs, source);
|
|
1419
|
+
const endField = source.fields.endDate || "endDate";
|
|
1420
|
+
if (!data[endField]) {
|
|
1421
|
+
data[endField] = effectiveEnd;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function buildQuickCreatePayload(params, source, defaultTimezone, schema) {
|
|
1425
|
+
const title = (params.title ?? "").trim();
|
|
1426
|
+
if (!title) throw new Error("Entry title is required.");
|
|
1427
|
+
const rawStart = (params.startDate ?? "").trim();
|
|
1428
|
+
if (!rawStart) throw new Error("Start date is required.");
|
|
1429
|
+
const { normalizedStart, effectiveEnd } = computeEffectiveEnd(
|
|
1430
|
+
rawStart,
|
|
1431
|
+
params.endDate
|
|
1432
|
+
);
|
|
1433
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
1434
|
+
const attrs = schema?.attributes ?? {};
|
|
1435
|
+
const isPluginBooking = isBookingCollectionSource(source, schema);
|
|
1436
|
+
const data = {
|
|
1437
|
+
[source.fields.title]: title,
|
|
1438
|
+
[source.fields.startDate]: rawStart
|
|
1439
|
+
};
|
|
1440
|
+
applyOptionalSourceFields(
|
|
1441
|
+
data,
|
|
1442
|
+
params,
|
|
1443
|
+
source,
|
|
1444
|
+
defaultTimezone,
|
|
1445
|
+
slug,
|
|
1446
|
+
effectiveEnd,
|
|
1447
|
+
isPluginBooking
|
|
1448
|
+
);
|
|
1449
|
+
applyCategoryIfApplicable(data, params, source, isPluginBooking);
|
|
1450
|
+
if (isPluginBooking) {
|
|
1451
|
+
applyBookingCollectionFields(
|
|
1452
|
+
data,
|
|
1453
|
+
params,
|
|
1454
|
+
source,
|
|
1455
|
+
attrs,
|
|
1456
|
+
title,
|
|
1457
|
+
normalizedStart,
|
|
1458
|
+
effectiveEnd
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
const shouldPublish = schema?.options?.draftAndPublish !== void 0 ? Boolean(schema.options.draftAndPublish) : source.uid !== "plugin::bookings.booking";
|
|
1462
|
+
return { data, shouldPublish };
|
|
1463
|
+
}
|
|
1464
|
+
function buildDirectUpdatePayload(params, source, existingDoc) {
|
|
1465
|
+
if (params.cancelOccurrence) {
|
|
1466
|
+
return source.fields.status ? { [source.fields.status]: "cancelled" } : {};
|
|
1467
|
+
}
|
|
1468
|
+
const rawExistingStart = existingDoc?.[source.fields.startDate];
|
|
1469
|
+
const isDateOnlyStart = typeof rawExistingStart === "string" && /^\d{4}-\d{2}-\d{2}$/.test(rawExistingStart);
|
|
1470
|
+
const payload = {
|
|
1471
|
+
[source.fields.startDate]: isDateOnlyStart ? params.newStartDate.slice(0, 10) : params.newStartDate
|
|
1472
|
+
};
|
|
1473
|
+
if (params.newEndDate && source.fields.endDate) {
|
|
1474
|
+
const rawExistingEnd = existingDoc?.[source.fields.endDate];
|
|
1475
|
+
const isDateOnlyEnd = isDateOnlyStart || typeof rawExistingEnd === "string" && /^\d{4}-\d{2}-\d{2}$/.test(rawExistingEnd);
|
|
1476
|
+
payload[source.fields.endDate] = isDateOnlyEnd ? params.newEndDate.slice(0, 10) : params.newEndDate;
|
|
1477
|
+
}
|
|
1478
|
+
return payload;
|
|
1479
|
+
}
|
|
1480
|
+
function buildRecurrenceOverridePayload(recurrence, rruleField, params) {
|
|
1481
|
+
const overrides = {
|
|
1482
|
+
...recurrence.overrides,
|
|
1483
|
+
[params.occurrenceDate]: {
|
|
1484
|
+
...recurrence.overrides?.[params.occurrenceDate],
|
|
1485
|
+
originalDate: params.occurrenceDate,
|
|
1486
|
+
cancelled: Boolean(params.cancelOccurrence),
|
|
1487
|
+
startDate: params.newStartDate || void 0,
|
|
1488
|
+
endDate: params.newEndDate || void 0
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
return { [rruleField]: { ...recurrence, overrides } };
|
|
1492
|
+
}
|
|
1493
|
+
function resolveQueryWindow(params) {
|
|
1494
|
+
const y = (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
1495
|
+
const defaultFrom = new Date(Date.UTC(y, 0, 1, 0, 0, 0)).toISOString();
|
|
1496
|
+
const defaultTo = new Date(Date.UTC(y, 11, 31, 23, 59, 59)).toISOString();
|
|
1497
|
+
const validFrom = params.from && !Number.isNaN(new Date(params.from).getTime());
|
|
1498
|
+
const validTo = params.to && !Number.isNaN(new Date(params.to).getTime());
|
|
1499
|
+
return {
|
|
1500
|
+
windowStart: validFrom ? params.from : defaultFrom,
|
|
1501
|
+
windowEnd: validTo ? params.to : defaultTo
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
function buildWindowFilters(source, windowStart, windowEnd) {
|
|
1505
|
+
if (source.fields.rrule) {
|
|
1506
|
+
return { [source.fields.startDate]: { $lte: windowEnd } };
|
|
1507
|
+
}
|
|
1508
|
+
if (source.fields.endDate) {
|
|
1509
|
+
return {
|
|
1510
|
+
[source.fields.startDate]: { $lte: windowEnd },
|
|
1511
|
+
[source.fields.endDate]: { $gte: windowStart }
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
return {
|
|
1515
|
+
[source.fields.startDate]: { $gte: windowStart, $lte: windowEnd }
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
function buildDocQueryOpts(params, source, windowStart, windowEnd, includeLocale) {
|
|
1519
|
+
const opts = {
|
|
1520
|
+
limit: 1e3,
|
|
1521
|
+
filters: params.documentId ? { documentId: { $eq: params.documentId } } : buildWindowFilters(source, windowStart, windowEnd),
|
|
1522
|
+
...{
|
|
1523
|
+
sort: { [source.fields.startDate]: "desc" }
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
if (includeLocale && params.locale && params.locale !== "auto") {
|
|
1527
|
+
opts.locale = params.locale;
|
|
1528
|
+
}
|
|
1529
|
+
if (!params.includeDrafts) opts.status = "published";
|
|
1530
|
+
return opts;
|
|
1531
|
+
}
|
|
1532
|
+
const docKey = (d) => String(d.documentId ?? d.id);
|
|
1533
|
+
async function fetchDocumentsForSource(strapi, source, params, windowStart, windowEnd) {
|
|
1534
|
+
const find = (withLocale) => strapi.documents(source.uid).findMany(
|
|
1535
|
+
buildDocQueryOpts(params, source, windowStart, windowEnd, withLocale)
|
|
1536
|
+
).catch(() => []);
|
|
1537
|
+
if (!params.locale || params.locale === "auto") return find(false);
|
|
1538
|
+
const [primary, fallback] = await Promise.all([find(true), find(false)]);
|
|
1539
|
+
const seen = new Set(primary.map(docKey));
|
|
1540
|
+
return [...primary, ...fallback.filter((d) => !seen.has(docKey(d)))];
|
|
1541
|
+
}
|
|
1542
|
+
function extractCustomFields(doc, keys) {
|
|
1543
|
+
if (!Array.isArray(keys) || keys.length === 0) return void 0;
|
|
1544
|
+
const extracted = {};
|
|
1545
|
+
for (const key of keys) {
|
|
1546
|
+
if (key && doc[key] !== void 0) extracted[key] = doc[key];
|
|
1547
|
+
}
|
|
1548
|
+
return Object.keys(extracted).length > 0 ? extracted : void 0;
|
|
1549
|
+
}
|
|
1550
|
+
const HEX_COLOR_RE$1 = /^#[0-9a-fA-F]{3,8}$/;
|
|
1551
|
+
function resolveDocColor(doc, source, categoryVal) {
|
|
1552
|
+
const catColor = categoryVal ? source.categoryColors?.[categoryVal] : void 0;
|
|
1553
|
+
if (typeof catColor === "string" && HEX_COLOR_RE$1.test(catColor.trim())) {
|
|
1554
|
+
return catColor.trim();
|
|
1555
|
+
}
|
|
1556
|
+
const docColorVal = source.fields.color ? doc[source.fields.color] : doc.color;
|
|
1557
|
+
if (typeof docColorVal === "string" && HEX_COLOR_RE$1.test(docColorVal.trim())) {
|
|
1558
|
+
return docColorVal.trim();
|
|
1559
|
+
}
|
|
1560
|
+
return HEX_COLOR_RE$1.test(source.color) ? source.color : "#4945ff";
|
|
1561
|
+
}
|
|
1562
|
+
const optStr = (v) => v ? v : void 0;
|
|
1563
|
+
const readField = (doc, key) => key ? String(doc[key] ?? "") : "";
|
|
1564
|
+
function buildOccurrenceItem(slot, meta) {
|
|
1565
|
+
const effectiveTitle = slot.override?.title || meta.baseTitle;
|
|
1566
|
+
const effectiveLocation = slot.override?.location || meta.baseLocation;
|
|
1567
|
+
const allDay = Boolean(meta.recurrence.allDay);
|
|
1568
|
+
const calendarLinks = buildCalendarLinks({
|
|
1569
|
+
documentId: meta.docId,
|
|
1570
|
+
title: effectiveTitle,
|
|
1571
|
+
startDate: slot.startDate,
|
|
1572
|
+
endDate: slot.endDate,
|
|
1573
|
+
allDay,
|
|
1574
|
+
summary: meta.summary,
|
|
1575
|
+
location: effectiveLocation,
|
|
1576
|
+
occurrenceDate: slot.occurrenceDate
|
|
1577
|
+
});
|
|
1578
|
+
const alternateDates = resolveOccurrenceAlternateDates({
|
|
1579
|
+
occurrenceDate: slot.occurrenceDate,
|
|
1580
|
+
baseDateKey: meta.baseDateKey,
|
|
1581
|
+
isRecurring: slot.isRecurring,
|
|
1582
|
+
doc: meta.doc,
|
|
1583
|
+
source: meta.source,
|
|
1584
|
+
secondaryCalendars: meta.secondaryCalendars,
|
|
1585
|
+
locale: meta.locale
|
|
1586
|
+
});
|
|
1587
|
+
return {
|
|
1588
|
+
occurrenceId: `${meta.source.id}:${meta.docId}:${slot.occurrenceDate}`,
|
|
1589
|
+
sourceId: meta.source.id,
|
|
1590
|
+
sourceLabel: meta.source.label,
|
|
1591
|
+
uid: meta.source.uid,
|
|
1592
|
+
documentId: meta.docId,
|
|
1593
|
+
locale: meta.docLocale,
|
|
1594
|
+
occurrenceDate: slot.occurrenceDate,
|
|
1595
|
+
startDate: slot.startDate,
|
|
1596
|
+
endDate: slot.endDate,
|
|
1597
|
+
allDay,
|
|
1598
|
+
timezone: meta.recurrence.timezone,
|
|
1599
|
+
title: effectiveTitle,
|
|
1600
|
+
slug: meta.slug,
|
|
1601
|
+
summary: optStr(meta.summary),
|
|
1602
|
+
location: optStr(effectiveLocation),
|
|
1603
|
+
category: meta.categoryVal,
|
|
1604
|
+
status: meta.statusVal,
|
|
1605
|
+
color: meta.color,
|
|
1606
|
+
isRecurring: slot.isRecurring,
|
|
1607
|
+
rrule: optStr(meta.recurrence.rrule),
|
|
1608
|
+
rscale: slot.rscale,
|
|
1609
|
+
isOverridden: slot.isOverridden,
|
|
1610
|
+
calendarLinks,
|
|
1611
|
+
alternateDates: Object.keys(alternateDates).length > 0 ? alternateDates : void 0,
|
|
1612
|
+
customFields: meta.customFields
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
function matchesCategoryParam(categoryVal, filterCategory) {
|
|
1616
|
+
if (!filterCategory || filterCategory === "all") return true;
|
|
1617
|
+
return categoryVal.toLowerCase() === filterCategory.toLowerCase();
|
|
1618
|
+
}
|
|
1619
|
+
function resolveDocRecurrence(doc, source, defaultTimezone, allDay) {
|
|
1620
|
+
const rawRecurrence = source.fields.rrule ? doc[source.fields.rrule] : null;
|
|
1621
|
+
const tz = typeof doc.timezone === "string" && doc.timezone ? doc.timezone : defaultTimezone;
|
|
1622
|
+
const recurrence = parseRRuleFieldValue(rawRecurrence, tz);
|
|
1623
|
+
if (allDay) recurrence.allDay = true;
|
|
1624
|
+
return recurrence;
|
|
1625
|
+
}
|
|
1626
|
+
function buildDocExpansionMeta(doc, docId, source, recurrence, categoryVal, baseDateKey, settings2, locale = "en") {
|
|
1627
|
+
return {
|
|
1628
|
+
doc,
|
|
1629
|
+
docId,
|
|
1630
|
+
docLocale: typeof doc.locale === "string" ? optStr(doc.locale) : void 0,
|
|
1631
|
+
source,
|
|
1632
|
+
recurrence,
|
|
1633
|
+
baseTitle: String(doc[source.fields.title] ?? doc.title ?? "Untitled"),
|
|
1634
|
+
baseLocation: readField(doc, source.fields.location),
|
|
1635
|
+
summary: readField(doc, source.fields.summary),
|
|
1636
|
+
slug: optStr(readField(doc, source.fields.slug)),
|
|
1637
|
+
categoryVal: optStr(categoryVal),
|
|
1638
|
+
statusVal: mapStatus(
|
|
1639
|
+
source.fields.status ? doc[source.fields.status] : doc.status
|
|
1640
|
+
),
|
|
1641
|
+
color: resolveDocColor(doc, source, categoryVal),
|
|
1642
|
+
baseDateKey,
|
|
1643
|
+
customFields: extractCustomFields(doc, source.fields.customFields),
|
|
1644
|
+
secondaryCalendars: settings2.secondaryCalendars ?? [],
|
|
1645
|
+
locale
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
function expandDocumentOccurrences(doc, source, settings2, params, windowStart, windowEnd) {
|
|
1649
|
+
const docId = String(doc.documentId ?? doc.id ?? "");
|
|
1650
|
+
if (!docId) return [];
|
|
1651
|
+
const startEnd = resolveStartEndFromDocument(doc, source);
|
|
1652
|
+
if (!startEnd) return [];
|
|
1653
|
+
const categoryVal = readField(doc, source.fields.category);
|
|
1654
|
+
if (!matchesCategoryParam(categoryVal, params.category)) return [];
|
|
1655
|
+
const recurrence = resolveDocRecurrence(
|
|
1656
|
+
doc,
|
|
1657
|
+
source,
|
|
1658
|
+
settings2.defaultTimezone,
|
|
1659
|
+
startEnd.allDay
|
|
1660
|
+
);
|
|
1661
|
+
const slots = expandRuleSlots({
|
|
1662
|
+
baseStartDate: startEnd.startDate,
|
|
1663
|
+
baseEndDate: startEnd.endDate,
|
|
1664
|
+
recurrence,
|
|
1665
|
+
windowStart,
|
|
1666
|
+
windowEnd
|
|
1667
|
+
});
|
|
1668
|
+
if (slots.length === 0) return [];
|
|
1669
|
+
const meta = buildDocExpansionMeta(
|
|
1670
|
+
doc,
|
|
1671
|
+
docId,
|
|
1672
|
+
source,
|
|
1673
|
+
recurrence,
|
|
1674
|
+
categoryVal,
|
|
1675
|
+
startEnd.startDate.slice(0, 10),
|
|
1676
|
+
settings2,
|
|
1677
|
+
params.locale
|
|
1678
|
+
);
|
|
1679
|
+
return slots.map((slot) => buildOccurrenceItem(slot, meta));
|
|
1680
|
+
}
|
|
1681
|
+
const occurrence = ({ strapi }) => {
|
|
1682
|
+
const getSettingsService = () => getCalendarService(strapi, PLUGIN_ID, "settings");
|
|
1683
|
+
return {
|
|
1684
|
+
registerCalendarFormatter: registerCalendarSystemFormatter,
|
|
1685
|
+
unregisterCalendarFormatter: unregisterCalendarSystemFormatter,
|
|
1686
|
+
formatDateInCalendarSystem,
|
|
1687
|
+
async getOccurrences(params) {
|
|
1688
|
+
const settings2 = await getSettingsService().getSettings();
|
|
1689
|
+
const { windowStart, windowEnd } = resolveQueryWindow(params);
|
|
1690
|
+
const activeSources = (settings2.sources ?? []).filter(
|
|
1691
|
+
(s) => s.enabled && (!params.publicOnly || s.publicFeed !== false) && matchesSourceFilter(s, params.sourceId)
|
|
1692
|
+
);
|
|
1693
|
+
const allOccurrences = [];
|
|
1694
|
+
for (const source of activeSources) {
|
|
1695
|
+
const docs = await fetchDocumentsForSource(
|
|
1696
|
+
strapi,
|
|
1697
|
+
source,
|
|
1698
|
+
params,
|
|
1699
|
+
windowStart,
|
|
1700
|
+
windowEnd
|
|
1701
|
+
);
|
|
1702
|
+
for (const doc of docs) {
|
|
1703
|
+
const items = expandDocumentOccurrences(
|
|
1704
|
+
doc,
|
|
1705
|
+
source,
|
|
1706
|
+
settings2,
|
|
1707
|
+
params,
|
|
1708
|
+
windowStart,
|
|
1709
|
+
windowEnd
|
|
1710
|
+
);
|
|
1711
|
+
allOccurrences.push(...items);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
return allOccurrences.sort(
|
|
1715
|
+
(a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
|
|
1716
|
+
);
|
|
1717
|
+
},
|
|
1718
|
+
async rescheduleOccurrence(params) {
|
|
1719
|
+
const settings2 = await getSettingsService().getSettings();
|
|
1720
|
+
const sources = settings2.sources ?? [];
|
|
1721
|
+
const source = params.sourceId || params.uid ? sources.find(
|
|
1722
|
+
(s) => s.id === params.sourceId || s.uid === params.uid
|
|
1723
|
+
) : sources[0];
|
|
1724
|
+
if (!source) throw new Error("Target calendar source is not configured.");
|
|
1725
|
+
const localeOpt = params.locale && params.locale !== "auto" ? { locale: params.locale } : {};
|
|
1726
|
+
const existing = await strapi.documents(source.uid).findOne({ documentId: params.documentId, ...localeOpt });
|
|
1727
|
+
if (!existing) {
|
|
1728
|
+
throw new Error(
|
|
1729
|
+
`Document ${params.documentId} not found in ${source.uid}.`
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
const rruleField = source.fields.rrule;
|
|
1733
|
+
const recurrence = rruleField ? parseRRuleFieldValue(existing[rruleField], settings2.defaultTimezone) : null;
|
|
1734
|
+
const data = recurrence?.rrule && !params.applyToSeries && rruleField ? buildRecurrenceOverridePayload(recurrence, rruleField, params) : buildDirectUpdatePayload(params, source, existing);
|
|
1735
|
+
const updatedDoc = await strapi.documents(source.uid).update({ documentId: params.documentId, data, ...localeOpt });
|
|
1736
|
+
return updatedDoc ?? {};
|
|
1737
|
+
},
|
|
1738
|
+
async quickCreateEntry(params) {
|
|
1739
|
+
const settings2 = await getSettingsService().getSettings();
|
|
1740
|
+
const source = resolveTargetSource(
|
|
1741
|
+
settings2.sources ?? [],
|
|
1742
|
+
params.sourceId
|
|
1743
|
+
);
|
|
1744
|
+
if (!source)
|
|
1745
|
+
throw new Error("No calendar collection source is configured.");
|
|
1746
|
+
const contentTypes = strapi.contentTypes ?? {};
|
|
1747
|
+
const { data, shouldPublish } = buildQuickCreatePayload(
|
|
1748
|
+
params,
|
|
1749
|
+
source,
|
|
1750
|
+
settings2.defaultTimezone,
|
|
1751
|
+
contentTypes[source.uid]
|
|
1752
|
+
);
|
|
1753
|
+
if (shouldPublish) {
|
|
1754
|
+
try {
|
|
1755
|
+
return await strapi.documents(source.uid).create({ data, status: "published" });
|
|
1756
|
+
} catch {
|
|
1757
|
+
return await strapi.documents(source.uid).create({ data });
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return strapi.documents(source.uid).create({ data });
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
};
|
|
1764
|
+
const registry = ({ strapi: _strapi }) => {
|
|
1765
|
+
const registeredSources = /* @__PURE__ */ new Map();
|
|
1766
|
+
const registeredCalendars = /* @__PURE__ */ new Map();
|
|
1767
|
+
return {
|
|
1768
|
+
registerSource(source) {
|
|
1769
|
+
if (!source || !source.id || !source.uid) {
|
|
1770
|
+
throw new Error("Calendar source registration requires id and uid.");
|
|
1771
|
+
}
|
|
1772
|
+
registeredSources.set(source.id, source);
|
|
1773
|
+
},
|
|
1774
|
+
unregisterSource(sourceId) {
|
|
1775
|
+
registeredSources.delete(sourceId);
|
|
1776
|
+
},
|
|
1777
|
+
getRegisteredSources() {
|
|
1778
|
+
return Array.from(registeredSources.values());
|
|
1779
|
+
},
|
|
1780
|
+
registerSecondaryCalendar(calendar) {
|
|
1781
|
+
if (!calendar || !calendar.id || !calendar.intlCalendar) {
|
|
1782
|
+
throw new Error(
|
|
1783
|
+
"Secondary calendar registration requires id and intlCalendar."
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
registeredCalendars.set(calendar.id, calendar);
|
|
1787
|
+
},
|
|
1788
|
+
getRegisteredSecondaryCalendars() {
|
|
1789
|
+
return Array.from(registeredCalendars.values());
|
|
1790
|
+
},
|
|
1791
|
+
registerCalendarFormatter(calendarId, formatter) {
|
|
1792
|
+
registerCalendarSystemFormatter(calendarId, formatter);
|
|
1793
|
+
},
|
|
1794
|
+
unregisterCalendarFormatter(calendarId) {
|
|
1795
|
+
unregisterCalendarSystemFormatter(calendarId);
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
};
|
|
1799
|
+
const CALENDAR_COLOR_PALETTES = {
|
|
1800
|
+
default: {
|
|
1801
|
+
id: "default",
|
|
1802
|
+
label: "Theme Default",
|
|
1803
|
+
lightPrimary: "#0284C7",
|
|
1804
|
+
darkPrimary: "#38BDF8",
|
|
1805
|
+
lightHeaderBg: "#E0F2FE",
|
|
1806
|
+
lightHeaderText: "#0C4A6E",
|
|
1807
|
+
lightTodayBg: "#F0F9FF",
|
|
1808
|
+
lightBadgeBg: "#E0F2FE",
|
|
1809
|
+
lightBadgeText: "#0369A1",
|
|
1810
|
+
lightBadgeBorder: "#7DD3FC",
|
|
1811
|
+
darkHeaderBg: "#1E293B",
|
|
1812
|
+
darkHeaderText: "#E2E8F0",
|
|
1813
|
+
darkTodayBg: "#172554",
|
|
1814
|
+
darkBadgeBg: "#1E293B",
|
|
1815
|
+
darkBadgeText: "#7DD3FC",
|
|
1816
|
+
darkBadgeBorder: "#0369A1"
|
|
1817
|
+
},
|
|
1818
|
+
purple: {
|
|
1819
|
+
id: "purple",
|
|
1820
|
+
label: "Monarch Purple",
|
|
1821
|
+
lightPrimary: "#6750A4",
|
|
1822
|
+
darkPrimary: "#D0BCFF",
|
|
1823
|
+
lightHeaderBg: "#F3EDF7",
|
|
1824
|
+
lightHeaderText: "#381E72",
|
|
1825
|
+
lightTodayBg: "#F7F2FA",
|
|
1826
|
+
lightBadgeBg: "#EADDFF",
|
|
1827
|
+
lightBadgeText: "#4F378B",
|
|
1828
|
+
lightBadgeBorder: "#D0BCFF",
|
|
1829
|
+
darkHeaderBg: "#211F26",
|
|
1830
|
+
darkHeaderText: "#E6E0E9",
|
|
1831
|
+
darkTodayBg: "#2B223C",
|
|
1832
|
+
darkBadgeBg: "#381E72",
|
|
1833
|
+
darkBadgeText: "#EADDFF",
|
|
1834
|
+
darkBadgeBorder: "#4F378B"
|
|
1835
|
+
},
|
|
1836
|
+
blue: {
|
|
1837
|
+
id: "blue",
|
|
1838
|
+
label: "Forma / Pulse Blue",
|
|
1839
|
+
lightPrimary: "#0F6CBD",
|
|
1840
|
+
darkPrimary: "#60A5FA",
|
|
1841
|
+
lightHeaderBg: "#EFF6FC",
|
|
1842
|
+
lightHeaderText: "#0A4478",
|
|
1843
|
+
lightTodayBg: "#F0F7FF",
|
|
1844
|
+
lightBadgeBg: "#DBEAFE",
|
|
1845
|
+
lightBadgeText: "#1E40AF",
|
|
1846
|
+
lightBadgeBorder: "#93C5FD",
|
|
1847
|
+
darkHeaderBg: "#172130",
|
|
1848
|
+
darkHeaderText: "#DBEAFE",
|
|
1849
|
+
darkTodayBg: "#172554",
|
|
1850
|
+
darkBadgeBg: "#1E3A8A",
|
|
1851
|
+
darkBadgeText: "#BFDBFE",
|
|
1852
|
+
darkBadgeBorder: "#2563EB"
|
|
1853
|
+
},
|
|
1854
|
+
indigo: {
|
|
1855
|
+
id: "indigo",
|
|
1856
|
+
label: "Breezy Indigo",
|
|
1857
|
+
lightPrimary: "#4F46E5",
|
|
1858
|
+
darkPrimary: "#818CF8",
|
|
1859
|
+
lightHeaderBg: "#EEF2FF",
|
|
1860
|
+
lightHeaderText: "#312E81",
|
|
1861
|
+
lightTodayBg: "#EEF2FF",
|
|
1862
|
+
lightBadgeBg: "#E0E7FF",
|
|
1863
|
+
lightBadgeText: "#3730A3",
|
|
1864
|
+
lightBadgeBorder: "#A5B4FC",
|
|
1865
|
+
darkHeaderBg: "#1E1B4B",
|
|
1866
|
+
darkHeaderText: "#E0E7FF",
|
|
1867
|
+
darkTodayBg: "#1E1B4B",
|
|
1868
|
+
darkBadgeBg: "#312E81",
|
|
1869
|
+
darkBadgeText: "#C7D2FE",
|
|
1870
|
+
darkBadgeBorder: "#4F46E5"
|
|
1871
|
+
},
|
|
1872
|
+
emerald: {
|
|
1873
|
+
id: "emerald",
|
|
1874
|
+
label: "Emerald Green",
|
|
1875
|
+
lightPrimary: "#059669",
|
|
1876
|
+
darkPrimary: "#34D399",
|
|
1877
|
+
lightHeaderBg: "#ECFDF5",
|
|
1878
|
+
lightHeaderText: "#065F46",
|
|
1879
|
+
lightTodayBg: "#F0FDF4",
|
|
1880
|
+
lightBadgeBg: "#D1FAE5",
|
|
1881
|
+
lightBadgeText: "#065F46",
|
|
1882
|
+
lightBadgeBorder: "#6EE7B7",
|
|
1883
|
+
darkHeaderBg: "#064E3B",
|
|
1884
|
+
darkHeaderText: "#D1FAE5",
|
|
1885
|
+
darkTodayBg: "#064E3B",
|
|
1886
|
+
darkBadgeBg: "#065F46",
|
|
1887
|
+
darkBadgeText: "#A7F3D0",
|
|
1888
|
+
darkBadgeBorder: "#059669"
|
|
1889
|
+
},
|
|
1890
|
+
rose: {
|
|
1891
|
+
id: "rose",
|
|
1892
|
+
label: "Pulse Rose",
|
|
1893
|
+
lightPrimary: "#E11D48",
|
|
1894
|
+
darkPrimary: "#FB7185",
|
|
1895
|
+
lightHeaderBg: "#FFF1F2",
|
|
1896
|
+
lightHeaderText: "#881337",
|
|
1897
|
+
lightTodayBg: "#FFF1F2",
|
|
1898
|
+
lightBadgeBg: "#FFE4E6",
|
|
1899
|
+
lightBadgeText: "#9F1239",
|
|
1900
|
+
lightBadgeBorder: "#FDA4AF",
|
|
1901
|
+
darkHeaderBg: "#3B0A19",
|
|
1902
|
+
darkHeaderText: "#FFE4E6",
|
|
1903
|
+
darkTodayBg: "#4C0519",
|
|
1904
|
+
darkBadgeBg: "#881337",
|
|
1905
|
+
darkBadgeText: "#FECDD3",
|
|
1906
|
+
darkBadgeBorder: "#E11D48"
|
|
1907
|
+
},
|
|
1908
|
+
amber: {
|
|
1909
|
+
id: "amber",
|
|
1910
|
+
label: "Monarch Amber",
|
|
1911
|
+
lightPrimary: "#D97706",
|
|
1912
|
+
darkPrimary: "#FBBF24",
|
|
1913
|
+
lightHeaderBg: "#FFFBEB",
|
|
1914
|
+
lightHeaderText: "#78350F",
|
|
1915
|
+
lightTodayBg: "#FEF3C7",
|
|
1916
|
+
lightBadgeBg: "#FEF3C7",
|
|
1917
|
+
lightBadgeText: "#92400E",
|
|
1918
|
+
lightBadgeBorder: "#FCD34D",
|
|
1919
|
+
darkHeaderBg: "#3B2506",
|
|
1920
|
+
darkHeaderText: "#FEF3C7",
|
|
1921
|
+
darkTodayBg: "#451A03",
|
|
1922
|
+
darkBadgeBg: "#78350F",
|
|
1923
|
+
darkBadgeText: "#FDE68A",
|
|
1924
|
+
darkBadgeBorder: "#D97706"
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
const CALENDAR_THEME_PRESETS = {
|
|
1928
|
+
breeze: {
|
|
1929
|
+
id: "breeze",
|
|
1930
|
+
label: "Breezy (Modern Default)",
|
|
1931
|
+
description: "FullCalendar Breezy style — airy sky & slate grid, soft-tinted event pills, and rounded-lg container",
|
|
1932
|
+
category: "modern",
|
|
1933
|
+
fontFamily: "'Inter Variable', Inter, system-ui, sans-serif",
|
|
1934
|
+
containerRadius: "10px",
|
|
1935
|
+
containerShadow: "0 1px 3px rgba(15, 23, 42, 0.06)",
|
|
1936
|
+
eventRadius: "6px",
|
|
1937
|
+
eventStyle: "tinted",
|
|
1938
|
+
dayNumberAlign: "start",
|
|
1939
|
+
todayHeaderAccent: "pill",
|
|
1940
|
+
primaryColor: "#0284C7",
|
|
1941
|
+
headerBg: "#F8FAFC",
|
|
1942
|
+
headerText: "#0F172A",
|
|
1943
|
+
cellBg: "#FFFFFF",
|
|
1944
|
+
cellAltBg: "#F8FAFC",
|
|
1945
|
+
cellHoverBg: "#F0F9FF",
|
|
1946
|
+
cellText: "#111827",
|
|
1947
|
+
cellMutedText: "#6B7280",
|
|
1948
|
+
borderColor: "#E5E7EB",
|
|
1949
|
+
todayBg: "#F0F9FF",
|
|
1950
|
+
todayBorder: "#0284C7",
|
|
1951
|
+
todayNumberBg: "#0284C7",
|
|
1952
|
+
todayNumberText: "#FFFFFF",
|
|
1953
|
+
badgeBg: "#E0F2FE",
|
|
1954
|
+
badgeText: "#0369A1",
|
|
1955
|
+
badgeBorder: "#7DD3FC"
|
|
1956
|
+
},
|
|
1957
|
+
monarch: {
|
|
1958
|
+
id: "monarch",
|
|
1959
|
+
label: "Monarch (Material 3)",
|
|
1960
|
+
description: "FullCalendar Monarch style — pill-shaped headers, centered day badges, tonal surfaces & rounded-xl frame",
|
|
1961
|
+
category: "modern",
|
|
1962
|
+
fontFamily: "'Roboto Variable', Roboto, system-ui, sans-serif",
|
|
1963
|
+
containerRadius: "14px",
|
|
1964
|
+
containerShadow: "0 2px 8px rgba(103, 80, 164, 0.08)",
|
|
1965
|
+
eventRadius: "6px",
|
|
1966
|
+
eventStyle: "solid",
|
|
1967
|
+
dayNumberAlign: "center",
|
|
1968
|
+
todayHeaderAccent: "pill",
|
|
1969
|
+
primaryColor: "#6750A4",
|
|
1970
|
+
headerBg: "#F7F2FA",
|
|
1971
|
+
headerText: "#1D1B20",
|
|
1972
|
+
cellBg: "#FFFFFF",
|
|
1973
|
+
cellAltBg: "#FAF8FC",
|
|
1974
|
+
cellHoverBg: "#F3EDF7",
|
|
1975
|
+
cellText: "#0C090C",
|
|
1976
|
+
cellMutedText: "#79747E",
|
|
1977
|
+
borderColor: "#DEDCE1",
|
|
1978
|
+
todayBg: "#F7F2FA",
|
|
1979
|
+
todayBorder: "#6750A4",
|
|
1980
|
+
todayNumberBg: "#895264",
|
|
1981
|
+
todayNumberText: "#FFFFFF",
|
|
1982
|
+
badgeBg: "#EADDFF",
|
|
1983
|
+
badgeText: "#4F378B",
|
|
1984
|
+
badgeBorder: "#D0BCFF"
|
|
1985
|
+
},
|
|
1986
|
+
forma: {
|
|
1987
|
+
id: "forma",
|
|
1988
|
+
label: "Forma (Fluent Enterprise)",
|
|
1989
|
+
description: "FullCalendar Forma style — architectural 6px left-accent event bars, top-accent Today bar & crisp 4px corners",
|
|
1990
|
+
category: "modern",
|
|
1991
|
+
fontFamily: "'Noto Sans Variable', 'Noto Sans', system-ui, sans-serif",
|
|
1992
|
+
containerRadius: "4px",
|
|
1993
|
+
containerShadow: "0 1px 2px rgba(0, 0, 0, 0.06)",
|
|
1994
|
+
eventRadius: "3px",
|
|
1995
|
+
eventStyle: "accent-bar",
|
|
1996
|
+
dayNumberAlign: "start",
|
|
1997
|
+
todayHeaderAccent: "top-bar",
|
|
1998
|
+
primaryColor: "#0F6CBD",
|
|
1999
|
+
headerBg: "#FFFFFF",
|
|
2000
|
+
headerText: "#030712",
|
|
2001
|
+
cellBg: "#FFFFFF",
|
|
2002
|
+
cellAltBg: "#F9FAFB",
|
|
2003
|
+
cellHoverBg: "#EFF6FC",
|
|
2004
|
+
cellText: "#030712",
|
|
2005
|
+
cellMutedText: "#6B7280",
|
|
2006
|
+
borderColor: "#E0E0E0",
|
|
2007
|
+
todayBg: "#F5F9FF",
|
|
2008
|
+
todayBorder: "#0F6CBD",
|
|
2009
|
+
todayNumberBg: "#0F6CBD",
|
|
2010
|
+
todayNumberText: "#FFFFFF",
|
|
2011
|
+
badgeBg: "#EFF6FC",
|
|
2012
|
+
badgeText: "#0F6CBD",
|
|
2013
|
+
badgeBorder: "#B4D6FA"
|
|
2014
|
+
},
|
|
2015
|
+
pulse: {
|
|
2016
|
+
id: "pulse",
|
|
2017
|
+
label: "Pulse (Apple / macOS)",
|
|
2018
|
+
description: "FullCalendar Pulse style — right-aligned day numbers, crimson Today circle, elevated controls & dotted minor slots",
|
|
2019
|
+
category: "modern",
|
|
2020
|
+
fontFamily: "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif",
|
|
2021
|
+
containerRadius: "8px",
|
|
2022
|
+
containerShadow: "0 1px 3px rgba(0, 0, 0, 0.12)",
|
|
2023
|
+
eventRadius: "6px",
|
|
2024
|
+
eventStyle: "solid",
|
|
2025
|
+
dayNumberAlign: "end",
|
|
2026
|
+
todayHeaderAccent: "pill",
|
|
2027
|
+
primaryColor: "#117AFF",
|
|
2028
|
+
headerBg: "#FFFFFF",
|
|
2029
|
+
headerText: "#232D3E",
|
|
2030
|
+
cellBg: "#FFFFFF",
|
|
2031
|
+
cellAltBg: "#F9FAFB",
|
|
2032
|
+
cellHoverBg: "#F0F7FF",
|
|
2033
|
+
cellText: "#232D3E",
|
|
2034
|
+
cellMutedText: "#545C6C",
|
|
2035
|
+
borderColor: "#E5E7EB",
|
|
2036
|
+
todayBg: "#FFF5F5",
|
|
2037
|
+
todayBorder: "#FD453B",
|
|
2038
|
+
todayNumberBg: "#FD453B",
|
|
2039
|
+
todayNumberText: "#FFFFFF",
|
|
2040
|
+
badgeBg: "#EFF6FF",
|
|
2041
|
+
badgeText: "#1D4ED8",
|
|
2042
|
+
badgeBorder: "#BFDBFE"
|
|
2043
|
+
},
|
|
2044
|
+
classic: {
|
|
2045
|
+
id: "classic",
|
|
2046
|
+
label: "Classic (FullCalendar Standard)",
|
|
2047
|
+
description: "Traditional FullCalendar v6/v7 studio grid with slate headers, warm yellow Today cell tint & solid events",
|
|
2048
|
+
category: "modern",
|
|
2049
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
2050
|
+
containerRadius: "6px",
|
|
2051
|
+
containerShadow: "none",
|
|
2052
|
+
eventRadius: "4px",
|
|
2053
|
+
eventStyle: "solid",
|
|
2054
|
+
dayNumberAlign: "end",
|
|
2055
|
+
todayHeaderAccent: "tint",
|
|
2056
|
+
primaryColor: "#3788D8",
|
|
2057
|
+
headerBg: "#F8FAFC",
|
|
2058
|
+
headerText: "#1E293B",
|
|
2059
|
+
cellBg: "#FFFFFF",
|
|
2060
|
+
cellAltBg: "#F8FAFC",
|
|
2061
|
+
cellHoverBg: "#F1F5F9",
|
|
2062
|
+
cellText: "#030712",
|
|
2063
|
+
cellMutedText: "#6B7280",
|
|
2064
|
+
borderColor: "#DDDDDD",
|
|
2065
|
+
todayBg: "#FEFCE8",
|
|
2066
|
+
todayBorder: "#EAB308",
|
|
2067
|
+
todayNumberBg: "#334155",
|
|
2068
|
+
todayNumberText: "#FFFFFF",
|
|
2069
|
+
badgeBg: "#F1F5F9",
|
|
2070
|
+
badgeText: "#334155",
|
|
2071
|
+
badgeBorder: "#CBD5E1"
|
|
2072
|
+
},
|
|
2073
|
+
shadcn: {
|
|
2074
|
+
id: "shadcn",
|
|
2075
|
+
label: "Shadcn UI (Zinc Minimal)",
|
|
2076
|
+
description: "Design-system theme matching Shadcn UI — ultra-clean 1px zinc borders, monochrome primary & tinted badges",
|
|
2077
|
+
category: "design-system",
|
|
2078
|
+
fontFamily: "'Inter Variable', Inter, system-ui, sans-serif",
|
|
2079
|
+
containerRadius: "8px",
|
|
2080
|
+
containerShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
|
2081
|
+
eventRadius: "5px",
|
|
2082
|
+
eventStyle: "tinted",
|
|
2083
|
+
dayNumberAlign: "start",
|
|
2084
|
+
todayHeaderAccent: "pill",
|
|
2085
|
+
primaryColor: "#18181B",
|
|
2086
|
+
headerBg: "#FAFAFA",
|
|
2087
|
+
headerText: "#09090B",
|
|
2088
|
+
cellBg: "#FFFFFF",
|
|
2089
|
+
cellAltBg: "#FAFAFA",
|
|
2090
|
+
cellHoverBg: "#F4F4F5",
|
|
2091
|
+
cellText: "#09090B",
|
|
2092
|
+
cellMutedText: "#71717A",
|
|
2093
|
+
borderColor: "#E4E4E7",
|
|
2094
|
+
todayBg: "#F4F4F5",
|
|
2095
|
+
todayBorder: "#18181B",
|
|
2096
|
+
todayNumberBg: "#18181B",
|
|
2097
|
+
todayNumberText: "#FAFAFA",
|
|
2098
|
+
badgeBg: "#F4F4F5",
|
|
2099
|
+
badgeText: "#18181B",
|
|
2100
|
+
badgeBorder: "#D4D4D8"
|
|
2101
|
+
},
|
|
2102
|
+
mui: {
|
|
2103
|
+
id: "mui",
|
|
2104
|
+
label: "Material UI (MUI Paper)",
|
|
2105
|
+
description: "Design-system theme matching Material UI — Paper surface elevation, Roboto uppercase headers & Material blue",
|
|
2106
|
+
category: "design-system",
|
|
2107
|
+
fontFamily: "'Roboto Variable', Roboto, Helvetica, Arial, sans-serif",
|
|
2108
|
+
containerRadius: "8px",
|
|
2109
|
+
containerShadow: "0px 2px 4px -1px rgba(0,0,0,0.08), 0px 4px 5px 0px rgba(0,0,0,0.05)",
|
|
2110
|
+
eventRadius: "4px",
|
|
2111
|
+
eventStyle: "solid",
|
|
2112
|
+
dayNumberAlign: "center",
|
|
2113
|
+
todayHeaderAccent: "pill",
|
|
2114
|
+
primaryColor: "#1976D2",
|
|
2115
|
+
headerBg: "#F5F7FA",
|
|
2116
|
+
headerText: "#1E293B",
|
|
2117
|
+
cellBg: "#FFFFFF",
|
|
2118
|
+
cellAltBg: "#FAFAFA",
|
|
2119
|
+
cellHoverBg: "#E3F2FD",
|
|
2120
|
+
cellText: "#212121",
|
|
2121
|
+
cellMutedText: "#757575",
|
|
2122
|
+
borderColor: "#E0E0E0",
|
|
2123
|
+
todayBg: "#E3F2FD",
|
|
2124
|
+
todayBorder: "#1976D2",
|
|
2125
|
+
todayNumberBg: "#1976D2",
|
|
2126
|
+
todayNumberText: "#FFFFFF",
|
|
2127
|
+
badgeBg: "#E3F2FD",
|
|
2128
|
+
badgeText: "#1565C0",
|
|
2129
|
+
badgeBorder: "#90CAF9"
|
|
2130
|
+
},
|
|
2131
|
+
emerald: {
|
|
2132
|
+
id: "emerald",
|
|
2133
|
+
label: "Emerald Mosque",
|
|
2134
|
+
description: "Lush emerald & teal palette inspired by traditional Islamic architecture",
|
|
2135
|
+
category: "heritage",
|
|
2136
|
+
fontFamily: "'Inter Variable', Inter, system-ui, sans-serif",
|
|
2137
|
+
containerRadius: "10px",
|
|
2138
|
+
containerShadow: "0 1px 3px rgba(13, 148, 136, 0.08)",
|
|
2139
|
+
eventRadius: "6px",
|
|
2140
|
+
eventStyle: "solid",
|
|
2141
|
+
dayNumberAlign: "start",
|
|
2142
|
+
todayHeaderAccent: "pill",
|
|
2143
|
+
primaryColor: "#0D9488",
|
|
2144
|
+
headerBg: "#CCFBF1",
|
|
2145
|
+
headerText: "#115E59",
|
|
2146
|
+
cellBg: "#FFFFFF",
|
|
2147
|
+
cellAltBg: "#F0FDFA",
|
|
2148
|
+
cellHoverBg: "#CCFBF1",
|
|
2149
|
+
cellText: "#0F172A",
|
|
2150
|
+
cellMutedText: "#475569",
|
|
2151
|
+
borderColor: "#99F6E4",
|
|
2152
|
+
todayBg: "#F0FDFA",
|
|
2153
|
+
todayBorder: "#0D9488",
|
|
2154
|
+
todayNumberBg: "#0D9488",
|
|
2155
|
+
todayNumberText: "#FFFFFF",
|
|
2156
|
+
badgeBg: "#CCFBF1",
|
|
2157
|
+
badgeText: "#0F766E",
|
|
2158
|
+
badgeBorder: "#5EEAD4"
|
|
2159
|
+
},
|
|
2160
|
+
sand: {
|
|
2161
|
+
id: "sand",
|
|
2162
|
+
label: "Warm Sand & Gold",
|
|
2163
|
+
description: "Warm parchment & amber tones for high-comfort reading",
|
|
2164
|
+
category: "heritage",
|
|
2165
|
+
fontFamily: "'Noto Sans Variable', 'Noto Sans', system-ui, sans-serif",
|
|
2166
|
+
containerRadius: "10px",
|
|
2167
|
+
containerShadow: "0 1px 3px rgba(180, 83, 9, 0.08)",
|
|
2168
|
+
eventRadius: "6px",
|
|
2169
|
+
eventStyle: "solid",
|
|
2170
|
+
dayNumberAlign: "start",
|
|
2171
|
+
todayHeaderAccent: "pill",
|
|
2172
|
+
primaryColor: "#B45309",
|
|
2173
|
+
headerBg: "#FEF3C7",
|
|
2174
|
+
headerText: "#78350F",
|
|
2175
|
+
cellBg: "#FFFBEB",
|
|
2176
|
+
cellAltBg: "#FEF9C3",
|
|
2177
|
+
cellHoverBg: "#FEF3C7",
|
|
2178
|
+
cellText: "#1C1917",
|
|
2179
|
+
cellMutedText: "#78716C",
|
|
2180
|
+
borderColor: "#FDE68A",
|
|
2181
|
+
todayBg: "#FEF3C7",
|
|
2182
|
+
todayBorder: "#D97706",
|
|
2183
|
+
todayNumberBg: "#B45309",
|
|
2184
|
+
todayNumberText: "#FFFFFF",
|
|
2185
|
+
badgeBg: "#FEF3C7",
|
|
2186
|
+
badgeText: "#92400E",
|
|
2187
|
+
badgeBorder: "#FCD34D"
|
|
2188
|
+
},
|
|
2189
|
+
midnight: {
|
|
2190
|
+
id: "midnight",
|
|
2191
|
+
label: "Midnight Slate",
|
|
2192
|
+
description: "Deep dark-mode slate grid with luminous sky accents",
|
|
2193
|
+
category: "heritage",
|
|
2194
|
+
fontFamily: "'Inter Variable', Inter, system-ui, sans-serif",
|
|
2195
|
+
containerRadius: "10px",
|
|
2196
|
+
containerShadow: "0 4px 12px rgba(2, 6, 23, 0.35)",
|
|
2197
|
+
eventRadius: "6px",
|
|
2198
|
+
eventStyle: "tinted",
|
|
2199
|
+
dayNumberAlign: "start",
|
|
2200
|
+
todayHeaderAccent: "pill",
|
|
2201
|
+
primaryColor: "#38BDF8",
|
|
2202
|
+
headerBg: "#1E293B",
|
|
2203
|
+
headerText: "#E2E8F0",
|
|
2204
|
+
cellBg: "#0F172A",
|
|
2205
|
+
cellAltBg: "#162032",
|
|
2206
|
+
cellHoverBg: "#1E293B",
|
|
2207
|
+
cellText: "#F8FAFC",
|
|
2208
|
+
cellMutedText: "#94A3B8",
|
|
2209
|
+
borderColor: "#334155",
|
|
2210
|
+
todayBg: "#172554",
|
|
2211
|
+
todayBorder: "#38BDF8",
|
|
2212
|
+
todayNumberBg: "#0284C7",
|
|
2213
|
+
todayNumberText: "#FFFFFF",
|
|
2214
|
+
badgeBg: "#1E293B",
|
|
2215
|
+
badgeText: "#7DD3FC",
|
|
2216
|
+
badgeBorder: "#0369A1"
|
|
2217
|
+
}
|
|
2218
|
+
};
|
|
2219
|
+
const BUILTIN_EVENTS_SOURCE = {
|
|
2220
|
+
id: "events",
|
|
2221
|
+
label: "Events",
|
|
2222
|
+
uid: "plugin::events.event",
|
|
2223
|
+
color: "#4945FF",
|
|
2224
|
+
enabled: true,
|
|
2225
|
+
publicFeed: true,
|
|
2226
|
+
fields: {
|
|
2227
|
+
title: "title",
|
|
2228
|
+
startDate: "startDate",
|
|
2229
|
+
endDate: "endDate",
|
|
2230
|
+
rrule: "recurrence",
|
|
2231
|
+
location: "location",
|
|
2232
|
+
category: "category",
|
|
2233
|
+
status: "eventStatus",
|
|
2234
|
+
summary: "summary",
|
|
2235
|
+
slug: "slug",
|
|
2236
|
+
color: "color"
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
const BUILTIN_BOOKINGS_SOURCE = {
|
|
2240
|
+
id: "bookings",
|
|
2241
|
+
label: "Bookings & Reservations",
|
|
2242
|
+
uid: "plugin::bookings.booking",
|
|
2243
|
+
color: "#0D9488",
|
|
2244
|
+
enabled: true,
|
|
2245
|
+
publicFeed: false,
|
|
2246
|
+
fields: {
|
|
2247
|
+
title: "title",
|
|
2248
|
+
startDate: "startDate",
|
|
2249
|
+
endDate: "endDate",
|
|
2250
|
+
status: "status",
|
|
2251
|
+
location: "assignedHost",
|
|
2252
|
+
summary: "notes",
|
|
2253
|
+
category: "status"
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
const STORE_KEY = "settings";
|
|
2257
|
+
const INTERNAL_PLUGIN_PREFIXES = [
|
|
2258
|
+
"plugin::upload",
|
|
2259
|
+
"plugin::i18n",
|
|
2260
|
+
"plugin::users-permissions",
|
|
2261
|
+
"plugin::content-releases",
|
|
2262
|
+
"plugin::review-workflows",
|
|
2263
|
+
"plugin::navigation",
|
|
2264
|
+
"plugin::email-designer-5",
|
|
2265
|
+
"plugin::strapi-cache",
|
|
2266
|
+
"plugin::typesense",
|
|
2267
|
+
"plugin::preview-button",
|
|
2268
|
+
"plugin::seo"
|
|
2269
|
+
];
|
|
2270
|
+
function appendAutoDetectedPluginSources(mergedSources, contentTypes) {
|
|
2271
|
+
for (const builtin of [BUILTIN_EVENTS_SOURCE, BUILTIN_BOOKINGS_SOURCE]) {
|
|
2272
|
+
if (contentTypes[builtin.uid] && !mergedSources.some((s) => s.uid === builtin.uid)) {
|
|
2273
|
+
mergedSources.push({ ...builtin, fields: { ...builtin.fields } });
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
function mergeUserConfigSources(mergedSources, userSources) {
|
|
2278
|
+
if (!Array.isArray(userSources)) return;
|
|
2279
|
+
for (const src of userSources) {
|
|
2280
|
+
if (!src?.id || !src.uid) continue;
|
|
2281
|
+
const idx = mergedSources.findIndex(
|
|
2282
|
+
(s) => s.id === src.id || s.uid === src.uid
|
|
2283
|
+
);
|
|
2284
|
+
if (idx >= 0) {
|
|
2285
|
+
mergedSources[idx] = {
|
|
2286
|
+
...mergedSources[idx],
|
|
2287
|
+
...src,
|
|
2288
|
+
fields: { ...mergedSources[idx].fields, ...src.fields }
|
|
2289
|
+
};
|
|
2290
|
+
} else {
|
|
2291
|
+
mergedSources.push(src);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
function mergeUserSecondaryCalendars(mergedCalendars, userCalendars) {
|
|
2296
|
+
if (!Array.isArray(userCalendars)) return;
|
|
2297
|
+
for (const cal of userCalendars) {
|
|
2298
|
+
if (!cal?.id || !cal.intlCalendar) continue;
|
|
2299
|
+
const idx = mergedCalendars.findIndex((c) => c.id === cal.id);
|
|
2300
|
+
if (idx >= 0) {
|
|
2301
|
+
mergedCalendars[idx] = { ...mergedCalendars[idx], ...cal };
|
|
2302
|
+
} else {
|
|
2303
|
+
mergedCalendars.push(cal);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
function mergePresetLists(defaults, stored) {
|
|
2308
|
+
const result = [...defaults ?? []];
|
|
2309
|
+
if (!Array.isArray(stored)) return result;
|
|
2310
|
+
for (const item of stored) {
|
|
2311
|
+
if (!item?.id || !item.rrule) continue;
|
|
2312
|
+
const idx = result.findIndex(
|
|
2313
|
+
(p) => p.id === item.id || p.rrule === item.rrule
|
|
2314
|
+
);
|
|
2315
|
+
if (idx >= 0) {
|
|
2316
|
+
result[idx] = { ...result[idx], ...item };
|
|
2317
|
+
} else {
|
|
2318
|
+
result.push(item);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
return result;
|
|
2322
|
+
}
|
|
2323
|
+
function mergeSourceLists(defaults, stored) {
|
|
2324
|
+
return Array.isArray(stored) ? [...stored] : [...defaults ?? []];
|
|
2325
|
+
}
|
|
2326
|
+
function mergeCalendarLists(defaults, stored) {
|
|
2327
|
+
return Array.isArray(stored) ? [...stored] : [...defaults ?? []];
|
|
2328
|
+
}
|
|
2329
|
+
function pickNonEmptyStr(val, fallback, maxLen) {
|
|
2330
|
+
if (typeof val === "string" && val.trim()) {
|
|
2331
|
+
const trimmed = val.trim();
|
|
2332
|
+
return maxLen ? trimmed.slice(0, maxLen) : trimmed;
|
|
2333
|
+
}
|
|
2334
|
+
return fallback;
|
|
2335
|
+
}
|
|
2336
|
+
function pickBool(val, fallback) {
|
|
2337
|
+
return typeof val === "boolean" ? val : fallback;
|
|
2338
|
+
}
|
|
2339
|
+
function pickEnum(val, allowed, fallback) {
|
|
2340
|
+
return typeof val === "string" && allowed.includes(val) ? val : fallback;
|
|
2341
|
+
}
|
|
2342
|
+
function pickArray(val, fallback) {
|
|
2343
|
+
return Array.isArray(val) ? val : fallback;
|
|
2344
|
+
}
|
|
2345
|
+
function pickRecordKey(val, record, fallback) {
|
|
2346
|
+
return typeof val === "string" && val in record ? val : fallback;
|
|
2347
|
+
}
|
|
2348
|
+
const HEX_COLOR_RE = /^#[0-9a-fA-F]{3,8}$/;
|
|
2349
|
+
function pickPrimaryHex(val, fallback) {
|
|
2350
|
+
return typeof val === "string" && HEX_COLOR_RE.test(val.trim()) ? val.trim() : fallback;
|
|
2351
|
+
}
|
|
2352
|
+
function resolveThemePatch(current, patch) {
|
|
2353
|
+
return {
|
|
2354
|
+
theme: pickRecordKey(
|
|
2355
|
+
patch.theme,
|
|
2356
|
+
CALENDAR_THEME_PRESETS,
|
|
2357
|
+
current.theme ?? "breeze"
|
|
2358
|
+
),
|
|
2359
|
+
colorScheme: pickEnum(
|
|
2360
|
+
patch.colorScheme,
|
|
2361
|
+
["auto", "light", "dark"],
|
|
2362
|
+
current.colorScheme ?? "auto"
|
|
2363
|
+
),
|
|
2364
|
+
themePalette: pickRecordKey(
|
|
2365
|
+
patch.themePalette,
|
|
2366
|
+
CALENDAR_COLOR_PALETTES,
|
|
2367
|
+
current.themePalette ?? "default"
|
|
2368
|
+
),
|
|
2369
|
+
secondaryCalendarLanguage: pickEnum(
|
|
2370
|
+
patch.secondaryCalendarLanguage,
|
|
2371
|
+
["local", "ar", "off"],
|
|
2372
|
+
current.secondaryCalendarLanguage ?? "local"
|
|
2373
|
+
),
|
|
2374
|
+
primaryColor: pickPrimaryHex(patch.primaryColor, current.primaryColor)
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
function resolveViewBooleansPatch(current, patch) {
|
|
2378
|
+
return {
|
|
2379
|
+
weekNumbers: pickBool(patch.weekNumbers, current.weekNumbers ?? false),
|
|
2380
|
+
timeFormat24h: pickBool(patch.timeFormat24h, current.timeFormat24h ?? true),
|
|
2381
|
+
showWeekends: pickBool(patch.showWeekends, current.showWeekends),
|
|
2382
|
+
showTodayButton: pickBool(patch.showTodayButton, current.showTodayButton),
|
|
2383
|
+
showCreateButton: pickBool(
|
|
2384
|
+
patch.showCreateButton,
|
|
2385
|
+
current.showCreateButton
|
|
2386
|
+
),
|
|
2387
|
+
monthView: pickBool(patch.monthView, current.monthView),
|
|
2388
|
+
weekView: pickBool(patch.weekView, current.weekView),
|
|
2389
|
+
dayView: pickBool(patch.dayView, current.dayView),
|
|
2390
|
+
agendaView: pickBool(patch.agendaView, current.agendaView),
|
|
2391
|
+
yearView: pickBool(patch.yearView, current.yearView)
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
function sanitizeSourceItem(src) {
|
|
2395
|
+
const categoryColors = src.categoryColors ? Object.fromEntries(
|
|
2396
|
+
Object.entries(src.categoryColors).filter(
|
|
2397
|
+
([, v]) => typeof v === "string" && HEX_COLOR_RE.test(v.trim())
|
|
2398
|
+
)
|
|
2399
|
+
) : void 0;
|
|
2400
|
+
return {
|
|
2401
|
+
...src,
|
|
2402
|
+
color: pickPrimaryHex(src.color, "#4945ff"),
|
|
2403
|
+
...categoryColors ? { categoryColors } : {}
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
function buildUpdatedSettings(current, patch) {
|
|
2407
|
+
const firstDay = typeof patch.firstDayOfWeek === "number" && patch.firstDayOfWeek >= 0 && patch.firstDayOfWeek <= 6 ? Math.floor(patch.firstDayOfWeek) : current.firstDayOfWeek;
|
|
2408
|
+
return {
|
|
2409
|
+
...current,
|
|
2410
|
+
...resolveThemePatch(current, patch),
|
|
2411
|
+
...resolveViewBooleansPatch(current, patch),
|
|
2412
|
+
defaultTimezone: pickNonEmptyStr(
|
|
2413
|
+
patch.defaultTimezone,
|
|
2414
|
+
current.defaultTimezone
|
|
2415
|
+
),
|
|
2416
|
+
defaultLocale: pickNonEmptyStr(
|
|
2417
|
+
patch.defaultLocale,
|
|
2418
|
+
current.defaultLocale ?? "auto"
|
|
2419
|
+
),
|
|
2420
|
+
direction: pickEnum(
|
|
2421
|
+
patch.direction,
|
|
2422
|
+
["auto", "ltr", "rtl"],
|
|
2423
|
+
current.direction ?? "auto"
|
|
2424
|
+
),
|
|
2425
|
+
defaultView: typeof patch.defaultView === "string" ? patch.defaultView : current.defaultView,
|
|
2426
|
+
firstDayOfWeek: firstDay,
|
|
2427
|
+
weekNumberCalculation: pickEnum(
|
|
2428
|
+
patch.weekNumberCalculation,
|
|
2429
|
+
["local", "ISO"],
|
|
2430
|
+
current.weekNumberCalculation ?? "ISO"
|
|
2431
|
+
),
|
|
2432
|
+
startHour: pickNonEmptyStr(patch.startHour, current.startHour),
|
|
2433
|
+
endHour: pickNonEmptyStr(patch.endHour, current.endHour),
|
|
2434
|
+
slotDuration: pickNonEmptyStr(patch.slotDuration, current.slotDuration),
|
|
2435
|
+
feedTitle: pickNonEmptyStr(patch.feedTitle, current.feedTitle, 140),
|
|
2436
|
+
businessHours: pickArray(patch.businessHours, current.businessHours),
|
|
2437
|
+
sources: pickArray(patch.sources, current.sources).map(sanitizeSourceItem),
|
|
2438
|
+
secondaryCalendars: pickArray(
|
|
2439
|
+
patch.secondaryCalendars,
|
|
2440
|
+
current.secondaryCalendars ?? []
|
|
2441
|
+
),
|
|
2442
|
+
recurrencePresets: pickArray(
|
|
2443
|
+
patch.recurrencePresets,
|
|
2444
|
+
current.recurrencePresets ?? []
|
|
2445
|
+
)
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
function isDiscoverableSchema(uid, schema) {
|
|
2449
|
+
if (!schema || typeof schema !== "object") return false;
|
|
2450
|
+
if (!uid.startsWith("api::") && !uid.startsWith("plugin::")) return false;
|
|
2451
|
+
if (INTERNAL_PLUGIN_PREFIXES.some((prefix) => uid.startsWith(prefix)))
|
|
2452
|
+
return false;
|
|
2453
|
+
const typed = schema;
|
|
2454
|
+
return !typed.kind || typed.kind === "collectionType";
|
|
2455
|
+
}
|
|
2456
|
+
function extractSchemaFields(attributes) {
|
|
2457
|
+
const fields = Object.entries(attributes).filter(([, def]) => def && typeof def.type === "string").map(([name, def]) => ({
|
|
2458
|
+
name,
|
|
2459
|
+
type: String(def.customField || def.type)
|
|
2460
|
+
}));
|
|
2461
|
+
if (!fields.some((f) => f.name === "publishedAt")) {
|
|
2462
|
+
fields.push({ name: "publishedAt", type: "datetime" });
|
|
2463
|
+
}
|
|
2464
|
+
if (!fields.some((f) => f.name === "createdAt")) {
|
|
2465
|
+
fields.push({ name: "createdAt", type: "datetime" });
|
|
2466
|
+
}
|
|
2467
|
+
return fields;
|
|
2468
|
+
}
|
|
2469
|
+
function scoreContentTypeUid(uid) {
|
|
2470
|
+
if (uid === "plugin::events.event") return 0;
|
|
2471
|
+
if (uid === "plugin::bookings.booking") return 1;
|
|
2472
|
+
return uid.startsWith("api::") ? 2 : 3;
|
|
2473
|
+
}
|
|
2474
|
+
function mergeStoredWithDefaults(defaults, stored) {
|
|
2475
|
+
return {
|
|
2476
|
+
...defaults,
|
|
2477
|
+
...stored,
|
|
2478
|
+
sources: mergeSourceLists(defaults.sources, stored.sources),
|
|
2479
|
+
secondaryCalendars: mergeCalendarLists(
|
|
2480
|
+
defaults.secondaryCalendars,
|
|
2481
|
+
stored.secondaryCalendars
|
|
2482
|
+
),
|
|
2483
|
+
recurrencePresets: mergePresetLists(
|
|
2484
|
+
defaults.recurrencePresets,
|
|
2485
|
+
stored.recurrencePresets
|
|
2486
|
+
)
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
const settings = ({ strapi }) => {
|
|
2490
|
+
const getStore = () => strapi.store({ type: "plugin", name: PLUGIN_ID });
|
|
2491
|
+
const getRegistryService = () => getCalendarService(strapi, PLUGIN_ID, "registry");
|
|
2492
|
+
const getConfigDefaults = () => {
|
|
2493
|
+
const userConfig = strapi.config.get(`plugin::${PLUGIN_ID}`) ?? {};
|
|
2494
|
+
const registry2 = getRegistryService();
|
|
2495
|
+
const mergedSources = [
|
|
2496
|
+
...DEFAULT_CALENDAR_SETTINGS.sources,
|
|
2497
|
+
...registry2?.getRegisteredSources?.() ?? []
|
|
2498
|
+
];
|
|
2499
|
+
appendAutoDetectedPluginSources(
|
|
2500
|
+
mergedSources,
|
|
2501
|
+
strapi.contentTypes ?? {}
|
|
2502
|
+
);
|
|
2503
|
+
mergeUserConfigSources(mergedSources, userConfig.sources);
|
|
2504
|
+
const mergedCalendars = [
|
|
2505
|
+
...DEFAULT_CALENDAR_SETTINGS.secondaryCalendars ?? [],
|
|
2506
|
+
...registry2?.getRegisteredSecondaryCalendars?.() ?? []
|
|
2507
|
+
];
|
|
2508
|
+
mergeUserSecondaryCalendars(mergedCalendars, userConfig.secondaryCalendars);
|
|
2509
|
+
return {
|
|
2510
|
+
...DEFAULT_CALENDAR_SETTINGS,
|
|
2511
|
+
...userConfig,
|
|
2512
|
+
sources: mergedSources,
|
|
2513
|
+
secondaryCalendars: mergedCalendars,
|
|
2514
|
+
recurrencePresets: mergePresetLists(
|
|
2515
|
+
DEFAULT_CALENDAR_SETTINGS.recurrencePresets,
|
|
2516
|
+
userConfig.recurrencePresets
|
|
2517
|
+
)
|
|
2518
|
+
};
|
|
2519
|
+
};
|
|
2520
|
+
const service = {
|
|
2521
|
+
async ensureInitialized() {
|
|
2522
|
+
const store = getStore();
|
|
2523
|
+
const existing = await store.get({
|
|
2524
|
+
key: STORE_KEY
|
|
2525
|
+
});
|
|
2526
|
+
const defaults = getConfigDefaults();
|
|
2527
|
+
const merged = existing ? mergeStoredWithDefaults(defaults, existing) : defaults;
|
|
2528
|
+
await store.set({ key: STORE_KEY, value: merged });
|
|
2529
|
+
return merged;
|
|
2530
|
+
},
|
|
2531
|
+
async getSettings() {
|
|
2532
|
+
const stored = await getStore().get({
|
|
2533
|
+
key: STORE_KEY
|
|
2534
|
+
});
|
|
2535
|
+
const defaults = getConfigDefaults();
|
|
2536
|
+
return stored ? mergeStoredWithDefaults(defaults, stored) : defaults;
|
|
2537
|
+
},
|
|
2538
|
+
async updateSettings(patch) {
|
|
2539
|
+
const next = buildUpdatedSettings(await service.getSettings(), patch);
|
|
2540
|
+
await getStore().set({ key: STORE_KEY, value: next });
|
|
2541
|
+
return next;
|
|
2542
|
+
},
|
|
2543
|
+
async clearSettings() {
|
|
2544
|
+
const defaults = getConfigDefaults();
|
|
2545
|
+
await getStore().set({ key: STORE_KEY, value: defaults });
|
|
2546
|
+
return defaults;
|
|
2547
|
+
},
|
|
2548
|
+
discoverContentTypes() {
|
|
2549
|
+
const contentTypes = strapi.contentTypes ?? {};
|
|
2550
|
+
const discovered = [];
|
|
2551
|
+
for (const [uid, schema] of Object.entries(contentTypes)) {
|
|
2552
|
+
if (!isDiscoverableSchema(uid, schema)) continue;
|
|
2553
|
+
const fields = extractSchemaFields(schema.attributes ?? {});
|
|
2554
|
+
if (fields.length === 0) continue;
|
|
2555
|
+
discovered.push({
|
|
2556
|
+
uid,
|
|
2557
|
+
displayName: String(schema.info?.displayName || uid),
|
|
2558
|
+
kind: String(schema.kind || "collectionType"),
|
|
2559
|
+
fields
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
discovered.sort(
|
|
2563
|
+
(a, b) => scoreContentTypeUid(a.uid) - scoreContentTypeUid(b.uid) || a.displayName.localeCompare(b.displayName)
|
|
2564
|
+
);
|
|
2565
|
+
return discovered;
|
|
2566
|
+
}
|
|
2567
|
+
};
|
|
2568
|
+
return service;
|
|
2569
|
+
};
|
|
2570
|
+
const services = {
|
|
2571
|
+
settings,
|
|
2572
|
+
registry,
|
|
2573
|
+
occurrence,
|
|
2574
|
+
ical
|
|
2575
|
+
};
|
|
2576
|
+
const index = {
|
|
2577
|
+
register,
|
|
2578
|
+
bootstrap,
|
|
2579
|
+
destroy,
|
|
2580
|
+
config,
|
|
2581
|
+
controllers,
|
|
2582
|
+
routes,
|
|
2583
|
+
services
|
|
2584
|
+
};
|
|
2585
|
+
exports.default = index;
|