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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +23 -2
  3. package/dist/admin/App-BrB9n7EO.mjs +371 -0
  4. package/dist/admin/App-ZlqOsILc.js +371 -0
  5. package/dist/admin/SettingsPage-LR6F2vyx.js +410 -0
  6. package/dist/admin/SettingsPage-Xas3gvn4.mjs +410 -0
  7. package/dist/admin/ar-9_2mxMp2.js +339 -0
  8. package/dist/admin/ar-CrM4yxqb.mjs +339 -0
  9. package/dist/admin/de-D3NbAP6M.mjs +339 -0
  10. package/dist/admin/de-DBaO-D7Z.js +339 -0
  11. package/dist/admin/en-Cu-rn_xM.mjs +339 -0
  12. package/dist/admin/en-DAoI7qkk.js +339 -0
  13. package/dist/admin/es-DUqsu82s.js +339 -0
  14. package/dist/admin/es-Y0zfya4P.mjs +339 -0
  15. package/dist/admin/fr-BkLta_o8.js +339 -0
  16. package/dist/admin/fr-BrzKRqet.mjs +339 -0
  17. package/dist/admin/getTranslation-CZAJyMSt.mjs +5 -0
  18. package/dist/admin/getTranslation-Cd0_4UtW.js +4 -0
  19. package/dist/admin/id-BZz53ViW.mjs +339 -0
  20. package/dist/admin/id-ByxSagzq.js +339 -0
  21. package/dist/admin/index.js +1497 -0
  22. package/dist/admin/index.mjs +1492 -0
  23. package/dist/admin/pt-BSCDUkdd.js +339 -0
  24. package/dist/admin/pt-DUfISuZf.mjs +339 -0
  25. package/dist/admin/src/components/Initializer.d.ts +6 -0
  26. package/dist/admin/src/components/PluginIcon.d.ts +3 -0
  27. package/dist/admin/src/components/RRuleInput.d.ts +3 -0
  28. package/dist/admin/src/components/bookings/BookingSidePanel.d.ts +10 -0
  29. package/dist/admin/src/components/bookings/BookingsGridCanvas.d.ts +20 -0
  30. package/dist/admin/src/components/bookings/QuickCreateBookingModal.d.ts +11 -0
  31. package/dist/admin/src/index.d.ts +24 -0
  32. package/dist/admin/src/pages/App.d.ts +3 -0
  33. package/dist/admin/src/pages/CalendarPage.d.ts +3 -0
  34. package/dist/admin/src/pages/SettingsPage.d.ts +3 -0
  35. package/dist/admin/src/permissions.d.ts +29 -0
  36. package/dist/admin/src/pluginId.d.ts +1 -0
  37. package/dist/admin/src/translations/cmLabels.d.ts +1 -0
  38. package/dist/admin/src/translations/cmPlaceholders.d.ts +1 -0
  39. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  40. package/dist/admin/tr-_nhc9hk6.mjs +339 -0
  41. package/dist/admin/tr-r17l5F2K.js +339 -0
  42. package/dist/admin/ur-8edBd0Qo.mjs +339 -0
  43. package/dist/admin/ur-Dw_TAOZ3.js +339 -0
  44. package/dist/admin/zh-CeR0swEm.mjs +339 -0
  45. package/dist/admin/zh-D0z5sxxN.js +339 -0
  46. package/dist/server/index.js +3547 -0
  47. package/dist/server/index.mjs +3545 -0
  48. package/dist/server/src/bootstrap.d.ts +6 -0
  49. package/dist/server/src/config/index.d.ts +6 -0
  50. package/dist/server/src/content-types/booking/schema.json.d.ts +117 -0
  51. package/dist/server/src/content-types/index.d.ts +265 -0
  52. package/dist/server/src/content-types/resource/schema.json.d.ts +147 -0
  53. package/dist/server/src/controllers/admin.d.ts +30 -0
  54. package/dist/server/src/controllers/content-api.d.ts +32 -0
  55. package/dist/server/src/controllers/index.d.ts +28 -0
  56. package/dist/server/src/destroy.d.ts +5 -0
  57. package/dist/server/src/errors.d.ts +18 -0
  58. package/dist/server/src/index.d.ts +347 -0
  59. package/dist/server/src/register.d.ts +5 -0
  60. package/dist/server/src/routes/admin.d.ts +14 -0
  61. package/dist/server/src/routes/content-api.d.ts +9 -0
  62. package/dist/server/src/routes/index.d.ts +30 -0
  63. package/dist/server/src/services/availability.d.ts +8 -0
  64. package/dist/server/src/services/index.d.ts +12 -0
  65. package/dist/server/src/services/reservation.d.ts +6 -0
  66. package/dist/server/src/services/reservationHelpers.d.ts +41 -0
  67. package/dist/server/src/services/reservationHostCapacity.d.ts +16 -0
  68. package/dist/server/src/services/reservationMutations.d.ts +32 -0
  69. package/dist/server/src/services/reservationSources.d.ts +4 -0
  70. package/dist/server/src/services/reservationValidators.d.ts +27 -0
  71. package/dist/server/src/services/settings.d.ts +8 -0
  72. package/dist/server/src/types.d.ts +99 -0
  73. package/dist/shared/calendarSystems.d.ts +3 -0
  74. package/dist/shared/icalAndCsv.d.ts +16 -0
  75. package/dist/shared/index.d.ts +6 -0
  76. package/dist/shared/index.js +752 -0
  77. package/dist/shared/index.mjs +729 -0
  78. package/dist/shared/pluginId.d.ts +2 -0
  79. package/dist/shared/slotEngine.d.ts +19 -0
  80. package/dist/shared/slotTimezone.d.ts +45 -0
  81. package/dist/shared/types.d.ts +245 -0
  82. package/package.json +111 -3
@@ -0,0 +1,729 @@
1
+ import { formatDateInCalendarSystem, extractCalendarParts } from "@vicacha-devs/strapi-plugin-calendar/shared";
2
+ import { SUPPORTED_INTL_CALENDARS, applyDayOffset, extractCalendarParts as extractCalendarParts2, formatDateInCalendarSystem as formatDateInCalendarSystem2, getRegisteredCalendarFormatters, registerCalendarSystemFormatter, resolveOccurrenceAlternateDates, unregisterCalendarSystemFormatter } from "@vicacha-devs/strapi-plugin-calendar/shared";
3
+ const PLUGIN_ID = "bookings";
4
+ function formatSecondaryCalendars(date, systems, locale = "en") {
5
+ const result = {};
6
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
7
+ return result;
8
+ }
9
+ const dateStr = date.toISOString().slice(0, 10);
10
+ for (const sys of systems) {
11
+ if (!sys || !sys.enabled || !sys.id) continue;
12
+ const formatted = formatDateInCalendarSystem(dateStr, sys, locale, {
13
+ compact: false
14
+ });
15
+ const compact = formatDateInCalendarSystem(dateStr, sys, locale, {
16
+ compact: true
17
+ });
18
+ const parts = extractCalendarParts(
19
+ date,
20
+ sys.intlCalendar,
21
+ sys.dayOffset ?? 0
22
+ );
23
+ if (formatted) {
24
+ result[sys.id] = {
25
+ calendarId: sys.id,
26
+ label: sys.label || sys.id,
27
+ formatted,
28
+ compact: compact || formatted,
29
+ intlCalendar: sys.intlCalendar,
30
+ year: parts?.year,
31
+ month: parts?.month,
32
+ day: parts?.day,
33
+ isOverridden: false
34
+ };
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ const DEFAULT_WEEKLY_SCHEDULE = {
40
+ mode: "interval",
41
+ days: {
42
+ "1": [{ start: "09:00", end: "17:00", label: "09:00-17:00" }],
43
+ "2": [{ start: "09:00", end: "17:00", label: "09:00-17:00" }],
44
+ "3": [{ start: "09:00", end: "17:00", label: "09:00-17:00" }],
45
+ "4": [{ start: "09:00", end: "17:00", label: "09:00-17:00" }],
46
+ "5": [{ start: "09:00", end: "17:00", label: "09:00-17:00" }]
47
+ }
48
+ };
49
+ const WEEKDAY_TO_KEY = {
50
+ Sun: "0",
51
+ Mon: "1",
52
+ Tue: "2",
53
+ Wed: "3",
54
+ Thu: "4",
55
+ Fri: "5",
56
+ Sat: "6"
57
+ };
58
+ const formatterCache = /* @__PURE__ */ new Map();
59
+ function getZonedFormatter(timeZone) {
60
+ const cached = formatterCache.get(timeZone);
61
+ if (cached) return cached;
62
+ const dtf = new Intl.DateTimeFormat("en-US", {
63
+ timeZone,
64
+ year: "numeric",
65
+ month: "2-digit",
66
+ day: "2-digit",
67
+ weekday: "short",
68
+ hour: "2-digit",
69
+ minute: "2-digit",
70
+ second: "2-digit",
71
+ hourCycle: "h23"
72
+ });
73
+ if (formatterCache.size > 128) {
74
+ formatterCache.clear();
75
+ }
76
+ formatterCache.set(timeZone, dtf);
77
+ return dtf;
78
+ }
79
+ function normalizeTimeZone(tz) {
80
+ const trimmed = typeof tz === "string" ? tz.trim() : "";
81
+ if (!trimmed) return "UTC";
82
+ try {
83
+ getZonedFormatter(trimmed);
84
+ return trimmed;
85
+ } catch {
86
+ return "UTC";
87
+ }
88
+ }
89
+ function pad2(n) {
90
+ return String(n).padStart(2, "0");
91
+ }
92
+ function extractZonedWallClock(epochMs, rawTimeZone) {
93
+ const timeZone = normalizeTimeZone(rawTimeZone);
94
+ const dtf = getZonedFormatter(timeZone);
95
+ const parts = dtf.formatToParts(new Date(epochMs));
96
+ let year = 1970;
97
+ let month = 1;
98
+ let day = 1;
99
+ let hours = 0;
100
+ let minutes = 0;
101
+ let seconds = 0;
102
+ let dayKey = "4";
103
+ for (const part of parts) {
104
+ if (part.type === "year") year = Number.parseInt(part.value, 10);
105
+ else if (part.type === "month") month = Number.parseInt(part.value, 10);
106
+ else if (part.type === "day") day = Number.parseInt(part.value, 10);
107
+ else if (part.type === "hour") {
108
+ const h = Number.parseInt(part.value, 10);
109
+ hours = h === 24 ? 0 : h;
110
+ } else if (part.type === "minute") {
111
+ minutes = Number.parseInt(part.value, 10);
112
+ } else if (part.type === "second") {
113
+ seconds = Number.parseInt(part.value, 10);
114
+ } else if (part.type === "weekday") {
115
+ dayKey = WEEKDAY_TO_KEY[part.value] ?? "0";
116
+ }
117
+ }
118
+ const dateStr = `${String(year).padStart(4, "0")}-${pad2(month)}-${pad2(day)}`;
119
+ const ms = (epochMs % 1e3 + 1e3) % 1e3;
120
+ return {
121
+ year,
122
+ month,
123
+ day,
124
+ dateStr,
125
+ dayKey,
126
+ hours,
127
+ minutes,
128
+ seconds,
129
+ ms,
130
+ minutesOfDay: hours * 60 + minutes
131
+ };
132
+ }
133
+ function getTimeZoneOffsetMs(instantMs, timeZone) {
134
+ const parts = extractZonedWallClock(instantMs, timeZone);
135
+ const zonedAsUtcMs = Date.UTC(
136
+ parts.year,
137
+ parts.month - 1,
138
+ parts.day,
139
+ parts.hours,
140
+ parts.minutes,
141
+ parts.seconds,
142
+ 0
143
+ );
144
+ const truncatedInstant = Math.floor(instantMs / 1e3) * 1e3;
145
+ return zonedAsUtcMs - truncatedInstant;
146
+ }
147
+ function parseIsoDateYmd(dateStr) {
148
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr.trim());
149
+ if (!match) return null;
150
+ const year = Number.parseInt(match[1], 10);
151
+ const month = Number.parseInt(match[2], 10);
152
+ const day = Number.parseInt(match[3], 10);
153
+ if (month < 1 || month > 12 || day < 1 || day > 31) return null;
154
+ const check = new Date(Date.UTC(year, month - 1, day));
155
+ if (check.getUTCFullYear() !== year || check.getUTCMonth() !== month - 1 || check.getUTCDate() !== day) {
156
+ return null;
157
+ }
158
+ return { year, month, day };
159
+ }
160
+ function wallClockToUtcMs(dateStr, totalMinutes, rawTimeZone, strictGap = false) {
161
+ const ymd = parseIsoDateYmd(dateStr);
162
+ if (!ymd) return null;
163
+ const timeZone = normalizeTimeZone(rawTimeZone);
164
+ const extraDays = Math.floor(totalMinutes / 1440);
165
+ const remMins = (totalMinutes % 1440 + 1440) % 1440;
166
+ const targetUtcMidnight = Date.UTC(
167
+ ymd.year,
168
+ ymd.month - 1,
169
+ ymd.day + extraDays,
170
+ 0,
171
+ 0,
172
+ 0,
173
+ 0
174
+ );
175
+ const naiveUtcMs = targetUtcMidnight + remMins * 6e4;
176
+ const offset1 = getTimeZoneOffsetMs(naiveUtcMs, timeZone);
177
+ const guess1 = naiveUtcMs - offset1;
178
+ const offset2 = getTimeZoneOffsetMs(guess1, timeZone);
179
+ const guess2 = naiveUtcMs - offset2;
180
+ const targetDateObj = new Date(targetUtcMidnight);
181
+ const expectedDateStr = targetDateObj.toISOString().slice(0, 10);
182
+ const check2 = extractZonedWallClock(guess2, timeZone);
183
+ if (check2.dateStr === expectedDateStr && check2.minutesOfDay === remMins) {
184
+ return guess2;
185
+ }
186
+ const check1 = extractZonedWallClock(guess1, timeZone);
187
+ if (check1.dateStr === expectedDateStr && check1.minutesOfDay === remMins) {
188
+ return guess1;
189
+ }
190
+ return strictGap ? null : guess1;
191
+ }
192
+ function parsePeriodTimestampInZone(raw, endOfDay, rawTimeZone) {
193
+ const trimmed = raw.trim();
194
+ if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
195
+ const ms = wallClockToUtcMs(
196
+ trimmed,
197
+ endOfDay ? 1440 : 0,
198
+ rawTimeZone,
199
+ false
200
+ );
201
+ if (ms === null) return Number.NaN;
202
+ return endOfDay ? ms - 1 : ms;
203
+ }
204
+ if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) {
205
+ if (!parseIsoDateYmd(trimmed.slice(0, 10))) {
206
+ return Number.NaN;
207
+ }
208
+ }
209
+ return Date.parse(trimmed);
210
+ }
211
+ function enumerateLocalDatesInRange(rangeStartMs, rangeEndMs, rawTimeZone, maxDays = 366) {
212
+ const timeZone = normalizeTimeZone(rawTimeZone);
213
+ const startWall = extractZonedWallClock(rangeStartMs, timeZone);
214
+ const endWall = extractZonedWallClock(rangeEndMs, timeZone);
215
+ const startUtcDay = Date.UTC(
216
+ startWall.year,
217
+ startWall.month - 1,
218
+ startWall.day
219
+ );
220
+ const endUtcDay = Date.UTC(endWall.year, endWall.month - 1, endWall.day);
221
+ const results = [];
222
+ for (let cursorUtc = startUtcDay, count = 0; cursorUtc <= endUtcDay && count < maxDays; cursorUtc += 864e5, count += 1) {
223
+ const d = new Date(cursorUtc);
224
+ results.push({
225
+ dateStr: d.toISOString().slice(0, 10),
226
+ dayKey: String(d.getUTCDay())
227
+ });
228
+ }
229
+ return results;
230
+ }
231
+ function parseTimeMinutes(hhmm) {
232
+ const match = /^(\d{1,2}):(\d{2})$/.exec(hhmm.trim());
233
+ if (!match) return 9 * 60;
234
+ const hours = Number.parseInt(match[1], 10);
235
+ const minutes = Number.parseInt(match[2], 10);
236
+ if (hours === 24 && minutes === 0) return 1440;
237
+ const clampedH = Math.min(23, Math.max(0, hours));
238
+ const clampedM = Math.min(59, Math.max(0, minutes));
239
+ return clampedH * 60 + clampedM;
240
+ }
241
+ function formatTimeMinutes(totalMinutes) {
242
+ const clamped = Math.max(0, Math.min(24 * 60, totalMinutes));
243
+ const hours = Math.floor(clamped / 60);
244
+ const mins = clamped % 60;
245
+ return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
246
+ }
247
+ function extractMonthDay(dateOrMmDd) {
248
+ const trimmed = dateOrMmDd.trim();
249
+ const isoDay = trimmed.length >= 10 ? trimmed.slice(0, 10) : trimmed;
250
+ if (/^\d{4}-\d{2}-\d{2}$/.test(isoDay)) return isoDay.slice(5, 10);
251
+ if (/^\d{2}-\d{2}$/.test(isoDay)) return isoDay;
252
+ return "";
253
+ }
254
+ function matchesYearlessRange(dateStr, startStr, endStr) {
255
+ const [t, s, e] = [dateStr, startStr, endStr].map(extractMonthDay);
256
+ if (!t || !s || !e) return false;
257
+ return s <= e ? t >= s && t <= e : t >= s || t <= e;
258
+ }
259
+ function matchesDateOrYearlessRange(dateStr, startStr, endStr, yearless = false) {
260
+ if (!dateStr || !startStr || !endStr) return false;
261
+ if (yearless) return matchesYearlessRange(dateStr, startStr, endStr);
262
+ const [t, s, e] = [dateStr, startStr, endStr].map(
263
+ (d) => d.trim().slice(0, 10)
264
+ );
265
+ return t >= s && t <= e;
266
+ }
267
+ function matchesSingleDateOrYearless(dateStr, ruleDateStr, yearless = false) {
268
+ return matchesDateOrYearlessRange(
269
+ dateStr,
270
+ ruleDateStr,
271
+ ruleDateStr,
272
+ yearless
273
+ );
274
+ }
275
+ function isSlotBlackedOut(slotStartMs, slotEndMs, blackoutDates, rawTimeZone) {
276
+ const timeZone = normalizeTimeZone(rawTimeZone);
277
+ const slotDateStr = extractZonedWallClock(slotStartMs, timeZone).dateStr;
278
+ return blackoutDates.some((period) => {
279
+ if (!period?.start || !period?.end) return false;
280
+ if (period.yearless) {
281
+ return matchesDateOrYearlessRange(
282
+ slotDateStr,
283
+ period.start,
284
+ period.end,
285
+ true
286
+ );
287
+ }
288
+ const startMs = parsePeriodTimestampInZone(period.start, false, timeZone);
289
+ const endMs = parsePeriodTimestampInZone(period.end, true, timeZone);
290
+ return !Number.isNaN(startMs) && !Number.isNaN(endMs) && slotStartMs < endMs && slotEndMs > startMs;
291
+ });
292
+ }
293
+ function findMatchingHoliday(dateStr, globalHolidays = [], resourceHolidays = []) {
294
+ return [
295
+ ...Array.isArray(globalHolidays) ? globalHolidays : [],
296
+ ...Array.isArray(resourceHolidays) ? resourceHolidays : []
297
+ ].filter(
298
+ (r) => Boolean(r?.date) && matchesSingleDateOrYearless(dateStr, r.date, Boolean(r.yearless))
299
+ ).pop();
300
+ }
301
+ function findWinningSeasonalSchedule(dateStr, globalSeasonalSchedules = [], resourceSeasonals = []) {
302
+ return [
303
+ ...Array.isArray(globalSeasonalSchedules) ? globalSeasonalSchedules : [],
304
+ ...Array.isArray(resourceSeasonals) ? resourceSeasonals : []
305
+ ].filter(
306
+ (s) => s?.schedule && matchesDateOrYearlessRange(
307
+ dateStr,
308
+ s.startDate,
309
+ s.endDate,
310
+ Boolean(s.yearless)
311
+ )
312
+ ).sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0)).pop();
313
+ }
314
+ function resolveDailyScheduleForDate(dateStr, dayKey, resource, globalSeasonalSchedules = [], globalHolidays = []) {
315
+ const baseSchedule = resource.weeklySchedule?.days ? resource.weeklySchedule : DEFAULT_WEEKLY_SCHEDULE;
316
+ const baseMode = baseSchedule.mode ?? "interval";
317
+ const baseDurationMins = Math.max(5, resource.slotDurationMinutes || 60);
318
+ const holiday = findMatchingHoliday(
319
+ dateStr,
320
+ globalHolidays,
321
+ resource.holidayRules
322
+ );
323
+ if (holiday) {
324
+ const hasCustom = Array.isArray(holiday.windows) && holiday.windows.length > 0;
325
+ const isClosed = (holiday.isClosed ?? !hasCustom) || !hasCustom;
326
+ return {
327
+ mode: isClosed ? baseMode : holiday.mode ?? baseMode,
328
+ durationMins: baseDurationMins,
329
+ windows: isClosed ? [] : holiday.windows ?? [],
330
+ scheduleLabel: holiday.name
331
+ };
332
+ }
333
+ const seasonal = findWinningSeasonalSchedule(
334
+ dateStr,
335
+ globalSeasonalSchedules,
336
+ resource.seasonalSchedules
337
+ );
338
+ if (seasonal) {
339
+ return {
340
+ mode: seasonal.schedule.mode ?? baseMode,
341
+ durationMins: Math.max(
342
+ 5,
343
+ seasonal.slotDurationMinutes ?? baseDurationMins
344
+ ),
345
+ windows: seasonal.schedule.days?.[dayKey] ?? [],
346
+ scheduleLabel: seasonal.label
347
+ };
348
+ }
349
+ return {
350
+ mode: baseMode,
351
+ durationMins: baseDurationMins,
352
+ windows: baseSchedule.days?.[dayKey] ?? []
353
+ };
354
+ }
355
+ function isActiveBookingStatus(status) {
356
+ return status === "pending" || status === "confirmed";
357
+ }
358
+ function doesBookingOverlapSlot(booking, slotStartMs, slotEndMs, bufferBeforeMinutes = 0, bufferAfterMinutes = 0) {
359
+ if (!isActiveBookingStatus(booking.status)) return false;
360
+ const bStart = Date.parse(booking.startDate);
361
+ const bEnd = Date.parse(booking.endDate);
362
+ if (Number.isNaN(bStart) || Number.isNaN(bEnd)) return false;
363
+ return slotStartMs < bEnd + bufferAfterMinutes * 6e4 && slotEndMs > bStart - bufferBeforeMinutes * 6e4;
364
+ }
365
+ function selectAvailableHostsForSlot(resource, overlappingBookings, preferredLanguage) {
366
+ const rawHosts = Array.isArray(resource.assignedHosts) ? resource.assignedHosts : [];
367
+ const hosts = rawHosts.filter(
368
+ (h) => Boolean(h) && typeof h.id === "string" && h.id.trim().length > 0 && typeof h.name === "string" && h.name.trim().length > 0
369
+ );
370
+ if (hosts.length === 0) return [];
371
+ const busy = new Set(
372
+ overlappingBookings.map(
373
+ (b) => typeof b.assignedHost === "string" ? b.assignedHost.trim().toLowerCase() : ""
374
+ ).filter((v) => Boolean(v))
375
+ );
376
+ const freeHosts = hosts.filter(
377
+ (h) => !busy.has(h.id.trim().toLowerCase()) && !busy.has(h.name.trim().toLowerCase())
378
+ );
379
+ if (!preferredLanguage) return freeHosts;
380
+ const lang = preferredLanguage.trim().toLowerCase();
381
+ const matching = freeHosts.filter(
382
+ (h) => Array.isArray(h.languages) && h.languages.some(
383
+ (l) => typeof l === "string" && l.trim().toLowerCase() === lang
384
+ )
385
+ );
386
+ return matching.length > 0 ? matching : freeHosts;
387
+ }
388
+ function buildWindowCandidateBlocks(win, mode, durationMins, bufferBefore, bufferAfter, defaultCapacity) {
389
+ const winStartMins = parseTimeMinutes(win.start);
390
+ const winEndMins = parseTimeMinutes(win.end);
391
+ if (winEndMins <= winStartMins) return [];
392
+ const cap = win.capacity ?? defaultCapacity;
393
+ if (mode === "window") {
394
+ return [
395
+ {
396
+ startMins: winStartMins,
397
+ endMins: winEndMins,
398
+ label: win.label || `${win.start}-${win.end}`,
399
+ capacity: cap
400
+ }
401
+ ];
402
+ }
403
+ const blocks = [];
404
+ const stepMins = Math.max(5, durationMins + bufferBefore + bufferAfter);
405
+ for (let cursorMins = winStartMins; cursorMins + durationMins <= winEndMins; cursorMins += stepMins) {
406
+ const blockEndMins = cursorMins + durationMins;
407
+ const isExactMatch = Boolean(
408
+ win.label && winStartMins + durationMins === winEndMins
409
+ );
410
+ blocks.push({
411
+ startMins: cursorMins,
412
+ endMins: blockEndMins,
413
+ label: isExactMatch ? String(win.label) : `${formatTimeMinutes(cursorMins)}-${formatTimeMinutes(blockEndMins)}`,
414
+ capacity: cap
415
+ });
416
+ }
417
+ return blocks;
418
+ }
419
+ function evaluateCandidateBlockToSlot(block, slotStartMs, slotEndMs, dateStr, scheduleLabel, params, resourceBookings) {
420
+ const {
421
+ resource,
422
+ partySize = 1,
423
+ preferredLanguage,
424
+ secondaryCalendars = []
425
+ } = params;
426
+ const bufferBefore = Math.max(0, resource.bufferBeforeMinutes || 0);
427
+ const bufferAfter = Math.max(0, resource.bufferAfterMinutes || 0);
428
+ const overlapping = resourceBookings.filter(
429
+ (b) => doesBookingOverlapSlot(b, slotStartMs, slotEndMs, bufferBefore, bufferAfter)
430
+ );
431
+ const bookedCount = overlapping.reduce(
432
+ (sum, b) => sum + Math.max(1, Number(b.partySize) || 1),
433
+ 0
434
+ );
435
+ const capacity = Math.max(1, block.capacity || resource.capacityPerSlot || 1);
436
+ const remainingCapacity = Math.max(0, capacity - bookedCount);
437
+ const maxParty = resource.maxPartySize > 0 ? Math.min(capacity, resource.maxPartySize) : capacity;
438
+ const validParty = typeof partySize === "number" && Number.isInteger(partySize) && partySize >= 1;
439
+ return {
440
+ slotId: `${resource.documentId}:${dateStr}:${block.label}`,
441
+ resourceDocumentId: resource.documentId,
442
+ resourceName: resource.name,
443
+ resourceSlug: resource.slug,
444
+ start: new Date(slotStartMs).toISOString(),
445
+ end: new Date(slotEndMs).toISOString(),
446
+ date: dateStr,
447
+ slotLabel: block.label,
448
+ scheduleLabel,
449
+ capacity,
450
+ bookedCount,
451
+ remainingCapacity,
452
+ isAvailable: validParty && remainingCapacity >= partySize && partySize <= maxParty,
453
+ availableHosts: selectAvailableHostsForSlot(
454
+ resource,
455
+ overlapping,
456
+ preferredLanguage
457
+ ),
458
+ secondaryDates: secondaryCalendars.length > 0 ? formatSecondaryCalendars(new Date(slotStartMs), secondaryCalendars) : void 0
459
+ };
460
+ }
461
+ function computeAvailableSlots(params) {
462
+ const {
463
+ resource,
464
+ from,
465
+ to,
466
+ existingBookings,
467
+ globalSeasonalSchedules = [],
468
+ globalHolidays = [],
469
+ referenceNow
470
+ } = params;
471
+ const timeZone = normalizeTimeZone(resource.timezone);
472
+ const rangeStartMs = parsePeriodTimestampInZone(from, false, timeZone);
473
+ const rangeEndMs = parsePeriodTimestampInZone(to, true, timeZone);
474
+ if (!resource.isActive || Number.isNaN(rangeStartMs) || Number.isNaN(rangeEndMs) || rangeEndMs < rangeStartMs) {
475
+ return [];
476
+ }
477
+ const bufferBefore = Math.max(0, resource.bufferBeforeMinutes || 0);
478
+ const bufferAfter = Math.max(0, resource.bufferAfterMinutes || 0);
479
+ const resourceBookings = existingBookings.filter(
480
+ (b) => b.resourceDocumentId === resource.documentId && isActiveBookingStatus(b.status)
481
+ );
482
+ const nowMs = referenceNow instanceof Date && !Number.isNaN(referenceNow.getTime()) ? referenceNow.getTime() : null;
483
+ const minAllowedMs = nowMs !== null ? nowMs + Math.max(0, resource.minLeadTimeHours || 0) * 36e5 : null;
484
+ const maxAllowedMs = nowMs !== null && resource.maxAdvanceDays > 0 ? nowMs + resource.maxAdvanceDays * 864e5 : null;
485
+ const blackoutDates = resource.blackoutDates ?? [];
486
+ const slots = [];
487
+ const localDates = enumerateLocalDatesInRange(
488
+ rangeStartMs,
489
+ rangeEndMs,
490
+ timeZone,
491
+ 366
492
+ );
493
+ for (const { dateStr, dayKey } of localDates) {
494
+ const { mode, durationMins, windows, scheduleLabel } = resolveDailyScheduleForDate(
495
+ dateStr,
496
+ dayKey,
497
+ resource,
498
+ globalSeasonalSchedules,
499
+ globalHolidays
500
+ );
501
+ const dayBlocks = windows.flatMap(
502
+ (win) => buildWindowCandidateBlocks(
503
+ win,
504
+ mode,
505
+ durationMins,
506
+ bufferBefore,
507
+ bufferAfter,
508
+ resource.capacityPerSlot
509
+ )
510
+ );
511
+ for (const block of dayBlocks) {
512
+ const slotStartMs = wallClockToUtcMs(
513
+ dateStr,
514
+ block.startMins,
515
+ timeZone,
516
+ true
517
+ );
518
+ const slotEndMs = wallClockToUtcMs(
519
+ dateStr,
520
+ block.endMins,
521
+ timeZone,
522
+ false
523
+ );
524
+ if (slotStartMs === null || slotEndMs === null || slotEndMs <= slotStartMs) {
525
+ continue;
526
+ }
527
+ const permitted = slotEndMs > rangeStartMs && slotStartMs <= rangeEndMs && (minAllowedMs === null || slotStartMs >= minAllowedMs) && (maxAllowedMs === null || slotStartMs <= maxAllowedMs) && !isSlotBlackedOut(slotStartMs, slotEndMs, blackoutDates, timeZone);
528
+ if (permitted) {
529
+ slots.push(
530
+ evaluateCandidateBlockToSlot(
531
+ block,
532
+ slotStartMs,
533
+ slotEndMs,
534
+ dateStr,
535
+ scheduleLabel,
536
+ params,
537
+ resourceBookings
538
+ )
539
+ );
540
+ }
541
+ }
542
+ }
543
+ return slots;
544
+ }
545
+ function escapeICalText(text) {
546
+ return text.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\r\n|\r|\n/g, "\\n");
547
+ }
548
+ function formatICalDateUtc(isoDate) {
549
+ const date = new Date(isoDate);
550
+ if (Number.isNaN(date.getTime())) {
551
+ return "19700101T000000Z";
552
+ }
553
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
554
+ }
555
+ function computeFallbackOpaqueUid(seed) {
556
+ let h1 = 3735928559 ^ seed.length;
557
+ let h2 = 1103547991 ^ seed.length;
558
+ for (let i = 0; i < seed.length; i += 1) {
559
+ const ch = seed.charCodeAt(i);
560
+ h1 = Math.imul(h1 ^ ch, 2654435761);
561
+ h2 = Math.imul(h2 ^ ch, 1597334677);
562
+ }
563
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
564
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
565
+ const hex1 = (h1 >>> 0).toString(16).padStart(8, "0");
566
+ const hex2 = (h2 >>> 0).toString(16).padStart(8, "0");
567
+ return `opaque-${hex1}${hex2}`;
568
+ }
569
+ function foldICalLine(line) {
570
+ const encoder = new TextEncoder();
571
+ const maxOctets = 75;
572
+ if (encoder.encode(line).length <= maxOctets) {
573
+ return line;
574
+ }
575
+ const chunks = [];
576
+ let current = "";
577
+ for (const char of line) {
578
+ const candidate = current + char;
579
+ const limit = chunks.length === 0 ? maxOctets : maxOctets - 1;
580
+ if (encoder.encode(candidate).length > limit) {
581
+ chunks.push(current);
582
+ current = char;
583
+ } else {
584
+ current = candidate;
585
+ }
586
+ }
587
+ if (current.length > 0) {
588
+ chunks.push(current);
589
+ }
590
+ return chunks.join("\r\n ");
591
+ }
592
+ function buildBookingVEventLines(booking, nowStamp, options) {
593
+ const anonymize = Boolean(options?.anonymize);
594
+ const dtStart = formatICalDateUtc(booking.startDate);
595
+ const dtEnd = formatICalDateUtc(booking.endDate);
596
+ const statusMap = {
597
+ confirmed: "CONFIRMED",
598
+ completed: "CONFIRMED",
599
+ pending: "TENTATIVE",
600
+ cancelled: "CANCELLED",
601
+ no_show: "CANCELLED"
602
+ };
603
+ const uidLocal = anonymize ? options?.opaqueUids?.[booking.documentId] ?? computeFallbackOpaqueUid(`${booking.documentId}:${booking.startDate}`) : booking.bookingCode || booking.documentId;
604
+ const uid = `${uidLocal}@strapi-plugin-bookings`;
605
+ const summary = anonymize ? "Reserved" : booking.title || booking.customerName;
606
+ const descriptionParts = anonymize ? [
607
+ "Status: Busy",
608
+ ...booking.resourceName ? [`Resource: ${booking.resourceName}`] : []
609
+ ] : [
610
+ `Booking Code: ${booking.bookingCode}`,
611
+ `Customer: ${booking.customerName}`,
612
+ `Party Size: ${booking.partySize}`,
613
+ `Status: ${booking.status}`,
614
+ ...booking.resourceName ? [`Resource: ${booking.resourceName}`] : [],
615
+ ...booking.assignedHost ? [`Assigned Staff/Host: ${booking.assignedHost}`] : [],
616
+ ...booking.notes ? [`Notes: ${booking.notes}`] : []
617
+ ];
618
+ return [
619
+ "BEGIN:VEVENT",
620
+ foldICalLine(`UID:${escapeICalText(uid)}`),
621
+ `DTSTAMP:${nowStamp}`,
622
+ `DTSTART:${dtStart}`,
623
+ `DTEND:${dtEnd}`,
624
+ foldICalLine(`SUMMARY:${escapeICalText(summary)}`),
625
+ foldICalLine(`DESCRIPTION:${escapeICalText(descriptionParts.join("\n"))}`),
626
+ `STATUS:${statusMap[booking.status] || "TENTATIVE"}`,
627
+ "END:VEVENT"
628
+ ];
629
+ }
630
+ function generateBookingsICalFeed(bookings, calendarName = "Strapi Bookings & Reservations", options) {
631
+ const nowStamp = formatICalDateUtc((/* @__PURE__ */ new Date()).toISOString());
632
+ const lines = [
633
+ "BEGIN:VCALENDAR",
634
+ "VERSION:2.0",
635
+ "PRODID:-//Vicacha Devs//Strapi Plugin Bookings//EN",
636
+ "CALSCALE:GREGORIAN",
637
+ "METHOD:PUBLISH",
638
+ foldICalLine(`X-WR-CALNAME:${escapeICalText(calendarName)}`)
639
+ ];
640
+ for (const booking of bookings) {
641
+ lines.push(...buildBookingVEventLines(booking, nowStamp, options));
642
+ }
643
+ lines.push("END:VCALENDAR");
644
+ return `${lines.join("\r\n")}\r
645
+ `;
646
+ }
647
+ function sanitizeCsvCell(value) {
648
+ if (value === null || value === void 0) {
649
+ return '""';
650
+ }
651
+ let str = String(value);
652
+ if (/^[\s\u0085\u2028\u2029]*[=+\-@|]|^[\t\r\n]/.test(str)) {
653
+ str = `'${str}`;
654
+ }
655
+ const escaped = str.replace(/"/g, '""');
656
+ return `"${escaped}"`;
657
+ }
658
+ function generateBookingsCsv(bookings) {
659
+ const headers = [
660
+ "Booking Code",
661
+ "Resource",
662
+ "Customer Name",
663
+ "Customer Email",
664
+ "Customer Phone",
665
+ "Organization",
666
+ "Start Date",
667
+ "End Date",
668
+ "Slot Label",
669
+ "Party Size",
670
+ "Status",
671
+ "Preferred Language",
672
+ "Assigned Host",
673
+ "Notes"
674
+ ];
675
+ const rows = bookings.map(
676
+ (b) => [
677
+ b.bookingCode,
678
+ b.resourceName ?? "",
679
+ b.customerName,
680
+ b.customerEmail,
681
+ b.customerPhone ?? "",
682
+ b.organization ?? "",
683
+ b.startDate,
684
+ b.endDate,
685
+ b.slotLabel ?? "",
686
+ b.partySize,
687
+ b.status,
688
+ b.preferredLanguage ?? "",
689
+ b.assignedHost ?? "",
690
+ b.notes ?? ""
691
+ ].map(sanitizeCsvCell).join(",")
692
+ );
693
+ return [headers.map(sanitizeCsvCell).join(","), ...rows].join("\r\n");
694
+ }
695
+ export {
696
+ DEFAULT_WEEKLY_SCHEDULE,
697
+ PLUGIN_ID,
698
+ SUPPORTED_INTL_CALENDARS,
699
+ applyDayOffset,
700
+ computeAvailableSlots,
701
+ doesBookingOverlapSlot,
702
+ enumerateLocalDatesInRange,
703
+ escapeICalText,
704
+ extractCalendarParts2 as extractCalendarParts,
705
+ extractMonthDay,
706
+ extractZonedWallClock,
707
+ foldICalLine,
708
+ formatDateInCalendarSystem2 as formatDateInCalendarSystem,
709
+ formatSecondaryCalendars,
710
+ formatTimeMinutes,
711
+ generateBookingsCsv,
712
+ generateBookingsICalFeed,
713
+ getRegisteredCalendarFormatters,
714
+ isActiveBookingStatus,
715
+ isSlotBlackedOut,
716
+ matchesDateOrYearlessRange,
717
+ matchesSingleDateOrYearless,
718
+ normalizeTimeZone,
719
+ parseIsoDateYmd,
720
+ parsePeriodTimestampInZone,
721
+ parseTimeMinutes,
722
+ registerCalendarSystemFormatter,
723
+ resolveDailyScheduleForDate,
724
+ resolveOccurrenceAlternateDates,
725
+ sanitizeCsvCell,
726
+ selectAvailableHostsForSlot,
727
+ unregisterCalendarSystemFormatter,
728
+ wallClockToUtcMs
729
+ };