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